From 61496ca956a3575414c0ceecde5d43e5ebb026cd Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sun, 2 Jan 2022 12:23:43 +0100 Subject: [PATCH 01/54] Add Configuration object --- components/roode/configuration.cpp | 85 ++++++++++++++++++++++++++++++ components/roode/configuration.h | 27 ++++++++++ components/roode/roode.cpp | 2 +- components/roode/roode.h | 7 ++- 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 components/roode/configuration.cpp create mode 100644 components/roode/configuration.h diff --git a/components/roode/configuration.cpp b/components/roode/configuration.cpp new file mode 100644 index 00000000..0bf4d9a4 --- /dev/null +++ b/components/roode/configuration.cpp @@ -0,0 +1,85 @@ +#include "configuration.h" +#include "roode.h" +/* +This Object is used to store the sensor configratuin like calibration data, sensor mode, ROI settings etc. +*/ +namespace esphome +{ + namespace roode + { + void Calibration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) + { + distanceSensor.StopRanging(); + switch (sensor_mode) + { + case 0: // short mode + time_budget_in_ms = time_budget_in_ms_short; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Short); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); + } + ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); + break; + case 1: // medium mode + time_budget_in_ms = time_budget_in_ms_medium; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); + break; + case 2: // medium_long mode + time_budget_in_ms = time_budget_in_ms_medium_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 3: // long mode + time_budget_in_ms = time_budget_in_ms_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 4: // max mode + time_budget_in_ms = time_budget_in_ms_max; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); + break; + case 5: // custom mode + time_budget_in_ms = new_timing_budget; + delay_between_measurements = new_timing_budget + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); + break; + default: + break; + } + sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); + if (sensor_status != 0) + { + ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); + } + } + } +} \ No newline at end of file diff --git a/components/roode/configuration.h b/components/roode/configuration.h new file mode 100644 index 00000000..074ad240 --- /dev/null +++ b/components/roode/configuration.h @@ -0,0 +1,27 @@ + +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/application.h" +#include "EEPROM.h" +#include "VL53L1X_ULD.h" +#include +namespace esphome +{ + namespace roode + { + class Configuration + { + public: + // Calibration(VL53L1X_ULD distanceSensor); + void setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int timing_budget = 0); + + protected: + VL53L1X_ULD distanceSensor; + VL53L1_Error sensor_status = VL53L1_ERROR_NONE; + }; + } +} diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index ff7d43ae..f6438d10 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -83,7 +83,7 @@ namespace esphome center[0] = 167; center[1] = 231; distanceSensor.SetROI(Roode::roi_width_, Roode::roi_height_); - setSensorMode(sensor_mode, timing_budget_); + sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); DIST_THRESHOLD_MAX[0] = Roode::manual_threshold_; DIST_THRESHOLD_MAX[1] = Roode::manual_threshold_; publishSensorConfiguration(DIST_THRESHOLD_MAX, true); diff --git a/components/roode/roode.h b/components/roode/roode.h index 8b62db12..53d3c53a 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -7,6 +7,7 @@ #include "esphome/core/application.h" #include "VL53L1X_ULD.h" #include +#include "configuration.h" namespace esphome { @@ -94,17 +95,19 @@ namespace esphome uint16_t getDistance(); uint16_t distance = 0; - VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; - VL53L1_Error sensor_status = VL53L1_ERROR_NONE; + ERangeStatus rangeStatus; int DIST_THRESHOLD_MAX[2] = {0, 0}; // max treshold of the two zones int DIST_THRESHOLD_MIN[2] = {0, 0}; // min treshold of the two zones int roi_width_{6}; // width of the ROI int roi_height_{16}; // height of the ROI uint16_t peopleCounter{0}; + Configuration sensorConfiguration; protected: VL53L1X_ULD distanceSensor; + VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; + VL53L1_Error sensor_status = VL53L1_ERROR_NONE; sensor::Sensor *distance_sensor; number::Number *people_counter; sensor::Sensor *max_threshold_zone0_sensor; From 0f2fd4302d7ce42e0b6e50b84ad74ed38b545c87 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sun, 2 Jan 2022 12:24:02 +0100 Subject: [PATCH 02/54] Add UML draft --- Documents/Roode.drawio | 1 + 1 file changed, 1 insertion(+) create mode 100644 Documents/Roode.drawio diff --git a/Documents/Roode.drawio b/Documents/Roode.drawio new file mode 100644 index 00000000..66cfaf17 --- /dev/null +++ b/Documents/Roode.drawio @@ -0,0 +1 @@ +7ZpdT9swFIZ/TS6Z8tH045IUyqSBmFqJXbuJm1g4duS4pOXX7zixk4YUUSag2RZUofj18bF9/Ng5mFrePN3dCJQldzzC1HLtaGd5V5brzkZj+K2EfSX401klxIJEleQ0woo8Yy3aWt2SCOctQ8k5lSRriyFnDIeypSEheNE223Da7jVDMe4IqxDRrvqLRDKp1KlvN/p3TOLE9OzYumaNwsdY8C3T/THOcFWTIuNGm+YJinhxIHnXljcXnMvqKd3NMVVRNRGr2i1eqa2HLDCTJzWYoHCN/Mloin1vNl5fmIE9IbrVcVhyWFk9XLk30ckLklIEE/OCRKYURAceN5zJlTayoRwmhEa3aM+3aji5hMiYUpBwQZ7BHpnGUC2k5sAdK2+E0jmnXIBQBvGw0Uo5090InEOzn2baTi3dolyaoXBKUZaTdTk4ZZIiERMWcCl5qo3MrBYHPVuutyl/lFe1qjgy1mb1Kv8pCfUzRWtMgxqDF3PIpeCPNVMmbAuUEqq2ygMWEWLIRLOKh6PcIkpiBoUQponBYdBdbk3AExYS7w4kvfw3mKdYij2Y6NqJX7XQm3Sq179oiHfGWkvatOudpndZXHtucIMHTdyp9Dkd+iw3UJuXYLV7L9UZsM+6NMJkZZvEKsgvIn8EKBNTijfKgwocgTPgUsuSZ8pZhkLC4tvS5mrUKEsdESUVCZF4BboaUwFHImgc/G1oyUhCogizkiGJJFrXeyTjhMkyin4AH4jr3P7mWz7Maw5lpynDR5kLOecM5odIueoYGC+w4vwYD69u8LcJ0Ui449OQMHYfS8RoIKJ3RPjuOYkYD0T0jojJ9IxEeN2cBSa/IfFWIEk4G3KXfzR3qbOVj0xedG9L+JMCsRgC/q7u7BOgRxQiwJDEgQpw3mG/nugfbochifrsA9L7u5Iozx2I6B0RZ02iPG8gondEfGUSdTN62Dw6i/DH/ez+OclTvxgtL7ovjvsMVznU3XAH1Os8qkPZkdTq1Dsg1/vKS6CjJA4vrA84nt5g4vUjoBdZzNHhDa+tXnLxlbnM0eENt4S95OLsGY0/ZDT/ZUbz8qqmBynNcG19piPK73dKMxm46CMXZ09ppgMXfeTiE1MaKDbf/Kn+E9B8scq7/g0= \ No newline at end of file From fb72e406c8894c3bc44a5554171221d5ea82e87d Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 3 Jan 2022 23:31:56 +0100 Subject: [PATCH 03/54] Add Zone and Configuration Objects (WIP) --- Documents/diagram.puml | 51 ++++++++++++ components/roode/configuration.cpp | 2 +- components/roode/configuration.h | 10 ++- components/roode/roode.cpp | 120 ++++++++++++----------------- components/roode/roode.h | 20 +++-- components/roode/sensor.py | 21 +++-- components/roode/zone.cpp | 76 ++++++++++++++++++ components/roode/zone.h | 42 ++++++++++ peopleCounterDev.yaml | 8 +- 9 files changed, 262 insertions(+), 88 deletions(-) create mode 100644 Documents/diagram.puml create mode 100644 components/roode/zone.cpp create mode 100644 components/roode/zone.h diff --git a/Documents/diagram.puml b/Documents/diagram.puml new file mode 100644 index 00000000..59b26614 --- /dev/null +++ b/Documents/diagram.puml @@ -0,0 +1,51 @@ +@startuml +class Roode { + + void setup() + + void update() + + void loop() + + void dump_config() + + void getZoneDistance() + + void sendCounter(uint16_t counter) + + void recalibration() + + bool handleSensorStatus() + + uint16_t getDistance() + + VL53L1_Error sensor_status = VL53L1_ERROR_NONE + + VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE + # VL53L1X_ULD distanceSensor +} + +class Configuration { + + Zone getZoneConfigraution(uint8_t zone) + + int getTimingBudget() + # void setCorrectDistanceSettings(float average_zone_0, float average_zone_1) + # void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax) + # void setSensorMode(int sensor_mode, int timing_budget = 0) +} + +class Zone { + + Zone(Configuration *configuration) + + void calibrateThreshold(VL53L1X_ULD distanceSensor) + + void calibrateRoi(VL53L1X_ULD distanceSensor) + + uint16_t getMaxThreshold() + + uint16_t getMinThreshold() + + uint16_t getROIWidth() + + uint16_t getROIHeight() + + uint8_t getROICenter() + # int getOptimizedValues(int *values, int sum, int size) + # int getSum(int *values, int size) +} + +class PersistedNumber { + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_restore_value(bool restore) { this->restore_value_ = restore; } + + void setup() override; + # void control(float value) override + # bool restore_value_{false} + # ESPPreferenceObject pref_ +} + +Roode --> "<>" Configuration +Roode --> "<>" Zone +Roode --> "<>" PersistedNumber + +@enduml \ No newline at end of file diff --git a/components/roode/configuration.cpp b/components/roode/configuration.cpp index 0bf4d9a4..c68e3085 100644 --- a/components/roode/configuration.cpp +++ b/components/roode/configuration.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace roode { - void Calibration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) + void Configuration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) { distanceSensor.StopRanging(); switch (sensor_mode) diff --git a/components/roode/configuration.h b/components/roode/configuration.h index 074ad240..eae974df 100644 --- a/components/roode/configuration.h +++ b/components/roode/configuration.h @@ -6,9 +6,12 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/application.h" -#include "EEPROM.h" #include "VL53L1X_ULD.h" #include +#include "zone.h" + +static VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; +static VL53L1_Error sensor_status = VL53L1_ERROR_NONE; namespace esphome { namespace roode @@ -16,12 +19,15 @@ namespace esphome class Configuration { public: - // Calibration(VL53L1X_ULD distanceSensor); void setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int timing_budget = 0); + int getTimingBudget(); protected: VL53L1X_ULD distanceSensor; VL53L1_Error sensor_status = VL53L1_ERROR_NONE; + void setSensorMode(int sensor_mode, int timing_budget = 0); + void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); + void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); }; } } diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index f6438d10..818fde1d 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -4,9 +4,6 @@ namespace esphome { namespace roode { - static const char *const TAG = "Roode"; - static const char *const SETUP = "Setup"; - static const char *const CALIBRATION = "Calibration"; void Roode::dump_config() { ESP_LOGCONFIG(TAG, "dump config:"); @@ -62,9 +59,8 @@ namespace esphome } } } - + createZones(); ESP_LOGI(SETUP, "Using sampling with sampling size: %d", DISTANCES_ARRAY_SIZE); - if (invert_direction_) { ESP_LOGI(SETUP, "Inverting direction"); @@ -73,7 +69,6 @@ namespace esphome } if (calibration_active_) { - ESP_LOGI(SETUP, "Calibrating sensor"); calibration(distanceSensor); App.feed_wdt(); } @@ -94,19 +89,23 @@ namespace esphome void Roode::update() { - if (distance_sensor != nullptr) + if (distance_zone0 != nullptr) + { + distance_zone0->publish_state(zone0->getDistance()); + } + if (distance_zone1 != nullptr) { - distance_sensor->publish_state(distance); + distance_zone1->publish_state(zone1->getDistance()); } } void Roode::loop() { // unsigned long start = micros(); - getZoneDistance(); - zone++; - zone = zone % 2; - App.feed_wdt(); + distance = getAlternatingZoneDistances(); + doPathTracking(distance, zone); + handleSensorStatus(); + // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", zone0->getDistance(Roode::distanceSensor, Roode::sensor_status), zone1->getDistance(Roode::distanceSensor, Roode::sensor_status)); // unsigned long end = micros(); // unsigned long delta = end - start; // ESP_LOGI("Roode loop", "loop took %lu microseconds", delta); @@ -136,32 +135,29 @@ namespace esphome return check_status; } - uint16_t Roode::getDistance() + void Roode::createZones() { - // Checking if data is available. This can also be done through the hardware interrupt. See the ReadDistanceHardwareInterrupt for an example - uint8_t dataReady = false; - while (!dataReady) + zone0 = new Zone(Roode::roi_width_, Roode::roi_height_, center[0]); + zone1 = new Zone(Roode::roi_width_, Roode::roi_height_, center[1]); + } + uint16_t Roode::getAlternatingZoneDistances() + { + if (zone == zone0->getZoneId()) { - sensor_status += distanceSensor.CheckForDataReady(&dataReady); - delay(1); + distance = zone0->getNewDistance(distanceSensor); } - - // Get the results - uint16_t distance; - sensor_status += distanceSensor.GetDistanceInMm(&distance); - - if (sensor_status != VL53L1_ERROR_NONE) + else if (zone == zone1->getZoneId()) { - ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); - return sensor_status; + distance = zone1->getNewDistance(distanceSensor); } - // After reading the results reset the interrupt to be able to take another measurement - distanceSensor.ClearInterrupt(); + zone++; + zone = zone % 2; + App.feed_wdt(); return distance; } - void Roode::getZoneDistance() + void Roode::doPathTracking(uint16_t zoneDistance, uint8_t zone) { static int PathTrack[] = {0, 0, 0, 0}; static int PathTrackFillingSize = 1; // init this to 1 as we start from state where nobody is any of the zones @@ -171,24 +167,15 @@ namespace esphome int CurrentZoneStatus = NOBODY; int AllZonesCurrentStatus = 0; int AnEventHasOccured = 0; - sensor_status += distanceSensor.SetROICenter(center[zone]); - sensor_status += distanceSensor.StartRanging(); - last_sensor_status = sensor_status; - distance = getDistance(); - sensor_status += distanceSensor.StopRanging(); - if (!handleSensorStatus()) - { - return; - } uint16_t Distances[2][DISTANCES_ARRAY_SIZE]; uint16_t MinDistance; uint8_t i; if (DistancesTableSize[zone] < DISTANCES_ARRAY_SIZE) { + ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone, distance); Distances[zone][DistancesTableSize[zone]] = distance; DistancesTableSize[zone]++; - ESP_LOGD(SETUP, "Distances[%d][DistancesTableSize[zone]] = %d", zone, Distances[zone][DistancesTableSize[zone]]); } else { @@ -340,13 +327,14 @@ namespace esphome } } } - void Roode::updateCounter(int delta) { + void Roode::updateCounter(int delta) + { if (this->people_counter == nullptr) { return; } - auto next = this->people_counter->state + (float) delta; - ESP_LOGI(TAG, "Updating people count: %d", (int) next); + auto next = this->people_counter->state + (float)delta; + ESP_LOGI(TAG, "Updating people count: %d", (int)next); this->people_counter->set(next); } void Roode::recalibration() @@ -360,6 +348,10 @@ namespace esphome int ROI_size = min(8, max(4, function_of_the_distance)); Roode::roi_width_ = ROI_size; Roode::roi_height_ = ROI_size * 2; + zone0->setRoiWidth(roi_width_); + zone0->setRoiHeight(roi_height_); + zone1->setRoiWidth(roi_width_); + zone1->setRoiHeight(roi_height_); // now we set the position of the center of the two zones if (advised_sensor_orientation_) { @@ -414,12 +406,14 @@ namespace esphome break; } } + zone0->setRoiCenter(center[0]); + zone1->setRoiCenter(center[1]); // we will now repeat the calculations necessary to define the thresholds with the updated zones zone = 0; int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; sensor_status += distanceSensor.StopRanging(); - sensor_status += distanceSensor.SetROI(Roode::roi_width_, Roode::roi_height_); + sensor_status += distanceSensor.SetROI(zone0->getRoiWidth(), zone0->getRoiHeight()); sensor_status += distanceSensor.SetInterMeasurementInMs(delay_between_measurements); sensor_status += distanceSensor.StartRanging(); if (sensor_status != VL53L1_ERROR_NONE) @@ -429,19 +423,8 @@ namespace esphome for (int i = 0; i < number_attempts; i++) { - // increase sum of values in Zone 0 - distanceSensor.SetROICenter(center[zone]); - distance = getDistance(); - values_zone_0[i] = distance; - zone++; - zone = zone % 2; - App.feed_wdt(); - // increase sum of values in Zone 1 - distanceSensor.SetROICenter(center[zone]); - distance = getDistance(); - values_zone_1[i] = distance; - zone++; - zone = zone % 2; + values_zone_0[i] = getAlternatingZoneDistances(); + values_zone_1[i] = getAlternatingZoneDistances(); } optimized_zone_0 = getOptimizedValues(values_zone_0, getSum(values_zone_0, number_attempts), number_attempts); optimized_zone_1 = getOptimizedValues(values_zone_1, getSum(values_zone_1, number_attempts), number_attempts); @@ -578,6 +561,7 @@ namespace esphome void Roode::calibration(VL53L1X_ULD distanceSensor) { + ESP_LOGI(SETUP, "Calibrating sensor"); distanceSensor.StopRanging(); // the sensor does 100 measurements for each zone (zones are predefined) time_budget_in_ms = time_budget_in_ms_medium; @@ -604,28 +588,24 @@ namespace esphome roi_height_ = roi_width_; } + zone0->setRoiWidth(roi_width_); + zone0->setRoiHeight(roi_height_); + zone1->setRoiWidth(roi_width_); + zone1->setRoiHeight(roi_height_); + zone0->setRoiCenter(center[0]); + zone1->setRoiCenter(center[1]); + zone = 0; int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; - distanceSensor.SetROI(Roode::roi_width_, Roode::roi_height_); + sensor_status += distanceSensor.SetROI(zone0->getRoiWidth(), zone0->getRoiHeight()); distanceSensor.SetInterMeasurementInMs(delay_between_measurements); distanceSensor.StartRanging(); for (int i = 0; i < number_attempts; i++) { - // increase sum of values in Zone 0 - distanceSensor.SetROICenter(center[zone]); - distance = getDistance(); - values_zone_0[i] = distance; - zone++; - zone = zone % 2; - App.feed_wdt(); - // increase sum of values in Zone 1 - distanceSensor.SetROICenter(center[zone]); - distance = getDistance(); - values_zone_1[i] = distance; - zone++; - zone = zone % 2; + values_zone_0[i] = getAlternatingZoneDistances(); + values_zone_1[i] = getAlternatingZoneDistances(); } // after we have computed the sum for each zone, we can compute the average distance of each zone diff --git a/components/roode/roode.h b/components/roode/roode.h index 53d3c53a..0dce8381 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -8,6 +8,7 @@ #include "VL53L1X_ULD.h" #include #include "configuration.h" +#include "zone.h" namespace esphome { @@ -19,7 +20,9 @@ namespace esphome #define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 static int LEFT = 0; static int RIGHT = 1; - + static const char *const TAG = "Roode"; + static const char *const SETUP = "Setup"; + static const char *const CALIBRATION = "Calibration"; /* ##### CALIBRATION ##### */ @@ -76,7 +79,8 @@ namespace esphome void set_restore_values(bool val) { restore_values_ = val; } void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } void set_sampling_size(uint8_t size) { DISTANCES_ARRAY_SIZE = size; } - void set_distance_sensor(sensor::Sensor *distance_sensor_) { distance_sensor = distance_sensor_; } + void set_distance_zone0(sensor::Sensor *distance_zone0_) { distance_zone0 = distance_zone0_; } + void set_distance_zone1(sensor::Sensor *distance_zone1_) { distance_zone1 = distance_zone1_; } void set_people_counter(number::Number *counter) { this->people_counter = counter; } void set_max_threshold_zone0_sensor(sensor::Sensor *max_threshold_zone0_sensor_) { max_threshold_zone0_sensor = max_threshold_zone0_sensor_; } void set_max_threshold_zone1_sensor(sensor::Sensor *max_threshold_zone1_sensor_) { max_threshold_zone1_sensor = max_threshold_zone1_sensor_; } @@ -89,10 +93,10 @@ namespace esphome void set_version_text_sensor(text_sensor::TextSensor *version_sensor_) { version_sensor = version_sensor_; } void set_entry_exit_event_text_sensor(text_sensor::TextSensor *entry_exit_event_sensor_) { entry_exit_event_sensor = entry_exit_event_sensor_; } void set_sensor_mode(int sensor_mode_) { sensor_mode = sensor_mode_; } - void getZoneDistance(); + uint16_t getAlternatingZoneDistances(); + void doPathTracking(uint16_t zoneDistance, uint8_t zone); void recalibration(); bool handleSensorStatus(); - uint16_t getDistance(); uint16_t distance = 0; @@ -106,9 +110,10 @@ namespace esphome protected: VL53L1X_ULD distanceSensor; - VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; - VL53L1_Error sensor_status = VL53L1_ERROR_NONE; - sensor::Sensor *distance_sensor; + Zone *zone0; + Zone *zone1; + sensor::Sensor *distance_zone0; + sensor::Sensor *distance_zone1; number::Number *people_counter; sensor::Sensor *max_threshold_zone0_sensor; sensor::Sensor *max_threshold_zone1_sensor; @@ -121,6 +126,7 @@ namespace esphome text_sensor::TextSensor *version_sensor; text_sensor::TextSensor *entry_exit_event_sensor; + void createZones(); void roi_calibration(VL53L1X_ULD distanceSensor, int optimized_zone_0, int optimized_zone_1); void calibration(VL53L1X_ULD distanceSensor); void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); diff --git a/components/roode/sensor.py b/components/roode/sensor.py index 8badda26..4333254f 100644 --- a/components/roode/sensor.py +++ b/components/roode/sensor.py @@ -13,7 +13,8 @@ DEPENDENCIES = ["roode"] -CONF_DISTANCE = "distance_sensor" +CONF_DISTANCE_ZONE0 = "distance_zone0" +CONF_DISTANCE_ZONE1 = "distance_zone1" CONF_MAX_THRESHOLD_ZONE0 = "max_threshold_zone0" CONF_MAX_THRESHOLD_ZONE1 = "max_threshold_zone1" CONF_MIN_THRESHOLD_ZONE0 = "min_threshold_zone0" @@ -24,7 +25,14 @@ CONFIG_SCHEMA = sensor.sensor_schema().extend( { - cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + cv.Optional(CONF_DISTANCE_ZONE0): sensor.sensor_schema( + icon=ICON_RULER, + unit_of_measurement="mm", + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_DISTANCE_ZONE1): sensor.sensor_schema( icon=ICON_RULER, unit_of_measurement="mm", accuracy_decimals=0, @@ -85,9 +93,12 @@ async def to_code(config): var = await cg.get_variable(config[CONF_ROODE_ID]) - if CONF_DISTANCE in config: - distance = await sensor.new_sensor(config[CONF_DISTANCE]) - cg.add(var.set_distance_sensor(distance)) + if CONF_DISTANCE_ZONE0 in config: + distance = await sensor.new_sensor(config[CONF_DISTANCE_ZONE0]) + cg.add(var.set_distance_zone0(distance)) + if CONF_DISTANCE_ZONE1 in config: + distance = await sensor.new_sensor(config[CONF_DISTANCE_ZONE1]) + cg.add(var.set_distance_zone1(distance)) if CONF_MAX_THRESHOLD_ZONE0 in config: count = await sensor.new_sensor(config[CONF_MAX_THRESHOLD_ZONE0]) cg.add(var.set_max_threshold_zone0_sensor(count)) diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp new file mode 100644 index 00000000..df84312d --- /dev/null +++ b/components/roode/zone.cpp @@ -0,0 +1,76 @@ +#include "zone.h" + +static uint8_t zone_id = 0; +namespace esphome +{ + namespace roode + { + Zone::Zone(int roi_width, int roi_height, int roi_center) + { + this->id = zone_id++; + ESP_LOGD(TAG, "Zone(%d, %d, %d)", roi_width, roi_height, roi_center); + this->roi_width = roi_width; + this->roi_height = roi_height; + this->roi_center = roi_center; + } + + uint16_t Zone::getNewDistance(VL53L1X_ULD &distanceSensor) + { + last_sensor_status = sensor_status; + + sensor_status += distanceSensor.StartRanging(); + sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); + // Checking if data is available. This can also be done through the hardware interrupt. See the ReadDistanceHardwareInterrupt for an example + uint8_t dataReady = false; + while (!dataReady) + { + sensor_status += distanceSensor.CheckForDataReady(&dataReady); + delay(1); + } + + // Get the results + sensor_status += distanceSensor.GetDistanceInMm(&distance); + + if (sensor_status != VL53L1_ERROR_NONE) + { + ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); + return sensor_status; + } + // After reading the results reset the interrupt to be able to take another measurement + sensor_status += distanceSensor.ClearInterrupt(); + return distance; + } + uint16_t Zone::getDistance() + { + return this->distance; + } + uint16_t Zone::getRoiWidth() + { + return this->roi_width; + } + uint16_t Zone::getRoiHeight() + { + return this->roi_height; + } + uint16_t Zone::getRoiCenter() + { + return this->roi_center; + } + uint16_t Zone::setRoiWidth(uint16_t new_roi_width) + { + return this->roi_width = new_roi_width; + } + uint16_t Zone::setRoiHeight(uint16_t new_roi_height) + { + return this->roi_height = new_roi_height; + } + uint16_t Zone::setRoiCenter(uint16_t new_roi_center) + { + return this->roi_center = new_roi_center; + } + uint8_t Zone::getZoneId() + { + return this->id; + } + } +} \ No newline at end of file diff --git a/components/roode/zone.h b/components/roode/zone.h new file mode 100644 index 00000000..9e3d297d --- /dev/null +++ b/components/roode/zone.h @@ -0,0 +1,42 @@ + +#pragma once +#include "VL53L1X_ULD.h" +#include +#include "esphome/core/log.h" +#include "configuration.h" +static const char *const TAG = "Zone"; +namespace esphome +{ + namespace roode + { + class Zone + { + public: + Zone(int roi_width, int roi_height, int roi_center); + uint16_t getNewDistance(VL53L1X_ULD &distanceSensor); + uint16_t calibrateThreshold(); + uint16_t calibrateRoi(); + uint16_t getMinThreshold(); + uint16_t getMaxThreshold(); + uint16_t getRoiWidth(); + uint16_t getRoiHeight(); + uint16_t getRoiCenter(); + uint16_t setRoiWidth(uint16_t new_roi_width); + uint16_t setRoiHeight(uint16_t new_roi_height); + uint16_t setRoiCenter(uint16_t new_roi_center); + uint8_t getZoneId(); + uint16_t getDistance(); + bool handleSensorStatus(); + + protected: + int getSum(int *values, int size); + int getOptimizedValues(int *values, int sum, int size); + void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); + uint16_t roi_width; + uint16_t roi_height; + uint16_t roi_center; + uint8_t id; + uint16_t distance; + }; + } +} diff --git a/peopleCounterDev.yaml b/peopleCounterDev.yaml index 5e5513c7..bcf60918 100644 --- a/peopleCounterDev.yaml +++ b/peopleCounterDev.yaml @@ -1,5 +1,5 @@ substitutions: - devicename: roodedev + devicename: roode-office friendly_name: $devicename external_components: @@ -87,8 +87,10 @@ binary_sensor: sensor: - platform: roode id: hallway - distance_sensor: - name: $friendly_name distance + distance_zone0: + name: $friendly_name distance zone 0 + distance_zone1: + name: $friendly_name distance zone 1 max_threshold_zone0: name: $friendly_name max zone 0 max_threshold_zone1: From 2f8660e31d98091fe2d5400af512ad07b5afe671 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 3 Jan 2022 23:32:14 +0100 Subject: [PATCH 04/54] Revert device name --- peopleCounterDev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peopleCounterDev.yaml b/peopleCounterDev.yaml index bcf60918..d2049929 100644 --- a/peopleCounterDev.yaml +++ b/peopleCounterDev.yaml @@ -1,5 +1,5 @@ substitutions: - devicename: roode-office + devicename: roodedev friendly_name: $devicename external_components: From 4e123efbee1cef44c5249cdaccf990ea1b7ebee2 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 18:22:54 +0100 Subject: [PATCH 05/54] Refactor alternate zoning --- components/roode/roode.cpp | 55 +++++++++++++++----------------------- components/roode/roode.h | 4 +-- components/roode/zone.cpp | 2 +- components/roode/zone.h | 13 ++++++++- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 818fde1d..1a08b784 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -103,8 +103,9 @@ namespace esphome { // unsigned long start = micros(); distance = getAlternatingZoneDistances(); - doPathTracking(distance, zone); + doPathTracking(this->current_zone); handleSensorStatus(); + this->current_zone = this->current_zone == this->zone0 ? this->zone1 : this->zone0; // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", zone0->getDistance(Roode::distanceSensor, Roode::sensor_status), zone1->getDistance(Roode::distanceSensor, Roode::sensor_status)); // unsigned long end = micros(); // unsigned long delta = end - start; @@ -139,25 +140,16 @@ namespace esphome { zone0 = new Zone(Roode::roi_width_, Roode::roi_height_, center[0]); zone1 = new Zone(Roode::roi_width_, Roode::roi_height_, center[1]); + current_zone = zone0; } uint16_t Roode::getAlternatingZoneDistances() { - if (zone == zone0->getZoneId()) - { - distance = zone0->getNewDistance(distanceSensor); - } - else if (zone == zone1->getZoneId()) - { - distance = zone1->getNewDistance(distanceSensor); - } - - zone++; - zone = zone % 2; + this->current_zone->readDistance(distanceSensor); App.feed_wdt(); - return distance; + return this->current_zone->getDistance(); } - void Roode::doPathTracking(uint16_t zoneDistance, uint8_t zone) + void Roode::doPathTracking(Zone *zone) { static int PathTrack[] = {0, 0, 0, 0}; static int PathTrackFillingSize = 1; // init this to 1 as we start from state where nobody is any of the zones @@ -171,35 +163,35 @@ namespace esphome uint16_t Distances[2][DISTANCES_ARRAY_SIZE]; uint16_t MinDistance; uint8_t i; - if (DistancesTableSize[zone] < DISTANCES_ARRAY_SIZE) + if (DistancesTableSize[zone->getZoneId()] < DISTANCES_ARRAY_SIZE) { - ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone, distance); - Distances[zone][DistancesTableSize[zone]] = distance; - DistancesTableSize[zone]++; + ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone->getZoneId(), distance); + Distances[zone->getZoneId()][DistancesTableSize[zone->getZoneId()]] = zone->getDistance(); + DistancesTableSize[zone->getZoneId()]++; } else { for (i = 1; i < DISTANCES_ARRAY_SIZE; i++) - Distances[zone][i - 1] = Distances[zone][i]; - Distances[zone][DISTANCES_ARRAY_SIZE - 1] = distance; - ESP_LOGD(SETUP, "Distances[%d][DISTANCES_ARRAY_SIZE - 1] = %d", zone, Distances[zone][DISTANCES_ARRAY_SIZE - 1]); + Distances[zone->getZoneId()][i - 1] = Distances[zone->getZoneId()][i]; + Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1] = distance; + ESP_LOGD(SETUP, "Distances[%d][DISTANCES_ARRAY_SIZE - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1]); } - ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone, Distances[zone][0]); - ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone, Distances[zone][1]); + ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone, Distances[zone->getZoneId()][0]); + ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone, Distances[zone->getZoneId()][1]); // pick up the min distance - MinDistance = Distances[zone][0]; - if (DistancesTableSize[zone] >= 2) + MinDistance = Distances[zone->getZoneId()][0]; + if (DistancesTableSize[zone->getZoneId()] >= 2) { - for (i = 1; i < DistancesTableSize[zone]; i++) + for (i = 1; i < DistancesTableSize[zone->getZoneId()]; i++) { - if (Distances[zone][i] < MinDistance) - MinDistance = Distances[zone][i]; + if (Distances[zone->getZoneId()][i] < MinDistance) + MinDistance = Distances[zone->getZoneId()][i]; } } distance = MinDistance; // PathTrack algorithm - if (distance < DIST_THRESHOLD_MAX[zone] && distance > DIST_THRESHOLD_MIN[zone]) + if (distance < DIST_THRESHOLD_MAX[zone->getZoneId()] && distance > DIST_THRESHOLD_MIN[zone->getZoneId()]) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; @@ -210,7 +202,7 @@ namespace esphome } // left zone - if (zone == LEFT) + if (zone->getZoneId() == LEFT) { if (CurrentZoneStatus != LeftPreviousStatus) { @@ -409,7 +401,6 @@ namespace esphome zone0->setRoiCenter(center[0]); zone1->setRoiCenter(center[1]); // we will now repeat the calculations necessary to define the thresholds with the updated zones - zone = 0; int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; sensor_status += distanceSensor.StopRanging(); @@ -595,8 +586,6 @@ namespace esphome zone0->setRoiCenter(center[0]); zone1->setRoiCenter(center[1]); - zone = 0; - int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; sensor_status += distanceSensor.SetROI(zone0->getRoiWidth(), zone0->getRoiHeight()); diff --git a/components/roode/roode.h b/components/roode/roode.h index 0dce8381..b9d36e5e 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -27,7 +27,6 @@ namespace esphome ##### CALIBRATION ##### */ static int center[2] = {0, 0}; /* center of the two zones */ - static int zone = 0; /* Use the VL53L1X_SetTimingBudget function to set the TB in milliseconds. The TB values available are [15, 20, @@ -94,7 +93,7 @@ namespace esphome void set_entry_exit_event_text_sensor(text_sensor::TextSensor *entry_exit_event_sensor_) { entry_exit_event_sensor = entry_exit_event_sensor_; } void set_sensor_mode(int sensor_mode_) { sensor_mode = sensor_mode_; } uint16_t getAlternatingZoneDistances(); - void doPathTracking(uint16_t zoneDistance, uint8_t zone); + void doPathTracking(Zone *zone); void recalibration(); bool handleSensorStatus(); @@ -112,6 +111,7 @@ namespace esphome VL53L1X_ULD distanceSensor; Zone *zone0; Zone *zone1; + Zone *current_zone; sensor::Sensor *distance_zone0; sensor::Sensor *distance_zone1; number::Number *people_counter; diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index df84312d..222e62f7 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -14,7 +14,7 @@ namespace esphome this->roi_center = roi_center; } - uint16_t Zone::getNewDistance(VL53L1X_ULD &distanceSensor) + uint16_t Zone::readDistance(VL53L1X_ULD &distanceSensor) { last_sensor_status = sensor_status; diff --git a/components/roode/zone.h b/components/roode/zone.h index 9e3d297d..46340fa1 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -9,11 +9,22 @@ namespace esphome { namespace roode { + struct ROI + { + uint16_t width; + uint16_t height; + uint16_t center; + }; + struct Threshold + { + uint16_t min; + uint16_t max; + }; class Zone { public: Zone(int roi_width, int roi_height, int roi_center); - uint16_t getNewDistance(VL53L1X_ULD &distanceSensor); + uint16_t readDistance(VL53L1X_ULD &distanceSensor); uint16_t calibrateThreshold(); uint16_t calibrateRoi(); uint16_t getMinThreshold(); From 2d4cc555af939953171fe41a85d25f9162f25f52 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 19:01:20 +0100 Subject: [PATCH 06/54] Implement proper ROI settings --- components/roode/roode.cpp | 28 ++++++++-------------------- components/roode/zone.cpp | 30 +++++++++++++++++++----------- components/roode/zone.h | 9 ++++++--- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 1a08b784..4253b617 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -340,10 +340,8 @@ namespace esphome int ROI_size = min(8, max(4, function_of_the_distance)); Roode::roi_width_ = ROI_size; Roode::roi_height_ = ROI_size * 2; - zone0->setRoiWidth(roi_width_); - zone0->setRoiHeight(roi_height_); - zone1->setRoiWidth(roi_width_); - zone1->setRoiHeight(roi_height_); + zone0->updateRoi(roi_width_, roi_height_, center[0]); + zone1->updateRoi(roi_width_, roi_height_, center[1]); // now we set the position of the center of the two zones if (advised_sensor_orientation_) { @@ -403,20 +401,15 @@ namespace esphome // we will now repeat the calculations necessary to define the thresholds with the updated zones int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; - sensor_status += distanceSensor.StopRanging(); - sensor_status += distanceSensor.SetROI(zone0->getRoiWidth(), zone0->getRoiHeight()); - sensor_status += distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - sensor_status += distanceSensor.StartRanging(); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Error in calibration: %d", sensor_status); - } + distanceSensor.SetInterMeasurementInMs(delay_between_measurements); + current_zone = zone0; for (int i = 0; i < number_attempts; i++) { values_zone_0[i] = getAlternatingZoneDistances(); values_zone_1[i] = getAlternatingZoneDistances(); } + optimized_zone_0 = getOptimizedValues(values_zone_0, getSum(values_zone_0, number_attempts), number_attempts); optimized_zone_1 = getOptimizedValues(values_zone_1, getSum(values_zone_1, number_attempts), number_attempts); } @@ -579,18 +572,13 @@ namespace esphome roi_height_ = roi_width_; } - zone0->setRoiWidth(roi_width_); - zone0->setRoiHeight(roi_height_); - zone1->setRoiWidth(roi_width_); - zone1->setRoiHeight(roi_height_); - zone0->setRoiCenter(center[0]); - zone1->setRoiCenter(center[1]); + zone0->updateRoi(roi_width_, roi_height_, center[0]); + zone1->updateRoi(roi_width_, roi_height_, center[1]); int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; - sensor_status += distanceSensor.SetROI(zone0->getRoiWidth(), zone0->getRoiHeight()); distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - distanceSensor.StartRanging(); + current_zone = zone0; for (int i = 0; i < number_attempts; i++) { values_zone_0[i] = getAlternatingZoneDistances(); diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 222e62f7..6ab53b3d 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -12,14 +12,15 @@ namespace esphome this->roi_width = roi_width; this->roi_height = roi_height; this->roi_center = roi_center; + this->roi = {roi_width, roi_height, roi_center}; } uint16_t Zone::readDistance(VL53L1X_ULD &distanceSensor) { last_sensor_status = sensor_status; - - sensor_status += distanceSensor.StartRanging(); + sensor_status += distanceSensor.SetROI(this->getRoiWidth(), this->getRoiHeight()); sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); + sensor_status += distanceSensor.StartRanging(); // Checking if data is available. This can also be done through the hardware interrupt. See the ReadDistanceHardwareInterrupt for an example uint8_t dataReady = false; while (!dataReady) @@ -38,6 +39,7 @@ namespace esphome } // After reading the results reset the interrupt to be able to take another measurement sensor_status += distanceSensor.ClearInterrupt(); + sensor_status += distanceSensor.StopRanging(); return distance; } uint16_t Zone::getDistance() @@ -46,27 +48,33 @@ namespace esphome } uint16_t Zone::getRoiWidth() { - return this->roi_width; + return this->roi.width; } uint16_t Zone::getRoiHeight() { - return this->roi_height; + return this->roi.height; } uint16_t Zone::getRoiCenter() { - return this->roi_center; + return this->roi.center; + } + void Zone::setRoiWidth(uint16_t new_roi_width) + { + this->roi.width = new_roi_width; } - uint16_t Zone::setRoiWidth(uint16_t new_roi_width) + void Zone::setRoiHeight(uint16_t new_roi_height) { - return this->roi_width = new_roi_width; + this->roi.height = new_roi_height; } - uint16_t Zone::setRoiHeight(uint16_t new_roi_height) + void Zone::setRoiCenter(uint16_t new_roi_center) { - return this->roi_height = new_roi_height; + this->roi.center = new_roi_center; } - uint16_t Zone::setRoiCenter(uint16_t new_roi_center) + void Zone::updateRoi(uint16_t new_width, uint16_t new_height, uint16_t new_center) { - return this->roi_center = new_roi_center; + this->roi.width = new_width; + this->roi.height = new_height; + this->roi.center = new_center; } uint8_t Zone::getZoneId() { diff --git a/components/roode/zone.h b/components/roode/zone.h index 46340fa1..a2734c28 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -32,9 +32,10 @@ namespace esphome uint16_t getRoiWidth(); uint16_t getRoiHeight(); uint16_t getRoiCenter(); - uint16_t setRoiWidth(uint16_t new_roi_width); - uint16_t setRoiHeight(uint16_t new_roi_height); - uint16_t setRoiCenter(uint16_t new_roi_center); + void setRoiWidth(uint16_t new_roi_width); + void setRoiHeight(uint16_t new_roi_height); + void setRoiCenter(uint16_t new_roi_center); + void updateRoi(uint16_t new_width, uint16_t new_height, uint16_t new_center); uint8_t getZoneId(); uint16_t getDistance(); bool handleSensorStatus(); @@ -43,6 +44,8 @@ namespace esphome int getSum(int *values, int size); int getOptimizedValues(int *values, int sum, int size); void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); + ROI roi; + Threshold threshold; uint16_t roi_width; uint16_t roi_height; uint16_t roi_center; From 69d1743f1c9ac270c0080cc333063a7292022cfa Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 19:21:49 +0100 Subject: [PATCH 07/54] Add fullset entry and exit zones with correct defaults --- components/roode/roode.cpp | 44 ++++++++++++++++++-------------------- components/roode/roode.h | 2 +- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 4253b617..0a6bd365 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -59,7 +59,6 @@ namespace esphome } } } - createZones(); ESP_LOGI(SETUP, "Using sampling with sampling size: %d", DISTANCES_ARRAY_SIZE); if (invert_direction_) { @@ -67,6 +66,9 @@ namespace esphome LEFT = 1; RIGHT = 0; } + ESP_LOGI(SETUP, "Creating entry and exit zones."); + createEntryAndExitZone(); + if (calibration_active_) { calibration(distanceSensor); @@ -75,9 +77,6 @@ namespace esphome if (manual_active_) { ESP_LOGI(SETUP, "Manual sensor configuration"); - center[0] = 167; - center[1] = 231; - distanceSensor.SetROI(Roode::roi_width_, Roode::roi_height_); sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); DIST_THRESHOLD_MAX[0] = Roode::manual_threshold_; DIST_THRESHOLD_MAX[1] = Roode::manual_threshold_; @@ -136,10 +135,23 @@ namespace esphome return check_status; } - void Roode::createZones() + void Roode::createEntryAndExitZone() { - zone0 = new Zone(Roode::roi_width_, Roode::roi_height_, center[0]); - zone1 = new Zone(Roode::roi_width_, Roode::roi_height_, center[1]); + if (advised_sensor_orientation_) + { + center[0] = 167; + center[1] = 231; + zone0 = new Zone(Roode::roi_width_, Roode::roi_height_, center[0]); + zone1 = new Zone(Roode::roi_width_, Roode::roi_height_, center[1]); + } + else + { + center[0] = 195; + center[1] = 60; + zone1 = new Zone(Roode::roi_height_, Roode::roi_width_, center[1]); + zone0 = new Zone(Roode::roi_height_, Roode::roi_width_, center[0]); + } + current_zone = zone0; } uint16_t Roode::getAlternatingZoneDistances() @@ -176,8 +188,8 @@ namespace esphome Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1] = distance; ESP_LOGD(SETUP, "Distances[%d][DISTANCES_ARRAY_SIZE - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1]); } - ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone, Distances[zone->getZoneId()][0]); - ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone, Distances[zone->getZoneId()][1]); + ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][0]); + ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][1]); // pick up the min distance MinDistance = Distances[zone->getZoneId()][0]; if (DistancesTableSize[zone->getZoneId()] >= 2) @@ -557,20 +569,6 @@ namespace esphome { ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, sensor_status); } - if (advised_sensor_orientation_) - { - center[0] = 167; - center[1] = 231; - } - else - { - center[0] = 195; - center[1] = 60; - uint16_t roi_width_temp = roi_width_; - uint16_t roi_height_temp = roi_height_; - roi_width_ = roi_height_; - roi_height_ = roi_width_; - } zone0->updateRoi(roi_width_, roi_height_, center[0]); zone1->updateRoi(roi_width_, roi_height_, center[1]); diff --git a/components/roode/roode.h b/components/roode/roode.h index b9d36e5e..aa77aef5 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -126,7 +126,7 @@ namespace esphome text_sensor::TextSensor *version_sensor; text_sensor::TextSensor *entry_exit_event_sensor; - void createZones(); + void createEntryAndExitZone(); void roi_calibration(VL53L1X_ULD distanceSensor, int optimized_zone_0, int optimized_zone_1); void calibration(VL53L1X_ULD distanceSensor); void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); From e4c08e2c76eb08dbeba9f9f294dd408d4118df39 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 20:33:25 +0100 Subject: [PATCH 08/54] Add zone user configuration --- README.md | 4 +- components/roode/__init__.py | 64 +++++++++++++++++++--- components/roode/roode.cpp | 83 +++++++++++++++------------- components/roode/roode.h | 52 ++++++++++-------- components/roode/sensor.py | 102 +++++++++++++++++++++-------------- peopleCounter.yaml | 8 +-- peopleCounter32.yaml | 8 +-- peopleCounterDev.yaml | 35 ++++++++---- 8 files changed, 227 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 6889c2b5..ac8c16ce 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,9 @@ sensor: name: $friendly_name distance filters: - delta: 100.0 - threshold_zone0: + threshold_entry: name: $friendly_name Zone 0 - threshold_zone1: + threshold_exit: name: $friendly_name Zone 1 roi_height: name: $friendly_name ROI height diff --git a/components/roode/__init__.py b/components/roode/__init__.py index e6954753..d6c5f713 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_OR, ) @@ -35,18 +36,16 @@ CONF_SAMPLING_SIZE = "size" CONF_ROI = "roi" CONF_ROI_ACTIVE = "roi_active" +CONF_ZONES = "zones" +CONF_ENTRY_ZONE = "entry" +CONF_EXIT_ZONE = "exit" CONF_SENSOR_OFFSET_CALIBRATION = "sensor_offset_calibration" CONF_SENSOR_XTALK_CALIBRATION = "sensor_xtalk_calibration" -TYPES = [ - CONF_INVERT_DIRECTION, - CONF_ADVISED_SENSOR_ORIENTATION, - CONF_I2C_ADDRESS, -] +TYPES = [CONF_ADVISED_SENSOR_ORIENTATION, CONF_I2C_ADDRESS] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Roode), - cv.Optional(CONF_INVERT_DIRECTION, default="false"): cv.boolean, cv.Optional(CONF_ADVISED_SENSOR_ORIENTATION, default="true"): cv.boolean, cv.Optional(CONF_I2C_ADDRESS, default=0x29): cv.uint8_t, cv.Optional(CONF_SAMPLING, default=2): cv.Any( @@ -104,12 +103,45 @@ cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range(min=4, max=16), } ), + cv.Optional(CONF_ZONES): cv.Schema( + { + cv.Optional(CONF_INVERT_DIRECTION, default="false"): cv.boolean, + cv.Optional(CONF_ENTRY_ZONE): cv.Schema( + { + cv.Optional(CONF_ROI): cv.Schema( + { + cv.Optional(CONF_ROI_HEIGHT, default=16): cv.int_range( + min=4, max=16 + ), + cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range( + min=4, max=16 + ), + } + ), + } + ), + cv.Optional(CONF_EXIT_ZONE): cv.Schema( + { + cv.Optional(CONF_ROI): cv.Schema( + { + cv.Optional(CONF_ROI_HEIGHT, default=16): cv.int_range( + min=4, max=16 + ), + cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range( + min=4, max=16 + ), + } + ), + } + ), + } + ), } ).extend(cv.polling_component_schema("100ms")) def validate_roi_settings(config): - roi = config.get(CONF_ROI) + roi = config.get(CONF_ZONES).get(CONF_ROI) manual = config.get(CONF_MANUAL) if CONF_CALIBRATION in config: roi_calibration = config.get(CONF_CALIBRATION).get(CONF_ROI_CALIBRATION) @@ -155,13 +187,27 @@ def setup_sampling(config, hub): cg.add(getattr(hub, f"set_sampling_{CONF_SAMPLING_SIZE}")(sampling[key])) +def setup_zones(config, hub): + zones = config[CONF_ZONES] + for zone in zones: + if CONF_ENTRY_ZONE in zone or CONF_EXIT_ZONE in zone: + roi = zones[zone][CONF_ROI] + for key in roi: + cg.add(getattr(hub, f"set_{zone}_{key}")(roi[key])) + else: + cg.add(getattr(hub, f"set_{zone}")(zones[zone])) + + if CONF_INVERT_DIRECTION in zones: + cg.add(getattr(hub, "set_invert_direction")(zones[CONF_INVERT_DIRECTION])) + + async def to_code(config): hub = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(hub, config) cg.add_library("Wire", None) cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") - validate_roi_settings(config) + # validate_roi_settings(config) for key in TYPES: await setup_conf(config, key, hub) @@ -171,3 +217,5 @@ async def to_code(config): setup_calibration_mode(config, hub) if CONF_SAMPLING in config: setup_sampling(config, hub) + if CONF_ZONES in config: + setup_zones(config, hub) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 0a6bd365..dc549928 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -88,13 +88,13 @@ namespace esphome void Roode::update() { - if (distance_zone0 != nullptr) + if (distance_entry != nullptr) { - distance_zone0->publish_state(zone0->getDistance()); + distance_entry->publish_state(entry->getDistance()); } - if (distance_zone1 != nullptr) + if (distance_exit != nullptr) { - distance_zone1->publish_state(zone1->getDistance()); + distance_exit->publish_state(exit->getDistance()); } } @@ -104,8 +104,8 @@ namespace esphome distance = getAlternatingZoneDistances(); doPathTracking(this->current_zone); handleSensorStatus(); - this->current_zone = this->current_zone == this->zone0 ? this->zone1 : this->zone0; - // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", zone0->getDistance(Roode::distanceSensor, Roode::sensor_status), zone1->getDistance(Roode::distanceSensor, Roode::sensor_status)); + this->current_zone = this->current_zone == this->entry ? this->exit : this->entry; + // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", entry->getDistance(Roode::distanceSensor, Roode::sensor_status), exit->getDistance(Roode::distanceSensor, Roode::sensor_status)); // unsigned long end = micros(); // unsigned long delta = end - start; // ESP_LOGI("Roode loop", "loop took %lu microseconds", delta); @@ -141,18 +141,18 @@ namespace esphome { center[0] = 167; center[1] = 231; - zone0 = new Zone(Roode::roi_width_, Roode::roi_height_, center[0]); - zone1 = new Zone(Roode::roi_width_, Roode::roi_height_, center[1]); + entry = new Zone(entry_roi_width, entry_roi_height, center[0]); + exit = new Zone(exit_roi_width, exit_roi_height, center[1]); } else { center[0] = 195; center[1] = 60; - zone1 = new Zone(Roode::roi_height_, Roode::roi_width_, center[1]); - zone0 = new Zone(Roode::roi_height_, Roode::roi_width_, center[0]); + entry = new Zone(entry_roi_width, entry_roi_height, center[0]); + exit = new Zone(exit_roi_width, exit_roi_height, center[1]); } - current_zone = zone0; + current_zone = entry; } uint16_t Roode::getAlternatingZoneDistances() { @@ -350,10 +350,9 @@ namespace esphome // the value of the average distance is used for computing the optimal size of the ROI and consequently also the center of the two zones int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(optimized_zone_0, optimized_zone_1) / 1000))); int ROI_size = min(8, max(4, function_of_the_distance)); - Roode::roi_width_ = ROI_size; - Roode::roi_height_ = ROI_size * 2; - zone0->updateRoi(roi_width_, roi_height_, center[0]); - zone1->updateRoi(roi_width_, roi_height_, center[1]); + + entry->updateRoi(ROI_size, ROI_size * 2, center[0]); + exit->updateRoi(ROI_size, ROI_size * 2, center[1]); // now we set the position of the center of the two zones if (advised_sensor_orientation_) { @@ -408,14 +407,14 @@ namespace esphome break; } } - zone0->setRoiCenter(center[0]); - zone1->setRoiCenter(center[1]); + entry->setRoiCenter(center[0]); + exit->setRoiCenter(center[1]); // we will now repeat the calculations necessary to define the thresholds with the updated zones int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - current_zone = zone0; + current_zone = entry; for (int i = 0; i < number_attempts; i++) { values_zone_0[i] = getAlternatingZoneDistances(); @@ -569,14 +568,13 @@ namespace esphome { ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, sensor_status); } - - zone0->updateRoi(roi_width_, roi_height_, center[0]); - zone1->updateRoi(roi_width_, roi_height_, center[1]); + entry->updateRoi(entry_roi_width, entry_roi_height, center[0]); + exit->updateRoi(exit_roi_width, exit_roi_height, center[1]); int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - current_zone = zone0; + current_zone = entry; for (int i = 0; i < number_attempts; i++) { values_zone_0[i] = getAlternatingZoneDistances(); @@ -614,39 +612,48 @@ namespace esphome { if (isMax) { - ESP_LOGI(SETUP, "Max threshold zone0: %dmm", DIST_THRESHOLD_ARR[0]); - ESP_LOGI(SETUP, "Max threshold zone1: %dmm", DIST_THRESHOLD_ARR[1]); - if (max_threshold_zone0_sensor != nullptr) + ESP_LOGI(SETUP, "Max threshold entry: %dmm", DIST_THRESHOLD_ARR[0]); + ESP_LOGI(SETUP, "Max threshold exit: %dmm", DIST_THRESHOLD_ARR[1]); + if (max_threshold_entry_sensor != nullptr) { - max_threshold_zone0_sensor->publish_state(DIST_THRESHOLD_ARR[0]); + max_threshold_entry_sensor->publish_state(DIST_THRESHOLD_ARR[0]); } - if (max_threshold_zone1_sensor != nullptr) + if (max_threshold_exit_sensor != nullptr) { - max_threshold_zone1_sensor->publish_state(DIST_THRESHOLD_ARR[1]); + max_threshold_exit_sensor->publish_state(DIST_THRESHOLD_ARR[1]); } } else { - ESP_LOGI(SETUP, "Min threshold zone0: %dmm", DIST_THRESHOLD_ARR[0]); - ESP_LOGI(SETUP, "Min threshold zone1: %dmm", DIST_THRESHOLD_ARR[1]); - if (min_threshold_zone0_sensor != nullptr) + ESP_LOGI(SETUP, "Min threshold entry: %dmm", DIST_THRESHOLD_ARR[0]); + ESP_LOGI(SETUP, "Min threshold exit: %dmm", DIST_THRESHOLD_ARR[1]); + if (min_threshold_entry_sensor != nullptr) { - min_threshold_zone0_sensor->publish_state(DIST_THRESHOLD_ARR[0]); + min_threshold_entry_sensor->publish_state(DIST_THRESHOLD_ARR[0]); } - if (min_threshold_zone1_sensor != nullptr) + if (min_threshold_exit_sensor != nullptr) { - min_threshold_zone1_sensor->publish_state(DIST_THRESHOLD_ARR[1]); + min_threshold_exit_sensor->publish_state(DIST_THRESHOLD_ARR[1]); } } - if (roi_height_sensor != nullptr) + if (entry_roi_height_sensor != nullptr) + { + entry_roi_height_sensor->publish_state(entry->getRoiHeight()); + } + if (entry_roi_width_sensor != nullptr) + { + entry_roi_width_sensor->publish_state(entry->getRoiWidth()); + } + + if (exit_roi_height_sensor != nullptr) { - roi_height_sensor->publish_state(roi_height_); + exit_roi_height_sensor->publish_state(exit->getRoiHeight()); } - if (roi_width_sensor != nullptr) + if (exit_roi_width_sensor != nullptr) { - roi_width_sensor->publish_state(roi_width_); + exit_roi_width_sensor->publish_state(exit->getRoiWidth()); } } } diff --git a/components/roode/roode.h b/components/roode/roode.h index aa77aef5..53879f4a 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -71,22 +71,26 @@ namespace esphome void set_manual_threshold(int val) { manual_threshold_ = val; } void set_max_threshold_percentage(int val) { max_threshold_percentage_ = val; } void set_min_threshold_percentage(int val) { min_threshold_percentage_ = val; } - void set_roi_height(int height) { roi_height_ = height; } - void set_roi_width(int width) { roi_width_ = width; } + void set_entry_roi_height(int height) { entry_roi_height = height; } + void set_entry_roi_width(int width) { entry_roi_width = width; } + void set_exit_roi_height(int height) { exit_roi_height = height; } + void set_exit_roi_width(int width) { exit_roi_width = width; } void set_i2c_address(uint8_t address) { this->address_ = address; } void set_invert_direction(bool dir) { invert_direction_ = dir; } void set_restore_values(bool val) { restore_values_ = val; } void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } void set_sampling_size(uint8_t size) { DISTANCES_ARRAY_SIZE = size; } - void set_distance_zone0(sensor::Sensor *distance_zone0_) { distance_zone0 = distance_zone0_; } - void set_distance_zone1(sensor::Sensor *distance_zone1_) { distance_zone1 = distance_zone1_; } + void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } + void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } void set_people_counter(number::Number *counter) { this->people_counter = counter; } - void set_max_threshold_zone0_sensor(sensor::Sensor *max_threshold_zone0_sensor_) { max_threshold_zone0_sensor = max_threshold_zone0_sensor_; } - void set_max_threshold_zone1_sensor(sensor::Sensor *max_threshold_zone1_sensor_) { max_threshold_zone1_sensor = max_threshold_zone1_sensor_; } - void set_min_threshold_zone0_sensor(sensor::Sensor *min_threshold_zone0_sensor_) { min_threshold_zone0_sensor = min_threshold_zone0_sensor_; } - void set_min_threshold_zone1_sensor(sensor::Sensor *min_threshold_zone1_sensor_) { min_threshold_zone1_sensor = min_threshold_zone1_sensor_; } - void set_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { roi_height_sensor = roi_height_sensor_; } - void set_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { roi_width_sensor = roi_width_sensor_; } + void set_max_threshold_entry_sensor(sensor::Sensor *max_threshold_entry_sensor_) { max_threshold_entry_sensor = max_threshold_entry_sensor_; } + void set_max_threshold_exit_sensor(sensor::Sensor *max_threshold_exit_sensor_) { max_threshold_exit_sensor = max_threshold_exit_sensor_; } + void set_min_threshold_entry_sensor(sensor::Sensor *min_threshold_entry_sensor_) { min_threshold_entry_sensor = min_threshold_entry_sensor_; } + void set_min_threshold_exit_sensor(sensor::Sensor *min_threshold_exit_sensor_) { min_threshold_exit_sensor = min_threshold_exit_sensor_; } + void set_entry_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { entry_roi_height_sensor = roi_height_sensor_; } + void set_entry_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { entry_roi_width_sensor = roi_width_sensor_; } + void set_exit_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { exit_roi_height_sensor = roi_height_sensor_; } + void set_exit_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { exit_roi_width_sensor = roi_width_sensor_; } void set_sensor_status_sensor(sensor::Sensor *status_sensor_) { status_sensor = status_sensor_; } void set_presence_sensor_binary_sensor(binary_sensor::BinarySensor *presence_sensor_) { presence_sensor = presence_sensor_; } void set_version_text_sensor(text_sensor::TextSensor *version_sensor_) { version_sensor = version_sensor_; } @@ -102,25 +106,29 @@ namespace esphome ERangeStatus rangeStatus; int DIST_THRESHOLD_MAX[2] = {0, 0}; // max treshold of the two zones int DIST_THRESHOLD_MIN[2] = {0, 0}; // min treshold of the two zones - int roi_width_{6}; // width of the ROI - int roi_height_{16}; // height of the ROI + int entry_roi_width{6}; // width of the ROI + int entry_roi_height{16}; // height of the ROI + int exit_roi_width{6}; // width of the ROI + int exit_roi_height{16}; // height of the ROI uint16_t peopleCounter{0}; Configuration sensorConfiguration; protected: VL53L1X_ULD distanceSensor; - Zone *zone0; - Zone *zone1; + Zone *entry; + Zone *exit; Zone *current_zone; - sensor::Sensor *distance_zone0; - sensor::Sensor *distance_zone1; + sensor::Sensor *distance_entry; + sensor::Sensor *distance_exit; number::Number *people_counter; - sensor::Sensor *max_threshold_zone0_sensor; - sensor::Sensor *max_threshold_zone1_sensor; - sensor::Sensor *min_threshold_zone0_sensor; - sensor::Sensor *min_threshold_zone1_sensor; - sensor::Sensor *roi_height_sensor; - sensor::Sensor *roi_width_sensor; + sensor::Sensor *max_threshold_entry_sensor; + sensor::Sensor *max_threshold_exit_sensor; + sensor::Sensor *min_threshold_entry_sensor; + sensor::Sensor *min_threshold_exit_sensor; + sensor::Sensor *exit_roi_height_sensor; + sensor::Sensor *exit_roi_width_sensor; + sensor::Sensor *entry_roi_height_sensor; + sensor::Sensor *entry_roi_width_sensor; sensor::Sensor *status_sensor; binary_sensor::BinarySensor *presence_sensor; text_sensor::TextSensor *version_sensor; diff --git a/components/roode/sensor.py b/components/roode/sensor.py index 4333254f..cb93b2c6 100644 --- a/components/roode/sensor.py +++ b/components/roode/sensor.py @@ -13,68 +13,84 @@ DEPENDENCIES = ["roode"] -CONF_DISTANCE_ZONE0 = "distance_zone0" -CONF_DISTANCE_ZONE1 = "distance_zone1" -CONF_MAX_THRESHOLD_ZONE0 = "max_threshold_zone0" -CONF_MAX_THRESHOLD_ZONE1 = "max_threshold_zone1" -CONF_MIN_THRESHOLD_ZONE0 = "min_threshold_zone0" -CONF_MIN_THRESHOLD_ZONE1 = "min_threshold_zone1" -CONF_ROI_HEIGHT = "roi_height" -CONF_ROI_WIDTH = "roi_width" +CONF_DISTANCE_entry = "distance_entry" +CONF_DISTANCE_exit = "distance_exit" +CONF_MAX_THRESHOLD_entry = "max_threshold_entry" +CONF_MAX_THRESHOLD_exit = "max_threshold_exit" +CONF_MIN_THRESHOLD_entry = "min_threshold_entry" +CONF_MIN_THRESHOLD_exit = "min_threshold_exit" +CONF_ROI_HEIGHT_entry = "roi_height_entry" +CONF_ROI_WIDTH_entry = "roi_width_entry" +CONF_ROI_HEIGHT_exit = "roi_height_exit" +CONF_ROI_WIDTH_exit = "roi_width_exit" SENSOR_STATUS = "sensor_status" CONFIG_SCHEMA = sensor.sensor_schema().extend( { - cv.Optional(CONF_DISTANCE_ZONE0): sensor.sensor_schema( + cv.Optional(CONF_DISTANCE_entry): sensor.sensor_schema( icon=ICON_RULER, unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_DISTANCE_ZONE1): sensor.sensor_schema( + cv.Optional(CONF_DISTANCE_exit): sensor.sensor_schema( icon=ICON_RULER, unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_MAX_THRESHOLD_ZONE0): sensor.sensor_schema( + cv.Optional(CONF_MAX_THRESHOLD_entry): sensor.sensor_schema( icon="mdi:map-marker-distance", unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_MAX_THRESHOLD_ZONE1): sensor.sensor_schema( + cv.Optional(CONF_MAX_THRESHOLD_exit): sensor.sensor_schema( icon="mdi:map-marker-distance", unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_MIN_THRESHOLD_ZONE0): sensor.sensor_schema( + cv.Optional(CONF_MIN_THRESHOLD_entry): sensor.sensor_schema( icon="mdi:map-marker-distance", unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_MIN_THRESHOLD_ZONE1): sensor.sensor_schema( + cv.Optional(CONF_MIN_THRESHOLD_exit): sensor.sensor_schema( icon="mdi:map-marker-distance", unit_of_measurement="mm", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_ROI_HEIGHT): sensor.sensor_schema( + cv.Optional(CONF_ROI_HEIGHT_entry): sensor.sensor_schema( icon="mdi:table-row-height", unit_of_measurement="px", accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_ROI_WIDTH): sensor.sensor_schema( + cv.Optional(CONF_ROI_WIDTH_entry): sensor.sensor_schema( + icon="mdi:table-column-width", + unit_of_measurement="px", + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ROI_HEIGHT_exit): sensor.sensor_schema( + icon="mdi:table-row-height", + unit_of_measurement="px", + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ROI_WIDTH_exit): sensor.sensor_schema( icon="mdi:table-column-width", unit_of_measurement="px", accuracy_decimals=0, @@ -93,30 +109,36 @@ async def to_code(config): var = await cg.get_variable(config[CONF_ROODE_ID]) - if CONF_DISTANCE_ZONE0 in config: - distance = await sensor.new_sensor(config[CONF_DISTANCE_ZONE0]) - cg.add(var.set_distance_zone0(distance)) - if CONF_DISTANCE_ZONE1 in config: - distance = await sensor.new_sensor(config[CONF_DISTANCE_ZONE1]) - cg.add(var.set_distance_zone1(distance)) - if CONF_MAX_THRESHOLD_ZONE0 in config: - count = await sensor.new_sensor(config[CONF_MAX_THRESHOLD_ZONE0]) - cg.add(var.set_max_threshold_zone0_sensor(count)) - if CONF_MAX_THRESHOLD_ZONE1 in config: - count = await sensor.new_sensor(config[CONF_MAX_THRESHOLD_ZONE1]) - cg.add(var.set_max_threshold_zone1_sensor(count)) - if CONF_MIN_THRESHOLD_ZONE0 in config: - count = await sensor.new_sensor(config[CONF_MIN_THRESHOLD_ZONE0]) - cg.add(var.set_min_threshold_zone0_sensor(count)) - if CONF_MIN_THRESHOLD_ZONE1 in config: - count = await sensor.new_sensor(config[CONF_MIN_THRESHOLD_ZONE1]) - cg.add(var.set_min_threshold_zone1_sensor(count)) - if CONF_ROI_HEIGHT in config: - count = await sensor.new_sensor(config[CONF_ROI_HEIGHT]) - cg.add(var.set_roi_height_sensor(count)) - if CONF_ROI_WIDTH in config: - count = await sensor.new_sensor(config[CONF_ROI_WIDTH]) - cg.add(var.set_roi_width_sensor(count)) + if CONF_DISTANCE_entry in config: + distance = await sensor.new_sensor(config[CONF_DISTANCE_entry]) + cg.add(var.set_distance_entry(distance)) + if CONF_DISTANCE_exit in config: + distance = await sensor.new_sensor(config[CONF_DISTANCE_exit]) + cg.add(var.set_distance_exit(distance)) + if CONF_MAX_THRESHOLD_entry in config: + count = await sensor.new_sensor(config[CONF_MAX_THRESHOLD_entry]) + cg.add(var.set_max_threshold_entry_sensor(count)) + if CONF_MAX_THRESHOLD_exit in config: + count = await sensor.new_sensor(config[CONF_MAX_THRESHOLD_exit]) + cg.add(var.set_max_threshold_exit_sensor(count)) + if CONF_MIN_THRESHOLD_entry in config: + count = await sensor.new_sensor(config[CONF_MIN_THRESHOLD_entry]) + cg.add(var.set_min_threshold_entry_sensor(count)) + if CONF_MIN_THRESHOLD_exit in config: + count = await sensor.new_sensor(config[CONF_MIN_THRESHOLD_exit]) + cg.add(var.set_min_threshold_exit_sensor(count)) + if CONF_ROI_HEIGHT_entry in config: + count = await sensor.new_sensor(config[CONF_ROI_HEIGHT_entry]) + cg.add(var.set_entry_roi_height_sensor(count)) + if CONF_ROI_WIDTH_entry in config: + count = await sensor.new_sensor(config[CONF_ROI_WIDTH_entry]) + cg.add(var.set_entry_roi_width_sensor(count)) + if CONF_ROI_HEIGHT_exit in config: + count = await sensor.new_sensor(config[CONF_ROI_HEIGHT_exit]) + cg.add(var.set_exit_roi_height_sensor(count)) + if CONF_ROI_WIDTH_exit in config: + count = await sensor.new_sensor(config[CONF_ROI_WIDTH_exit]) + cg.add(var.set_exit_roi_width_sensor(count)) if SENSOR_STATUS in config: count = await sensor.new_sensor(config[SENSOR_STATUS]) cg.add(var.set_sensor_status_sensor(count)) diff --git a/peopleCounter.yaml b/peopleCounter.yaml index 37e79865..dffc2f72 100644 --- a/peopleCounter.yaml +++ b/peopleCounter.yaml @@ -103,13 +103,13 @@ sensor: name: $friendly_name distance filters: - delta: 10.0 - max_threshold_zone0: + max_threshold_entry: name: $friendly_name max zone 0 - max_threshold_zone1: + max_threshold_exit: name: $friendly_name max zone 1 - min_threshold_zone0: + min_threshold_entry: name: $friendly_name min zone 0 - min_threshold_zone1: + min_threshold_exit: name: $friendly_name min zone 1 roi_height: name: $friendly_name ROI height diff --git a/peopleCounter32.yaml b/peopleCounter32.yaml index 8dcf5aaf..3b7cd992 100644 --- a/peopleCounter32.yaml +++ b/peopleCounter32.yaml @@ -106,13 +106,13 @@ sensor: name: $friendly_name distance filters: - delta: 10.0 - max_threshold_zone0: + max_threshold_entry: name: $friendly_name max zone 0 - max_threshold_zone1: + max_threshold_exit: name: $friendly_name max zone 1 - min_threshold_zone0: + min_threshold_entry: name: $friendly_name min zone 0 - min_threshold_zone1: + min_threshold_exit: name: $friendly_name min zone 1 roi_height: name: $friendly_name ROI height diff --git a/peopleCounterDev.yaml b/peopleCounterDev.yaml index d2049929..2be644d2 100644 --- a/peopleCounterDev.yaml +++ b/peopleCounterDev.yaml @@ -67,7 +67,16 @@ roode: # manual_threshold: 1280 # timing_budget: 200 # sampling: 3 - invert_direction: true + zones: + invert_direction: true + entry: + roi: + roi_height: 12 + roi_width: 6 + exit: + roi: + roi_height: 12 + roi_width: 6 number: - platform: roode @@ -87,22 +96,26 @@ binary_sensor: sensor: - platform: roode id: hallway - distance_zone0: + distance_entry: name: $friendly_name distance zone 0 - distance_zone1: + distance_exit: name: $friendly_name distance zone 1 - max_threshold_zone0: + max_threshold_entry: name: $friendly_name max zone 0 - max_threshold_zone1: + max_threshold_exit: name: $friendly_name max zone 1 - min_threshold_zone0: + min_threshold_entry: name: $friendly_name min zone 0 - min_threshold_zone1: + min_threshold_exit: name: $friendly_name min zone 1 - roi_height: - name: $friendly_name ROI height - roi_width: - name: $friendly_name ROI width + 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 From 99d68a09340c78ad8b90cdf4ccbbe64cea99a598 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 21:18:50 +0100 Subject: [PATCH 09/54] Fix validation --- components/roode/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index d6c5f713..92a08a2b 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -141,7 +141,7 @@ def validate_roi_settings(config): - roi = config.get(CONF_ZONES).get(CONF_ROI) + roi = config.get(CONF_ZONES) manual = config.get(CONF_MANUAL) if CONF_CALIBRATION in config: roi_calibration = config.get(CONF_CALIBRATION).get(CONF_ROI_CALIBRATION) @@ -207,7 +207,7 @@ async def to_code(config): cg.add_library("Wire", None) cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") - # validate_roi_settings(config) + validate_roi_settings(config) for key in TYPES: await setup_conf(config, key, hub) From d05c1c93cf027687e702c9da1004c0e2298b1456 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Tue, 4 Jan 2022 21:19:31 +0100 Subject: [PATCH 10/54] Ommit LEFT/RIGHT constants --- components/roode/roode.cpp | 9 +-------- components/roode/roode.h | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index dc549928..947f51bd 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -60,12 +60,6 @@ namespace esphome } } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", DISTANCES_ARRAY_SIZE); - if (invert_direction_) - { - ESP_LOGI(SETUP, "Inverting direction"); - LEFT = 1; - RIGHT = 0; - } ESP_LOGI(SETUP, "Creating entry and exit zones."); createEntryAndExitZone(); @@ -214,7 +208,7 @@ namespace esphome } // left zone - if (zone->getZoneId() == LEFT) + if (zone == (this->invert_direction_ ? this->exit : this->entry)) { if (CurrentZoneStatus != LeftPreviousStatus) { @@ -356,7 +350,6 @@ namespace esphome // now we set the position of the center of the two zones if (advised_sensor_orientation_) { - switch (ROI_size) { case 4: diff --git a/components/roode/roode.h b/components/roode/roode.h index 53879f4a..5a525ef3 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -18,8 +18,6 @@ namespace esphome #define SOMEONE 1 #define VERSION "v1.4.1-beta" #define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 - static int LEFT = 0; - static int RIGHT = 1; static const char *const TAG = "Roode"; static const char *const SETUP = "Setup"; static const char *const CALIBRATION = "Calibration"; From 01e37a42408a813ae1f522bfd4facc2210e2b159 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Wed, 5 Jan 2022 18:57:40 +0100 Subject: [PATCH 11/54] Use Zone threshold --- components/roode/roode.cpp | 46 +++++++++++++++++++------------------- components/roode/roode.h | 6 ++--- components/roode/zone.cpp | 17 ++++++++++++++ components/roode/zone.h | 2 ++ 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 947f51bd..d0ef91b5 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -72,9 +72,9 @@ namespace esphome { ESP_LOGI(SETUP, "Manual sensor configuration"); sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); - DIST_THRESHOLD_MAX[0] = Roode::manual_threshold_; - DIST_THRESHOLD_MAX[1] = Roode::manual_threshold_; - publishSensorConfiguration(DIST_THRESHOLD_MAX, true); + entry->setMaxThreshold(Roode::manual_threshold_); + exit->setMaxThreshold(Roode::manual_threshold_); + publishSensorConfiguration(entry, exit, true); } distanceSensor.SetInterMeasurementInMs(delay_between_measurements); distanceSensor.StartRanging(); @@ -197,7 +197,7 @@ namespace esphome distance = MinDistance; // PathTrack algorithm - if (distance < DIST_THRESHOLD_MAX[zone->getZoneId()] && distance > DIST_THRESHOLD_MIN[zone->getZoneId()]) + if (distance < zone->getMaxThreshold() && distance > zone->getMinThreshold()) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; @@ -584,50 +584,50 @@ namespace esphome roi_calibration(distanceSensor, optimized_zone_0, optimized_zone_1); } - DIST_THRESHOLD_MAX[0] = optimized_zone_0 * max_threshold_percentage_ / 100; // they can be int values, as we are not interested in the decimal part when defining the threshold - DIST_THRESHOLD_MAX[1] = optimized_zone_1 * max_threshold_percentage_ / 100; - int hundred_threshold_zone_0 = DIST_THRESHOLD_MAX[0] / 100; - int hundred_threshold_zone_1 = DIST_THRESHOLD_MAX[1] / 100; - int unit_threshold_zone_0 = DIST_THRESHOLD_MAX[0] - 100 * hundred_threshold_zone_0; - int unit_threshold_zone_1 = DIST_THRESHOLD_MAX[1] - 100 * hundred_threshold_zone_1; - publishSensorConfiguration(DIST_THRESHOLD_MAX, true); + entry->setMaxThreshold(optimized_zone_0 * max_threshold_percentage_ / 100); // they can be int values, as we are not interested in the decimal part when defining the threshold + exit->setMaxThreshold(optimized_zone_1 * max_threshold_percentage_ / 100); + int hundred_threshold_zone_0 = entry->getMaxThreshold() / 100; + int hundred_threshold_zone_1 = exit->getMaxThreshold() / 100; + int unit_threshold_zone_0 = entry->getMaxThreshold() - 100 * hundred_threshold_zone_0; + int unit_threshold_zone_1 = exit->getMaxThreshold() - 100 * hundred_threshold_zone_1; + publishSensorConfiguration(entry, exit, true); App.feed_wdt(); if (min_threshold_percentage_ != 0) { - DIST_THRESHOLD_MIN[0] = optimized_zone_0 * min_threshold_percentage_ / 100; // they can be int values, as we are not interested in the decimal part when defining the threshold - DIST_THRESHOLD_MIN[1] = optimized_zone_1 * min_threshold_percentage_ / 100; - publishSensorConfiguration(DIST_THRESHOLD_MIN, false); + entry->setMinThreshold(optimized_zone_0 * min_threshold_percentage_ / 100); // they can be int values, as we are not interested in the decimal part when defining the threshold + exit->setMinThreshold(optimized_zone_1 * min_threshold_percentage_ / 100); + publishSensorConfiguration(entry, exit, false); } distanceSensor.StopRanging(); } - void Roode::publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax) + void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { if (isMax) { - ESP_LOGI(SETUP, "Max threshold entry: %dmm", DIST_THRESHOLD_ARR[0]); - ESP_LOGI(SETUP, "Max threshold exit: %dmm", DIST_THRESHOLD_ARR[1]); + ESP_LOGI(SETUP, "Max threshold entry: %dmm", entry->getMaxThreshold()); + ESP_LOGI(SETUP, "Max threshold exit: %dmm", exit->getMaxThreshold()); if (max_threshold_entry_sensor != nullptr) { - max_threshold_entry_sensor->publish_state(DIST_THRESHOLD_ARR[0]); + max_threshold_entry_sensor->publish_state(entry->getMaxThreshold()); } if (max_threshold_exit_sensor != nullptr) { - max_threshold_exit_sensor->publish_state(DIST_THRESHOLD_ARR[1]); + max_threshold_exit_sensor->publish_state(exit->getMaxThreshold()); } } else { - ESP_LOGI(SETUP, "Min threshold entry: %dmm", DIST_THRESHOLD_ARR[0]); - ESP_LOGI(SETUP, "Min threshold exit: %dmm", DIST_THRESHOLD_ARR[1]); + ESP_LOGI(SETUP, "Min threshold entry: %dmm", entry->getMaxThreshold()); + ESP_LOGI(SETUP, "Min threshold exit: %dmm", exit->getMaxThreshold()); if (min_threshold_entry_sensor != nullptr) { - min_threshold_entry_sensor->publish_state(DIST_THRESHOLD_ARR[0]); + min_threshold_entry_sensor->publish_state(entry->getMinThreshold()); } if (min_threshold_exit_sensor != nullptr) { - min_threshold_exit_sensor->publish_state(DIST_THRESHOLD_ARR[1]); + min_threshold_exit_sensor->publish_state(exit->getMinThreshold()); } } diff --git a/components/roode/roode.h b/components/roode/roode.h index 5a525ef3..2beff4da 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -102,8 +102,8 @@ namespace esphome uint16_t distance = 0; ERangeStatus rangeStatus; - int DIST_THRESHOLD_MAX[2] = {0, 0}; // max treshold of the two zones - int DIST_THRESHOLD_MIN[2] = {0, 0}; // min treshold of the two zones + // int DIST_THRESHOLD_MAX[2] = {0, 0}; // max treshold of the two zones + // int DIST_THRESHOLD_MIN[2] = {0, 0}; // min treshold of the two zones int entry_roi_width{6}; // width of the ROI int entry_roi_height{16}; // height of the ROI int exit_roi_width{6}; // width of the ROI @@ -137,7 +137,7 @@ namespace esphome void calibration(VL53L1X_ULD distanceSensor); void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); void setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); + void publishSensorConfiguration(Zone* entry, Zone* exit, bool isMax); int getOptimizedValues(int *values, int sum, int size); int getSum(int *values, int size); void updateCounter(int delta); diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 6ab53b3d..078e979a 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -76,6 +76,23 @@ namespace esphome this->roi.height = new_height; this->roi.center = new_center; } + void Zone::setMinThreshold(uint16_t min) + { + this->threshold.min = min; + } + void Zone::setMaxThreshold(uint16_t max) + { + this->threshold.max = max; + } + uint16_t Zone::getMinThreshold() + { + return this->threshold.min; + } + uint16_t Zone::getMaxThreshold() + { + return this->threshold.max; + } + uint8_t Zone::getZoneId() { return this->id; diff --git a/components/roode/zone.h b/components/roode/zone.h index a2734c28..1a8ae018 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -32,6 +32,8 @@ namespace esphome uint16_t getRoiWidth(); uint16_t getRoiHeight(); uint16_t getRoiCenter(); + void setMinThreshold(uint16_t min); + void setMaxThreshold(uint16_t max); void setRoiWidth(uint16_t new_roi_width); void setRoiHeight(uint16_t new_roi_height); void setRoiCenter(uint16_t new_roi_center); From f5a041dc313eab5e4cbfeb5d7073bd7674817fb6 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Wed, 5 Jan 2022 18:58:20 +0100 Subject: [PATCH 12/54] Remove old threshold --- components/roode/roode.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/roode/roode.h b/components/roode/roode.h index 2beff4da..82db759d 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -102,8 +102,6 @@ namespace esphome uint16_t distance = 0; ERangeStatus rangeStatus; - // int DIST_THRESHOLD_MAX[2] = {0, 0}; // max treshold of the two zones - // int DIST_THRESHOLD_MIN[2] = {0, 0}; // min treshold of the two zones int entry_roi_width{6}; // width of the ROI int entry_roi_height{16}; // height of the ROI int exit_roi_width{6}; // width of the ROI From 63073474394223a9b280edff6de2c96296c5b6e0 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Wed, 5 Jan 2022 18:58:56 +0100 Subject: [PATCH 13/54] Remove obvious comment --- components/roode/roode.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/roode/roode.h b/components/roode/roode.h index 82db759d..f96ab566 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -102,10 +102,10 @@ namespace esphome uint16_t distance = 0; ERangeStatus rangeStatus; - int entry_roi_width{6}; // width of the ROI - int entry_roi_height{16}; // height of the ROI - int exit_roi_width{6}; // width of the ROI - int exit_roi_height{16}; // height of the ROI + int entry_roi_width{6}; + int entry_roi_height{16}; + int exit_roi_width{6}; + int exit_roi_height{16}; uint16_t peopleCounter{0}; Configuration sensorConfiguration; @@ -135,7 +135,7 @@ namespace esphome void calibration(VL53L1X_ULD distanceSensor); void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); void setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(Zone* entry, Zone* exit, bool isMax); + void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); int getOptimizedValues(int *values, int sum, int size); int getSum(int *values, int size); void updateCounter(int delta); From b6256640f99acb8d416443ab6770b62f0bca4ad1 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 10:09:17 +0100 Subject: [PATCH 14/54] Remove center global --- components/roode/roode.cpp | 66 +++++++++++++++++--------------------- components/roode/roode.h | 4 --- components/roode/zone.cpp | 3 +- components/roode/zone.h | 2 +- 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index d0ef91b5..eedd0f15 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -72,8 +72,8 @@ namespace esphome { ESP_LOGI(SETUP, "Manual sensor configuration"); sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); - entry->setMaxThreshold(Roode::manual_threshold_); - exit->setMaxThreshold(Roode::manual_threshold_); + entry->setMaxThreshold(manual_threshold_); + exit->setMaxThreshold(manual_threshold_); publishSensorConfiguration(entry, exit, true); } distanceSensor.SetInterMeasurementInMs(delay_between_measurements); @@ -133,17 +133,13 @@ namespace esphome { if (advised_sensor_orientation_) { - center[0] = 167; - center[1] = 231; - entry = new Zone(entry_roi_width, entry_roi_height, center[0]); - exit = new Zone(exit_roi_width, exit_roi_height, center[1]); + entry = new Zone(entry_roi_width, entry_roi_height, 167); + exit = new Zone(exit_roi_width, exit_roi_height, 231); } else { - center[0] = 195; - center[1] = 60; - entry = new Zone(entry_roi_width, entry_roi_height, center[0]); - exit = new Zone(exit_roi_width, exit_roi_height, center[1]); + entry = new Zone(entry_roi_width, entry_roi_height, 195); + exit = new Zone(exit_roi_width, exit_roi_height, 60); } current_zone = entry; @@ -345,32 +341,32 @@ namespace esphome int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(optimized_zone_0, optimized_zone_1) / 1000))); int ROI_size = min(8, max(4, function_of_the_distance)); - entry->updateRoi(ROI_size, ROI_size * 2, center[0]); - exit->updateRoi(ROI_size, ROI_size * 2, center[1]); + entry->updateRoi(ROI_size, ROI_size * 2); + exit->updateRoi(ROI_size, ROI_size * 2); // now we set the position of the center of the two zones if (advised_sensor_orientation_) { switch (ROI_size) { case 4: - center[0] = 150; - center[1] = 247; + entry->setRoiCenter(150); + exit->setRoiCenter(247); break; case 5: - center[0] = 159; - center[1] = 239; + entry->setRoiCenter(159); + exit->setRoiCenter(239); break; case 6: - center[0] = 159; - center[1] = 239; + entry->setRoiCenter(159); + exit->setRoiCenter(239); break; case 7: - center[0] = 167; - center[1] = 231; + entry->setRoiCenter(167); + exit->setRoiCenter(231); break; case 8: - center[0] = 167; - center[1] = 231; + entry->setRoiCenter(167); + exit->setRoiCenter(231); break; } } @@ -379,29 +375,27 @@ namespace esphome switch (ROI_size) { case 4: - center[0] = 193; - center[1] = 58; + entry->setRoiCenter(193); + exit->setRoiCenter(58); break; case 5: - center[0] = 194; - center[1] = 59; + entry->setRoiCenter(194); + exit->setRoiCenter(59); break; case 6: - center[0] = 194; - center[1] = 59; + entry->setRoiCenter(194); + exit->setRoiCenter(59); break; case 7: - center[0] = 195; - center[1] = 60; + entry->setRoiCenter(195); + exit->setRoiCenter(60); break; case 8: - center[0] = 195; - center[1] = 60; + entry->setRoiCenter(195); + exit->setRoiCenter(60); break; } } - entry->setRoiCenter(center[0]); - exit->setRoiCenter(center[1]); // we will now repeat the calculations necessary to define the thresholds with the updated zones int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; @@ -561,8 +555,8 @@ namespace esphome { ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, sensor_status); } - entry->updateRoi(entry_roi_width, entry_roi_height, center[0]); - exit->updateRoi(exit_roi_width, exit_roi_height, center[1]); + entry->updateRoi(entry_roi_width, entry_roi_height); + exit->updateRoi(exit_roi_width, exit_roi_height); int *values_zone_0 = new int[number_attempts]; int *values_zone_1 = new int[number_attempts]; diff --git a/components/roode/roode.h b/components/roode/roode.h index f96ab566..b86857ca 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -21,10 +21,6 @@ namespace esphome static const char *const TAG = "Roode"; static const char *const SETUP = "Setup"; static const char *const CALIBRATION = "Calibration"; - /* - ##### CALIBRATION ##### - */ - static int center[2] = {0, 0}; /* center of the two zones */ /* Use the VL53L1X_SetTimingBudget function to set the TB in milliseconds. The TB values available are [15, 20, diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 078e979a..15948b31 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -70,11 +70,10 @@ namespace esphome { this->roi.center = new_roi_center; } - void Zone::updateRoi(uint16_t new_width, uint16_t new_height, uint16_t new_center) + void Zone::updateRoi(uint16_t new_width, uint16_t new_height) { this->roi.width = new_width; this->roi.height = new_height; - this->roi.center = new_center; } void Zone::setMinThreshold(uint16_t min) { diff --git a/components/roode/zone.h b/components/roode/zone.h index 1a8ae018..156c3650 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -37,7 +37,7 @@ namespace esphome void setRoiWidth(uint16_t new_roi_width); void setRoiHeight(uint16_t new_roi_height); void setRoiCenter(uint16_t new_roi_center); - void updateRoi(uint16_t new_width, uint16_t new_height, uint16_t new_center); + void updateRoi(uint16_t new_width, uint16_t new_height); uint8_t getZoneId(); uint16_t getDistance(); bool handleSensorStatus(); From 4db67ed581a9684534fd04ce5b78df1fe7588dd5 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 14:20:59 +0100 Subject: [PATCH 15/54] Move calibration to zone --- Documents/diagram.puml | 2 +- components/roode/__init__.py | 8 +- components/roode/configuration.h | 2 +- components/roode/roode.cpp | 164 ++++--------------------------- components/roode/roode.h | 6 +- components/roode/zone.cpp | 122 +++++++++++++++++++++-- components/roode/zone.h | 10 +- 7 files changed, 150 insertions(+), 164 deletions(-) diff --git a/Documents/diagram.puml b/Documents/diagram.puml index 59b26614..0b35c940 100644 --- a/Documents/diagram.puml +++ b/Documents/diagram.puml @@ -17,7 +17,7 @@ class Roode { class Configuration { + Zone getZoneConfigraution(uint8_t zone) + int getTimingBudget() - # void setCorrectDistanceSettings(float average_zone_0, float average_zone_1) + # void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance) # void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax) # void setSensorMode(int sensor_mode, int timing_budget = 0) } diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 92a08a2b..2441f06c 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -142,16 +142,18 @@ def validate_roi_settings(config): roi = config.get(CONF_ZONES) + entry = roi.get(CONF_ENTRY_ZONE) + exit = roi.get(CONF_EXIT_ZONE) manual = config.get(CONF_MANUAL) if CONF_CALIBRATION in config: roi_calibration = config.get(CONF_CALIBRATION).get(CONF_ROI_CALIBRATION) - if roi_calibration == True and roi != None: + if roi_calibration == True and (entry != None or exit != None): raise cv.Invalid( "ROI calibration cannot be used with manual ROI width and height" ) - if roi_calibration == False and roi == None: + if roi_calibration == False and (entry == None or exit == None): raise cv.Invalid("You need to set the ROI manually or use ROI calibration") - if manual != None and roi == None: + if manual != None and (roi == None or entry == None or exit == None): raise cv.Invalid("You need to set the ROI manually if manual mode is active") diff --git a/components/roode/configuration.h b/components/roode/configuration.h index eae974df..454b7829 100644 --- a/components/roode/configuration.h +++ b/components/roode/configuration.h @@ -27,7 +27,7 @@ namespace esphome VL53L1_Error sensor_status = VL53L1_ERROR_NONE; void setSensorMode(int sensor_mode, int timing_budget = 0); void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); - void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); + void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); }; } } diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index eedd0f15..725a45bc 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -65,7 +65,7 @@ namespace esphome if (calibration_active_) { - calibration(distanceSensor); + calibrateZones(distanceSensor); App.feed_wdt(); } if (manual_active_) @@ -77,7 +77,6 @@ namespace esphome publishSensorConfiguration(entry, exit, true); } distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - distanceSensor.StartRanging(); } void Roode::update() @@ -333,83 +332,7 @@ namespace esphome } void Roode::recalibration() { - calibration(distanceSensor); - } - void Roode::roi_calibration(VL53L1X_ULD distanceSensor, int optimized_zone_0, int optimized_zone_1) - { - // the value of the average distance is used for computing the optimal size of the ROI and consequently also the center of the two zones - int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(optimized_zone_0, optimized_zone_1) / 1000))); - int ROI_size = min(8, max(4, function_of_the_distance)); - - entry->updateRoi(ROI_size, ROI_size * 2); - exit->updateRoi(ROI_size, ROI_size * 2); - // now we set the position of the center of the two zones - if (advised_sensor_orientation_) - { - switch (ROI_size) - { - case 4: - entry->setRoiCenter(150); - exit->setRoiCenter(247); - break; - case 5: - entry->setRoiCenter(159); - exit->setRoiCenter(239); - break; - case 6: - entry->setRoiCenter(159); - exit->setRoiCenter(239); - break; - case 7: - entry->setRoiCenter(167); - exit->setRoiCenter(231); - break; - case 8: - entry->setRoiCenter(167); - exit->setRoiCenter(231); - break; - } - } - else - { - switch (ROI_size) - { - case 4: - entry->setRoiCenter(193); - exit->setRoiCenter(58); - break; - case 5: - entry->setRoiCenter(194); - exit->setRoiCenter(59); - break; - case 6: - entry->setRoiCenter(194); - exit->setRoiCenter(59); - break; - case 7: - entry->setRoiCenter(195); - exit->setRoiCenter(60); - break; - case 8: - entry->setRoiCenter(195); - exit->setRoiCenter(60); - break; - } - } - // we will now repeat the calculations necessary to define the thresholds with the updated zones - int *values_zone_0 = new int[number_attempts]; - int *values_zone_1 = new int[number_attempts]; - - distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - current_zone = entry; - for (int i = 0; i < number_attempts; i++) - { - values_zone_0[i] = getAlternatingZoneDistances(); - values_zone_1[i] = getAlternatingZoneDistances(); - } - - optimized_zone_0 = getOptimizedValues(values_zone_0, getSum(values_zone_0, number_attempts), number_attempts); - optimized_zone_1 = getOptimizedValues(values_zone_1, getSum(values_zone_1, number_attempts), number_attempts); + calibrateZones(distanceSensor); } void Roode::setSensorMode(int sensor_mode, int new_timing_budget) { @@ -486,66 +409,35 @@ namespace esphome } } - void Roode::setCorrectDistanceSettings(float average_zone_0, float average_zone_1) + void Roode::setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance) { - if (average_zone_0 <= short_distance_threshold || average_zone_1 <= short_distance_threshold) + if (average_entry_zone_distance <= short_distance_threshold || average_exit_zone_distance <= short_distance_threshold) { setSensorMode(0); } - if ((average_zone_0 > short_distance_threshold && average_zone_0 <= medium_distance_threshold) || (average_zone_1 > short_distance_threshold && average_zone_1 <= medium_distance_threshold)) + if ((average_entry_zone_distance > short_distance_threshold && average_entry_zone_distance <= medium_distance_threshold) || (average_exit_zone_distance > short_distance_threshold && average_exit_zone_distance <= medium_distance_threshold)) { setSensorMode(1); } - if ((average_zone_0 > medium_distance_threshold && average_zone_0 <= medium_long_distance_threshold) || (average_zone_1 > medium_distance_threshold && average_zone_1 <= medium_long_distance_threshold)) + if ((average_entry_zone_distance > medium_distance_threshold && average_entry_zone_distance <= medium_long_distance_threshold) || (average_exit_zone_distance > medium_distance_threshold && average_exit_zone_distance <= medium_long_distance_threshold)) { setSensorMode(2); } - if ((average_zone_0 > medium_long_distance_threshold && average_zone_0 <= long_distance_threshold) || (average_zone_1 > medium_long_distance_threshold && average_zone_1 <= long_distance_threshold)) + if ((average_entry_zone_distance > medium_long_distance_threshold && average_entry_zone_distance <= long_distance_threshold) || (average_exit_zone_distance > medium_long_distance_threshold && average_exit_zone_distance <= long_distance_threshold)) { setSensorMode(3); } - if (average_zone_0 > long_distance_threshold || average_zone_1 > long_distance_threshold) + if (average_entry_zone_distance > long_distance_threshold || average_exit_zone_distance > long_distance_threshold) { setSensorMode(4); } } - int Roode::getSum(int *array, int size) + void Roode::calibrateZones(VL53L1X_ULD distanceSensor) { - int sum = 0; - for (int i = 0; i < size; i++) - { - sum = sum + array[i]; - App.feed_wdt(); - } - return sum; - } - int Roode::getOptimizedValues(int *values, int sum, int size) - { - int sum_squared = 0; - int variance = 0; - int sd = 0; - int avg = sum / size; - - for (int i = 0; i < size; i++) - { - sum_squared = sum_squared + (values[i] * values[i]); - App.feed_wdt(); - } - variance = sum_squared / size - (avg * avg); - sd = sqrt(variance); - ESP_LOGD(CALIBRATION, "Zone AVG: %d", avg); - ESP_LOGD(CALIBRATION, "Zone 0 SD: %d", sd); - return avg - sd; - } - - void Roode::calibration(VL53L1X_ULD distanceSensor) - { - ESP_LOGI(SETUP, "Calibrating sensor"); - distanceSensor.StopRanging(); - // the sensor does 100 measurements for each zone (zones are predefined) + ESP_LOGI(SETUP, "Calibrating sensor zone"); time_budget_in_ms = time_budget_in_ms_medium; delay_between_measurements = time_budget_in_ms + 5; distanceSensor.SetDistanceMode(Long); @@ -555,31 +447,16 @@ namespace esphome { ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, sensor_status); } - entry->updateRoi(entry_roi_width, entry_roi_height); - exit->updateRoi(exit_roi_width, exit_roi_height); - - int *values_zone_0 = new int[number_attempts]; - int *values_zone_1 = new int[number_attempts]; - distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - current_zone = entry; - for (int i = 0; i < number_attempts; i++) - { - values_zone_0[i] = getAlternatingZoneDistances(); - values_zone_1[i] = getAlternatingZoneDistances(); - } - - // after we have computed the sum for each zone, we can compute the average distance of each zone - - optimized_zone_0 = getOptimizedValues(values_zone_0, getSum(values_zone_0, number_attempts), number_attempts); - optimized_zone_1 = getOptimizedValues(values_zone_1, getSum(values_zone_1, number_attempts), number_attempts); - setCorrectDistanceSettings(optimized_zone_0, optimized_zone_1); + int entry_threshold = entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + int exit_threshold = exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + setCorrectDistanceSettings(entry_threshold, exit_threshold); if (roi_calibration_) { - roi_calibration(distanceSensor, optimized_zone_0, optimized_zone_1); + entry->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); + entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + exit->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); + exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); } - - entry->setMaxThreshold(optimized_zone_0 * max_threshold_percentage_ / 100); // they can be int values, as we are not interested in the decimal part when defining the threshold - exit->setMaxThreshold(optimized_zone_1 * max_threshold_percentage_ / 100); int hundred_threshold_zone_0 = entry->getMaxThreshold() / 100; int hundred_threshold_zone_1 = exit->getMaxThreshold() / 100; int unit_threshold_zone_0 = entry->getMaxThreshold() - 100 * hundred_threshold_zone_0; @@ -588,19 +465,14 @@ namespace esphome App.feed_wdt(); if (min_threshold_percentage_ != 0) { - entry->setMinThreshold(optimized_zone_0 * min_threshold_percentage_ / 100); // they can be int values, as we are not interested in the decimal part when defining the threshold - exit->setMinThreshold(optimized_zone_1 * min_threshold_percentage_ / 100); publishSensorConfiguration(entry, exit, false); } - distanceSensor.StopRanging(); } void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { if (isMax) { - ESP_LOGI(SETUP, "Max threshold entry: %dmm", entry->getMaxThreshold()); - ESP_LOGI(SETUP, "Max threshold exit: %dmm", exit->getMaxThreshold()); if (max_threshold_entry_sensor != nullptr) { max_threshold_entry_sensor->publish_state(entry->getMaxThreshold()); @@ -613,8 +485,6 @@ namespace esphome } else { - ESP_LOGI(SETUP, "Min threshold entry: %dmm", entry->getMaxThreshold()); - ESP_LOGI(SETUP, "Min threshold exit: %dmm", exit->getMaxThreshold()); if (min_threshold_entry_sensor != nullptr) { min_threshold_entry_sensor->publish_state(entry->getMinThreshold()); diff --git a/components/roode/roode.h b/components/roode/roode.h index b86857ca..95cc5c8c 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -20,7 +20,7 @@ namespace esphome #define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 static const char *const TAG = "Roode"; static const char *const SETUP = "Setup"; - static const char *const CALIBRATION = "Calibration"; + static const char *const CALIBRATION = "Sensor Calibration"; /* Use the VL53L1X_SetTimingBudget function to set the TB in milliseconds. The TB values available are [15, 20, @@ -128,8 +128,8 @@ namespace esphome void createEntryAndExitZone(); void roi_calibration(VL53L1X_ULD distanceSensor, int optimized_zone_0, int optimized_zone_1); - void calibration(VL53L1X_ULD distanceSensor); - void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); + void calibrateZones(VL53L1X_ULD distanceSensor); + void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); void setSensorMode(int sensor_mode, int timing_budget = 0); void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); int getOptimizedValues(int *values, int sum, int size); diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 15948b31..de3f1d60 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -15,13 +15,14 @@ namespace esphome this->roi = {roi_width, roi_height, roi_center}; } - uint16_t Zone::readDistance(VL53L1X_ULD &distanceSensor) + VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { last_sensor_status = sensor_status; sensor_status += distanceSensor.SetROI(this->getRoiWidth(), this->getRoiHeight()); sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); sensor_status += distanceSensor.StartRanging(); - // Checking if data is available. This can also be done through the hardware interrupt. See the ReadDistanceHardwareInterrupt for an example + + // Wait for the measurement to be ready uint8_t dataReady = false; while (!dataReady) { @@ -32,16 +33,125 @@ namespace esphome // Get the results sensor_status += distanceSensor.GetDistanceInMm(&distance); + // After reading the results reset the interrupt to be able to take another measurement + sensor_status += distanceSensor.ClearInterrupt(); + sensor_status += distanceSensor.StopRanging(); if (sensor_status != VL53L1_ERROR_NONE) { ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); return sensor_status; } - // After reading the results reset the interrupt to be able to take another measurement - sensor_status += distanceSensor.ClearInterrupt(); - sensor_status += distanceSensor.StopRanging(); - return distance; + return sensor_status; + } + + int Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, uint16_t min_threshold_percentage) + { + int *zone_distances = new int[number_attempts]; + int sum = 0; + for (int i = 0; i < number_attempts; i++) + { + this->readDistance(distanceSensor); + zone_distances[i] = this->getDistance(); + sum += zone_distances[i]; + }; + int optimized_threshold = this->getOptimizedValues(zone_distances, sum, number_attempts); + this->setMaxThreshold(optimized_threshold * max_threshold_percentage / 100); + if (min_threshold_percentage > 0) + this->setMinThreshold(optimized_threshold * min_threshold_percentage / 100); + ESP_LOGI(CALIBRATION, "Calibrated threshold for zone. zoneId: %d, threshold: %d", this->getZoneId(), optimized_threshold); + return optimized_threshold; + } + void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation) + { + // the value of the average distance is used for computing the optimal size of the ROI and consequently also the center of the two zones + int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); + int ROI_size = min(8, max(4, function_of_the_distance)); + this->updateRoi(ROI_size, ROI_size * 2); + // now we set the position of the center of the two zones + if (sensor_orientation) + { + switch (ROI_size) + { + case 4: + if (this->getZoneId() == 0) + this->setRoiCenter(150); + this->setRoiCenter(247); + + break; + case 5: + if (this->getZoneId() == 0) + this->setRoiCenter(159); + this->setRoiCenter(239); + break; + case 6: + if (this->getZoneId() == 0) + this->setRoiCenter(159); + this->setRoiCenter(239); + break; + case 7: + if (this->getZoneId() == 0) + this->setRoiCenter(167); + this->setRoiCenter(231); + break; + case 8: + if (this->getZoneId() == 0) + this->setRoiCenter(167); + this->setRoiCenter(231); + break; + } + } + else + { + switch (ROI_size) + { + case 4: + if (this->getZoneId() == 0) + this->setRoiCenter(193); + this->setRoiCenter(58); + break; + case 5: + if (this->getZoneId() == 0) + this->setRoiCenter(194); + this->setRoiCenter(59); + break; + case 6: + if (this->getZoneId() == 0) + this->setRoiCenter(194); + this->setRoiCenter(59); + break; + case 7: + if (this->getZoneId() == 0) + this->setRoiCenter(195); + this->setRoiCenter(60); + break; + case 8: + if (this->getZoneId() == 0) + this->setRoiCenter(195); + this->setRoiCenter(60); + break; + } + } + ESP_LOGI(CALIBRATION, "Calibrated ROI for zone. zoneId: %d, width: %d, height: %d, center: %d", this->getZoneId(), this->getRoiWidth(), this->getRoiHeight(), this->getRoiCenter()); } + int Zone::getOptimizedValues(int *values, int sum, int size) + { + int sum_squared = 0; + int variance = 0; + int sd = 0; + int avg = sum / size; + + for (int i = 0; i < size; i++) + { + sum_squared = sum_squared + (values[i] * values[i]); + App.feed_wdt(); + } + variance = sum_squared / size - (avg * avg); + sd = sqrt(variance); + ESP_LOGD(CALIBRATION, "Zone AVG: %d", avg); + ESP_LOGD(CALIBRATION, "Zone SD: %d", sd); + return avg - sd; + } + uint16_t Zone::getDistance() { return this->distance; diff --git a/components/roode/zone.h b/components/roode/zone.h index 156c3650..8e1ea7bb 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -4,7 +4,9 @@ #include #include "esphome/core/log.h" #include "configuration.h" + static const char *const TAG = "Zone"; +static const char *const CALIBRATION = "Zone calibration"; namespace esphome { namespace roode @@ -18,14 +20,17 @@ namespace esphome struct Threshold { uint16_t min; + uint16_t min_percentage; uint16_t max; + uint16_t max_percentage; }; class Zone { public: Zone(int roi_width, int roi_height, int roi_center); - uint16_t readDistance(VL53L1X_ULD &distanceSensor); - uint16_t calibrateThreshold(); + VL53L1_Error readDistance(VL53L1X_ULD &distanceSensor); + int calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, uint16_t min_threshold_percentage); + void roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation); uint16_t calibrateRoi(); uint16_t getMinThreshold(); uint16_t getMaxThreshold(); @@ -45,7 +50,6 @@ namespace esphome protected: int getSum(int *values, int size); int getOptimizedValues(int *values, int sum, int size); - void setCorrectDistanceSettings(float average_zone_0, float average_zone_1); ROI roi; Threshold threshold; uint16_t roi_width; From 7ef424e29cd933c45fb54b138dcf230962f0d910 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 14:35:57 +0100 Subject: [PATCH 16/54] Fix ROI calibration --- components/roode/roode.cpp | 12 ++++----- components/roode/roode.h | 23 ++++++------------ components/roode/zone.cpp | 50 ++++++++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 725a45bc..044a1f88 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -59,7 +59,7 @@ namespace esphome } } } - ESP_LOGI(SETUP, "Using sampling with sampling size: %d", DISTANCES_ARRAY_SIZE); + ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); ESP_LOGI(SETUP, "Creating entry and exit zones."); createEntryAndExitZone(); @@ -161,10 +161,10 @@ namespace esphome int AllZonesCurrentStatus = 0; int AnEventHasOccured = 0; - uint16_t Distances[2][DISTANCES_ARRAY_SIZE]; + uint16_t Distances[2][samples]; uint16_t MinDistance; uint8_t i; - if (DistancesTableSize[zone->getZoneId()] < DISTANCES_ARRAY_SIZE) + if (DistancesTableSize[zone->getZoneId()] < samples) { ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone->getZoneId(), distance); Distances[zone->getZoneId()][DistancesTableSize[zone->getZoneId()]] = zone->getDistance(); @@ -172,10 +172,10 @@ namespace esphome } else { - for (i = 1; i < DISTANCES_ARRAY_SIZE; i++) + for (i = 1; i < samples; i++) Distances[zone->getZoneId()][i - 1] = Distances[zone->getZoneId()][i]; - Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1] = distance; - ESP_LOGD(SETUP, "Distances[%d][DISTANCES_ARRAY_SIZE - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][DISTANCES_ARRAY_SIZE - 1]); + Distances[zone->getZoneId()][samples - 1] = distance; + ESP_LOGD(SETUP, "Distances[%d][samples - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][samples - 1]); } ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][0]); ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][1]); diff --git a/components/roode/roode.h b/components/roode/roode.h index 95cc5c8c..4cda7412 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -73,7 +73,7 @@ namespace esphome void set_invert_direction(bool dir) { invert_direction_ = dir; } void set_restore_values(bool val) { restore_values_ = val; } void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } - void set_sampling_size(uint8_t size) { DISTANCES_ARRAY_SIZE = size; } + void set_sampling_size(uint8_t size) { samples = size; } void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } void set_people_counter(number::Number *counter) { this->people_counter = counter; } @@ -96,13 +96,10 @@ namespace esphome bool handleSensorStatus(); uint16_t distance = 0; - - ERangeStatus rangeStatus; - int entry_roi_width{6}; - int entry_roi_height{16}; - int exit_roi_width{6}; - int exit_roi_height{16}; - uint16_t peopleCounter{0}; + int entry_roi_width{6}; + int entry_roi_height{16}; + int exit_roi_width{6}; + int exit_roi_height{16}; Configuration sensorConfiguration; protected: @@ -127,13 +124,10 @@ namespace esphome text_sensor::TextSensor *entry_exit_event_sensor; void createEntryAndExitZone(); - void roi_calibration(VL53L1X_ULD distanceSensor, int optimized_zone_0, int optimized_zone_1); void calibrateZones(VL53L1X_ULD distanceSensor); void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); void setSensorMode(int sensor_mode, int timing_budget = 0); void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); - int getOptimizedValues(int *values, int sum, int size); - int getSum(int *values, int size); void updateCounter(int delta); bool calibration_active_{false}; bool manual_active_{false}; @@ -144,22 +138,19 @@ namespace esphome int sensor_mode{-1}; bool advised_sensor_orientation_{true}; bool sampling_active_{false}; - uint8_t DISTANCES_ARRAY_SIZE{2}; + uint8_t samples{2}; uint8_t address_ = 0x29; bool invert_direction_{false}; bool restore_values_{false}; uint64_t max_threshold_percentage_{85}; uint64_t min_threshold_percentage_{0}; uint64_t manual_threshold_{2000}; - int number_attempts = 20; + int number_attempts = 20; // TO DO: make this configurable int timing_budget_{-1}; int short_distance_threshold = 1300; int medium_distance_threshold = 2000; int medium_long_distance_threshold = 2700; int long_distance_threshold = 3400; - bool status = false; - int optimized_zone_0; - int optimized_zone_1; }; } // namespace roode diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index de3f1d60..29b60b50 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -73,29 +73,44 @@ namespace esphome switch (ROI_size) { case 4: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(150); + break; + } this->setRoiCenter(247); break; case 5: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(159); + break; + } this->setRoiCenter(239); break; case 6: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(159); + break; + } this->setRoiCenter(239); break; case 7: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(167); + break; + } this->setRoiCenter(231); break; case 8: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(167); + break; + } this->setRoiCenter(231); break; } @@ -105,28 +120,43 @@ namespace esphome switch (ROI_size) { case 4: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(193); + break; + } this->setRoiCenter(58); break; case 5: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(194); + break; + } this->setRoiCenter(59); break; case 6: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(194); + break; + } this->setRoiCenter(59); break; case 7: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(195); + break; + } this->setRoiCenter(60); break; case 8: - if (this->getZoneId() == 0) + if (this->getZoneId() == 0U) + { this->setRoiCenter(195); + break; + } this->setRoiCenter(60); break; } From 87550618948fe162aeb88c4ccbdb97d11d5eb541 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 14:42:07 +0100 Subject: [PATCH 17/54] Hotfix GHA unti #87 is done --- .github/workflows/main.yml | 8 ++------ peopleCounterDev.yaml | 21 +++++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fbb895cd..d9e25005 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,12 +14,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -U esphome - - name: Validate ESP8266 Config - run: esphome config peopleCounter.yaml - - name: Build ESP8266 - run: esphome compile peopleCounter.yaml - name: Validate ESP32 Config - run: esphome config peopleCounter32.yaml + run: esphome config peopleCounterDev.yaml - name: Build ESP32 - run: esphome compile peopleCounter32.yaml + run: esphome compile peopleCounterDev.yaml diff --git a/peopleCounterDev.yaml b/peopleCounterDev.yaml index 2be644d2..1978accc 100644 --- a/peopleCounterDev.yaml +++ b/peopleCounterDev.yaml @@ -53,9 +53,6 @@ roode: id: roode_platform i2c_address: 0x29 update_interval: 10ms - # roi: - # roi_height: 16 - # roi_width: 6 calibration: max_threshold_percentage: 85 min_threshold_percentage: 5 @@ -68,15 +65,15 @@ roode: # timing_budget: 200 # sampling: 3 zones: - invert_direction: true - entry: - roi: - roi_height: 12 - roi_width: 6 - exit: - roi: - roi_height: 12 - roi_width: 6 + invert_direction: false + # entry: + # roi: + # roi_height: 12 + # roi_width: 6 + # exit: + # roi: + # roi_height: 12 + # roi_width: 6 number: - platform: roode From a5248e83583942570b4316b43acf1047fe2f16e3 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 14:43:12 +0100 Subject: [PATCH 18/54] Rename step --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9e25005..93ddde05 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: uses: actions/setup-python@master with: python-version: '3.x' - - name: Install Platform IO + - name: Install ESPHome run: | python -m pip install --upgrade pip pip install -U esphome From db2553b29fbc12b7f4f0f8d76e33f71bb804cd02 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 15:39:03 +0100 Subject: [PATCH 19/54] Cleanup --- components/roode/roode.cpp | 17 ++++++----------- components/roode/roode.h | 12 ++++++------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 044a1f88..1a979d8d 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -94,7 +94,7 @@ namespace esphome void Roode::loop() { // unsigned long start = micros(); - distance = getAlternatingZoneDistances(); + getAlternatingZoneDistances(); doPathTracking(this->current_zone); handleSensorStatus(); this->current_zone = this->current_zone == this->entry ? this->exit : this->entry; @@ -145,9 +145,9 @@ namespace esphome } uint16_t Roode::getAlternatingZoneDistances() { - this->current_zone->readDistance(distanceSensor); + sensor_status += this->current_zone->readDistance(distanceSensor); App.feed_wdt(); - return this->current_zone->getDistance(); + return sensor_status; } void Roode::doPathTracking(Zone *zone) @@ -156,7 +156,6 @@ namespace esphome static int PathTrackFillingSize = 1; // init this to 1 as we start from state where nobody is any of the zones static int LeftPreviousStatus = NOBODY; static int RightPreviousStatus = NOBODY; - static uint8_t DistancesTableSize[2] = {0, 0}; int CurrentZoneStatus = NOBODY; int AllZonesCurrentStatus = 0; int AnEventHasOccured = 0; @@ -166,7 +165,7 @@ namespace esphome uint8_t i; if (DistancesTableSize[zone->getZoneId()] < samples) { - ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone->getZoneId(), distance); + ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone->getZoneId(), zone->getDistance()); Distances[zone->getZoneId()][DistancesTableSize[zone->getZoneId()]] = zone->getDistance(); DistancesTableSize[zone->getZoneId()]++; } @@ -174,11 +173,9 @@ namespace esphome { for (i = 1; i < samples; i++) Distances[zone->getZoneId()][i - 1] = Distances[zone->getZoneId()][i]; - Distances[zone->getZoneId()][samples - 1] = distance; + Distances[zone->getZoneId()][samples - 1] = zone->getDistance(); ESP_LOGD(SETUP, "Distances[%d][samples - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][samples - 1]); } - ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][0]); - ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone->getZoneId(), Distances[zone->getZoneId()][1]); // pick up the min distance MinDistance = Distances[zone->getZoneId()][0]; if (DistancesTableSize[zone->getZoneId()] >= 2) @@ -189,10 +186,9 @@ namespace esphome MinDistance = Distances[zone->getZoneId()][i]; } } - distance = MinDistance; // PathTrack algorithm - if (distance < zone->getMaxThreshold() && distance > zone->getMinThreshold()) + if (MinDistance < zone->getMaxThreshold() && MinDistance > zone->getMinThreshold()) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; @@ -336,7 +332,6 @@ namespace esphome } void Roode::setSensorMode(int sensor_mode, int new_timing_budget) { - distanceSensor.StopRanging(); switch (sensor_mode) { case 0: // short mode diff --git a/components/roode/roode.h b/components/roode/roode.h index 4cda7412..707ce854 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -47,6 +47,8 @@ namespace esphome static int time_budget_in_ms_long = 100; static int time_budget_in_ms_max = 200; // max range: 4m + static uint8_t DistancesTableSize[2] = {0, 0}; + class Roode : public PollingComponent { public: @@ -94,12 +96,6 @@ namespace esphome void doPathTracking(Zone *zone); void recalibration(); bool handleSensorStatus(); - - uint16_t distance = 0; - int entry_roi_width{6}; - int entry_roi_height{16}; - int exit_roi_width{6}; - int exit_roi_height{16}; Configuration sensorConfiguration; protected: @@ -151,6 +147,10 @@ namespace esphome int medium_distance_threshold = 2000; int medium_long_distance_threshold = 2700; int long_distance_threshold = 3400; + int entry_roi_width{6}; + int entry_roi_height{16}; + int exit_roi_width{6}; + int exit_roi_height{16}; }; } // namespace roode From 3d086bc431086e2e1cf81b34ad5f3b6cb02ca833 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 16:09:25 +0100 Subject: [PATCH 20/54] Fix sensor_status update --- components/roode/roode.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 1a979d8d..5e995ce3 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -108,7 +108,7 @@ namespace esphome { ESP_LOGD(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 (last_sensor_status != sensor_status && sensor_status == VL53L1_ERROR_NONE) { if (status_sensor != nullptr) { @@ -254,7 +254,6 @@ namespace esphome // if nobody anywhere lets check if an exit or entry has happened if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { - // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 3 2) and last event is 0 (nobobdy anywhere) if (PathTrackFillingSize == 4) { @@ -263,7 +262,7 @@ namespace esphome if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) { // This an exit - ESP_LOGD("Roode pathTracking", "Exit detected."); + ESP_LOGI("Roode pathTracking", "Exit detected."); DistancesTableSize[0] = 0; DistancesTableSize[1] = 0; this->updateCounter(-1); @@ -275,7 +274,7 @@ namespace esphome else if ((PathTrack[1] == 2) && (PathTrack[2] == 3) && (PathTrack[3] == 1)) { // This an entry - ESP_LOGD("Roode pathTracking", "Entry detected."); + ESP_LOGI("Roode pathTracking", "Entry detected."); this->updateCounter(1); if (entry_exit_event_sensor != nullptr) { From f71a0bf5d9f98deee9c0cfe3903f0d36a9674938 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 21:53:49 +0100 Subject: [PATCH 21/54] Fix 8266 - without the delay its not counting --- components/roode/roode.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 5e995ce3..5d0461f7 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -246,6 +246,7 @@ namespace esphome // if an event has occured if (AnEventHasOccured) { + delay(1); if (PathTrackFillingSize < 4) { PathTrackFillingSize++; @@ -254,6 +255,7 @@ namespace esphome // if nobody anywhere lets check if an exit or entry has happened if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { + delay(1); // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 3 2) and last event is 0 (nobobdy anywhere) if (PathTrackFillingSize == 4) { From 3d9c8e052ca517e213ac8c82118cbd022df75efc Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 22:03:22 +0100 Subject: [PATCH 22/54] Adapt configs --- fonts/Roboto-Regular.ttf | Bin 0 -> 158604 bytes peopleCounter.yaml | 68 ++++--- peopleCounter32.yaml | 57 ++++-- ...CounterDev.yaml => peopleCounter32Dev.yaml | 52 +++-- peopleCounter8266Dev.yaml | 180 ++++++++++++++++++ 5 files changed, 297 insertions(+), 60 deletions(-) create mode 100644 fonts/Roboto-Regular.ttf rename peopleCounterDev.yaml => peopleCounter32Dev.yaml (80%) create mode 100644 peopleCounter8266Dev.yaml diff --git a/fonts/Roboto-Regular.ttf b/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7d9a6c4c32d7e920b549caf531e390733496b6e0 GIT binary patch literal 158604 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
v%Q*Qgy;tdJj4ZONjTx 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

sDMz7$^SDZV4x`K*fov0l~+M89O^^Om^v{t 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 literal 0 HcmV?d00001 diff --git a/peopleCounter.yaml b/peopleCounter.yaml index dffc2f72..f0771a6b 100644 --- a/peopleCounter.yaml +++ b/peopleCounter.yaml @@ -32,16 +32,6 @@ api: - service: recalibrate then: - lambda: "id(roode_platform)->recalibration();" - - service: set_max_threshold - variables: - newThreshold: int - then: - - lambda: "id(roode_platform)->set_max_threshold_percentage(newThreshold);id(roode_platform)->recalibration();" - - service: set_min_threshold - variables: - newThreshold: int - then: - - lambda: "id(roode_platform)->set_min_threshold_percentage(newThreshold);id(roode_platform)->recalibration();" ota: password: !secret ota_password @@ -64,9 +54,6 @@ roode: id: roode_platform i2c_address: 0x29 update_interval: 10ms - # roi: - # roi_height: 16 - # roi_width: 6 calibration: max_threshold_percentage: 85 min_threshold_percentage: 5 @@ -77,32 +64,52 @@ roode: # sensor_mode: 3 # manual_threshold: 1280 # timing_budget: 200 - sampling: - size: 3 - invert_direction: true + # sampling: 3 + zones: + invert_direction: true + # entry: + # roi: + # roi_height: 12 + # roi_width: 6 + # exit: + # roi: + # roi_height: 12 + # roi_width: 6 + +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: + id: counter name: $friendly_name people counter -switch: - - platform: restart - name: $friendly_name Restart binary_sensor: - platform: status - name: $friendly_name Status + name: $friendly_name API Status - platform: roode presence_sensor: name: $friendly_name presence sensor: - platform: roode - id: hallway - distance_sensor: - name: $friendly_name distance + id: roode_sensors + distance_entry: + name: $friendly_name distance zone 0 filters: - - delta: 10.0 + - delta: 100 + distance_exit: + name: $friendly_name distance zone 1 + filters: + - delta: 100 max_threshold_entry: name: $friendly_name max zone 0 max_threshold_exit: @@ -111,10 +118,14 @@ sensor: name: $friendly_name min zone 0 min_threshold_exit: name: $friendly_name min zone 1 - roi_height: - name: $friendly_name ROI height - roi_width: - name: $friendly_name ROI width + 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 @@ -150,6 +161,7 @@ text_sensor: - platform: roode version: name: $friendly_name version + - platform: roode entry_exit_event: name: $friendly_name last direction - platform: template diff --git a/peopleCounter32.yaml b/peopleCounter32.yaml index 3b7cd992..71957ae9 100644 --- a/peopleCounter32.yaml +++ b/peopleCounter32.yaml @@ -67,9 +67,6 @@ roode: id: roode_platform i2c_address: 0x29 update_interval: 10ms - # roi: - # roi_height: 16 - # roi_width: 6 calibration: max_threshold_percentage: 85 min_threshold_percentage: 5 @@ -80,32 +77,51 @@ roode: # sensor_mode: 3 # manual_threshold: 1280 # timing_budget: 200 - sampling: - size: 3 - invert_direction: true + # sampling: 3 + zones: + invert_direction: true + # entry: + # roi: + # roi_height: 12 + # roi_width: 6 + # exit: + # roi: + # roi_height: 12 + # roi_width: 6 + +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 -switch: - - platform: restart - name: $friendly_name Restart binary_sensor: - platform: status - name: $friendly_name Status + name: $friendly_name API Status - platform: roode presence_sensor: name: $friendly_name presence sensor: - platform: roode - id: hallway - distance_sensor: - name: $friendly_name distance + id: roode_sensors + distance_entry: + name: $friendly_name distance zone 0 + filters: + - delta: 100 + distance_exit: + name: $friendly_name distance zone 1 filters: - - delta: 10.0 + - delta: 100 max_threshold_entry: name: $friendly_name max zone 0 max_threshold_exit: @@ -114,13 +130,16 @@ sensor: name: $friendly_name min zone 0 min_threshold_exit: name: $friendly_name min zone 1 - roi_height: - name: $friendly_name ROI height - roi_width: - name: $friendly_name ROI width + 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 - - platform: wifi_signal name: $friendly_name RSSI update_interval: 60s diff --git a/peopleCounterDev.yaml b/peopleCounter32Dev.yaml similarity index 80% rename from peopleCounterDev.yaml rename to peopleCounter32Dev.yaml index 1978accc..37597f15 100644 --- a/peopleCounterDev.yaml +++ b/peopleCounter32Dev.yaml @@ -1,5 +1,5 @@ substitutions: - devicename: roodedev + devicename: roode32dev friendly_name: $devicename external_components: @@ -13,14 +13,15 @@ esp32: board: wemos_d1_mini32 framework: type: arduino - + wifi: networks: - ssid: !secret ssid1 password: !secret ssid1_password + use_address: $devicename fast_connect: True - power_save_mode: light - domain: .shieldnet + power_save_mode: none + domain: .local captive_portal: @@ -43,16 +44,16 @@ web_server: # Enable logging logger: - level: DEBUG + level: INFO i2c: - sda: 21 - scl: 22 + sda: 4 + scl: 5 roode: id: roode_platform i2c_address: 0x29 - update_interval: 10ms + update_interval: 100ms calibration: max_threshold_percentage: 85 min_threshold_percentage: 5 @@ -65,7 +66,7 @@ roode: # timing_budget: 200 # sampling: 3 zones: - invert_direction: false + invert_direction: true # entry: # roi: # roi_height: 12 @@ -75,14 +76,22 @@ roode: # roi_height: 12 # roi_width: 6 +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: + id: counter name: $friendly_name people counter -switch: - - platform: restart - name: $friendly_name Restart binary_sensor: - platform: status name: $friendly_name API Status @@ -92,11 +101,15 @@ binary_sensor: sensor: - platform: roode - id: hallway + id: roode_sensors distance_entry: name: $friendly_name distance zone 0 + filters: + - delta: 100 distance_exit: name: $friendly_name distance zone 1 + filters: + - delta: 100 max_threshold_entry: name: $friendly_name max zone 0 max_threshold_exit: @@ -155,3 +168,16 @@ text_sensor: name: $friendly_name Uptime Human Readable id: uptime_human icon: mdi:clock-start + +font: + - file: "fonts/Roboto-Regular.ttf" + id: my_font + size: 20 +display: + - platform: ssd1306_i2c + model: "SSD1306 128x64" + id: screen + reset_pin: D0 + address: 0x3C + lambda: |- + it.printf(127, 60, id(my_font), TextAlign::BASELINE_RIGHT , "Counter: %d", id(counter)->state); diff --git a/peopleCounter8266Dev.yaml b/peopleCounter8266Dev.yaml new file mode 100644 index 00000000..5a4f77e7 --- /dev/null +++ b/peopleCounter8266Dev.yaml @@ -0,0 +1,180 @@ +substitutions: + devicename: roode8266dev + friendly_name: $devicename + +external_components: + refresh: always + source: components + +esphome: + name: $devicename + platform: ESP8266 + board: d1_mini + +wifi: + networks: + - ssid: !secret ssid1 + password: !secret ssid1_password + use_address: $devicename + fast_connect: True + power_save_mode: none + domain: .local + +captive_portal: + +api: + password: !secret api_password + reboot_timeout: 60min + services: + - service: recalibrate + then: + - lambda: "id(roode_platform)->recalibration();" + +ota: + password: !secret ota_password + +web_server: + port: 80 + auth: + username: admin + password: !secret web_password + +# Enable logging +logger: + level: INFO + +i2c: + sda: 4 + scl: 5 + +roode: + id: roode_platform + i2c_address: 0x29 + update_interval: 100ms + calibration: + max_threshold_percentage: 85 + min_threshold_percentage: 5 + roi_calibration: true + # sensor_offset_calibration: 8 + # sensor_xtalk_calibration: 53406 + # manual: + # sensor_mode: 3 + # manual_threshold: 1280 + # timing_budget: 200 + # sampling: 3 + zones: + invert_direction: true + # entry: + # roi: + # roi_height: 12 + # roi_width: 6 + # exit: + # roi: + # roi_height: 12 + # roi_width: 6 + +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: + id: counter + name: $friendly_name people counter + +binary_sensor: + - platform: status + name: $friendly_name API Status + - platform: roode + presence_sensor: + name: $friendly_name presence + +sensor: + - platform: roode + id: roode_sensors + distance_entry: + name: $friendly_name distance zone 0 + filters: + - delta: 100 + distance_exit: + name: $friendly_name distance zone 1 + filters: + - delta: 100 + 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 + + - platform: wifi_signal + name: $friendly_name RSSI + update_interval: 60s + + - platform: uptime + name: Uptime Sensor + id: uptime_sensor + update_interval: 60s + internal: true + on_raw_value: + then: + - text_sensor.template.publish: + id: uptime_human + state: !lambda |- + int seconds = round(id(uptime_sensor).raw_state); + int days = seconds / (24 * 3600); + seconds = seconds % (24 * 3600); + int hours = seconds / 3600; + seconds = seconds % 3600; + int minutes = seconds / 60; + seconds = seconds % 60; + return ( + (days ? String(days) + "d " : "") + + (hours ? String(hours) + "h " : "") + + (minutes ? String(minutes) + "m " : "") + + (String(seconds) + "s") + ).c_str(); + +text_sensor: + - platform: roode + version: + name: $friendly_name version + - platform: roode + entry_exit_event: + name: $friendly_name last direction + - platform: template + name: $friendly_name Uptime Human Readable + id: uptime_human + icon: mdi:clock-start + +font: + - file: "fonts/Roboto-Regular.ttf" + id: my_font + size: 20 +display: + - platform: ssd1306_i2c + model: "SSD1306 128x64" + id: screen + reset_pin: D0 + address: 0x3C + lambda: |- + it.printf(127, 60, id(my_font), TextAlign::BASELINE_RIGHT , "Counter: %d", id(counter)->state); From adfba27b599f594d01827d238af9b775c75461a3 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Thu, 6 Jan 2022 22:51:24 +0100 Subject: [PATCH 23/54] Add OLED config --- peopleCounter32Dev.yaml | 11 ++++++++--- peopleCounter8266Dev.yaml | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/peopleCounter32Dev.yaml b/peopleCounter32Dev.yaml index 37597f15..4e4eeaa8 100644 --- a/peopleCounter32Dev.yaml +++ b/peopleCounter32Dev.yaml @@ -13,7 +13,7 @@ esp32: board: wemos_d1_mini32 framework: type: arduino - + wifi: networks: - ssid: !secret ssid1 @@ -89,7 +89,7 @@ button: number: - platform: roode people_counter: - id: counter + id: peopleCounter name: $friendly_name people counter binary_sensor: @@ -104,10 +104,12 @@ sensor: id: roode_sensors distance_entry: name: $friendly_name distance zone 0 + id: entryDist filters: - delta: 100 distance_exit: name: $friendly_name distance zone 1 + id: exitDist filters: - delta: 100 max_threshold_entry: @@ -179,5 +181,8 @@ display: id: screen reset_pin: D0 address: 0x3C + update_interval: 500ms lambda: |- - it.printf(127, 60, id(my_font), TextAlign::BASELINE_RIGHT , "Counter: %d", id(counter)->state); + it.printf(0, 0, id(my_font), "Counter: %d", (int)id(peopleCounter).state); + it.printf(0, 20, id(my_font), "Entry dist: %d", (int)id(entryDist).state); + it.printf(0, 40, id(my_font), "Exit dist: %d", (int)id(exitDist).state); diff --git a/peopleCounter8266Dev.yaml b/peopleCounter8266Dev.yaml index 5a4f77e7..f427f64f 100644 --- a/peopleCounter8266Dev.yaml +++ b/peopleCounter8266Dev.yaml @@ -86,7 +86,7 @@ button: number: - platform: roode people_counter: - id: counter + id: peopleCounter name: $friendly_name people counter binary_sensor: @@ -101,10 +101,12 @@ sensor: id: roode_sensors distance_entry: name: $friendly_name distance zone 0 + id: entryDist filters: - delta: 100 distance_exit: name: $friendly_name distance zone 1 + id: exitDist filters: - delta: 100 max_threshold_entry: @@ -169,12 +171,15 @@ text_sensor: font: - file: "fonts/Roboto-Regular.ttf" id: my_font - size: 20 + size: 14 display: - platform: ssd1306_i2c model: "SSD1306 128x64" id: screen reset_pin: D0 address: 0x3C + update_interval: 500ms lambda: |- - it.printf(127, 60, id(my_font), TextAlign::BASELINE_RIGHT , "Counter: %d", id(counter)->state); + it.printf(0, 0, id(my_font), "Counter: %d", (int)id(peopleCounter).state); + it.printf(0, 20, id(my_font), "Entry dist: %d", (int)id(entryDist).state); + it.printf(0, 40, id(my_font), "Exit dist: %d", (int)id(exitDist).state); From f567923c17619cfee46317f9abe1e5e0600c7ac6 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 10 Jan 2022 11:13:28 +0100 Subject: [PATCH 24/54] Add esphome format style --- .clang-format | 137 +++++ components/roode/configuration.cpp | 153 +++-- components/roode/configuration.h | 44 +- components/roode/roode.cpp | 878 +++++++++++++---------------- components/roode/roode.h | 293 +++++----- components/roode/zone.cpp | 429 +++++++------- components/roode/zone.h | 110 ++-- 7 files changed, 1034 insertions(+), 1010 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..8faa7375 --- /dev/null +++ b/.clang-format @@ -0,0 +1,137 @@ +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 2000 +PointerAlignment: Right +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 2 +UseTab: Never \ No newline at end of file diff --git a/components/roode/configuration.cpp b/components/roode/configuration.cpp index c68e3085..33d068bf 100644 --- a/components/roode/configuration.cpp +++ b/components/roode/configuration.cpp @@ -1,85 +1,76 @@ #include "configuration.h" + #include "roode.h" /* -This Object is used to store the sensor configratuin like calibration data, sensor mode, ROI settings etc. +This Object is used to store the sensor configratuin like calibration data, +sensor mode, ROI settings etc. */ -namespace esphome -{ - namespace roode - { - void Configuration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) - { - distanceSensor.StopRanging(); - switch (sensor_mode) - { - case 0: // short mode - time_budget_in_ms = time_budget_in_ms_short; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Short); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); - } - ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); - break; - case 1: // medium mode - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); - break; - case 2: // medium_long mode - time_budget_in_ms = time_budget_in_ms_medium_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 3: // long mode - time_budget_in_ms = time_budget_in_ms_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 4: // max mode - time_budget_in_ms = time_budget_in_ms_max; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); - break; - case 5: // custom mode - time_budget_in_ms = new_timing_budget; - delay_between_measurements = new_timing_budget + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); - break; - default: - break; - } - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); - if (sensor_status != 0) - { - ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); - } - } - } -} \ No newline at end of file +namespace esphome { +namespace roode { +void Configuration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) { + distanceSensor.StopRanging(); + switch (sensor_mode) { + case 0: // short mode + time_budget_in_ms = time_budget_in_ms_short; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Short); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); + } + ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); + break; + case 1: // medium mode + time_budget_in_ms = time_budget_in_ms_medium; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); + break; + case 2: // medium_long mode + time_budget_in_ms = time_budget_in_ms_medium_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 3: // long mode + time_budget_in_ms = time_budget_in_ms_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 4: // max mode + time_budget_in_ms = time_budget_in_ms_max; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); + break; + case 5: // custom mode + time_budget_in_ms = new_timing_budget; + delay_between_measurements = new_timing_budget + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); + break; + default: + break; + } + sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); + if (sensor_status != 0) { + ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); + } +} +} // namespace roode +} // namespace esphome \ No newline at end of file diff --git a/components/roode/configuration.h b/components/roode/configuration.h index 454b7829..fbe447d8 100644 --- a/components/roode/configuration.h +++ b/components/roode/configuration.h @@ -1,33 +1,31 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" +#include + +#include "VL53L1X_ULD.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/application.h" -#include "VL53L1X_ULD.h" -#include +#include "esphome/core/component.h" #include "zone.h" static VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; static VL53L1_Error sensor_status = VL53L1_ERROR_NONE; -namespace esphome -{ - namespace roode - { - class Configuration - { - public: - void setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int timing_budget = 0); - int getTimingBudget(); +namespace esphome { +namespace roode { +class Configuration { + public: + void setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int timing_budget = 0); + int getTimingBudget(); - protected: - VL53L1X_ULD distanceSensor; - VL53L1_Error sensor_status = VL53L1_ERROR_NONE; - void setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); - void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); - }; - } -} + protected: + VL53L1X_ULD distanceSensor; + VL53L1_Error sensor_status = VL53L1_ERROR_NONE; + void setSensorMode(int sensor_mode, int timing_budget = 0); + void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); + void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); +}; +} // namespace roode +} // namespace esphome diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 5d0461f7..228b2eee 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -1,513 +1,421 @@ -#include "esphome/core/log.h" #include "roode.h" -namespace esphome -{ - namespace roode - { - void Roode::dump_config() - { - ESP_LOGCONFIG(TAG, "dump config:"); - LOG_I2C_DEVICE(this); - - LOG_UPDATE_INTERVAL(this); - } - void Roode::setup() - { - ESP_LOGI(SETUP, "Booting Roode %s", VERSION); - if (version_sensor != nullptr) - { - version_sensor->publish_state(VERSION); - } - Wire.begin(); - Wire.setClock(400000); - - // Initialize the sensor, give the special I2C_address to the Begin function - // Set a different I2C address - // This address is stored as long as the sensor is powered. To revert this change you can unplug and replug the power to the sensor - distanceSensor.SetI2CAddress(VL53L1X_ULD_I2C_ADDRESS); - - sensor_status = distanceSensor.Begin(VL53L1X_ULD_I2C_ADDRESS); - if (sensor_status != VL53L1_ERROR_NONE) - { - // If the sensor could not be initialized print out the error code. -7 is timeout - ESP_LOGE(SETUP, "Could not initialize the sensor, error code: %d", sensor_status); - while (1) - { - } - } - if (sensor_offset_calibration_ != -1) - { - ESP_LOGI(CALIBRATION, "Setting sensor offset calibration to %d", sensor_offset_calibration_); - sensor_status = distanceSensor.SetOffsetInMm(sensor_offset_calibration_); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - while (1) - { - } - } - } - if (sensor_xtalk_calibration_ != -1) - { - ESP_LOGI(CALIBRATION, "Setting sensor xtalk calibration to %d", sensor_xtalk_calibration_); - sensor_status = distanceSensor.SetXTalk(sensor_xtalk_calibration_); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - while (1) - { - } - } - } - ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); - ESP_LOGI(SETUP, "Creating entry and exit zones."); - createEntryAndExitZone(); - if (calibration_active_) - { - calibrateZones(distanceSensor); - App.feed_wdt(); - } - if (manual_active_) - { - ESP_LOGI(SETUP, "Manual sensor configuration"); - sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); - entry->setMaxThreshold(manual_threshold_); - exit->setMaxThreshold(manual_threshold_); - publishSensorConfiguration(entry, exit, true); - } - distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - } +#include "esphome/core/log.h" +namespace esphome { +namespace roode { +void Roode::dump_config() { + ESP_LOGCONFIG(TAG, "dump config:"); + LOG_I2C_DEVICE(this); - void Roode::update() - { - if (distance_entry != nullptr) - { - distance_entry->publish_state(entry->getDistance()); - } - if (distance_exit != nullptr) - { - distance_exit->publish_state(exit->getDistance()); - } - } + LOG_UPDATE_INTERVAL(this); +} +void Roode::setup() { + ESP_LOGI(SETUP, "Booting Roode %s", VERSION); + if (version_sensor != nullptr) { + version_sensor->publish_state(VERSION); + } + Wire.begin(); + Wire.setClock(400000); - void Roode::loop() - { - // unsigned long start = micros(); - getAlternatingZoneDistances(); - doPathTracking(this->current_zone); - handleSensorStatus(); - this->current_zone = this->current_zone == this->entry ? this->exit : this->entry; - // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", entry->getDistance(Roode::distanceSensor, Roode::sensor_status), exit->getDistance(Roode::distanceSensor, Roode::sensor_status)); - // unsigned long end = micros(); - // unsigned long delta = end - start; - // ESP_LOGI("Roode loop", "loop took %lu microseconds", delta); - } + // Initialize the sensor, give the special I2C_address to the Begin function + // Set a different I2C address + // This address is stored as long as the sensor is powered. To revert this + // change you can unplug and replug the power to the sensor + distanceSensor.SetI2CAddress(VL53L1X_ULD_I2C_ADDRESS); - bool Roode::handleSensorStatus() - { - ESP_LOGD(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) - { - status_sensor->publish_state(sensor_status); - } - check_status = true; - } - if (sensor_status < 28 && sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(TAG, "Ranging failed with an error. status: %d", sensor_status); - status_sensor->publish_state(sensor_status); - check_status = false; - } + sensor_status = distanceSensor.Begin(VL53L1X_ULD_I2C_ADDRESS); + if (sensor_status != VL53L1_ERROR_NONE) { + // If the sensor could not be initialized print out the error code. -7 is + // timeout + ESP_LOGE(SETUP, "Could not initialize the sensor, error code: %d", sensor_status); + while (1) { + } + } + if (sensor_offset_calibration_ != -1) { + ESP_LOGI(CALIBRATION, "Setting sensor offset calibration to %d", sensor_offset_calibration_); + sensor_status = distanceSensor.SetOffsetInMm(sensor_offset_calibration_); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); + while (1) { + } + } + } + if (sensor_xtalk_calibration_ != -1) { + ESP_LOGI(CALIBRATION, "Setting sensor xtalk calibration to %d", sensor_xtalk_calibration_); + sensor_status = distanceSensor.SetXTalk(sensor_xtalk_calibration_); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); + while (1) { + } + } + } + ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); + ESP_LOGI(SETUP, "Creating entry and exit zones."); + createEntryAndExitZone(); - last_sensor_status = sensor_status; - sensor_status = VL53L1_ERROR_NONE; - return check_status; - } + if (calibration_active_) { + calibrateZones(distanceSensor); + App.feed_wdt(); + } + if (manual_active_) { + ESP_LOGI(SETUP, "Manual sensor configuration"); + sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); + entry->setMaxThreshold(manual_threshold_); + exit->setMaxThreshold(manual_threshold_); + publishSensorConfiguration(entry, exit, true); + } + distanceSensor.SetInterMeasurementInMs(delay_between_measurements); +} - void Roode::createEntryAndExitZone() - { - if (advised_sensor_orientation_) - { - entry = new Zone(entry_roi_width, entry_roi_height, 167); - exit = new Zone(exit_roi_width, exit_roi_height, 231); - } - else - { - entry = new Zone(entry_roi_width, entry_roi_height, 195); - exit = new Zone(exit_roi_width, exit_roi_height, 60); - } +void Roode::update() { + if (distance_entry != nullptr) { + distance_entry->publish_state(entry->getDistance()); + } + if (distance_exit != nullptr) { + distance_exit->publish_state(exit->getDistance()); + } +} - current_zone = entry; - } - uint16_t Roode::getAlternatingZoneDistances() - { - sensor_status += this->current_zone->readDistance(distanceSensor); - App.feed_wdt(); - return sensor_status; - } +void Roode::loop() { + // unsigned long start = micros(); + getAlternatingZoneDistances(); + // uint16_t samplingDistance = sampling(this->current_zone); + doPathTracking(this->current_zone); + handleSensorStatus(); + this->current_zone = this->current_zone == this->entry ? this->exit : this->entry; + // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", + // entry->getDistance(Roode::distanceSensor, Roode::sensor_status), + // exit->getDistance(Roode::distanceSensor, Roode::sensor_status)); unsigned + // long end = micros(); unsigned long delta = end - start; ESP_LOGI("Roode + // loop", "loop took %lu microseconds", delta); +} - void Roode::doPathTracking(Zone *zone) - { - static int PathTrack[] = {0, 0, 0, 0}; - static int PathTrackFillingSize = 1; // init this to 1 as we start from state where nobody is any of the zones - static int LeftPreviousStatus = NOBODY; - static int RightPreviousStatus = NOBODY; - int CurrentZoneStatus = NOBODY; - int AllZonesCurrentStatus = 0; - int AnEventHasOccured = 0; +bool Roode::handleSensorStatus() { + ESP_LOGD(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) { + status_sensor->publish_state(sensor_status); + } + check_status = true; + } + if (sensor_status < 28 && sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Ranging failed with an error. status: %d", sensor_status); + status_sensor->publish_state(sensor_status); + check_status = false; + } - uint16_t Distances[2][samples]; - uint16_t MinDistance; - uint8_t i; - if (DistancesTableSize[zone->getZoneId()] < samples) - { - ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zone->getZoneId(), zone->getDistance()); - Distances[zone->getZoneId()][DistancesTableSize[zone->getZoneId()]] = zone->getDistance(); - DistancesTableSize[zone->getZoneId()]++; - } - else - { - for (i = 1; i < samples; i++) - Distances[zone->getZoneId()][i - 1] = Distances[zone->getZoneId()][i]; - Distances[zone->getZoneId()][samples - 1] = zone->getDistance(); - ESP_LOGD(SETUP, "Distances[%d][samples - 1] = %d", zone->getZoneId(), Distances[zone->getZoneId()][samples - 1]); - } - // pick up the min distance - MinDistance = Distances[zone->getZoneId()][0]; - if (DistancesTableSize[zone->getZoneId()] >= 2) - { - for (i = 1; i < DistancesTableSize[zone->getZoneId()]; i++) - { - if (Distances[zone->getZoneId()][i] < MinDistance) - MinDistance = Distances[zone->getZoneId()][i]; - } - } + last_sensor_status = sensor_status; + sensor_status = VL53L1_ERROR_NONE; + return check_status; +} - // PathTrack algorithm - if (MinDistance < zone->getMaxThreshold() && MinDistance > zone->getMinThreshold()) - { - // Someone is in the sensing area - CurrentZoneStatus = SOMEONE; - if (presence_sensor != nullptr) - { - presence_sensor->publish_state(true); - } - } +void Roode::createEntryAndExitZone() { + if (advised_sensor_orientation_) { + entry = new Zone(entry_roi_width, entry_roi_height, 167, samples); + exit = new Zone(exit_roi_width, exit_roi_height, 231, samples); + } else { + entry = new Zone(entry_roi_width, entry_roi_height, 195, samples); + exit = new Zone(exit_roi_width, exit_roi_height, 60, samples); + } - // left zone - if (zone == (this->invert_direction_ ? this->exit : this->entry)) - { - if (CurrentZoneStatus != LeftPreviousStatus) - { - // event in left zone has occured - AnEventHasOccured = 1; + current_zone = entry; +} +VL53L1_Error Roode::getAlternatingZoneDistances() { + sensor_status += this->current_zone->readDistance(distanceSensor); + App.feed_wdt(); + return sensor_status; +} - if (CurrentZoneStatus == SOMEONE) - { - AllZonesCurrentStatus += 1; - } - // need to check right zone as well ... - if (RightPreviousStatus == SOMEONE) - { - // event in right zone has occured - AllZonesCurrentStatus += 2; - } - // remember for next time - LeftPreviousStatus = CurrentZoneStatus; - } - } - // right zone - else - { - if (CurrentZoneStatus != RightPreviousStatus) - { +void Roode::doPathTracking(Zone *zone) { + static int PathTrack[] = {0, 0, 0, 0}; + static int PathTrackFillingSize = 1; // init this to 1 as we start from state + // where nobody is any of the zones + static int LeftPreviousStatus = NOBODY; + static int RightPreviousStatus = NOBODY; + int CurrentZoneStatus = NOBODY; + int AllZonesCurrentStatus = 0; + int AnEventHasOccured = 0; + // PathTrack algorithm + uint16_t sampledDistance = zone->getMinDistance(); + if (sampledDistance == 0) { + return; + } + if (sampledDistance < zone->getMaxThreshold() && sampledDistance > zone->getMinThreshold()) { + // Someone is in the sensing area + CurrentZoneStatus = SOMEONE; + if (presence_sensor != nullptr) { + presence_sensor->publish_state(true); + } + } - // event in right zone has occured - AnEventHasOccured = 1; - if (CurrentZoneStatus == SOMEONE) - { - AllZonesCurrentStatus += 2; - } - // need to check left zone as well ... - if (LeftPreviousStatus == SOMEONE) - { - // event in left zone has occured - AllZonesCurrentStatus += 1; - } - // remember for next time - RightPreviousStatus = CurrentZoneStatus; - } - } + // left zone + if (zone == (this->invert_direction_ ? this->exit : this->entry)) { + if (CurrentZoneStatus != LeftPreviousStatus) { + // event in left zone has occured + AnEventHasOccured = 1; - // if an event has occured - if (AnEventHasOccured) - { - delay(1); - if (PathTrackFillingSize < 4) - { - PathTrackFillingSize++; - } + if (CurrentZoneStatus == SOMEONE) { + AllZonesCurrentStatus += 1; + } + // need to check right zone as well ... + if (RightPreviousStatus == SOMEONE) { + // event in right zone has occured + AllZonesCurrentStatus += 2; + } + // remember for next time + LeftPreviousStatus = CurrentZoneStatus; + } + } + // right zone + else { + if (CurrentZoneStatus != RightPreviousStatus) { + // event in right zone has occured + AnEventHasOccured = 1; + if (CurrentZoneStatus == SOMEONE) { + AllZonesCurrentStatus += 2; + } + // need to check left zone as well ... + if (LeftPreviousStatus == SOMEONE) { + // event in left zone has occured + AllZonesCurrentStatus += 1; + } + // remember for next time + RightPreviousStatus = CurrentZoneStatus; + } + } - // if nobody anywhere lets check if an exit or entry has happened - if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) - { - delay(1); - // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 3 2) and last event is 0 (nobobdy anywhere) - if (PathTrackFillingSize == 4) - { - // check exit or entry. no need to check PathTrack[0] == 0 , it is always the case + // if an event has occured + if (AnEventHasOccured) { + delay(1); + ESP_LOGE(TAG, "Event has occured, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); + if (PathTrackFillingSize < 4) { + PathTrackFillingSize++; + } - if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) - { - // This an exit - ESP_LOGI("Roode pathTracking", "Exit detected."); - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; - this->updateCounter(-1); - if (entry_exit_event_sensor != nullptr) - { - entry_exit_event_sensor->publish_state("Exit"); - } - } - else if ((PathTrack[1] == 2) && (PathTrack[2] == 3) && (PathTrack[3] == 1)) - { - // This an entry - ESP_LOGI("Roode pathTracking", "Entry detected."); - this->updateCounter(1); - if (entry_exit_event_sensor != nullptr) - { - entry_exit_event_sensor->publish_state("Entry"); - } - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; - } - else - { - // reset the table filling size also in case of unexpected path - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; - } - } + // if nobody anywhere lets check if an exit or entry has happened + if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { + delay(1); + ESP_LOGE(TAG, "Nobody anywhere, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); + // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 + // 3 2) and last event is 0 (nobobdy anywhere) + if (PathTrackFillingSize == 4) { + // check exit or entry. no need to check PathTrack[0] == 0 , it is + // always the case - PathTrackFillingSize = 1; - } - else - { - // update PathTrack - // example of PathTrack update - // 0 - // 0 1 - // 0 1 3 - // 0 1 3 1 - // 0 1 3 3 - // 0 1 3 2 ==> if next is 0 : check if exit - PathTrack[PathTrackFillingSize - 1] = AllZonesCurrentStatus; - } - } - if (presence_sensor != nullptr) - { - if (CurrentZoneStatus == NOBODY && LeftPreviousStatus == NOBODY && RightPreviousStatus == NOBODY) - { - // nobody is in the sensing area - presence_sensor->publish_state(false); - } - } - } - void Roode::updateCounter(int delta) - { - if (this->people_counter == nullptr) - { - return; - } - auto next = this->people_counter->state + (float)delta; - ESP_LOGI(TAG, "Updating people count: %d", (int)next); - this->people_counter->set(next); - } - void Roode::recalibration() - { - calibrateZones(distanceSensor); - } - void Roode::setSensorMode(int sensor_mode, int new_timing_budget) - { - switch (sensor_mode) - { - case 0: // short mode - time_budget_in_ms = time_budget_in_ms_short; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Short); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); - } - ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); - break; - case 1: // medium mode - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); - break; - case 2: // medium_long mode - time_budget_in_ms = time_budget_in_ms_medium_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 3: // long mode - time_budget_in_ms = time_budget_in_ms_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 4: // max mode - time_budget_in_ms = time_budget_in_ms_max; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); - break; - case 5: // custom mode - time_budget_in_ms = new_timing_budget; - delay_between_measurements = new_timing_budget + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); - break; - default: - break; - } - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); - if (sensor_status != 0) - { - ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); - } + if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) { + // This an exit + ESP_LOGI("Roode pathTracking", "Exit detected."); + DistancesTableSize[0] = 0; + DistancesTableSize[1] = 0; + this->updateCounter(-1); + if (entry_exit_event_sensor != nullptr) { + entry_exit_event_sensor->publish_state("Exit"); + } + } else if ((PathTrack[1] == 2) && (PathTrack[2] == 3) && (PathTrack[3] == 1)) { + // This an entry + ESP_LOGI("Roode pathTracking", "Entry detected."); + this->updateCounter(1); + if (entry_exit_event_sensor != nullptr) { + entry_exit_event_sensor->publish_state("Entry"); + } + DistancesTableSize[0] = 0; + DistancesTableSize[1] = 0; + } else { + // reset the table filling size also in case of unexpected path + DistancesTableSize[0] = 0; + DistancesTableSize[1] = 0; } + } - void Roode::setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance) - { - if (average_entry_zone_distance <= short_distance_threshold || average_exit_zone_distance <= short_distance_threshold) - { - setSensorMode(0); - } + PathTrackFillingSize = 1; + } else { + // update PathTrack + // example of PathTrack update + // 0 + // 0 1 + // 0 1 3 + // 0 1 3 1 + // 0 1 3 3 + // 0 1 3 2 ==> if next is 0 : check if exit + PathTrack[PathTrackFillingSize - 1] = AllZonesCurrentStatus; + } + } + if (presence_sensor != nullptr) { + if (CurrentZoneStatus == NOBODY && LeftPreviousStatus == NOBODY && RightPreviousStatus == NOBODY) { + // nobody is in the sensing area + presence_sensor->publish_state(false); + } + } +} +void Roode::updateCounter(int delta) { + if (this->people_counter == nullptr) { + return; + } + auto next = this->people_counter->state + (float) delta; + ESP_LOGI(TAG, "Updating people count: %d", (int) next); + this->people_counter->set(next); +} +void Roode::recalibration() { calibrateZones(distanceSensor); } +void Roode::setSensorMode(int sensor_mode, int new_timing_budget) { + switch (sensor_mode) { + case 0: // short mode + time_budget_in_ms = time_budget_in_ms_short; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Short); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); + } + ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); + break; + case 1: // medium mode + time_budget_in_ms = time_budget_in_ms_medium; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); + break; + case 2: // medium_long mode + time_budget_in_ms = time_budget_in_ms_medium_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 3: // long mode + time_budget_in_ms = time_budget_in_ms_long; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); + break; + case 4: // max mode + time_budget_in_ms = time_budget_in_ms_max; + delay_between_measurements = time_budget_in_ms + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); + break; + case 5: // custom mode + time_budget_in_ms = new_timing_budget; + delay_between_measurements = new_timing_budget + 5; + sensor_status = distanceSensor.SetDistanceMode(Long); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); + } + ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); + break; + default: + break; + } + sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); + if (sensor_status != 0) { + ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); + } +} - if ((average_entry_zone_distance > short_distance_threshold && average_entry_zone_distance <= medium_distance_threshold) || (average_exit_zone_distance > short_distance_threshold && average_exit_zone_distance <= medium_distance_threshold)) - { - setSensorMode(1); - } +void Roode::setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance) { + if (average_entry_zone_distance <= short_distance_threshold || + average_exit_zone_distance <= short_distance_threshold) { + setSensorMode(0); + } - if ((average_entry_zone_distance > medium_distance_threshold && average_entry_zone_distance <= medium_long_distance_threshold) || (average_exit_zone_distance > medium_distance_threshold && average_exit_zone_distance <= medium_long_distance_threshold)) - { - setSensorMode(2); - } - if ((average_entry_zone_distance > medium_long_distance_threshold && average_entry_zone_distance <= long_distance_threshold) || (average_exit_zone_distance > medium_long_distance_threshold && average_exit_zone_distance <= long_distance_threshold)) - { - setSensorMode(3); - } - if (average_entry_zone_distance > long_distance_threshold || average_exit_zone_distance > long_distance_threshold) - { - setSensorMode(4); - } - } + if ((average_entry_zone_distance > short_distance_threshold && + average_entry_zone_distance <= medium_distance_threshold) || + (average_exit_zone_distance > short_distance_threshold && + average_exit_zone_distance <= medium_distance_threshold)) { + setSensorMode(1); + } - void Roode::calibrateZones(VL53L1X_ULD distanceSensor) - { - ESP_LOGI(SETUP, "Calibrating sensor zone"); - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - distanceSensor.SetDistanceMode(Long); - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); + if ((average_entry_zone_distance > medium_distance_threshold && + average_entry_zone_distance <= medium_long_distance_threshold) || + (average_exit_zone_distance > medium_distance_threshold && + average_exit_zone_distance <= medium_long_distance_threshold)) { + setSensorMode(2); + } + if ((average_entry_zone_distance > medium_long_distance_threshold && + average_entry_zone_distance <= long_distance_threshold) || + (average_exit_zone_distance > medium_long_distance_threshold && + average_exit_zone_distance <= long_distance_threshold)) { + setSensorMode(3); + } + if (average_entry_zone_distance > long_distance_threshold || average_exit_zone_distance > long_distance_threshold) { + setSensorMode(4); + } +} - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, sensor_status); - } - int entry_threshold = entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - int exit_threshold = exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - setCorrectDistanceSettings(entry_threshold, exit_threshold); - if (roi_calibration_) - { - entry->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); - entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - exit->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); - exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - } - int hundred_threshold_zone_0 = entry->getMaxThreshold() / 100; - int hundred_threshold_zone_1 = exit->getMaxThreshold() / 100; - int unit_threshold_zone_0 = entry->getMaxThreshold() - 100 * hundred_threshold_zone_0; - int unit_threshold_zone_1 = exit->getMaxThreshold() - 100 * hundred_threshold_zone_1; - publishSensorConfiguration(entry, exit, true); - App.feed_wdt(); - if (min_threshold_percentage_ != 0) - { - publishSensorConfiguration(entry, exit, false); - } - } - - void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) - { - if (isMax) - { - if (max_threshold_entry_sensor != nullptr) - { - max_threshold_entry_sensor->publish_state(entry->getMaxThreshold()); - } +void Roode::calibrateZones(VL53L1X_ULD distanceSensor) { + ESP_LOGI(SETUP, "Calibrating sensor zone"); + time_budget_in_ms = time_budget_in_ms_medium; + delay_between_measurements = time_budget_in_ms + 5; + distanceSensor.SetDistanceMode(Long); + sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); - if (max_threshold_exit_sensor != nullptr) - { - max_threshold_exit_sensor->publish_state(exit->getMaxThreshold()); - } - } - else - { - if (min_threshold_entry_sensor != nullptr) - { - min_threshold_entry_sensor->publish_state(entry->getMinThreshold()); - } - if (min_threshold_exit_sensor != nullptr) - { - min_threshold_exit_sensor->publish_state(exit->getMinThreshold()); - } - } + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, + sensor_status); + } + int entry_threshold = + entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + int exit_threshold = + exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + setCorrectDistanceSettings(entry_threshold, exit_threshold); + if (roi_calibration_) { + entry->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); + entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + exit->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); + exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); + } + int hundred_threshold_zone_0 = entry->getMaxThreshold() / 100; + int hundred_threshold_zone_1 = exit->getMaxThreshold() / 100; + int unit_threshold_zone_0 = entry->getMaxThreshold() - 100 * hundred_threshold_zone_0; + int unit_threshold_zone_1 = exit->getMaxThreshold() - 100 * hundred_threshold_zone_1; + publishSensorConfiguration(entry, exit, true); + App.feed_wdt(); + if (min_threshold_percentage_ != 0) { + publishSensorConfiguration(entry, exit, false); + } +} - if (entry_roi_height_sensor != nullptr) - { - entry_roi_height_sensor->publish_state(entry->getRoiHeight()); - } - if (entry_roi_width_sensor != nullptr) - { - entry_roi_width_sensor->publish_state(entry->getRoiWidth()); - } +void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { + if (isMax) { + if (max_threshold_entry_sensor != nullptr) { + max_threshold_entry_sensor->publish_state(entry->getMaxThreshold()); + } - if (exit_roi_height_sensor != nullptr) - { - exit_roi_height_sensor->publish_state(exit->getRoiHeight()); - } - if (exit_roi_width_sensor != nullptr) - { - exit_roi_width_sensor->publish_state(exit->getRoiWidth()); - } - } + if (max_threshold_exit_sensor != nullptr) { + max_threshold_exit_sensor->publish_state(exit->getMaxThreshold()); } -} \ No newline at end of file + } else { + if (min_threshold_entry_sensor != nullptr) { + min_threshold_entry_sensor->publish_state(entry->getMinThreshold()); + } + if (min_threshold_exit_sensor != nullptr) { + min_threshold_exit_sensor->publish_state(exit->getMinThreshold()); + } + } + + if (entry_roi_height_sensor != nullptr) { + entry_roi_height_sensor->publish_state(entry->getRoiHeight()); + } + if (entry_roi_width_sensor != nullptr) { + entry_roi_width_sensor->publish_state(entry->getRoiWidth()); + } + + if (exit_roi_height_sensor != nullptr) { + exit_roi_height_sensor->publish_state(exit->getRoiHeight()); + } + if (exit_roi_width_sensor != nullptr) { + exit_roi_width_sensor->publish_state(exit->getRoiWidth()); + } +} +} // namespace roode +} // namespace esphome \ No newline at end of file diff --git a/components/roode/roode.h b/components/roode/roode.h index 707ce854..94248804 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -1,157 +1,172 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" +#include + +#include "VL53L1X_ULD.h" +#include "configuration.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/application.h" -#include "VL53L1X_ULD.h" -#include -#include "configuration.h" +#include "esphome/core/component.h" #include "zone.h" -namespace esphome -{ - namespace roode - { +namespace esphome { +namespace roode { #define NOBODY 0 #define SOMEONE 1 #define VERSION "v1.4.1-beta" -#define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 - static const char *const TAG = "Roode"; - static const char *const SETUP = "Setup"; - static const char *const CALIBRATION = "Sensor Calibration"; +#define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 +static const char *const TAG = "Roode"; +static const char *const SETUP = "Setup"; +static const char *const CALIBRATION = "Sensor Calibration"; - /* - Use the VL53L1X_SetTimingBudget function to set the TB in milliseconds. The TB values available are [15, 20, - 33, 50, 100, 200, 500]. This function must be called after VL53L1X_SetDistanceMode. - Note: 15 ms only works with Short distance mode. 100 ms is the default value. - The TB can be adjusted to improve the standard deviation (SD) of the measurement. - Increasing the TB, decreases the SD but increases the power consumption. - */ +/* +Use the VL53L1X_SetTimingBudget function to set the TB in milliseconds. The TB +values available are [15, 20, 33, 50, 100, 200, 500]. This function must be +called after VL53L1X_SetDistanceMode. Note: 15 ms only works with Short distance +mode. 100 ms is the default value. The TB can be adjusted to improve the +standard deviation (SD) of the measurement. Increasing the TB, decreases the SD +but increases the power consumption. +*/ - static int delay_between_measurements = 0; - static int time_budget_in_ms = 0; +static int delay_between_measurements = 0; +static int time_budget_in_ms = 0; - /* - Parameters which define the time between two different measurements in various modes (https://www.st.com/resource/en/datasheet/vl53l1x.pdf) - The timing budget and inter-measurement period should not be called when the sensor is - ranging. The user has to stop the ranging, change these parameters, and restart ranging - The minimum inter-measurement period must be longer than the timing budget + 4 ms. - // Lowest possible is 15ms with the ULD API (https://www.st.com/resource/en/user_manual/um2510-a-guide-to-using-the-vl53l1x-ultra-lite-driver-stmicroelectronics.pdf) - Valid values: [15,20,33,50,100,200,500] - */ - static int time_budget_in_ms_short = 15; // max range: 1.3m - static int time_budget_in_ms_medium = 33; - static int time_budget_in_ms_medium_long = 50; - static int time_budget_in_ms_long = 100; - static int time_budget_in_ms_max = 200; // max range: 4m +/* +Parameters which define the time between two different measurements in various +modes (https://www.st.com/resource/en/datasheet/vl53l1x.pdf) The timing budget +and inter-measurement period should not be called when the sensor is ranging. +The user has to stop the ranging, change these parameters, and restart ranging +The minimum inter-measurement period must be longer than the timing budget + 4 +ms. +// Lowest possible is 15ms with the ULD API +(https://www.st.com/resource/en/user_manual/um2510-a-guide-to-using-the-vl53l1x-ultra-lite-driver-stmicroelectronics.pdf) +Valid values: [15,20,33,50,100,200,500] +*/ +static int time_budget_in_ms_short = 15; // max range: 1.3m +static int time_budget_in_ms_medium = 33; +static int time_budget_in_ms_medium_long = 50; +static int time_budget_in_ms_long = 100; +static int time_budget_in_ms_max = 200; // max range: 4m - static uint8_t DistancesTableSize[2] = {0, 0}; +static uint8_t DistancesTableSize[2] = {0, 0}; - class Roode : public PollingComponent - { - public: - void setup() override; - void update() override; - void loop() override; - void dump_config() override; +class Roode : public PollingComponent { + public: + void setup() override; + void update() override; + void loop() override; + void dump_config() override; - void set_calibration_active(bool val) { calibration_active_ = val; } - void set_manual_active(bool val) { manual_active_ = val; } - void set_roi_active(bool val) { roi_active_ = val; } - void set_roi_calibration(bool val) { roi_calibration_ = val; } - void set_sensor_offset_calibration(int val) { sensor_offset_calibration_ = val; } - void set_sensor_xtalk_calibration(int val) { sensor_xtalk_calibration_ = val; } - void set_timing_budget(int timing_budget) { timing_budget_ = timing_budget; } - void set_manual_threshold(int val) { manual_threshold_ = val; } - void set_max_threshold_percentage(int val) { max_threshold_percentage_ = val; } - void set_min_threshold_percentage(int val) { min_threshold_percentage_ = val; } - void set_entry_roi_height(int height) { entry_roi_height = height; } - void set_entry_roi_width(int width) { entry_roi_width = width; } - void set_exit_roi_height(int height) { exit_roi_height = height; } - void set_exit_roi_width(int width) { exit_roi_width = width; } - void set_i2c_address(uint8_t address) { this->address_ = address; } - void set_invert_direction(bool dir) { invert_direction_ = dir; } - void set_restore_values(bool val) { restore_values_ = val; } - void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } - void set_sampling_size(uint8_t size) { samples = size; } - void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } - void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } - void set_people_counter(number::Number *counter) { this->people_counter = counter; } - void set_max_threshold_entry_sensor(sensor::Sensor *max_threshold_entry_sensor_) { max_threshold_entry_sensor = max_threshold_entry_sensor_; } - void set_max_threshold_exit_sensor(sensor::Sensor *max_threshold_exit_sensor_) { max_threshold_exit_sensor = max_threshold_exit_sensor_; } - void set_min_threshold_entry_sensor(sensor::Sensor *min_threshold_entry_sensor_) { min_threshold_entry_sensor = min_threshold_entry_sensor_; } - void set_min_threshold_exit_sensor(sensor::Sensor *min_threshold_exit_sensor_) { min_threshold_exit_sensor = min_threshold_exit_sensor_; } - void set_entry_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { entry_roi_height_sensor = roi_height_sensor_; } - void set_entry_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { entry_roi_width_sensor = roi_width_sensor_; } - void set_exit_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { exit_roi_height_sensor = roi_height_sensor_; } - void set_exit_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { exit_roi_width_sensor = roi_width_sensor_; } - void set_sensor_status_sensor(sensor::Sensor *status_sensor_) { status_sensor = status_sensor_; } - void set_presence_sensor_binary_sensor(binary_sensor::BinarySensor *presence_sensor_) { presence_sensor = presence_sensor_; } - void set_version_text_sensor(text_sensor::TextSensor *version_sensor_) { version_sensor = version_sensor_; } - void set_entry_exit_event_text_sensor(text_sensor::TextSensor *entry_exit_event_sensor_) { entry_exit_event_sensor = entry_exit_event_sensor_; } - void set_sensor_mode(int sensor_mode_) { sensor_mode = sensor_mode_; } - uint16_t getAlternatingZoneDistances(); - void doPathTracking(Zone *zone); - void recalibration(); - bool handleSensorStatus(); - Configuration sensorConfiguration; + void set_calibration_active(bool val) { calibration_active_ = val; } + void set_manual_active(bool val) { manual_active_ = val; } + void set_roi_active(bool val) { roi_active_ = val; } + void set_roi_calibration(bool val) { roi_calibration_ = val; } + void set_sensor_offset_calibration(int val) { sensor_offset_calibration_ = val; } + void set_sensor_xtalk_calibration(int val) { sensor_xtalk_calibration_ = val; } + void set_timing_budget(int timing_budget) { timing_budget_ = timing_budget; } + void set_manual_threshold(int val) { manual_threshold_ = val; } + void set_max_threshold_percentage(int val) { max_threshold_percentage_ = val; } + void set_min_threshold_percentage(int val) { min_threshold_percentage_ = val; } + void set_entry_roi_height(int height) { entry_roi_height = height; } + void set_entry_roi_width(int width) { entry_roi_width = width; } + void set_exit_roi_height(int height) { exit_roi_height = height; } + void set_exit_roi_width(int width) { exit_roi_width = width; } + void set_i2c_address(uint8_t address) { this->address_ = address; } + void set_invert_direction(bool dir) { invert_direction_ = dir; } + void set_restore_values(bool val) { restore_values_ = val; } + void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } + void set_sampling_size(uint8_t size) { samples = size; } + void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } + void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } + void set_people_counter(number::Number *counter) { this->people_counter = counter; } + void set_max_threshold_entry_sensor(sensor::Sensor *max_threshold_entry_sensor_) { + max_threshold_entry_sensor = max_threshold_entry_sensor_; + } + void set_max_threshold_exit_sensor(sensor::Sensor *max_threshold_exit_sensor_) { + max_threshold_exit_sensor = max_threshold_exit_sensor_; + } + void set_min_threshold_entry_sensor(sensor::Sensor *min_threshold_entry_sensor_) { + min_threshold_entry_sensor = min_threshold_entry_sensor_; + } + void set_min_threshold_exit_sensor(sensor::Sensor *min_threshold_exit_sensor_) { + min_threshold_exit_sensor = min_threshold_exit_sensor_; + } + void set_entry_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { entry_roi_height_sensor = roi_height_sensor_; } + void set_entry_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { entry_roi_width_sensor = roi_width_sensor_; } + void set_exit_roi_height_sensor(sensor::Sensor *roi_height_sensor_) { exit_roi_height_sensor = roi_height_sensor_; } + void set_exit_roi_width_sensor(sensor::Sensor *roi_width_sensor_) { exit_roi_width_sensor = roi_width_sensor_; } + void set_sensor_status_sensor(sensor::Sensor *status_sensor_) { status_sensor = status_sensor_; } + void set_presence_sensor_binary_sensor(binary_sensor::BinarySensor *presence_sensor_) { + presence_sensor = presence_sensor_; + } + void set_version_text_sensor(text_sensor::TextSensor *version_sensor_) { version_sensor = version_sensor_; } + void set_entry_exit_event_text_sensor(text_sensor::TextSensor *entry_exit_event_sensor_) { + entry_exit_event_sensor = entry_exit_event_sensor_; + } + void set_sensor_mode(int sensor_mode_) { sensor_mode = sensor_mode_; } + VL53L1_Error getAlternatingZoneDistances(); + void doPathTracking(Zone *zone); + void recalibration(); + bool handleSensorStatus(); + Configuration sensorConfiguration; - protected: - VL53L1X_ULD distanceSensor; - Zone *entry; - Zone *exit; - Zone *current_zone; - sensor::Sensor *distance_entry; - sensor::Sensor *distance_exit; - number::Number *people_counter; - sensor::Sensor *max_threshold_entry_sensor; - sensor::Sensor *max_threshold_exit_sensor; - sensor::Sensor *min_threshold_entry_sensor; - sensor::Sensor *min_threshold_exit_sensor; - sensor::Sensor *exit_roi_height_sensor; - sensor::Sensor *exit_roi_width_sensor; - sensor::Sensor *entry_roi_height_sensor; - sensor::Sensor *entry_roi_width_sensor; - sensor::Sensor *status_sensor; - binary_sensor::BinarySensor *presence_sensor; - text_sensor::TextSensor *version_sensor; - text_sensor::TextSensor *entry_exit_event_sensor; + protected: + VL53L1X_ULD distanceSensor; + Zone *entry; + Zone *exit; + Zone *current_zone; + sensor::Sensor *distance_entry; + sensor::Sensor *distance_exit; + number::Number *people_counter; + sensor::Sensor *max_threshold_entry_sensor; + sensor::Sensor *max_threshold_exit_sensor; + sensor::Sensor *min_threshold_entry_sensor; + sensor::Sensor *min_threshold_exit_sensor; + sensor::Sensor *exit_roi_height_sensor; + sensor::Sensor *exit_roi_width_sensor; + sensor::Sensor *entry_roi_height_sensor; + sensor::Sensor *entry_roi_width_sensor; + sensor::Sensor *status_sensor; + binary_sensor::BinarySensor *presence_sensor; + text_sensor::TextSensor *version_sensor; + text_sensor::TextSensor *entry_exit_event_sensor; - void createEntryAndExitZone(); - void calibrateZones(VL53L1X_ULD distanceSensor); - void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); - void setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); - void updateCounter(int delta); - bool calibration_active_{false}; - bool manual_active_{false}; - bool roi_active_{false}; - bool roi_calibration_{false}; - int sensor_offset_calibration_{-1}; - int sensor_xtalk_calibration_{-1}; - int sensor_mode{-1}; - bool advised_sensor_orientation_{true}; - bool sampling_active_{false}; - uint8_t samples{2}; - uint8_t address_ = 0x29; - bool invert_direction_{false}; - bool restore_values_{false}; - uint64_t max_threshold_percentage_{85}; - uint64_t min_threshold_percentage_{0}; - uint64_t manual_threshold_{2000}; - int number_attempts = 20; // TO DO: make this configurable - int timing_budget_{-1}; - int short_distance_threshold = 1300; - int medium_distance_threshold = 2000; - int medium_long_distance_threshold = 2700; - int long_distance_threshold = 3400; - int entry_roi_width{6}; - int entry_roi_height{16}; - int exit_roi_width{6}; - int exit_roi_height{16}; - }; + void createEntryAndExitZone(); + void calibrateZones(VL53L1X_ULD distanceSensor); + void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); + void setSensorMode(int sensor_mode, int timing_budget = 0); + void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); + void updateCounter(int delta); + uint16_t sampling(Zone *zone); + bool calibration_active_{false}; + bool manual_active_{false}; + bool roi_active_{false}; + bool roi_calibration_{false}; + int sensor_offset_calibration_{-1}; + int sensor_xtalk_calibration_{-1}; + int sensor_mode{-1}; + bool advised_sensor_orientation_{true}; + bool sampling_active_{false}; + uint8_t samples{2}; + uint8_t address_ = 0x29; + bool invert_direction_{false}; + bool restore_values_{false}; + uint64_t max_threshold_percentage_{85}; + uint64_t min_threshold_percentage_{0}; + uint64_t manual_threshold_{2000}; + int number_attempts = 20; // TO DO: make this configurable + int timing_budget_{-1}; + int short_distance_threshold = 1300; + int medium_distance_threshold = 2000; + int medium_long_distance_threshold = 2700; + int long_distance_threshold = 3400; + int entry_roi_width{6}; + int entry_roi_height{16}; + int exit_roi_width{6}; + int exit_roi_height{16}; +}; - } // namespace roode -} // namespace esphome +} // namespace roode +} // namespace esphome diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 29b60b50..3c89a2a6 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -1,240 +1,213 @@ #include "zone.h" static uint8_t zone_id = 0; -namespace esphome -{ - namespace roode - { - Zone::Zone(int roi_width, int roi_height, int roi_center) - { - this->id = zone_id++; - ESP_LOGD(TAG, "Zone(%d, %d, %d)", roi_width, roi_height, roi_center); - this->roi_width = roi_width; - this->roi_height = roi_height; - this->roi_center = roi_center; - this->roi = {roi_width, roi_height, roi_center}; - } +namespace esphome { +namespace roode { +Zone::Zone(int roi_width, int roi_height, int roi_center, int sample_size) { + this->id = zone_id++; + ESP_LOGD(TAG, "Zone(%d, %d, %d)", roi_width, roi_height, roi_center); + this->roi_width = roi_width; + this->roi_height = roi_height; + this->roi_center = roi_center; + this->roi = {roi_width, roi_height, roi_center}; + this->sample_size = sample_size; + this->Distances = new int[sample_size]; +} - VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) - { - last_sensor_status = sensor_status; - sensor_status += distanceSensor.SetROI(this->getRoiWidth(), this->getRoiHeight()); - sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); - sensor_status += distanceSensor.StartRanging(); +VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { + last_sensor_status = sensor_status; + sensor_status += distanceSensor.SetROI(this->getRoiWidth(), this->getRoiHeight()); + sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); + sensor_status += distanceSensor.StartRanging(); - // Wait for the measurement to be ready - uint8_t dataReady = false; - while (!dataReady) - { - sensor_status += distanceSensor.CheckForDataReady(&dataReady); - delay(1); - } + // Wait for the measurement to be ready + uint8_t dataReady = false; + while (!dataReady) { + sensor_status += distanceSensor.CheckForDataReady(&dataReady); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Data not ready yet. error code: %d", sensor_status); + return sensor_status; + } + delay(1); + } - // Get the results - sensor_status += distanceSensor.GetDistanceInMm(&distance); + // Get the results + sensor_status += distanceSensor.GetDistanceInMm(&distance); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); + return sensor_status; + } - // After reading the results reset the interrupt to be able to take another measurement - sensor_status += distanceSensor.ClearInterrupt(); - sensor_status += distanceSensor.StopRanging(); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); - return sensor_status; - } - return sensor_status; - } + // Fill sampling array + if (samples < sample_size) { + Distances[samples] = this->getDistance(); + samples++; + } else { + for (int i = 1; i < sample_size; i++) + Distances[i - 1] = Distances[i]; + Distances[sample_size - 1] = this->getDistance(); + } + min_distance = Distances[0]; + if (sample_size >= 2) { + for (int i = 1; i < sample_size; i++) { + if (Distances[i] < min_distance) { + min_distance = Distances[i]; + } + } + } - int Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, uint16_t min_threshold_percentage) - { - int *zone_distances = new int[number_attempts]; - int sum = 0; - for (int i = 0; i < number_attempts; i++) - { - this->readDistance(distanceSensor); - zone_distances[i] = this->getDistance(); - sum += zone_distances[i]; - }; - int optimized_threshold = this->getOptimizedValues(zone_distances, sum, number_attempts); - this->setMaxThreshold(optimized_threshold * max_threshold_percentage / 100); - if (min_threshold_percentage > 0) - this->setMinThreshold(optimized_threshold * min_threshold_percentage / 100); - ESP_LOGI(CALIBRATION, "Calibrated threshold for zone. zoneId: %d, threshold: %d", this->getZoneId(), optimized_threshold); - return optimized_threshold; - } - void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation) - { - // the value of the average distance is used for computing the optimal size of the ROI and consequently also the center of the two zones - int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); - int ROI_size = min(8, max(4, function_of_the_distance)); - this->updateRoi(ROI_size, ROI_size * 2); - // now we set the position of the center of the two zones - if (sensor_orientation) - { - switch (ROI_size) - { - case 4: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(150); - break; - } - this->setRoiCenter(247); + // After reading the results reset the interrupt to be able to take another + // measurement + sensor_status += distanceSensor.ClearInterrupt(); + sensor_status += distanceSensor.StopRanging(); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not stop ranging, error code: %d", sensor_status); + return sensor_status; + } - break; - case 5: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(159); - break; - } - this->setRoiCenter(239); - break; - case 6: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(159); - break; - } - this->setRoiCenter(239); - break; - case 7: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(167); - break; - } - this->setRoiCenter(231); - break; - case 8: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(167); - break; - } - this->setRoiCenter(231); - break; - } - } - else - { - switch (ROI_size) - { - case 4: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(193); - break; - } - this->setRoiCenter(58); - break; - case 5: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(194); - break; - } - this->setRoiCenter(59); - break; - case 6: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(194); - break; - } - this->setRoiCenter(59); - break; - case 7: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(195); - break; - } - this->setRoiCenter(60); - break; - case 8: - if (this->getZoneId() == 0U) - { - this->setRoiCenter(195); - break; - } - this->setRoiCenter(60); - break; - } - } - ESP_LOGI(CALIBRATION, "Calibrated ROI for zone. zoneId: %d, width: %d, height: %d, center: %d", this->getZoneId(), this->getRoiWidth(), this->getRoiHeight(), this->getRoiCenter()); - } - int Zone::getOptimizedValues(int *values, int sum, int size) - { - int sum_squared = 0; - int variance = 0; - int sd = 0; - int avg = sum / size; + return sensor_status; +} - for (int i = 0; i < size; i++) - { - sum_squared = sum_squared + (values[i] * values[i]); - App.feed_wdt(); - } - variance = sum_squared / size - (avg * avg); - sd = sqrt(variance); - ESP_LOGD(CALIBRATION, "Zone AVG: %d", avg); - ESP_LOGD(CALIBRATION, "Zone SD: %d", sd); - return avg - sd; - } +int Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, + uint16_t min_threshold_percentage) { + int *zone_distances = new int[number_attempts]; + int sum = 0; + for (int i = 0; i < number_attempts; i++) { + this->readDistance(distanceSensor); + zone_distances[i] = this->getDistance(); + sum += zone_distances[i]; + }; + int optimized_threshold = this->getOptimizedValues(zone_distances, sum, number_attempts); + this->setMaxThreshold(optimized_threshold * max_threshold_percentage / 100); + if (min_threshold_percentage > 0) + this->setMinThreshold(optimized_threshold * min_threshold_percentage / 100); + ESP_LOGI(CALIBRATION, "Calibrated threshold for zone. zoneId: %d, threshold: %d", this->getZoneId(), + optimized_threshold); + return optimized_threshold; +} +void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, + bool sensor_orientation) { + // the value of the average distance is used for computing the optimal size of + // the ROI and consequently also the center of the two zones + int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); + int ROI_size = min(8, max(4, function_of_the_distance)); + this->updateRoi(ROI_size, ROI_size * 2); + // now we set the position of the center of the two zones + if (sensor_orientation) { + switch (ROI_size) { + case 4: + if (this->getZoneId() == 0U) { + this->setRoiCenter(150); + break; + } + this->setRoiCenter(247); - uint16_t Zone::getDistance() - { - return this->distance; - } - uint16_t Zone::getRoiWidth() - { - return this->roi.width; - } - uint16_t Zone::getRoiHeight() - { - return this->roi.height; - } - uint16_t Zone::getRoiCenter() - { - return this->roi.center; - } - void Zone::setRoiWidth(uint16_t new_roi_width) - { - this->roi.width = new_roi_width; - } - void Zone::setRoiHeight(uint16_t new_roi_height) - { - this->roi.height = new_roi_height; - } - void Zone::setRoiCenter(uint16_t new_roi_center) - { - this->roi.center = new_roi_center; - } - void Zone::updateRoi(uint16_t new_width, uint16_t new_height) - { - this->roi.width = new_width; - this->roi.height = new_height; - } - void Zone::setMinThreshold(uint16_t min) - { - this->threshold.min = min; - } - void Zone::setMaxThreshold(uint16_t max) - { - this->threshold.max = max; - } - uint16_t Zone::getMinThreshold() - { - return this->threshold.min; - } - uint16_t Zone::getMaxThreshold() - { - return this->threshold.max; - } - - uint8_t Zone::getZoneId() - { - return this->id; - } + break; + case 5: + if (this->getZoneId() == 0U) { + this->setRoiCenter(159); + break; + } + this->setRoiCenter(239); + break; + case 6: + if (this->getZoneId() == 0U) { + this->setRoiCenter(159); + break; + } + this->setRoiCenter(239); + break; + case 7: + if (this->getZoneId() == 0U) { + this->setRoiCenter(167); + break; + } + this->setRoiCenter(231); + break; + case 8: + if (this->getZoneId() == 0U) { + this->setRoiCenter(167); + break; + } + this->setRoiCenter(231); + break; + } + } else { + switch (ROI_size) { + case 4: + if (this->getZoneId() == 0U) { + this->setRoiCenter(193); + break; + } + this->setRoiCenter(58); + break; + case 5: + if (this->getZoneId() == 0U) { + this->setRoiCenter(194); + break; + } + this->setRoiCenter(59); + break; + case 6: + if (this->getZoneId() == 0U) { + this->setRoiCenter(194); + break; + } + this->setRoiCenter(59); + break; + case 7: + if (this->getZoneId() == 0U) { + this->setRoiCenter(195); + break; + } + this->setRoiCenter(60); + break; + case 8: + if (this->getZoneId() == 0U) { + this->setRoiCenter(195); + break; + } + this->setRoiCenter(60); + break; } -} \ No newline at end of file + } + ESP_LOGI(CALIBRATION, "Calibrated ROI for zone. zoneId: %d, width: %d, height: %d, center: %d", this->getZoneId(), + this->getRoiWidth(), this->getRoiHeight(), this->getRoiCenter()); +} +int Zone::getOptimizedValues(int *values, int sum, int size) { + int sum_squared = 0; + int variance = 0; + int sd = 0; + int avg = sum / size; + + for (int i = 0; i < size; i++) { + sum_squared = sum_squared + (values[i] * values[i]); + App.feed_wdt(); + } + variance = sum_squared / size - (avg * avg); + sd = sqrt(variance); + ESP_LOGD(CALIBRATION, "Zone AVG: %d", avg); + ESP_LOGD(CALIBRATION, "Zone SD: %d", sd); + return avg - sd; +} + +uint16_t Zone::getDistance() { return this->distance; } +uint16_t Zone::getMinDistance() { return this->min_distance; } +uint16_t Zone::getRoiWidth() { return this->roi.width; } +uint16_t Zone::getRoiHeight() { return this->roi.height; } +uint16_t Zone::getRoiCenter() { return this->roi.center; } +void Zone::setRoiWidth(uint16_t new_roi_width) { this->roi.width = new_roi_width; } +void Zone::setRoiHeight(uint16_t new_roi_height) { this->roi.height = new_roi_height; } +void Zone::setRoiCenter(uint16_t new_roi_center) { this->roi.center = new_roi_center; } +void Zone::updateRoi(uint16_t new_width, uint16_t new_height) { + this->roi.width = new_width; + this->roi.height = new_height; +} +void Zone::setMinThreshold(uint16_t min) { this->threshold.min = min; } +void Zone::setMaxThreshold(uint16_t max) { this->threshold.max = max; } +uint16_t Zone::getMinThreshold() { return this->threshold.min; } +uint16_t Zone::getMaxThreshold() { return this->threshold.max; } + +uint8_t Zone::getZoneId() { return this->id; } +} // namespace roode +} // namespace esphome \ No newline at end of file diff --git a/components/roode/zone.h b/components/roode/zone.h index 8e1ea7bb..800cec83 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -1,62 +1,64 @@ #pragma once -#include "VL53L1X_ULD.h" #include -#include "esphome/core/log.h" + +#include "VL53L1X_ULD.h" #include "configuration.h" +#include "esphome/core/log.h" static const char *const TAG = "Zone"; static const char *const CALIBRATION = "Zone calibration"; -namespace esphome -{ - namespace roode - { - struct ROI - { - uint16_t width; - uint16_t height; - uint16_t center; - }; - struct Threshold - { - uint16_t min; - uint16_t min_percentage; - uint16_t max; - uint16_t max_percentage; - }; - class Zone - { - public: - Zone(int roi_width, int roi_height, int roi_center); - VL53L1_Error readDistance(VL53L1X_ULD &distanceSensor); - int calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, uint16_t min_threshold_percentage); - void roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation); - uint16_t calibrateRoi(); - uint16_t getMinThreshold(); - uint16_t getMaxThreshold(); - uint16_t getRoiWidth(); - uint16_t getRoiHeight(); - uint16_t getRoiCenter(); - void setMinThreshold(uint16_t min); - void setMaxThreshold(uint16_t max); - void setRoiWidth(uint16_t new_roi_width); - void setRoiHeight(uint16_t new_roi_height); - void setRoiCenter(uint16_t new_roi_center); - void updateRoi(uint16_t new_width, uint16_t new_height); - uint8_t getZoneId(); - uint16_t getDistance(); - bool handleSensorStatus(); +namespace esphome { +namespace roode { +struct ROI { + uint16_t width; + uint16_t height; + uint16_t center; +}; +struct Threshold { + uint16_t min; + uint16_t min_percentage; + uint16_t max; + uint16_t max_percentage; +}; +class Zone { + public: + Zone(int roi_width, int roi_height, int roi_center, int sample_size); + VL53L1_Error readDistance(VL53L1X_ULD &distanceSensor); + int calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, + uint16_t min_threshold_percentage); + void roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation); + uint16_t calibrateRoi(); + uint16_t getMinThreshold(); + uint16_t getMaxThreshold(); + uint16_t getRoiWidth(); + uint16_t getRoiHeight(); + uint16_t getRoiCenter(); + void setMinThreshold(uint16_t min); + void setMaxThreshold(uint16_t max); + void setRoiWidth(uint16_t new_roi_width); + void setRoiHeight(uint16_t new_roi_height); + void setRoiCenter(uint16_t new_roi_center); + void updateRoi(uint16_t new_width, uint16_t new_height); + uint8_t getZoneId(); + uint16_t getDistance(); + uint16_t getMinDistance(); + bool handleSensorStatus(); - protected: - int getSum(int *values, int size); - int getOptimizedValues(int *values, int sum, int size); - ROI roi; - Threshold threshold; - uint16_t roi_width; - uint16_t roi_height; - uint16_t roi_center; - uint8_t id; - uint16_t distance; - }; - } -} + protected: + int getSum(int *values, int size); + int getOptimizedValues(int *values, int sum, int size); + ROI roi; + Threshold threshold; + uint16_t roi_width; + uint16_t roi_height; + uint16_t roi_center; + uint8_t id; + uint16_t distance; + int *Distances; + uint8_t sample_size; + uint8_t samples; + uint16_t min_distance; +}; +} // namespace roode +} // namespace esphome From 9177ffbd3ce15ecf79a23942bd589bb8e9f4ea3e Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 10 Jan 2022 11:15:50 +0100 Subject: [PATCH 25/54] Set correct log levels --- components/roode/roode.cpp | 4 ++-- components/roode/zone.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 228b2eee..55f6343a 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -188,7 +188,7 @@ void Roode::doPathTracking(Zone *zone) { // if an event has occured if (AnEventHasOccured) { delay(1); - ESP_LOGE(TAG, "Event has occured, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); + ESP_LOGD(TAG, "Event has occured, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); if (PathTrackFillingSize < 4) { PathTrackFillingSize++; } @@ -196,7 +196,7 @@ void Roode::doPathTracking(Zone *zone) { // if nobody anywhere lets check if an exit or entry has happened if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { delay(1); - ESP_LOGE(TAG, "Nobody anywhere, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); + ESP_LOGD(TAG, "Nobody anywhere, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 // 3 2) and last event is 0 (nobobdy anywhere) if (PathTrackFillingSize == 4) { diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 3c89a2a6..5879737d 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -25,7 +25,7 @@ VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { while (!dataReady) { sensor_status += distanceSensor.CheckForDataReady(&dataReady); if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Data not ready yet. error code: %d", sensor_status); + ESP_LOGD(TAG, "Data not ready yet. error code: %d", sensor_status); return sensor_status; } delay(1); @@ -34,7 +34,7 @@ VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { // Get the results sensor_status += distanceSensor.GetDistanceInMm(&distance); if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not get distance, error code: %d", sensor_status); + ESP_LOGD(TAG, "Could not get distance, error code: %d", sensor_status); return sensor_status; } @@ -61,7 +61,7 @@ VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { sensor_status += distanceSensor.ClearInterrupt(); sensor_status += distanceSensor.StopRanging(); if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not stop ranging, error code: %d", sensor_status); + ESP_LOGD(TAG, "Could not stop ranging, error code: %d", sensor_status); return sensor_status; } From 9411f59de1d0ab20780bf1307c74a4eb64c0bfc7 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 10 Jan 2022 16:44:55 +0100 Subject: [PATCH 26/54] Remove unnecessary check --- components/roode/roode.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 55f6343a..929ec79c 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -138,9 +138,6 @@ void Roode::doPathTracking(Zone *zone) { int AnEventHasOccured = 0; // PathTrack algorithm uint16_t sampledDistance = zone->getMinDistance(); - if (sampledDistance == 0) { - return; - } if (sampledDistance < zone->getMaxThreshold() && sampledDistance > zone->getMinThreshold()) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; From 841302a8fd3e1e8d6c7c42150207402be3af799f Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 10 Jan 2022 16:47:47 +0100 Subject: [PATCH 27/54] Rename files once again --- .github/workflows/main.yml | 4 ++-- peopleCounter.yaml => peopleCounter8266.yaml | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename peopleCounter.yaml => peopleCounter8266.yaml (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93ddde05..42bd65fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: python -m pip install --upgrade pip pip install -U esphome - name: Validate ESP32 Config - run: esphome config peopleCounterDev.yaml + run: esphome config peopleCounter32Dev.yaml - name: Build ESP32 - run: esphome compile peopleCounterDev.yaml + run: esphome compile peopleCounter32Dev.yaml diff --git a/peopleCounter.yaml b/peopleCounter8266.yaml similarity index 100% rename from peopleCounter.yaml rename to peopleCounter8266.yaml From d70c7760278f8d20ff63e21317bd98939cdb5f6e Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Mon, 10 Jan 2022 16:49:23 +0100 Subject: [PATCH 28/54] Add pillow package --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 42bd65fc..a9f3b68d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,7 @@ jobs: 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 From d3519cd272a96a3fc1db5e2f95be912fed8b16c6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 10 Jan 2022 23:36:27 -0600 Subject: [PATCH 29/54] Refactor roode configuration --- components/roode/__init__.py | 381 +++++++++++++++++---------------- components/roode/orientation.h | 13 ++ components/roode/ranging.h | 34 +++ components/roode/roode.cpp | 251 +++++++++------------- components/roode/roode.h | 62 ++---- components/roode/zone.cpp | 174 +++++---------- components/roode/zone.h | 63 +++--- peopleCounter32Dev.yaml | 39 ++-- 8 files changed, 474 insertions(+), 543 deletions(-) create mode 100644 components/roode/orientation.h create mode 100644 components/roode/ranging.h diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 2441f06c..6bbc2841 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -1,11 +1,17 @@ from re import I +from typing import Dict, Union, Any import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_HEIGHT, CONF_ID, - CONF_OR, + CONF_INTERRUPT, + CONF_INVERT, + CONF_OFFSET, + CONF_PINS, + CONF_WIDTH, ) - +import esphome.pins as pins # DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor", "number"] @@ -16,208 +22,217 @@ roode_ns = cg.esphome_ns.namespace("roode") Roode = roode_ns.class_("Roode", cg.PollingComponent) -CONF_ROI_HEIGHT = "roi_height" -CONF_ROI_WIDTH = "roi_width" -CONF_ADVISED_SENSOR_ORIENTATION = "advised_sensor_orientation" +CONF_AUTO = "auto" +CONF_ORIENTATION = "orientation" CONF_CALIBRATION = "calibration" -CONF_ROI_CALIBRATION = "roi_calibration" -CONF_INVERT_DIRECTION = "invert_direction" -CONF_MAX_THRESHOLD_PERCENTAGE = "max_threshold_percentage" -CONF_MIN_THRESHOLD_PERCENTAGE = "min_threshold_percentage" -CONF_MANUAL_THRESHOLD = "manual_threshold" -CONF_THRESHOLD_PERCENTAGE = "threshold_percentage" +CONF_DETECTION_THRESHOLDS = "detection_thresholds" +CONF_DISTANCE_MODE = "distance_mode" CONF_I2C_ADDRESS = "i2c_address" -CONF_SENSOR_MODE = "sensor_mode" -CONF_MANUAL = "manual" -CONF_MANUAL_ACTIVE = "manual_active" -CONF_CALIBRATION_ACTIVE = "calibration_active" -CONF_TIMING_BUDGET = "timing_budget" -CONF_SAMPLING = "sampling" -CONF_SAMPLING_SIZE = "size" -CONF_ROI = "roi" -CONF_ROI_ACTIVE = "roi_active" -CONF_ZONES = "zones" CONF_ENTRY_ZONE = "entry" CONF_EXIT_ZONE = "exit" -CONF_SENSOR_OFFSET_CALIBRATION = "sensor_offset_calibration" -CONF_SENSOR_XTALK_CALIBRATION = "sensor_xtalk_calibration" -TYPES = [CONF_ADVISED_SENSOR_ORIENTATION, CONF_I2C_ADDRESS] +CONF_CENTER = "center" +CONF_MAX = "max" +CONF_MIN = "min" +CONF_ROI = "roi" +CONF_SAMPLING = "sampling" +CONF_TIMING_BUDGET = "timing_budget" +CONF_XSHUT = "xshut" +CONF_XTALK = "xtalk" +CONF_ZONES = "zones" + +Orientation = roode_ns.enum("Orientation") +ORIENTATION_VALUES = { + "parallel": Orientation.Parallel, + "perpendicular": Orientation.Perpendicular, +} + +DistanceMode = cg.global_ns.enum("EDistanceMode") +DISTANCE_MODES = { + CONF_AUTO: CONF_AUTO, + "short": DistanceMode.Short, + "long": DistanceMode.Long, +} + +TIMING_BUDGET_ALIASES = { + CONF_AUTO: CONF_AUTO, + "shortest": 15, + "short": 20, + "medium": 33, + "medium long": 50, + "long": 100, + "longer": 200, + "longest": 500, + "15": 15, + "20": 20, + "33": 33, + "50": 50, + "100": 100, + "200": 200, + "500": 500, +} + +int16_t = cv.int_range(min=-32768, max=32768) # signed +roi_range = cv.int_range(min=4, max=16) + + +def NullableSchema(*args, default: Any = None, **kwargs): + """ + Same as Schema but will convert nulls to empty objects. Useful when all the schema keys are optional. + Allows YAML lines to be commented out leaving an "empty dict" which is mistakenly parsed as None. + """ + + def none_to_empty(value): + if value is None: + return {} if default is None else default + raise cv.Invalid("Expected none") + + return cv.Any(cv.Schema(*args, **kwargs), none_to_empty) + + +ROI_SCHEMA = cv.Any( + NullableSchema( + { + cv.Optional(CONF_HEIGHT): roi_range, + cv.Optional(CONF_WIDTH): roi_range, + cv.Optional(CONF_CENTER): cv.uint8_t, + }, + ), + cv.one_of(CONF_AUTO), +) + +THRESHOLDS_SCHEMA = NullableSchema( + { + cv.Optional(CONF_MIN): cv.Any(cv.uint16_t, cv.percentage), + cv.Optional(CONF_MAX): cv.Any(cv.uint16_t, cv.percentage), + } +) + +ZONE_SCHEMA = NullableSchema( + { + cv.Optional(CONF_ROI, default={}): ROI_SCHEMA, + cv.Optional(CONF_DETECTION_THRESHOLDS, default={}): THRESHOLDS_SCHEMA, + } +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Roode), - cv.Optional(CONF_ADVISED_SENSOR_ORIENTATION, default="true"): cv.boolean, - cv.Optional(CONF_I2C_ADDRESS, default=0x29): cv.uint8_t, - cv.Optional(CONF_SAMPLING, default=2): cv.Any( - cv.int_range(1, 255), - cv.Schema( - { - cv.Optional(CONF_SAMPLING_SIZE, default=2): cv.int_range(1, 255), - } - ), - ), - cv.Exclusive( - CONF_CALIBRATION, - "mode", - f"Only one mode, {CONF_MANUAL} or {CONF_CALIBRATION} is usable", - ): cv.Schema( + cv.Optional(CONF_I2C_ADDRESS, default=0x29): cv.i2c_address, + cv.Optional(CONF_PINS, default={}): NullableSchema( { - cv.Optional(CONF_CALIBRATION_ACTIVE, default="true"): cv.boolean, - cv.Optional(CONF_MAX_THRESHOLD_PERCENTAGE, default=85): cv.int_range( - min=50, max=100 - ), - cv.Optional(CONF_MIN_THRESHOLD_PERCENTAGE, default=0): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_ROI_CALIBRATION, default="false"): cv.boolean, - cv.Optional(CONF_SENSOR_OFFSET_CALIBRATION, default=-1): cv.int_, - cv.Optional(CONF_SENSOR_XTALK_CALIBRATION, default=-1): cv.int_, + cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.gpio_output_pin_schema, } ), - cv.Exclusive( - CONF_MANUAL, - "mode", - f"Only one mode, {CONF_MANUAL} or {CONF_CALIBRATION} is usable", - ): cv.Schema( + cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( { - cv.Optional(CONF_MANUAL_ACTIVE, default="true"): cv.boolean, - cv.Optional(CONF_TIMING_BUDGET, default=15): cv.one_of( - 15, 20, 33, 50, 100, 200, 500 + cv.Optional(CONF_DISTANCE_MODE, default=CONF_AUTO): cv.enum( + DISTANCE_MODES ), - cv.Inclusive( - CONF_SENSOR_MODE, - "manual_mode", - f"{CONF_SENSOR_MODE}, {CONF_ROI_HEIGHT}, {CONF_ROI_WIDTH} and {CONF_MANUAL_THRESHOLD} must be used together", - ): cv.int_range(min=-1, max=5), - cv.Inclusive( - CONF_MANUAL_THRESHOLD, - "manual_mode", - f"{CONF_SENSOR_MODE}, {CONF_ROI_HEIGHT}, {CONF_ROI_WIDTH} and {CONF_MANUAL_THRESHOLD} must be used together", - ): cv.int_range(min=40, max=4000), - } - ), - cv.Optional(CONF_ROI): cv.Schema( - { - cv.Optional(CONF_ROI_ACTIVE, default="true"): cv.boolean, - cv.Optional(CONF_ROI_HEIGHT, default=16): cv.int_range(min=4, max=16), - cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range(min=4, max=16), + cv.Optional(CONF_TIMING_BUDGET, default=CONF_AUTO): cv.enum( + TIMING_BUDGET_ALIASES + ), + cv.Optional(CONF_XTALK): cv.uint16_t, + cv.Optional(CONF_OFFSET): int16_t, } ), - cv.Optional(CONF_ZONES): cv.Schema( + cv.Optional(CONF_ORIENTATION, default="parallel"): cv.enum(ORIENTATION_VALUES), + cv.Optional(CONF_SAMPLING, default=2): cv.All(cv.uint8_t, cv.Range(min=1)), + cv.Optional(CONF_ROI, default={}): ROI_SCHEMA, + cv.Optional(CONF_DETECTION_THRESHOLDS, default={}): THRESHOLDS_SCHEMA, + cv.Optional(CONF_ZONES, default={}): NullableSchema( { - cv.Optional(CONF_INVERT_DIRECTION, default="false"): cv.boolean, - cv.Optional(CONF_ENTRY_ZONE): cv.Schema( - { - cv.Optional(CONF_ROI): cv.Schema( - { - cv.Optional(CONF_ROI_HEIGHT, default=16): cv.int_range( - min=4, max=16 - ), - cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range( - min=4, max=16 - ), - } - ), - } - ), - cv.Optional(CONF_EXIT_ZONE): cv.Schema( - { - cv.Optional(CONF_ROI): cv.Schema( - { - cv.Optional(CONF_ROI_HEIGHT, default=16): cv.int_range( - min=4, max=16 - ), - cv.Optional(CONF_ROI_WIDTH, default=6): cv.int_range( - min=4, max=16 - ), - } - ), - } - ), + cv.Optional(CONF_INVERT, default=False): cv.boolean, + cv.Optional(CONF_ENTRY_ZONE, default={}): ZONE_SCHEMA, + cv.Optional(CONF_EXIT_ZONE, default={}): ZONE_SCHEMA, } ), } -).extend(cv.polling_component_schema("100ms")) - - -def validate_roi_settings(config): - roi = config.get(CONF_ZONES) - entry = roi.get(CONF_ENTRY_ZONE) - exit = roi.get(CONF_EXIT_ZONE) - manual = config.get(CONF_MANUAL) - if CONF_CALIBRATION in config: - roi_calibration = config.get(CONF_CALIBRATION).get(CONF_ROI_CALIBRATION) - if roi_calibration == True and (entry != None or exit != None): - raise cv.Invalid( - "ROI calibration cannot be used with manual ROI width and height" - ) - if roi_calibration == False and (entry == None or exit == None): - raise cv.Invalid("You need to set the ROI manually or use ROI calibration") - if manual != None and (roi == None or entry == None or exit == None): - raise cv.Invalid("You need to set the ROI manually if manual mode is active") - - -async def setup_conf(config, key, hub): - if key in config: - cg.add(getattr(hub, f"set_{key}")(config[key])) - - -def setup_manual_mode(config, hub): - manual = config[CONF_MANUAL] - for key in manual: - cg.add(getattr(hub, f"set_{key}")(manual[key])) - - -def setup_calibration_mode(config, hub): - calibration = config[CONF_CALIBRATION] - for key in calibration: - cg.add(getattr(hub, f"set_{key}")(calibration[key])) - - -def setup_manual_roi(config, hub): - roi = config[CONF_ROI] - for key in roi: - cg.add(getattr(hub, f"set_{key}")(roi[key])) - - -def setup_sampling(config, hub): - sampling = config[CONF_SAMPLING] - if isinstance(sampling, int): - cg.add(getattr(hub, f"set_sampling_{CONF_SAMPLING_SIZE}")(sampling)) - else: - for key in sampling: - cg.add(getattr(hub, f"set_sampling_{CONF_SAMPLING_SIZE}")(sampling[key])) - - -def setup_zones(config, hub): - zones = config[CONF_ZONES] - for zone in zones: - if CONF_ENTRY_ZONE in zone or CONF_EXIT_ZONE in zone: - roi = zones[zone][CONF_ROI] - for key in roi: - cg.add(getattr(hub, f"set_{zone}_{key}")(roi[key])) - else: - cg.add(getattr(hub, f"set_{zone}")(zones[zone])) - - if CONF_INVERT_DIRECTION in zones: - cg.add(getattr(hub, "set_invert_direction")(zones[CONF_INVERT_DIRECTION])) +) -async def to_code(config): - hub = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(hub, config) +async def to_code(config: Dict): cg.add_library("Wire", None) cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") - validate_roi_settings(config) - - for key in TYPES: - await setup_conf(config, key, hub) - if CONF_MANUAL in config: - setup_manual_mode(config, hub) - if CONF_CALIBRATION in config: - setup_calibration_mode(config, hub) - if CONF_SAMPLING in config: - setup_sampling(config, hub) - if CONF_ZONES in config: - setup_zones(config, hub) + roode = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(roode, config) + + await setup_hardware(config, roode) + await setup_calibration(config[CONF_CALIBRATION], roode) + await setup_algorithm(config, roode) + + +async def setup_hardware(config: Dict, roode: cg.Pvariable): + cg.add(roode.set_i2c_address(config[CONF_I2C_ADDRESS])) + pins = config[CONF_PINS] + if CONF_INTERRUPT in pins: + interrupt = await cg.gpio_pin_expression(pins[CONF_INTERRUPT]) + cg.add(roode.set_interrupt_pin(interrupt)) + if CONF_XSHUT in pins: + xshut = await cg.gpio_pin_expression(pins[CONF_XSHUT]) + cg.add(roode.set_xshut_pin(xshut)) + + +async def setup_calibration(config: Dict, roode: cg.Pvariable): + if config.get(CONF_DISTANCE_MODE, CONF_AUTO) != CONF_AUTO: + cg.add(roode.set_distance_mode(config[CONF_DISTANCE_MODE])) + if config.get(CONF_TIMING_BUDGET, CONF_AUTO) != CONF_AUTO: + cg.add(roode.set_timing_budget(config[CONF_TIMING_BUDGET])) + if CONF_XTALK in config: + cg.add(roode.set_sensor_xtalk_calibration(config[CONF_XTALK])) + if CONF_OFFSET in config: + cg.add(roode.set_sensor_offset_calibration(config[CONF_OFFSET])) + + +async def setup_algorithm(config: Dict, roode: cg.Pvariable): + cg.add(roode.set_orientation(config[CONF_ORIENTATION])) + cg.add(roode.set_sampling_size(config[CONF_SAMPLING])) + cg.add(roode.set_invert_direction(config[CONF_ZONES][CONF_INVERT])) + setup_zone(CONF_ENTRY_ZONE, config, roode) + setup_zone(CONF_EXIT_ZONE, config, roode) + + +def setup_zone(name: str, config: Dict, roode: cg.Pvariable): + zone_config = config[CONF_ZONES][name] + zone_var = cg.MockObj(f"{roode}->{name}", "->") + + roi_var = cg.MockObj(f"{zone_var}->roi", "->") + setup_roi(roi_var, zone_config.get(CONF_ROI, {}), config.get(CONF_ROI, {})) + + threshold_var = cg.MockObj(f"{zone_var}->threshold", "->") + setup_thresholds( + threshold_var, + zone_config.get(CONF_DETECTION_THRESHOLDS, {}), + config.get(CONF_DETECTION_THRESHOLDS, {}), + ) + + +def setup_roi(var: cg.MockObj, config: Union[Dict, str], fallback: Union[Dict, str]): + config: Dict = ( + config if config != "auto" else {CONF_HEIGHT: "auto", CONF_WIDTH: "auto"} + ) + fallback: Dict = ( + fallback if fallback != "auto" else {CONF_HEIGHT: "auto", CONF_WIDTH: "auto"} + ) + height = config.get(CONF_HEIGHT, fallback.get(CONF_HEIGHT, 16)) + width = config.get(CONF_WIDTH, fallback.get(CONF_WIDTH, 6)) + if height != "auto": + cg.add(var.set_height(height)) + if width != "auto": + cg.add(var.set_width(width)) + if CONF_CENTER in config: + cg.add(var.set_center(config[CONF_CENTER])) + + +def setup_thresholds(var: cg.MockObj, config: Dict, fallback: Dict): + min = config.get(CONF_MIN, fallback.get(CONF_MIN, 0.0)) + max = config.get(CONF_MAX, fallback.get(CONF_MAX, 0.85)) + if isinstance(min, float): + cg.add(var.set_min_percentage(int(min * 100))) + else: + cg.add(var.set_min(min)) + if isinstance(max, float): + cg.add(var.set_max_percentage(int(max * 100))) + else: + cg.add(var.set_max(max)) diff --git a/components/roode/orientation.h b/components/roode/orientation.h new file mode 100644 index 00000000..d81c5617 --- /dev/null +++ b/components/roode/orientation.h @@ -0,0 +1,13 @@ +#pragma once + +namespace esphome { +namespace roode { + +/** + * The orientation of the sensor's two pads in relation to the entryway being tracked. + * The advised orientation is parallel as it maximizes the placement of the two ROIs/zones. + */ +enum Orientation { Parallel, Perpendicular }; + +} // namespace roode +} // namespace esphome diff --git a/components/roode/ranging.h b/components/roode/ranging.h new file mode 100644 index 00000000..4bba7e12 --- /dev/null +++ b/components/roode/ranging.h @@ -0,0 +1,34 @@ +#pragma once +#include "VL53L1X_ULD.h" + +namespace esphome { +namespace roode { + +struct RangingConfig { + explicit RangingConfig(uint16_t timing_budget) + : mode{timing_budget == 15 ? Short : Long}, timing_budget{timing_budget} {} + + explicit RangingConfig(uint16_t timing_budget, EDistanceMode mode) : mode{mode}, timing_budget{timing_budget} {} + + EDistanceMode const mode; + uint16_t const timing_budget; + uint16_t const delay_between_measurements = timing_budget + 5; +}; + +namespace Ranging { +static const RangingConfig *Shortest = new RangingConfig(15); +static const RangingConfig *Short = new RangingConfig(20); +static const RangingConfig *Medium = new RangingConfig(33); +static const RangingConfig *MediumLong = new RangingConfig(50); +static const RangingConfig *Long = new RangingConfig(100); +static const RangingConfig *Longer = new RangingConfig(200); +static const RangingConfig *Longest = new RangingConfig(500); + +static const RangingConfig *Custom(uint16_t timing_budget) { return new RangingConfig(timing_budget); }; +static const RangingConfig *Custom(uint16_t timing_budget, EDistanceMode mode) { + return new RangingConfig(timing_budget, mode); +}; +} // namespace Ranging + +} // namespace roode +} // namespace esphome diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 929ec79c..cccecb8d 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -28,16 +28,16 @@ void Roode::setup() { // If the sensor could not be initialized print out the error code. -7 is // timeout ESP_LOGE(SETUP, "Could not initialize the sensor, error code: %d", sensor_status); - while (1) { - } + this->mark_failed(); + return; } if (sensor_offset_calibration_ != -1) { ESP_LOGI(CALIBRATION, "Setting sensor offset calibration to %d", sensor_offset_calibration_); sensor_status = distanceSensor.SetOffsetInMm(sensor_offset_calibration_); if (sensor_status != VL53L1_ERROR_NONE) { ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - while (1) { - } + this->mark_failed(); + return; } } if (sensor_xtalk_calibration_ != -1) { @@ -45,26 +45,15 @@ void Roode::setup() { sensor_status = distanceSensor.SetXTalk(sensor_xtalk_calibration_); if (sensor_status != VL53L1_ERROR_NONE) { ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - while (1) { - } + this->mark_failed(); + return; } } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); ESP_LOGI(SETUP, "Creating entry and exit zones."); createEntryAndExitZone(); - if (calibration_active_) { - calibrateZones(distanceSensor); - App.feed_wdt(); - } - if (manual_active_) { - ESP_LOGI(SETUP, "Manual sensor configuration"); - sensorConfiguration.setSensorMode(distanceSensor, sensor_mode, timing_budget_); - entry->setMaxThreshold(manual_threshold_); - exit->setMaxThreshold(manual_threshold_); - publishSensorConfiguration(entry, exit, true); - } - distanceSensor.SetInterMeasurementInMs(delay_between_measurements); + calibrateZones(distanceSensor); } void Roode::update() { @@ -91,7 +80,7 @@ void Roode::loop() { } bool Roode::handleSensorStatus() { - ESP_LOGD(TAG, "Sensor status: %d, Last sensor status: %d", sensor_status, last_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) { @@ -111,16 +100,14 @@ bool Roode::handleSensorStatus() { } void Roode::createEntryAndExitZone() { - if (advised_sensor_orientation_) { - entry = new Zone(entry_roi_width, entry_roi_height, 167, samples); - exit = new Zone(exit_roi_width, exit_roi_height, 231, samples); - } else { - entry = new Zone(entry_roi_width, entry_roi_height, 195, samples); - exit = new Zone(exit_roi_width, exit_roi_height, 60, samples); + if (!entry->roi->center) { + entry->roi->center = orientation_ == Parallel ? 167 : 195; + } + if (!exit->roi->center) { + exit->roi->center = orientation_ == Parallel ? 231 : 60; } - - current_zone = entry; } + VL53L1_Error Roode::getAlternatingZoneDistances() { sensor_status += this->current_zone->readDistance(distanceSensor); App.feed_wdt(); @@ -136,9 +123,33 @@ void Roode::doPathTracking(Zone *zone) { int CurrentZoneStatus = NOBODY; int AllZonesCurrentStatus = 0; int AnEventHasOccured = 0; + + uint16_t Distances[2][samples]; + uint16_t MinDistance; + uint8_t i; + uint8_t zoneId = zone == this->entry ? 0 : 1; + auto zoneName = zone == this->entry ? "entry" : "exit "; + if (DistancesTableSize[zoneId] < samples) { + ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zoneId, zone->getDistance()); + Distances[zoneId][DistancesTableSize[zoneId]] = zone->getDistance(); + DistancesTableSize[zoneId]++; + } else { + for (i = 1; i < samples; i++) + Distances[zoneId][i - 1] = Distances[zoneId][i]; + Distances[zoneId][samples - 1] = zone->getDistance(); + ESP_LOGD(SETUP, "Distance[%s] = %d", zoneName, Distances[zoneId][samples - 1]); + } + // pick up the min distance + MinDistance = Distances[zoneId][0]; + if (DistancesTableSize[zoneId] >= 2) { + for (i = 1; i < DistancesTableSize[zoneId]; i++) { + if (Distances[zoneId][i] < MinDistance) + MinDistance = Distances[zoneId][i]; + } + } + // PathTrack algorithm - uint16_t sampledDistance = zone->getMinDistance(); - if (sampledDistance < zone->getMaxThreshold() && sampledDistance > zone->getMinThreshold()) { + if (MinDistance < zone->threshold->max && MinDistance > zone->threshold->min) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; if (presence_sensor != nullptr) { @@ -254,164 +265,112 @@ void Roode::updateCounter(int delta) { this->people_counter->set(next); } void Roode::recalibration() { calibrateZones(distanceSensor); } -void Roode::setSensorMode(int sensor_mode, int new_timing_budget) { - switch (sensor_mode) { - case 0: // short mode - time_budget_in_ms = time_budget_in_ms_short; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Short); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); - } - ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); - break; - case 1: // medium mode - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); - break; - case 2: // medium_long mode - time_budget_in_ms = time_budget_in_ms_medium_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 3: // long mode - time_budget_in_ms = time_budget_in_ms_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 4: // max mode - time_budget_in_ms = time_budget_in_ms_max; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); - break; - case 5: // custom mode - time_budget_in_ms = new_timing_budget; - delay_between_measurements = new_timing_budget + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); - break; - default: - break; + +void Roode::setRangingMode(const RangingConfig *mode) { + time_budget_in_ms = mode->timing_budget; + delay_between_measurements = mode->delay_between_measurements; + + sensor_status = distanceSensor.SetDistanceMode(mode->mode); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", mode->mode); + } + sensor_status = distanceSensor.SetTimingBudgetInMs(mode->timing_budget); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); } - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); - if (sensor_status != 0) { - ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); + sensor_status = distanceSensor.SetInterMeasurementInMs(mode->delay_between_measurements); + if (sensor_status != VL53L1_ERROR_NONE) { + ESP_LOGE(SETUP, "Could not set measurement delay. %d ms", mode->delay_between_measurements); } + + ESP_LOGI(SETUP, "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, + mode->delay_between_measurements, mode->mode); } -void Roode::setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance) { - if (average_entry_zone_distance <= short_distance_threshold || - average_exit_zone_distance <= short_distance_threshold) { - setSensorMode(0); +const RangingConfig *Roode::determineRangingMode(uint16_t average_entry_zone_distance, + uint16_t average_exit_zone_distance) { + if (this->timing_budget.has_value()) { + auto time_budget = this->timing_budget.value(); + if (this->distance_mode.has_value()) { + EDistanceMode &mode = this->distance_mode.value(); + return Ranging::Custom(time_budget, mode); + } + return Ranging::Custom(time_budget); } - if ((average_entry_zone_distance > short_distance_threshold && - average_entry_zone_distance <= medium_distance_threshold) || - (average_exit_zone_distance > short_distance_threshold && - average_exit_zone_distance <= medium_distance_threshold)) { - setSensorMode(1); + uint16_t min = average_entry_zone_distance < average_exit_zone_distance ? average_entry_zone_distance + : average_exit_zone_distance; + uint16_t max = average_entry_zone_distance > average_exit_zone_distance ? average_entry_zone_distance + : average_exit_zone_distance; + if (min <= short_distance_threshold) { + return Ranging::Short; } - - if ((average_entry_zone_distance > medium_distance_threshold && - average_entry_zone_distance <= medium_long_distance_threshold) || - (average_exit_zone_distance > medium_distance_threshold && - average_exit_zone_distance <= medium_long_distance_threshold)) { - setSensorMode(2); + if (max > short_distance_threshold && min <= medium_distance_threshold) { + return Ranging::Medium; } - if ((average_entry_zone_distance > medium_long_distance_threshold && - average_entry_zone_distance <= long_distance_threshold) || - (average_exit_zone_distance > medium_long_distance_threshold && - average_exit_zone_distance <= long_distance_threshold)) { - setSensorMode(3); + if (max > medium_distance_threshold && min <= medium_long_distance_threshold) { + return Ranging::MediumLong; } - if (average_entry_zone_distance > long_distance_threshold || average_exit_zone_distance > long_distance_threshold) { - setSensorMode(4); + if (max > medium_long_distance_threshold && min <= long_distance_threshold) { + return Ranging::Long; } + return Ranging::Longest; } void Roode::calibrateZones(VL53L1X_ULD distanceSensor) { ESP_LOGI(SETUP, "Calibrating sensor zone"); - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - distanceSensor.SetDistanceMode(Long); - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); + calibrateDistance(); + + entry->roi_calibration(distanceSensor, entry->threshold->idle, exit->threshold->idle, orientation_); + entry->calibrateThreshold(distanceSensor, number_attempts); + exit->roi_calibration(distanceSensor, entry->threshold->idle, exit->threshold->idle, orientation_); + exit->calibrateThreshold(distanceSensor, number_attempts); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(CALIBRATION, "Could not set timing budget. timing_budget: %d ms, status: %d", time_budget_in_ms, - sensor_status); - } - int entry_threshold = - entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - int exit_threshold = - exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - setCorrectDistanceSettings(entry_threshold, exit_threshold); - if (roi_calibration_) { - entry->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); - entry->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - exit->roi_calibration(distanceSensor, entry_threshold, exit_threshold, advised_sensor_orientation_); - exit->calibrateThreshold(distanceSensor, number_attempts, max_threshold_percentage_, min_threshold_percentage_); - } - int hundred_threshold_zone_0 = entry->getMaxThreshold() / 100; - int hundred_threshold_zone_1 = exit->getMaxThreshold() / 100; - int unit_threshold_zone_0 = entry->getMaxThreshold() - 100 * hundred_threshold_zone_0; - int unit_threshold_zone_1 = exit->getMaxThreshold() - 100 * hundred_threshold_zone_1; publishSensorConfiguration(entry, exit, true); App.feed_wdt(); - if (min_threshold_percentage_ != 0) { - publishSensorConfiguration(entry, exit, false); - } + publishSensorConfiguration(entry, exit, false); +} + +void Roode::calibrateDistance() { + setRangingMode(Ranging::Medium); + + entry->calibrateThreshold(distanceSensor, number_attempts); + exit->calibrateThreshold(distanceSensor, number_attempts); + + auto *mode = determineRangingMode(entry->threshold->idle, exit->threshold->idle); + setRangingMode(mode); } void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { if (isMax) { if (max_threshold_entry_sensor != nullptr) { - max_threshold_entry_sensor->publish_state(entry->getMaxThreshold()); + max_threshold_entry_sensor->publish_state(entry->threshold->max); } if (max_threshold_exit_sensor != nullptr) { - max_threshold_exit_sensor->publish_state(exit->getMaxThreshold()); + max_threshold_exit_sensor->publish_state(exit->threshold->max); } } else { if (min_threshold_entry_sensor != nullptr) { - min_threshold_entry_sensor->publish_state(entry->getMinThreshold()); + min_threshold_entry_sensor->publish_state(entry->threshold->min); } if (min_threshold_exit_sensor != nullptr) { - min_threshold_exit_sensor->publish_state(exit->getMinThreshold()); + min_threshold_exit_sensor->publish_state(exit->threshold->min); } } if (entry_roi_height_sensor != nullptr) { - entry_roi_height_sensor->publish_state(entry->getRoiHeight()); + entry_roi_height_sensor->publish_state(entry->roi->height); } if (entry_roi_width_sensor != nullptr) { - entry_roi_width_sensor->publish_state(entry->getRoiWidth()); + entry_roi_width_sensor->publish_state(entry->roi->width); } if (exit_roi_height_sensor != nullptr) { - exit_roi_height_sensor->publish_state(exit->getRoiHeight()); + exit_roi_height_sensor->publish_state(exit->roi->height); } if (exit_roi_width_sensor != nullptr) { - exit_roi_width_sensor->publish_state(exit->getRoiWidth()); + exit_roi_width_sensor->publish_state(exit->roi->width); } } } // namespace roode diff --git a/components/roode/roode.h b/components/roode/roode.h index 94248804..8cb9691b 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -9,7 +9,9 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#include "orientation.h" #include "zone.h" +#include "ranging.h" namespace esphome { namespace roode { @@ -59,24 +61,15 @@ class Roode : public PollingComponent { void loop() override; void dump_config() override; - void set_calibration_active(bool val) { calibration_active_ = val; } - void set_manual_active(bool val) { manual_active_ = val; } - void set_roi_active(bool val) { roi_active_ = val; } - void set_roi_calibration(bool val) { roi_calibration_ = val; } void set_sensor_offset_calibration(int val) { sensor_offset_calibration_ = val; } void set_sensor_xtalk_calibration(int val) { sensor_xtalk_calibration_ = val; } - void set_timing_budget(int timing_budget) { timing_budget_ = timing_budget; } - void set_manual_threshold(int val) { manual_threshold_ = val; } - void set_max_threshold_percentage(int val) { max_threshold_percentage_ = val; } - void set_min_threshold_percentage(int val) { min_threshold_percentage_ = val; } - void set_entry_roi_height(int height) { entry_roi_height = height; } - void set_entry_roi_width(int width) { entry_roi_width = width; } - void set_exit_roi_height(int height) { exit_roi_height = height; } - void set_exit_roi_width(int width) { exit_roi_width = width; } + void set_distance_mode(EDistanceMode mode) { this->distance_mode.value() = mode; } + void set_timing_budget(uint16_t budget) { this->timing_budget.value() = budget; } void set_i2c_address(uint8_t address) { this->address_ = address; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } + void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } void set_invert_direction(bool dir) { invert_direction_ = dir; } - void set_restore_values(bool val) { restore_values_ = val; } - void set_advised_sensor_orientation(bool val) { advised_sensor_orientation_ = val; } + void set_orientation(Orientation val) { orientation_ = val; } void set_sampling_size(uint8_t size) { samples = size; } void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } @@ -105,18 +98,13 @@ class Roode : public PollingComponent { void set_entry_exit_event_text_sensor(text_sensor::TextSensor *entry_exit_event_sensor_) { entry_exit_event_sensor = entry_exit_event_sensor_; } - void set_sensor_mode(int sensor_mode_) { sensor_mode = sensor_mode_; } - VL53L1_Error getAlternatingZoneDistances(); - void doPathTracking(Zone *zone); void recalibration(); - bool handleSensorStatus(); - Configuration sensorConfiguration; + Zone *entry = new Zone(0); + Zone *exit = new Zone(1); protected: VL53L1X_ULD distanceSensor; - Zone *entry; - Zone *exit; - Zone *current_zone; + Zone *current_zone = entry; sensor::Sensor *distance_entry; sensor::Sensor *distance_exit; number::Number *people_counter; @@ -133,39 +121,31 @@ class Roode : public PollingComponent { text_sensor::TextSensor *version_sensor; text_sensor::TextSensor *entry_exit_event_sensor; + VL53L1_Error getAlternatingZoneDistances(); + void doPathTracking(Zone *zone); + bool handleSensorStatus(); void createEntryAndExitZone(); + void calibrateDistance(); void calibrateZones(VL53L1X_ULD distanceSensor); - void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); - void setSensorMode(int sensor_mode, int timing_budget = 0); + const RangingConfig *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); + void setRangingMode(const RangingConfig *mode); void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); void updateCounter(int delta); - uint16_t sampling(Zone *zone); - bool calibration_active_{false}; - bool manual_active_{false}; - bool roi_active_{false}; - bool roi_calibration_{false}; + InternalGPIOPin *xshut_pin; + InternalGPIOPin *interrupt_pin; int sensor_offset_calibration_{-1}; int sensor_xtalk_calibration_{-1}; - int sensor_mode{-1}; - bool advised_sensor_orientation_{true}; - bool sampling_active_{false}; + optional distance_mode; + optional timing_budget; + Orientation orientation_{Parallel}; uint8_t samples{2}; uint8_t address_ = 0x29; bool invert_direction_{false}; - bool restore_values_{false}; - uint64_t max_threshold_percentage_{85}; - uint64_t min_threshold_percentage_{0}; - uint64_t manual_threshold_{2000}; int number_attempts = 20; // TO DO: make this configurable - int timing_budget_{-1}; int short_distance_threshold = 1300; int medium_distance_threshold = 2000; int medium_long_distance_threshold = 2700; int long_distance_threshold = 3400; - int entry_roi_width{6}; - int entry_roi_height{16}; - int exit_roi_width{6}; - int exit_roi_height{16}; }; } // namespace roode diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 5879737d..a86c2c45 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -1,23 +1,11 @@ #include "zone.h" -static uint8_t zone_id = 0; namespace esphome { namespace roode { -Zone::Zone(int roi_width, int roi_height, int roi_center, int sample_size) { - this->id = zone_id++; - ESP_LOGD(TAG, "Zone(%d, %d, %d)", roi_width, roi_height, roi_center); - this->roi_width = roi_width; - this->roi_height = roi_height; - this->roi_center = roi_center; - this->roi = {roi_width, roi_height, roi_center}; - this->sample_size = sample_size; - this->Distances = new int[sample_size]; -} - VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { last_sensor_status = sensor_status; - sensor_status += distanceSensor.SetROI(this->getRoiWidth(), this->getRoiHeight()); - sensor_status += distanceSensor.SetROICenter(this->getRoiCenter()); + sensor_status += distanceSensor.SetROI(roi->width, roi->height); + sensor_status += distanceSensor.SetROICenter(roi->center); sensor_status += distanceSensor.StartRanging(); // Wait for the measurement to be ready @@ -68,8 +56,7 @@ VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { return sensor_status; } -int Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, - uint16_t min_threshold_percentage) { +void Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts) { int *zone_distances = new int[number_attempts]; int sum = 0; for (int i = 0; i < number_attempts; i++) { @@ -77,103 +64,68 @@ int Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, u zone_distances[i] = this->getDistance(); sum += zone_distances[i]; }; - int optimized_threshold = this->getOptimizedValues(zone_distances, sum, number_attempts); - this->setMaxThreshold(optimized_threshold * max_threshold_percentage / 100); - if (min_threshold_percentage > 0) - this->setMinThreshold(optimized_threshold * min_threshold_percentage / 100); - ESP_LOGI(CALIBRATION, "Calibrated threshold for zone. zoneId: %d, threshold: %d", this->getZoneId(), - optimized_threshold); - return optimized_threshold; + threshold->idle = this->getOptimizedValues(zone_distances, sum, number_attempts); + + if (threshold->max_percentage.has_value()) { + threshold->max = (threshold->idle * threshold->max_percentage.value()) / 100; + } + if (threshold->min_percentage.has_value()) { + threshold->min = (threshold->idle * threshold->min_percentage.value()) / 100; + } + ESP_LOGI(CALIBRATION, "Calibrated threshold for zone. zoneId: %d, idle: %d, min: %d (%d%%), max: %d (%d%%)", id, + threshold->idle, threshold->min, + threshold->min_percentage.value_or((threshold->min * 100) / threshold->idle), threshold->max, + threshold->max_percentage.value_or((threshold->max * 100) / threshold->idle)); } -void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, - bool sensor_orientation) { - // the value of the average distance is used for computing the optimal size of - // the ROI and consequently also the center of the two zones + +void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, uint16_t entry_threshold, uint16_t exit_threshold, + Orientation orientation) { + // the value of the average distance is used for computing the optimal size of the ROI and consequently also the + // center of the two zones int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); int ROI_size = min(8, max(4, function_of_the_distance)); - this->updateRoi(ROI_size, ROI_size * 2); - // now we set the position of the center of the two zones - if (sensor_orientation) { - switch (ROI_size) { - case 4: - if (this->getZoneId() == 0U) { - this->setRoiCenter(150); - break; - } - this->setRoiCenter(247); - - break; - case 5: - if (this->getZoneId() == 0U) { - this->setRoiCenter(159); - break; - } - this->setRoiCenter(239); - break; - case 6: - if (this->getZoneId() == 0U) { - this->setRoiCenter(159); - break; - } - this->setRoiCenter(239); - break; - case 7: - if (this->getZoneId() == 0U) { - this->setRoiCenter(167); - break; - } - this->setRoiCenter(231); - break; - case 8: - if (this->getZoneId() == 0U) { - this->setRoiCenter(167); + if (!this->roi->width) { + this->roi->width = ROI_size; + } + if (!this->roi->height) { + this->roi->height = ROI_size * 2; + } + if (!this->roi->center) { + // now we set the position of the center of the two zones + if (orientation == Parallel) { + switch (ROI_size) { + case 4: + this->roi->center = this->id == 0U ? 150 : 247; break; - } - this->setRoiCenter(231); - break; - } - } else { - switch (ROI_size) { - case 4: - if (this->getZoneId() == 0U) { - this->setRoiCenter(193); + case 5: + case 6: + this->roi->center = this->id == 0U ? 159 : 239; break; - } - this->setRoiCenter(58); - break; - case 5: - if (this->getZoneId() == 0U) { - this->setRoiCenter(194); + case 7: + case 8: + this->roi->center = this->id == 0U ? 167 : 231; break; - } - this->setRoiCenter(59); - break; - case 6: - if (this->getZoneId() == 0U) { - this->setRoiCenter(194); + } + } else { + switch (ROI_size) { + case 4: + this->roi->center = this->id == 0U ? 193 : 58; break; - } - this->setRoiCenter(59); - break; - case 7: - if (this->getZoneId() == 0U) { - this->setRoiCenter(195); + case 5: + case 6: + this->roi->center = this->id == 0U ? 194 : 59; break; - } - this->setRoiCenter(60); - break; - case 8: - if (this->getZoneId() == 0U) { - this->setRoiCenter(195); + case 7: + case 8: + this->roi->center = this->id == 0U ? 195 : 60; break; - } - this->setRoiCenter(60); - break; + } } } - ESP_LOGI(CALIBRATION, "Calibrated ROI for zone. zoneId: %d, width: %d, height: %d, center: %d", this->getZoneId(), - this->getRoiWidth(), this->getRoiHeight(), this->getRoiCenter()); + ESP_LOGI(CALIBRATION, "Calibrated ROI for zone. zoneId: %d, width: %d, height: %d, center: %d", id, roi->width, + roi->height, roi->center); } + int Zone::getOptimizedValues(int *values, int sum, int size) { int sum_squared = 0; int variance = 0; @@ -191,23 +143,7 @@ int Zone::getOptimizedValues(int *values, int sum, int size) { return avg - sd; } -uint16_t Zone::getDistance() { return this->distance; } -uint16_t Zone::getMinDistance() { return this->min_distance; } -uint16_t Zone::getRoiWidth() { return this->roi.width; } -uint16_t Zone::getRoiHeight() { return this->roi.height; } -uint16_t Zone::getRoiCenter() { return this->roi.center; } -void Zone::setRoiWidth(uint16_t new_roi_width) { this->roi.width = new_roi_width; } -void Zone::setRoiHeight(uint16_t new_roi_height) { this->roi.height = new_roi_height; } -void Zone::setRoiCenter(uint16_t new_roi_center) { this->roi.center = new_roi_center; } -void Zone::updateRoi(uint16_t new_width, uint16_t new_height) { - this->roi.width = new_width; - this->roi.height = new_height; -} -void Zone::setMinThreshold(uint16_t min) { this->threshold.min = min; } -void Zone::setMaxThreshold(uint16_t max) { this->threshold.max = max; } -uint16_t Zone::getMinThreshold() { return this->threshold.min; } -uint16_t Zone::getMaxThreshold() { return this->threshold.max; } - -uint8_t Zone::getZoneId() { return this->id; } +uint16_t Zone::getDistance() const { return this->distance; } +uint16_t Zone::getMinDistance() const { return this->min_distance; } } // namespace roode } // namespace esphome \ No newline at end of file diff --git a/components/roode/zone.h b/components/roode/zone.h index 800cec83..a6a6e3c3 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -1,59 +1,54 @@ - #pragma once #include #include "VL53L1X_ULD.h" -#include "configuration.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" +#include "esphome/core/optional.h" +#include "orientation.h" +#include "configuration.h" static const char *const TAG = "Zone"; static const char *const CALIBRATION = "Zone calibration"; namespace esphome { namespace roode { struct ROI { - uint16_t width; - uint16_t height; - uint16_t center; + uint8_t width; + uint8_t height; + uint8_t center; + void set_width(uint8_t width) { this->width = width; } + void set_height(uint8_t height) { this->height = height; } + void set_center(uint8_t center) { this->center = center; } }; + struct Threshold { + /** Automatically determined idling distance (average of several measurements) */ + uint16_t idle; uint16_t min; - uint16_t min_percentage; + optional min_percentage{}; uint16_t max; - uint16_t max_percentage; + optional max_percentage{}; + void set_min(uint16_t min) { this->min = min; } + void set_min_percentage(uint8_t min) { this->min_percentage = min; } + void set_max(uint16_t max) { this->max = max; } + void set_max_percentage(uint8_t max) { this->max_percentage = max; } }; + class Zone { public: - Zone(int roi_width, int roi_height, int roi_center, int sample_size); + explicit Zone(uint8_t id) : id{id} {}; VL53L1_Error readDistance(VL53L1X_ULD &distanceSensor); - int calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts, uint16_t max_threshold_percentage, - uint16_t min_threshold_percentage); - void roi_calibration(VL53L1X_ULD &distanceSensor, int entry_threshold, int exit_threshold, bool sensor_orientation); - uint16_t calibrateRoi(); - uint16_t getMinThreshold(); - uint16_t getMaxThreshold(); - uint16_t getRoiWidth(); - uint16_t getRoiHeight(); - uint16_t getRoiCenter(); - void setMinThreshold(uint16_t min); - void setMaxThreshold(uint16_t max); - void setRoiWidth(uint16_t new_roi_width); - void setRoiHeight(uint16_t new_roi_height); - void setRoiCenter(uint16_t new_roi_center); - void updateRoi(uint16_t new_width, uint16_t new_height); - uint8_t getZoneId(); - uint16_t getDistance(); - uint16_t getMinDistance(); - bool handleSensorStatus(); + void calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts); + void roi_calibration(VL53L1X_ULD &distanceSensor, uint16_t entry_threshold, uint16_t exit_threshold, + Orientation orientation); + const uint8_t id; + uint16_t getDistance() const; + uint16_t getMinDistance() const; + ROI *roi = new ROI(); + Threshold *threshold = new Threshold(); protected: - int getSum(int *values, int size); int getOptimizedValues(int *values, int sum, int size); - ROI roi; - Threshold threshold; - uint16_t roi_width; - uint16_t roi_height; - uint16_t roi_center; - uint8_t id; uint16_t distance; int *Distances; uint8_t sample_size; diff --git a/peopleCounter32Dev.yaml b/peopleCounter32Dev.yaml index 4e4eeaa8..073136c8 100644 --- a/peopleCounter32Dev.yaml +++ b/peopleCounter32Dev.yaml @@ -53,28 +53,27 @@ i2c: roode: id: roode_platform i2c_address: 0x29 - update_interval: 100ms calibration: - max_threshold_percentage: 85 - min_threshold_percentage: 5 - roi_calibration: true - # sensor_offset_calibration: 8 - # sensor_xtalk_calibration: 53406 - # manual: - # sensor_mode: 3 - # manual_threshold: 1280 - # timing_budget: 200 - # sampling: 3 + sensor_mode: 3 + timing_budget: 200 + offset: 8 + xtalk: 53406 + sampling: 1 + detection_thresholds: + max: 85% + min: 5% + roi: + height: 8 + width: 8 zones: - invert_direction: true - # entry: - # roi: - # roi_height: 12 - # roi_width: 6 - # exit: - # roi: - # roi_height: 12 - # roi_width: 6 + invert: true + entry: + roi: + height: 4 + width: 4 + exit: + roi: + width: 6 button: - platform: restart From b2dd032b609c20ebad80e5d825f8ab85ba252e36 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jan 2022 19:58:10 -0600 Subject: [PATCH 30/54] Remove custom ranging option & consolidate to single "ranging" config key --- components/roode/__init__.py | 46 ++++++++++-------------------------- components/roode/ranging.h | 30 ++++++++++------------- components/roode/roode.cpp | 17 +++++-------- components/roode/roode.h | 10 ++++---- 4 files changed, 35 insertions(+), 68 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 6bbc2841..2be83059 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -26,16 +26,15 @@ CONF_ORIENTATION = "orientation" CONF_CALIBRATION = "calibration" CONF_DETECTION_THRESHOLDS = "detection_thresholds" -CONF_DISTANCE_MODE = "distance_mode" CONF_I2C_ADDRESS = "i2c_address" CONF_ENTRY_ZONE = "entry" CONF_EXIT_ZONE = "exit" CONF_CENTER = "center" CONF_MAX = "max" CONF_MIN = "min" +CONF_RANGING_MODE = "ranging" CONF_ROI = "roi" CONF_SAMPLING = "sampling" -CONF_TIMING_BUDGET = "timing_budget" CONF_XSHUT = "xshut" CONF_XTALK = "xtalk" CONF_ZONES = "zones" @@ -46,29 +45,15 @@ "perpendicular": Orientation.Perpendicular, } -DistanceMode = cg.global_ns.enum("EDistanceMode") -DISTANCE_MODES = { +Ranging = roode_ns.namespace("Ranging") +RANGING_MODES = { CONF_AUTO: CONF_AUTO, - "short": DistanceMode.Short, - "long": DistanceMode.Long, -} - -TIMING_BUDGET_ALIASES = { - CONF_AUTO: CONF_AUTO, - "shortest": 15, - "short": 20, - "medium": 33, - "medium long": 50, - "long": 100, - "longer": 200, - "longest": 500, - "15": 15, - "20": 20, - "33": 33, - "50": 50, - "100": 100, - "200": 200, - "500": 500, + "shortest": Ranging.Shortest, + "short": Ranging.Short, + "medium": Ranging.Medium, + "long": Ranging.Long, + "longer": Ranging.Longer, + "longest": Ranging.Longest, } int16_t = cv.int_range(min=-32768, max=32768) # signed @@ -126,11 +111,8 @@ def none_to_empty(value): ), cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( { - cv.Optional(CONF_DISTANCE_MODE, default=CONF_AUTO): cv.enum( - DISTANCE_MODES - ), - cv.Optional(CONF_TIMING_BUDGET, default=CONF_AUTO): cv.enum( - TIMING_BUDGET_ALIASES + cv.Optional(CONF_RANGING_MODE, default=CONF_AUTO): cv.enum( + RANGING_MODES ), cv.Optional(CONF_XTALK): cv.uint16_t, cv.Optional(CONF_OFFSET): int16_t, @@ -175,10 +157,8 @@ async def setup_hardware(config: Dict, roode: cg.Pvariable): async def setup_calibration(config: Dict, roode: cg.Pvariable): - if config.get(CONF_DISTANCE_MODE, CONF_AUTO) != CONF_AUTO: - cg.add(roode.set_distance_mode(config[CONF_DISTANCE_MODE])) - if config.get(CONF_TIMING_BUDGET, CONF_AUTO) != CONF_AUTO: - cg.add(roode.set_timing_budget(config[CONF_TIMING_BUDGET])) + if config.get(CONF_RANGING_MODE, CONF_AUTO) != CONF_AUTO: + cg.add(roode.set_ranging_mode(config[CONF_RANGING_MODE])) if CONF_XTALK in config: cg.add(roode.set_sensor_xtalk_calibration(config[CONF_XTALK])) if CONF_OFFSET in config: diff --git a/components/roode/ranging.h b/components/roode/ranging.h index 4bba7e12..7f062533 100644 --- a/components/roode/ranging.h +++ b/components/roode/ranging.h @@ -4,30 +4,24 @@ namespace esphome { namespace roode { -struct RangingConfig { - explicit RangingConfig(uint16_t timing_budget) - : mode{timing_budget == 15 ? Short : Long}, timing_budget{timing_budget} {} +struct RangingMode { + explicit RangingMode(uint16_t timing_budget, EDistanceMode mode = Long) + : timing_budget{timing_budget}, mode{mode} {} - explicit RangingConfig(uint16_t timing_budget, EDistanceMode mode) : mode{mode}, timing_budget{timing_budget} {} - - EDistanceMode const mode; uint16_t const timing_budget; uint16_t const delay_between_measurements = timing_budget + 5; + EDistanceMode const mode; }; namespace Ranging { -static const RangingConfig *Shortest = new RangingConfig(15); -static const RangingConfig *Short = new RangingConfig(20); -static const RangingConfig *Medium = new RangingConfig(33); -static const RangingConfig *MediumLong = new RangingConfig(50); -static const RangingConfig *Long = new RangingConfig(100); -static const RangingConfig *Longer = new RangingConfig(200); -static const RangingConfig *Longest = new RangingConfig(500); - -static const RangingConfig *Custom(uint16_t timing_budget) { return new RangingConfig(timing_budget); }; -static const RangingConfig *Custom(uint16_t timing_budget, EDistanceMode mode) { - return new RangingConfig(timing_budget, mode); -}; +// NOLINTBEGIN(cert-err58-cpp) +__attribute__((unused)) static const RangingMode *Shortest = new RangingMode(15, Short); +__attribute__((unused)) static const RangingMode *Short = new RangingMode(20); +__attribute__((unused)) static const RangingMode *Medium = new RangingMode(33); +__attribute__((unused)) static const RangingMode *Long = new RangingMode(50); +__attribute__((unused)) static const RangingMode *Longer = new RangingMode(100); +__attribute__((unused)) static const RangingMode *Longest = new RangingMode(200); +// NOLINTEND(cert-err58-cpp) } // namespace Ranging } // namespace roode diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index cccecb8d..e4fdb3c6 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -266,7 +266,7 @@ void Roode::updateCounter(int delta) { } void Roode::recalibration() { calibrateZones(distanceSensor); } -void Roode::setRangingMode(const RangingConfig *mode) { +void Roode::setRangingMode(const RangingMode *mode) { time_budget_in_ms = mode->timing_budget; delay_between_measurements = mode->delay_between_measurements; @@ -287,15 +287,10 @@ void Roode::setRangingMode(const RangingConfig *mode) { mode->delay_between_measurements, mode->mode); } -const RangingConfig *Roode::determineRangingMode(uint16_t average_entry_zone_distance, +const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance) { - if (this->timing_budget.has_value()) { - auto time_budget = this->timing_budget.value(); - if (this->distance_mode.has_value()) { - EDistanceMode &mode = this->distance_mode.value(); - return Ranging::Custom(time_budget, mode); - } - return Ranging::Custom(time_budget); + if (this->ranging_mode.has_value()) { + return this->ranging_mode.value(); } uint16_t min = average_entry_zone_distance < average_exit_zone_distance ? average_entry_zone_distance @@ -309,10 +304,10 @@ const RangingConfig *Roode::determineRangingMode(uint16_t average_entry_zone_dis return Ranging::Medium; } if (max > medium_distance_threshold && min <= medium_long_distance_threshold) { - return Ranging::MediumLong; + return Ranging::Long; } if (max > medium_long_distance_threshold && min <= long_distance_threshold) { - return Ranging::Long; + return Ranging::Longer; } return Ranging::Longest; } diff --git a/components/roode/roode.h b/components/roode/roode.h index 8cb9691b..1a54fcd6 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -63,8 +63,7 @@ class Roode : public PollingComponent { void set_sensor_offset_calibration(int val) { sensor_offset_calibration_ = val; } void set_sensor_xtalk_calibration(int val) { sensor_xtalk_calibration_ = val; } - void set_distance_mode(EDistanceMode mode) { this->distance_mode.value() = mode; } - void set_timing_budget(uint16_t budget) { this->timing_budget.value() = budget; } + void set_ranging_mode(const RangingMode *mode) { this->ranging_mode = mode; } void set_i2c_address(uint8_t address) { this->address_ = address; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } @@ -127,16 +126,15 @@ class Roode : public PollingComponent { void createEntryAndExitZone(); void calibrateDistance(); void calibrateZones(VL53L1X_ULD distanceSensor); - const RangingConfig *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); - void setRangingMode(const RangingConfig *mode); + const RangingMode *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); + void setRangingMode(const RangingMode *mode); void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); void updateCounter(int delta); InternalGPIOPin *xshut_pin; InternalGPIOPin *interrupt_pin; int sensor_offset_calibration_{-1}; int sensor_xtalk_calibration_{-1}; - optional distance_mode; - optional timing_budget; + optional ranging_mode{}; Orientation orientation_{Parallel}; uint8_t samples{2}; uint8_t address_ = 0x29; From 24fc64f54d2f15599882acd9aaca44f79b4ae87b Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 11 Jan 2022 22:51:27 -0600 Subject: [PATCH 31/54] Refactor sensor config to new sensor wrapper object --- components/roode/__init__.py | 17 +++-- components/roode/ranging.h | 2 +- components/roode/roi.h | 16 +++++ components/roode/roode.cpp | 79 +++------------------- components/roode/roode.h | 21 ++---- components/roode/tof_sensor.cpp | 113 ++++++++++++++++++++++++++++++++ components/roode/tof_sensor.h | 49 ++++++++++++++ components/roode/zone.cpp | 41 +++--------- components/roode/zone.h | 18 ++--- 9 files changed, 217 insertions(+), 139 deletions(-) create mode 100644 components/roode/roi.h create mode 100644 components/roode/tof_sensor.cpp create mode 100644 components/roode/tof_sensor.h diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 2be83059..4ff0fd44 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -102,7 +102,7 @@ def none_to_empty(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Roode), - cv.Optional(CONF_I2C_ADDRESS, default=0x29): cv.i2c_address, + cv.Optional(CONF_I2C_ADDRESS): cv.i2c_address, cv.Optional(CONF_PINS, default={}): NullableSchema( { cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, @@ -146,23 +146,26 @@ async def to_code(config: Dict): async def setup_hardware(config: Dict, roode: cg.Pvariable): - cg.add(roode.set_i2c_address(config[CONF_I2C_ADDRESS])) + tof = cg.MockObj(f"{roode}->get_tof_sensor()", "->") + if CONF_I2C_ADDRESS in config: + cg.add(tof.set_i2c_address(config[CONF_I2C_ADDRESS])) pins = config[CONF_PINS] if CONF_INTERRUPT in pins: interrupt = await cg.gpio_pin_expression(pins[CONF_INTERRUPT]) - cg.add(roode.set_interrupt_pin(interrupt)) + cg.add(tof.set_interrupt_pin(interrupt)) if CONF_XSHUT in pins: xshut = await cg.gpio_pin_expression(pins[CONF_XSHUT]) - cg.add(roode.set_xshut_pin(xshut)) + cg.add(tof.set_xshut_pin(xshut)) async def setup_calibration(config: Dict, roode: cg.Pvariable): + tof = cg.MockObj(f"{roode}->get_tof_sensor()", "->") if config.get(CONF_RANGING_MODE, CONF_AUTO) != CONF_AUTO: - cg.add(roode.set_ranging_mode(config[CONF_RANGING_MODE])) + cg.add(tof.set_ranging_mode_override(config[CONF_RANGING_MODE])) if CONF_XTALK in config: - cg.add(roode.set_sensor_xtalk_calibration(config[CONF_XTALK])) + cg.add(tof.set_xtalk(config[CONF_XTALK])) if CONF_OFFSET in config: - cg.add(roode.set_sensor_offset_calibration(config[CONF_OFFSET])) + cg.add(tof.set_offset(config[CONF_OFFSET])) async def setup_algorithm(config: Dict, roode: cg.Pvariable): diff --git a/components/roode/ranging.h b/components/roode/ranging.h index 7f062533..603f81c7 100644 --- a/components/roode/ranging.h +++ b/components/roode/ranging.h @@ -5,7 +5,7 @@ namespace esphome { namespace roode { struct RangingMode { - explicit RangingMode(uint16_t timing_budget, EDistanceMode mode = Long) + explicit RangingMode(uint16_t timing_budget, EDistanceMode mode = EDistanceMode::Long) : timing_budget{timing_budget}, mode{mode} {} uint16_t const timing_budget; diff --git a/components/roode/roi.h b/components/roode/roi.h new file mode 100644 index 00000000..90a9b48b --- /dev/null +++ b/components/roode/roi.h @@ -0,0 +1,16 @@ +#pragma once + +namespace esphome { +namespace roode { + +struct ROI { + uint8_t width; + uint8_t height; + uint8_t center; + 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; } +}; + +} // namespace roode +} // namespace esphome diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index e4fdb3c6..5658a86b 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -5,8 +5,6 @@ namespace esphome { namespace roode { void Roode::dump_config() { ESP_LOGCONFIG(TAG, "dump config:"); - LOG_I2C_DEVICE(this); - LOG_UPDATE_INTERVAL(this); } void Roode::setup() { @@ -14,46 +12,10 @@ void Roode::setup() { if (version_sensor != nullptr) { version_sensor->publish_state(VERSION); } - Wire.begin(); - Wire.setClock(400000); - - // Initialize the sensor, give the special I2C_address to the Begin function - // Set a different I2C address - // This address is stored as long as the sensor is powered. To revert this - // change you can unplug and replug the power to the sensor - distanceSensor.SetI2CAddress(VL53L1X_ULD_I2C_ADDRESS); - - sensor_status = distanceSensor.Begin(VL53L1X_ULD_I2C_ADDRESS); - if (sensor_status != VL53L1_ERROR_NONE) { - // If the sensor could not be initialized print out the error code. -7 is - // timeout - ESP_LOGE(SETUP, "Could not initialize the sensor, error code: %d", sensor_status); - this->mark_failed(); - return; - } - if (sensor_offset_calibration_ != -1) { - ESP_LOGI(CALIBRATION, "Setting sensor offset calibration to %d", sensor_offset_calibration_); - sensor_status = distanceSensor.SetOffsetInMm(sensor_offset_calibration_); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - this->mark_failed(); - return; - } - } - if (sensor_xtalk_calibration_ != -1) { - ESP_LOGI(CALIBRATION, "Setting sensor xtalk calibration to %d", sensor_xtalk_calibration_); - sensor_status = distanceSensor.SetXTalk(sensor_xtalk_calibration_); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set sensor offset calibration, error code: %d", sensor_status); - this->mark_failed(); - return; - } - } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); ESP_LOGI(SETUP, "Creating entry and exit zones."); createEntryAndExitZone(); - - calibrateZones(distanceSensor); + calibrateZones(); } void Roode::update() { @@ -109,7 +71,7 @@ void Roode::createEntryAndExitZone() { } VL53L1_Error Roode::getAlternatingZoneDistances() { - sensor_status += this->current_zone->readDistance(distanceSensor); + this->current_zone->readDistance(distanceSensor); App.feed_wdt(); return sensor_status; } @@ -264,33 +226,12 @@ void Roode::updateCounter(int delta) { ESP_LOGI(TAG, "Updating people count: %d", (int) next); this->people_counter->set(next); } -void Roode::recalibration() { calibrateZones(distanceSensor); } - -void Roode::setRangingMode(const RangingMode *mode) { - time_budget_in_ms = mode->timing_budget; - delay_between_measurements = mode->delay_between_measurements; - - sensor_status = distanceSensor.SetDistanceMode(mode->mode); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", mode->mode); - } - sensor_status = distanceSensor.SetTimingBudgetInMs(mode->timing_budget); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); - } - sensor_status = distanceSensor.SetInterMeasurementInMs(mode->delay_between_measurements); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set measurement delay. %d ms", mode->delay_between_measurements); - } - - ESP_LOGI(SETUP, "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, - mode->delay_between_measurements, mode->mode); -} +void Roode::recalibration() { calibrateZones(); } const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance) { - if (this->ranging_mode.has_value()) { - return this->ranging_mode.value(); + if (this->distanceSensor->get_ranging_mode_override().has_value()) { + return this->distanceSensor->get_ranging_mode_override().value(); } uint16_t min = average_entry_zone_distance < average_exit_zone_distance ? average_entry_zone_distance @@ -312,13 +253,13 @@ const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_dista return Ranging::Longest; } -void Roode::calibrateZones(VL53L1X_ULD distanceSensor) { +void Roode::calibrateZones() { ESP_LOGI(SETUP, "Calibrating sensor zone"); calibrateDistance(); - entry->roi_calibration(distanceSensor, entry->threshold->idle, exit->threshold->idle, orientation_); + entry->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); entry->calibrateThreshold(distanceSensor, number_attempts); - exit->roi_calibration(distanceSensor, entry->threshold->idle, exit->threshold->idle, orientation_); + exit->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); exit->calibrateThreshold(distanceSensor, number_attempts); publishSensorConfiguration(entry, exit, true); @@ -327,13 +268,13 @@ void Roode::calibrateZones(VL53L1X_ULD distanceSensor) { } void Roode::calibrateDistance() { - setRangingMode(Ranging::Medium); + distanceSensor->set_ranging_mode(Ranging::Medium); entry->calibrateThreshold(distanceSensor, number_attempts); exit->calibrateThreshold(distanceSensor, number_attempts); auto *mode = determineRangingMode(entry->threshold->idle, exit->threshold->idle); - setRangingMode(mode); + distanceSensor->set_ranging_mode(mode); } void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { diff --git a/components/roode/roode.h b/components/roode/roode.h index 1a54fcd6..e51b38bd 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -12,13 +12,13 @@ #include "orientation.h" #include "zone.h" #include "ranging.h" +#include "tof_sensor.h" namespace esphome { namespace roode { #define NOBODY 0 #define SOMEONE 1 #define VERSION "v1.4.1-beta" -#define VL53L1X_ULD_I2C_ADDRESS 0x52 // Default address is 0x52 static const char *const TAG = "Roode"; static const char *const SETUP = "Setup"; static const char *const CALIBRATION = "Sensor Calibration"; @@ -61,12 +61,8 @@ class Roode : public PollingComponent { void loop() override; void dump_config() override; - void set_sensor_offset_calibration(int val) { sensor_offset_calibration_ = val; } - void set_sensor_xtalk_calibration(int val) { sensor_xtalk_calibration_ = val; } - void set_ranging_mode(const RangingMode *mode) { this->ranging_mode = mode; } - void set_i2c_address(uint8_t address) { this->address_ = address; } - void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } - void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } + TofSensor *get_tof_sensor() { return this->distanceSensor; } + void set_tof_sensor(TofSensor *sensor) { this->distanceSensor = sensor; } void set_invert_direction(bool dir) { invert_direction_ = dir; } void set_orientation(Orientation val) { orientation_ = val; } void set_sampling_size(uint8_t size) { samples = size; } @@ -102,7 +98,7 @@ class Roode : public PollingComponent { Zone *exit = new Zone(1); protected: - VL53L1X_ULD distanceSensor; + TofSensor *distanceSensor; Zone *current_zone = entry; sensor::Sensor *distance_entry; sensor::Sensor *distance_exit; @@ -125,19 +121,12 @@ class Roode : public PollingComponent { bool handleSensorStatus(); void createEntryAndExitZone(); void calibrateDistance(); - void calibrateZones(VL53L1X_ULD distanceSensor); + void calibrateZones(); const RangingMode *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); - void setRangingMode(const RangingMode *mode); void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); void updateCounter(int delta); - InternalGPIOPin *xshut_pin; - InternalGPIOPin *interrupt_pin; - int sensor_offset_calibration_{-1}; - int sensor_xtalk_calibration_{-1}; - optional ranging_mode{}; Orientation orientation_{Parallel}; uint8_t samples{2}; - uint8_t address_ = 0x29; bool invert_direction_{false}; int number_attempts = 20; // TO DO: make this configurable int short_distance_threshold = 1300; diff --git a/components/roode/tof_sensor.cpp b/components/roode/tof_sensor.cpp new file mode 100644 index 00000000..4ade56af --- /dev/null +++ b/components/roode/tof_sensor.cpp @@ -0,0 +1,113 @@ +#include "tof_sensor.h" + +namespace esphome { +namespace roode { + +void TofSensor::dump_config() { + ESP_LOGCONFIG("VL53L1X", "dump config:"); + if (this->address.has_value()) { + ESP_LOGCONFIG("VL53L1X", " Address: 0x%02X", this->address.value()); + } +} + +void TofSensor::setup() { + Wire.begin(); + Wire.setClock(400000); + + // TODO use xshut_pin, if given, to change address + auto status = this->address.has_value() ? this->sensor.Begin(this->address.value()) : this->sensor.Begin(); + if (status != VL53L1_ERROR_NONE) { + // If the sensor could not be initialized print out the error code. -7 is timeout + ESP_LOGE("VL53L1X", "Could not initialize the sensor, error code: %d", status); + this->mark_failed(); + return; + } + + if (this->offset.has_value()) { + ESP_LOGI("VL53L1X", "Setting sensor offset calibration to %d", this->offset.value()); + status = this->sensor.SetOffsetInMm(this->offset.value()); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE("VL53L1X", "Could not set sensor offset calibration, error code: %d", status); + this->mark_failed(); + return; + } + } + + if (this->xtalk.has_value()) { + ESP_LOGI("VL53L1X", "Setting sensor xtalk calibration to %d", this->xtalk.value()); + status = this->sensor.SetXTalk(this->xtalk.value()); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE("VL53L1X", "Could not set sensor offset calibration, error code: %d", status); + this->mark_failed(); + return; + } + } +} + +void TofSensor::set_ranging_mode(const RangingMode *mode) { + auto status = this->sensor.SetDistanceMode(mode->mode); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE("VL53L1X", "Could not set distance mode. mode: %d", mode->mode); + } + + status = this->sensor.SetTimingBudgetInMs(mode->timing_budget); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE("VL53L1X", "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); + } + + status = this->sensor.SetInterMeasurementInMs(mode->delay_between_measurements); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE("VL53L1X", "Could not set measurement delay. %d ms", mode->delay_between_measurements); + } + + ESP_LOGI("VL53L1X", "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, + mode->delay_between_measurements, mode->mode); +} + +optional TofSensor::read_distance(ROI *roi, VL53L1_Error &status) { + if (this->is_failed()) { + return {}; + } + + status = this->sensor.SetROI(roi->width, roi->height); + status += this->sensor.SetROICenter(roi->center); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGD("VL53L1X", "Could not set ROI, error code: %d", status); + return {}; + } + + status = this->sensor.StartRanging(); + + // Wait for the measurement to be ready + // TODO use interrupt_pin, if given, to await data ready instead of polling + uint8_t dataReady = false; + while (!dataReady) { + status += this->sensor.CheckForDataReady(&dataReady); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGD("VL53L1X", "Data not ready yet, error code: %d", status); + return {}; + } + delay(1); + } + + // Get the results + uint16_t distance; + status += this->sensor.GetDistanceInMm(&distance); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGD("VL53L1X", "Could not get distance, error code: %d", status); + return {}; + } + + // 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_LOGD("VL53L1X", "Could not stop ranging, error code: %d", status); + return {}; + } + + return optional(distance); +} + +} // namespace roode +} // namespace esphome diff --git a/components/roode/tof_sensor.h b/components/roode/tof_sensor.h new file mode 100644 index 00000000..8e3f4b8f --- /dev/null +++ b/components/roode/tof_sensor.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include + +#include "VL53L1X_ULD.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/log.h" +#include "ranging.h" +#include "roi.h" + +namespace esphome { +namespace roode { + +/** + * A wrapper for the VL53L1X, Time-of-Flight (ToF), laser-ranging sensor. + * This stores user calibration info. + */ +class TofSensor : public Component { + public: + void setup() override; + void dump_config() override; + // After GPIO, but before default...Is this needed? not sure. + float get_setup_priority() const override { return setup_priority::DATA; }; + + optional read_distance(ROI *roi, VL53L1_Error &error); + void set_ranging_mode(const RangingMode *mode); + + void set_i2c_address(uint8_t val) { this->address = val; } + void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } + optional get_ranging_mode_override() { return this->ranging_mode_override; } + 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; } + + protected: + VL53L1X_ULD sensor; + optional address{}; + optional xshut_pin{}; + optional interrupt_pin{}; + optional ranging_mode_override{}; + optional offset{}; + optional xtalk{}; +}; + +} // namespace roode +} // namespace esphome diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index a86c2c45..b1a78c2b 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -2,38 +2,22 @@ namespace esphome { namespace roode { -VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { +VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { last_sensor_status = sensor_status; - sensor_status += distanceSensor.SetROI(roi->width, roi->height); - sensor_status += distanceSensor.SetROICenter(roi->center); - sensor_status += distanceSensor.StartRanging(); - // Wait for the measurement to be ready - uint8_t dataReady = false; - while (!dataReady) { - sensor_status += distanceSensor.CheckForDataReady(&dataReady); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGD(TAG, "Data not ready yet. error code: %d", sensor_status); - return sensor_status; - } - delay(1); - } - - // Get the results - sensor_status += distanceSensor.GetDistanceInMm(&distance); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGD(TAG, "Could not get distance, error code: %d", sensor_status); + auto result = distanceSensor->read_distance(roi, sensor_status); + if (!result.has_value()) { return sensor_status; } // Fill sampling array if (samples < sample_size) { - Distances[samples] = this->getDistance(); + Distances[samples] = result.value(); samples++; } else { for (int i = 1; i < sample_size; i++) Distances[i - 1] = Distances[i]; - Distances[sample_size - 1] = this->getDistance(); + Distances[sample_size - 1] = result.value(); } min_distance = Distances[0]; if (sample_size >= 2) { @@ -43,20 +27,12 @@ VL53L1_Error Zone::readDistance(VL53L1X_ULD &distanceSensor) { } } } - - // After reading the results reset the interrupt to be able to take another - // measurement - sensor_status += distanceSensor.ClearInterrupt(); - sensor_status += distanceSensor.StopRanging(); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGD(TAG, "Could not stop ranging, error code: %d", sensor_status); - return sensor_status; - } + this->distance = result.value(); return sensor_status; } -void Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts) { +void Zone::calibrateThreshold(TofSensor *distanceSensor, int number_attempts) { int *zone_distances = new int[number_attempts]; int sum = 0; for (int i = 0; i < number_attempts; i++) { @@ -78,8 +54,7 @@ void Zone::calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts) threshold->max_percentage.value_or((threshold->max * 100) / threshold->idle)); } -void Zone::roi_calibration(VL53L1X_ULD &distanceSensor, uint16_t entry_threshold, uint16_t exit_threshold, - Orientation orientation) { +void Zone::roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Orientation orientation) { // the value of the average distance is used for computing the optimal size of the ROI and consequently also the // center of the two zones int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); diff --git a/components/roode/zone.h b/components/roode/zone.h index a6a6e3c3..2141402c 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -7,20 +7,13 @@ #include "esphome/core/optional.h" #include "orientation.h" #include "configuration.h" +#include "roi.h" +#include "tof_sensor.h" static const char *const TAG = "Zone"; static const char *const CALIBRATION = "Zone calibration"; namespace esphome { namespace roode { -struct ROI { - uint8_t width; - uint8_t height; - uint8_t center; - void set_width(uint8_t width) { this->width = width; } - void set_height(uint8_t height) { this->height = height; } - void set_center(uint8_t center) { this->center = center; } -}; - struct Threshold { /** Automatically determined idling distance (average of several measurements) */ uint16_t idle; @@ -37,10 +30,9 @@ struct Threshold { class Zone { public: explicit Zone(uint8_t id) : id{id} {}; - VL53L1_Error readDistance(VL53L1X_ULD &distanceSensor); - void calibrateThreshold(VL53L1X_ULD &distanceSensor, int number_attempts); - void roi_calibration(VL53L1X_ULD &distanceSensor, uint16_t entry_threshold, uint16_t exit_threshold, - Orientation orientation); + VL53L1_Error readDistance(TofSensor *distanceSensor); + void calibrateThreshold(TofSensor *distanceSensor, int number_attempts); + void roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Orientation orientation); const uint8_t id; uint16_t getDistance() const; uint16_t getMinDistance() const; From 6358eed662c999e32aed9f791f2ca215a4cd2e34 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jan 2022 12:36:09 -0600 Subject: [PATCH 32/54] Refactor VL53L1X sensor into entirely separate component --- components/roode/__init__.py | 104 +++-------------- components/roode/roode.cpp | 4 +- components/roode/roode.h | 9 +- components/roode/zone.cpp | 1 + components/roode/zone.h | 7 +- components/vl53l1x/__init__.py | 105 ++++++++++++++++++ components/{roode => vl53l1x}/ranging.h | 4 +- components/{roode => vl53l1x}/roi.h | 4 +- .../tof_sensor.cpp => vl53l1x/vl53l1x.cpp} | 63 ++++++----- .../{roode/tof_sensor.h => vl53l1x/vl53l1x.h} | 8 +- 10 files changed, 179 insertions(+), 130 deletions(-) create mode 100644 components/vl53l1x/__init__.py rename components/{roode => vl53l1x}/ranging.h (95%) rename components/{roode => vl53l1x}/roi.h (86%) rename components/{roode/tof_sensor.cpp => vl53l1x/vl53l1x.cpp} (53%) rename components/{roode/tof_sensor.h => vl53l1x/vl53l1x.h} (88%) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 4ff0fd44..d27b18bf 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -1,19 +1,16 @@ -from re import I -from typing import Dict, Union, Any +from typing import Dict, Union import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_HEIGHT, CONF_ID, - CONF_INTERRUPT, CONF_INVERT, - CONF_OFFSET, - CONF_PINS, + CONF_SENSOR, CONF_WIDTH, ) -import esphome.pins as pins +from ..vl53l1x import NullableSchema, VL53L1X -# DEPENDENCIES = ["i2c"] +DEPENDENCIES = ["vl53l1x"] AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor", "number"] MULTI_CONF = True @@ -24,19 +21,14 @@ CONF_AUTO = "auto" CONF_ORIENTATION = "orientation" -CONF_CALIBRATION = "calibration" CONF_DETECTION_THRESHOLDS = "detection_thresholds" -CONF_I2C_ADDRESS = "i2c_address" CONF_ENTRY_ZONE = "entry" CONF_EXIT_ZONE = "exit" CONF_CENTER = "center" CONF_MAX = "max" CONF_MIN = "min" -CONF_RANGING_MODE = "ranging" CONF_ROI = "roi" CONF_SAMPLING = "sampling" -CONF_XSHUT = "xshut" -CONF_XTALK = "xtalk" CONF_ZONES = "zones" Orientation = roode_ns.enum("Orientation") @@ -45,35 +37,9 @@ "perpendicular": Orientation.Perpendicular, } -Ranging = roode_ns.namespace("Ranging") -RANGING_MODES = { - CONF_AUTO: CONF_AUTO, - "shortest": Ranging.Shortest, - "short": Ranging.Short, - "medium": Ranging.Medium, - "long": Ranging.Long, - "longer": Ranging.Longer, - "longest": Ranging.Longest, -} - -int16_t = cv.int_range(min=-32768, max=32768) # signed roi_range = cv.int_range(min=4, max=16) -def NullableSchema(*args, default: Any = None, **kwargs): - """ - Same as Schema but will convert nulls to empty objects. Useful when all the schema keys are optional. - Allows YAML lines to be commented out leaving an "empty dict" which is mistakenly parsed as None. - """ - - def none_to_empty(value): - if value is None: - return {} if default is None else default - raise cv.Invalid("Expected none") - - return cv.Any(cv.Schema(*args, **kwargs), none_to_empty) - - ROI_SCHEMA = cv.Any( NullableSchema( { @@ -102,22 +68,7 @@ def none_to_empty(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Roode), - cv.Optional(CONF_I2C_ADDRESS): cv.i2c_address, - cv.Optional(CONF_PINS, default={}): NullableSchema( - { - cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, - cv.Optional(CONF_INTERRUPT): pins.gpio_output_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.uint16_t, - cv.Optional(CONF_OFFSET): int16_t, - } - ), + cv.Required(CONF_SENSOR): cv.use_id(VL53L1X), cv.Optional(CONF_ORIENTATION, default="parallel"): cv.enum(ORIENTATION_VALUES), cv.Optional(CONF_SAMPLING, default=2): cv.All(cv.uint8_t, cv.Range(min=1)), cv.Optional(CONF_ROI, default={}): ROI_SCHEMA, @@ -134,41 +85,12 @@ def none_to_empty(value): async def to_code(config: Dict): - cg.add_library("Wire", None) - cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") - roode = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(roode, config) - await setup_hardware(config, roode) - await setup_calibration(config[CONF_CALIBRATION], roode) - await setup_algorithm(config, roode) - - -async def setup_hardware(config: Dict, roode: cg.Pvariable): - tof = cg.MockObj(f"{roode}->get_tof_sensor()", "->") - if CONF_I2C_ADDRESS in config: - cg.add(tof.set_i2c_address(config[CONF_I2C_ADDRESS])) - pins = config[CONF_PINS] - if CONF_INTERRUPT in pins: - interrupt = await cg.gpio_pin_expression(pins[CONF_INTERRUPT]) - cg.add(tof.set_interrupt_pin(interrupt)) - if CONF_XSHUT in pins: - xshut = await cg.gpio_pin_expression(pins[CONF_XSHUT]) - cg.add(tof.set_xshut_pin(xshut)) - - -async def setup_calibration(config: Dict, roode: cg.Pvariable): - tof = cg.MockObj(f"{roode}->get_tof_sensor()", "->") - if config.get(CONF_RANGING_MODE, CONF_AUTO) != CONF_AUTO: - cg.add(tof.set_ranging_mode_override(config[CONF_RANGING_MODE])) - if CONF_XTALK in config: - cg.add(tof.set_xtalk(config[CONF_XTALK])) - if CONF_OFFSET in config: - cg.add(tof.set_offset(config[CONF_OFFSET])) - + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(roode.set_tof_sensor(sens)) -async def setup_algorithm(config: Dict, roode: cg.Pvariable): cg.add(roode.set_orientation(config[CONF_ORIENTATION])) cg.add(roode.set_sampling_size(config[CONF_SAMPLING])) cg.add(roode.set_invert_direction(config[CONF_ZONES][CONF_INVERT])) @@ -193,16 +115,20 @@ def setup_zone(name: str, config: Dict, roode: cg.Pvariable): def setup_roi(var: cg.MockObj, config: Union[Dict, str], fallback: Union[Dict, str]): config: Dict = ( - config if config != "auto" else {CONF_HEIGHT: "auto", CONF_WIDTH: "auto"} + config + if config != CONF_AUTO + else {CONF_HEIGHT: CONF_AUTO, CONF_WIDTH: CONF_AUTO} ) fallback: Dict = ( - fallback if fallback != "auto" else {CONF_HEIGHT: "auto", CONF_WIDTH: "auto"} + fallback + if fallback != CONF_AUTO + else {CONF_HEIGHT: CONF_AUTO, CONF_WIDTH: CONF_AUTO} ) height = config.get(CONF_HEIGHT, fallback.get(CONF_HEIGHT, 16)) width = config.get(CONF_WIDTH, fallback.get(CONF_WIDTH, 6)) - if height != "auto": + if height != CONF_AUTO: cg.add(var.set_height(height)) - if width != "auto": + if width != CONF_AUTO: cg.add(var.set_width(width)) if CONF_CENTER in config: cg.add(var.set_center(config[CONF_CENTER])) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 5658a86b..aa9f614a 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -1,6 +1,5 @@ #include "roode.h" -#include "esphome/core/log.h" namespace esphome { namespace roode { void Roode::dump_config() { @@ -254,7 +253,7 @@ const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_dista } void Roode::calibrateZones() { - ESP_LOGI(SETUP, "Calibrating sensor zone"); + ESP_LOGI(SETUP, "Calibrating sensor zones"); calibrateDistance(); entry->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); @@ -265,6 +264,7 @@ void Roode::calibrateZones() { publishSensorConfiguration(entry, exit, true); App.feed_wdt(); publishSensorConfiguration(entry, exit, false); + ESP_LOGI(SETUP, "Finished calibrating sensor zones"); } void Roode::calibrateDistance() { diff --git a/components/roode/roode.h b/components/roode/roode.h index e51b38bd..39846c34 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -1,18 +1,19 @@ #pragma once #include -#include "VL53L1X_ULD.h" #include "configuration.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "../vl53l1x/vl53l1x.h" #include "orientation.h" #include "zone.h" -#include "ranging.h" -#include "tof_sensor.h" + +using namespace esphome::vl53l1x; +using TofSensor = esphome::vl53l1x::VL53L1X; namespace esphome { namespace roode { diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index b1a78c2b..6f1361c5 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -33,6 +33,7 @@ VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { } void Zone::calibrateThreshold(TofSensor *distanceSensor, int number_attempts) { + ESP_LOGI(CALIBRATION, "Beginning. zoneId: %d", id); int *zone_distances = new int[number_attempts]; int sum = 0; for (int i = 0; i < number_attempts; i++) { diff --git a/components/roode/zone.h b/components/roode/zone.h index 2141402c..2ac31ba0 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -1,14 +1,15 @@ #pragma once #include -#include "VL53L1X_ULD.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" +#include "../vl53l1x/vl53l1x.h" #include "orientation.h" #include "configuration.h" -#include "roi.h" -#include "tof_sensor.h" + +using TofSensor = esphome::vl53l1x::VL53L1X; +using esphome::vl53l1x::ROI; static const char *const TAG = "Zone"; static const char *const CALIBRATION = "Zone calibration"; diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py new file mode 100644 index 00000000..b299ae9b --- /dev/null +++ b/components/vl53l1x/__init__.py @@ -0,0 +1,105 @@ +from typing import Dict, Any +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INTERRUPT, + CONF_OFFSET, + CONF_PINS, +) +import esphome.pins as pins + +# DEPENDENCIES = ["i2c"] +AUTO_LOAD = [] + +vl53l1x_ns = cg.esphome_ns.namespace("vl53l1x") +VL53L1X = vl53l1x_ns.class_("VL53L1X", cg.Component) + +CONF_AUTO = "auto" +CONF_CALIBRATION = "calibration" +CONF_I2C_ADDRESS = "i2c_address" +CONF_RANGING_MODE = "ranging" +CONF_XSHUT = "xshut" +CONF_XTALK = "xtalk" + +Ranging = vl53l1x_ns.namespace("Ranging") +RANGING_MODES = { + CONF_AUTO: CONF_AUTO, + "shortest": Ranging.Shortest, + "short": Ranging.Short, + "medium": Ranging.Medium, + "long": Ranging.Long, + "longer": Ranging.Longer, + "longest": Ranging.Longest, +} + +int16_t = cv.int_range(min=-32768, max=32768) # signed + + +def NullableSchema(*args, default: Any = None, **kwargs): + """ + Same as Schema but will convert nulls to empty objects. Useful when all the schema keys are optional. + Allows YAML lines to be commented out leaving an "empty dict" which is mistakenly parsed as None. + """ + + def none_to_empty(value): + if value is None: + return {} if default is None else default + raise cv.Invalid("Expected none") + + return cv.Any(cv.Schema(*args, **kwargs), none_to_empty) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(VL53L1X), + cv.Optional(CONF_I2C_ADDRESS): cv.i2c_address, + cv.Optional(CONF_PINS, default={}): NullableSchema( + { + cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.gpio_output_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.uint16_t, + cv.Optional(CONF_OFFSET): int16_t, + } + ), + } +) + + +async def to_code(config: Dict): + cg.add_library("Wire", None) + cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") + + vl53l1x = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(vl53l1x, config) + + await setup_hardware(vl53l1x, config) + await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) + + +async def setup_hardware(vl53l1x: cg.Pvariable, config: Dict): + if CONF_I2C_ADDRESS in config: + cg.add(vl53l1x.set_i2c_address(config[CONF_I2C_ADDRESS])) + pins = config[CONF_PINS] + if CONF_INTERRUPT in pins: + interrupt = await cg.gpio_pin_expression(pins[CONF_INTERRUPT]) + cg.add(vl53l1x.set_interrupt_pin(interrupt)) + if CONF_XSHUT in pins: + xshut = await cg.gpio_pin_expression(pins[CONF_XSHUT]) + cg.add(vl53l1x.set_xshut_pin(xshut)) + + +async def setup_calibration(vl53l1x: cg.Pvariable, config: Dict): + if config.get(CONF_RANGING_MODE, CONF_AUTO) != CONF_AUTO: + cg.add(vl53l1x.set_ranging_mode_override(config[CONF_RANGING_MODE])) + if CONF_XTALK in config: + cg.add(vl53l1x.set_xtalk(config[CONF_XTALK])) + if CONF_OFFSET in config: + cg.add(vl53l1x.set_offset(config[CONF_OFFSET])) diff --git a/components/roode/ranging.h b/components/vl53l1x/ranging.h similarity index 95% rename from components/roode/ranging.h rename to components/vl53l1x/ranging.h index 603f81c7..8201f402 100644 --- a/components/roode/ranging.h +++ b/components/vl53l1x/ranging.h @@ -2,7 +2,7 @@ #include "VL53L1X_ULD.h" namespace esphome { -namespace roode { +namespace vl53l1x { struct RangingMode { explicit RangingMode(uint16_t timing_budget, EDistanceMode mode = EDistanceMode::Long) @@ -24,5 +24,5 @@ __attribute__((unused)) static const RangingMode *Longest = new RangingMode(200) // NOLINTEND(cert-err58-cpp) } // namespace Ranging -} // namespace roode +} // namespace vl53l1x } // namespace esphome diff --git a/components/roode/roi.h b/components/vl53l1x/roi.h similarity index 86% rename from components/roode/roi.h rename to components/vl53l1x/roi.h index 90a9b48b..c5c6ad4f 100644 --- a/components/roode/roi.h +++ b/components/vl53l1x/roi.h @@ -1,7 +1,7 @@ #pragma once namespace esphome { -namespace roode { +namespace vl53l1x { struct ROI { uint8_t width; @@ -12,5 +12,5 @@ struct ROI { void set_center(uint8_t val) { this->center = val; } }; -} // namespace roode +} // namespace vl53l1x } // namespace esphome diff --git a/components/roode/tof_sensor.cpp b/components/vl53l1x/vl53l1x.cpp similarity index 53% rename from components/roode/tof_sensor.cpp rename to components/vl53l1x/vl53l1x.cpp index 4ade56af..a0cb4aff 100644 --- a/components/roode/tof_sensor.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -1,78 +1,90 @@ -#include "tof_sensor.h" +#include "vl53l1x.h" namespace esphome { -namespace roode { +namespace vl53l1x { -void TofSensor::dump_config() { - ESP_LOGCONFIG("VL53L1X", "dump config:"); +void VL53L1X::dump_config() { + ESP_LOGCONFIG(TAG, "dump config:"); if (this->address.has_value()) { - ESP_LOGCONFIG("VL53L1X", " Address: 0x%02X", this->address.value()); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address.value()); } } -void TofSensor::setup() { - Wire.begin(); - Wire.setClock(400000); +void VL53L1X::setup() { + auto speed = Wire.getClock(); + if (speed < 400000) { + ESP_LOGW(TAG, "Slow clock speed, speed: %d", speed); + } // TODO use xshut_pin, if given, to change address auto status = this->address.has_value() ? this->sensor.Begin(this->address.value()) : this->sensor.Begin(); if (status != VL53L1_ERROR_NONE) { // If the sensor could not be initialized print out the error code. -7 is timeout - ESP_LOGE("VL53L1X", "Could not initialize the sensor, error code: %d", status); + ESP_LOGE(TAG, "Could not initialize the sensor, error code: %d", status); this->mark_failed(); return; } if (this->offset.has_value()) { - ESP_LOGI("VL53L1X", "Setting sensor offset calibration to %d", this->offset.value()); + ESP_LOGI(TAG, "Setting sensor offset calibration to %d", this->offset.value()); status = this->sensor.SetOffsetInMm(this->offset.value()); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE("VL53L1X", "Could not set sensor offset calibration, error code: %d", status); + ESP_LOGE(TAG, "Could not set sensor offset calibration, error code: %d", status); this->mark_failed(); return; } } if (this->xtalk.has_value()) { - ESP_LOGI("VL53L1X", "Setting sensor xtalk calibration to %d", this->xtalk.value()); + ESP_LOGI(TAG, "Setting sensor xtalk calibration to %d", this->xtalk.value()); status = this->sensor.SetXTalk(this->xtalk.value()); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE("VL53L1X", "Could not set sensor offset calibration, error code: %d", status); + ESP_LOGE(TAG, "Could not set sensor offset calibration, error code: %d", status); this->mark_failed(); return; } } + + ESP_LOGI(TAG, "Setup complete"); } -void TofSensor::set_ranging_mode(const RangingMode *mode) { +void VL53L1X::set_ranging_mode(const RangingMode *mode) { + if (this->is_failed()) { + ESP_LOGE(TAG, "Cannot set ranging mode while component is failed"); + return; + } + auto status = this->sensor.SetDistanceMode(mode->mode); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE("VL53L1X", "Could not set distance mode. mode: %d", mode->mode); + ESP_LOGE(TAG, "Could not set distance mode. mode: %d", mode->mode); } status = this->sensor.SetTimingBudgetInMs(mode->timing_budget); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE("VL53L1X", "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); + ESP_LOGE(TAG, "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); } status = this->sensor.SetInterMeasurementInMs(mode->delay_between_measurements); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE("VL53L1X", "Could not set measurement delay. %d ms", mode->delay_between_measurements); + ESP_LOGE(TAG, "Could not set measurement delay. %d ms", mode->delay_between_measurements); } - ESP_LOGI("VL53L1X", "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, + ESP_LOGI(TAG, "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, mode->delay_between_measurements, mode->mode); } -optional TofSensor::read_distance(ROI *roi, VL53L1_Error &status) { +optional VL53L1X::read_distance(ROI *roi, VL53L1_Error &status) { if (this->is_failed()) { + ESP_LOGE(TAG, "Cannot read distance while component is failed"); return {}; } + ESP_LOGV(TAG, "Beginning distance read"); + status = this->sensor.SetROI(roi->width, roi->height); status += this->sensor.SetROICenter(roi->center); if (status != VL53L1_ERROR_NONE) { - ESP_LOGD("VL53L1X", "Could not set ROI, error code: %d", status); + ESP_LOGE(TAG, "Could not set ROI, error code: %d", status); return {}; } @@ -84,7 +96,7 @@ optional TofSensor::read_distance(ROI *roi, VL53L1_Error &status) { while (!dataReady) { status += this->sensor.CheckForDataReady(&dataReady); if (status != VL53L1_ERROR_NONE) { - ESP_LOGD("VL53L1X", "Data not ready yet, error code: %d", status); + ESP_LOGE(TAG, "Failed to check if data is ready, error code: %d", status); return {}; } delay(1); @@ -94,7 +106,7 @@ optional TofSensor::read_distance(ROI *roi, VL53L1_Error &status) { uint16_t distance; status += this->sensor.GetDistanceInMm(&distance); if (status != VL53L1_ERROR_NONE) { - ESP_LOGD("VL53L1X", "Could not get distance, error code: %d", status); + ESP_LOGE(TAG, "Could not get distance, error code: %d", status); return {}; } @@ -102,12 +114,13 @@ optional TofSensor::read_distance(ROI *roi, VL53L1_Error &status) { status = this->sensor.ClearInterrupt(); status += this->sensor.StopRanging(); if (status != VL53L1_ERROR_NONE) { - ESP_LOGD("VL53L1X", "Could not stop ranging, error code: %d", status); + ESP_LOGE(TAG, "Could not stop ranging, error code: %d", status); return {}; } - return optional(distance); + ESP_LOGV(TAG, "Finished distance read"); + return {distance}; } -} // namespace roode +} // namespace vl53l1x } // namespace esphome diff --git a/components/roode/tof_sensor.h b/components/vl53l1x/vl53l1x.h similarity index 88% rename from components/roode/tof_sensor.h rename to components/vl53l1x/vl53l1x.h index 8e3f4b8f..7ef3ea53 100644 --- a/components/roode/tof_sensor.h +++ b/components/vl53l1x/vl53l1x.h @@ -11,13 +11,14 @@ #include "roi.h" namespace esphome { -namespace roode { +namespace vl53l1x { +static const char *const TAG = "VL53L1X"; /** * A wrapper for the VL53L1X, Time-of-Flight (ToF), laser-ranging sensor. * This stores user calibration info. */ -class TofSensor : public Component { +class VL53L1X : public Component { public: void setup() override; void dump_config() override; @@ -40,10 +41,11 @@ class TofSensor : public Component { optional address{}; optional xshut_pin{}; optional interrupt_pin{}; + /** Mode from user config, which can be get/set independently of current mode */ optional ranging_mode_override{}; optional offset{}; optional xtalk{}; }; -} // namespace roode +} // namespace vl53l1x } // namespace esphome From 190b3f194d667777425d50d102dd7fce75833bc9 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jan 2022 13:36:34 -0600 Subject: [PATCH 33/54] Update VL53L1X to extend I2CDevice Although we don't completely use this I2C abstraction yet, it gets us closer. --- components/vl53l1x/__init__.py | 13 +++++-------- components/vl53l1x/vl53l1x.cpp | 6 ++---- components/vl53l1x/vl53l1x.h | 4 +--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index b299ae9b..f41a8409 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -1,4 +1,5 @@ from typing import Dict, Any +from esphome.components import i2c import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -9,15 +10,14 @@ ) import esphome.pins as pins -# DEPENDENCIES = ["i2c"] -AUTO_LOAD = [] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["i2c"] vl53l1x_ns = cg.esphome_ns.namespace("vl53l1x") VL53L1X = vl53l1x_ns.class_("VL53L1X", cg.Component) CONF_AUTO = "auto" CONF_CALIBRATION = "calibration" -CONF_I2C_ADDRESS = "i2c_address" CONF_RANGING_MODE = "ranging" CONF_XSHUT = "xshut" CONF_XTALK = "xtalk" @@ -53,7 +53,6 @@ def none_to_empty(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(VL53L1X), - cv.Optional(CONF_I2C_ADDRESS): cv.i2c_address, cv.Optional(CONF_PINS, default={}): NullableSchema( { cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, @@ -70,23 +69,21 @@ def none_to_empty(value): } ), } -) +).extend(i2c.i2c_device_schema(0x52)) async def to_code(config: Dict): - cg.add_library("Wire", None) cg.add_library("rneurink", "1.2.3", "VL53L1X_ULD") vl53l1x = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(vl53l1x, config) + await i2c.register_i2c_device(vl53l1x, config) await setup_hardware(vl53l1x, config) await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) async def setup_hardware(vl53l1x: cg.Pvariable, config: Dict): - if CONF_I2C_ADDRESS in config: - cg.add(vl53l1x.set_i2c_address(config[CONF_I2C_ADDRESS])) pins = config[CONF_PINS] if CONF_INTERRUPT in pins: interrupt = await cg.gpio_pin_expression(pins[CONF_INTERRUPT]) diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index a0cb4aff..9d361207 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -5,9 +5,7 @@ namespace vl53l1x { void VL53L1X::dump_config() { ESP_LOGCONFIG(TAG, "dump config:"); - if (this->address.has_value()) { - ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address.value()); - } + LOG_I2C_DEVICE(this) } void VL53L1X::setup() { @@ -17,7 +15,7 @@ void VL53L1X::setup() { } // TODO use xshut_pin, if given, to change address - auto status = this->address.has_value() ? this->sensor.Begin(this->address.value()) : this->sensor.Begin(); + auto status = this->sensor.Begin(this->address_); 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); diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h index 7ef3ea53..f7d47ea5 100644 --- a/components/vl53l1x/vl53l1x.h +++ b/components/vl53l1x/vl53l1x.h @@ -18,7 +18,7 @@ static const char *const TAG = "VL53L1X"; * A wrapper for the VL53L1X, Time-of-Flight (ToF), laser-ranging sensor. * This stores user calibration info. */ -class VL53L1X : public Component { +class VL53L1X : public i2c::I2CDevice, public Component { public: void setup() override; void dump_config() override; @@ -28,7 +28,6 @@ class VL53L1X : public Component { optional read_distance(ROI *roi, VL53L1_Error &error); void set_ranging_mode(const RangingMode *mode); - void set_i2c_address(uint8_t val) { this->address = val; } void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } optional get_ranging_mode_override() { return this->ranging_mode_override; } @@ -38,7 +37,6 @@ class VL53L1X : public Component { protected: VL53L1X_ULD sensor; - optional address{}; optional xshut_pin{}; optional interrupt_pin{}; /** Mode from user config, which can be get/set independently of current mode */ From b313082072b032c1234a4ca72f9a3cac1b74b5b4 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jan 2022 13:40:31 -0600 Subject: [PATCH 34/54] Increase default i2c frequency to what we hardcoded previously --- components/vl53l1x/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index f41a8409..df7d491f 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -1,9 +1,13 @@ from typing import Dict, Any from esphome.components import i2c +from esphome.core import CORE import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_FREQUENCY, CONF_ID, + CONF_I2C, + CONF_I2C_ID, CONF_INTERRUPT, CONF_OFFSET, CONF_PINS, @@ -79,6 +83,15 @@ async def to_code(config: Dict): await cg.register_component(vl53l1x, config) await i2c.register_i2c_device(vl53l1x, config) + # If i2c frequency has not been explicitly set, then increase it to our recommended + i2c_id = config[CONF_I2C_ID] + i2c_config = next( + entry for entry in CORE.config[CONF_I2C] if entry[CONF_ID] == i2c_id + ) + if i2c_config[CONF_FREQUENCY] == 50000: # default + i2c_var = await cg.get_variable(i2c_id) + cg.add(i2c_var.set_frequency(400000)) + await setup_hardware(vl53l1x, config) await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) From f4ba7be3c8e8f5d5462e5c0cb20bc4a4b75c25b1 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jan 2022 13:48:54 -0600 Subject: [PATCH 35/54] Add multi conf comment for multiple tof sensors --- components/vl53l1x/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index df7d491f..414a9b6e 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -16,6 +16,7 @@ DEPENDENCIES = ["i2c"] AUTO_LOAD = ["i2c"] +MULTI_CONF = False # TODO enable when we support multiple addresses vl53l1x_ns = cg.esphome_ns.namespace("vl53l1x") VL53L1X = vl53l1x_ns.class_("VL53L1X", cg.Component) From c85d6c0eab39cb0ced4a02516826302d98e5dd44 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 12 Jan 2022 17:26:33 -0600 Subject: [PATCH 36/54] Fix sampling in Zone --- components/roode/roode.h | 6 +++++- components/roode/zone.cpp | 26 +++++++------------------- components/roode/zone.h | 8 ++++---- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/components/roode/roode.h b/components/roode/roode.h index 39846c34..1758f0a9 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -66,7 +66,11 @@ class Roode : public PollingComponent { void set_tof_sensor(TofSensor *sensor) { this->distanceSensor = sensor; } void set_invert_direction(bool dir) { invert_direction_ = dir; } void set_orientation(Orientation val) { orientation_ = val; } - void set_sampling_size(uint8_t size) { samples = size; } + void set_sampling_size(uint8_t size) { + samples = size; + entry->set_max_samples(size); + exit->set_max_samples(size); + } void set_distance_entry(sensor::Sensor *distance_entry_) { distance_entry = distance_entry_; } void set_distance_exit(sensor::Sensor *distance_exit_) { distance_exit = distance_exit_; } void set_people_counter(number::Number *counter) { this->people_counter = counter; } diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 6f1361c5..687a84d9 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -10,24 +10,12 @@ VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { return sensor_status; } - // Fill sampling array - if (samples < sample_size) { - Distances[samples] = result.value(); - samples++; - } else { - for (int i = 1; i < sample_size; i++) - Distances[i - 1] = Distances[i]; - Distances[sample_size - 1] = result.value(); - } - min_distance = Distances[0]; - if (sample_size >= 2) { - for (int i = 1; i < sample_size; i++) { - if (Distances[i] < min_distance) { - min_distance = Distances[i]; - } - } - } - this->distance = result.value(); + last_distance = result.value(); + samples.insert(samples.begin(), result.value()); + if (samples.size() > max_samples) { + samples.pop_back(); + }; + min_distance = *std::min_element(samples.begin(), samples.end()); return sensor_status; } @@ -119,7 +107,7 @@ int Zone::getOptimizedValues(int *values, int sum, int size) { return avg - sd; } -uint16_t Zone::getDistance() const { return this->distance; } +uint16_t Zone::getDistance() const { return this->last_distance; } uint16_t Zone::getMinDistance() const { return this->min_distance; } } // namespace roode } // namespace esphome \ No newline at end of file diff --git a/components/roode/zone.h b/components/roode/zone.h index 2ac31ba0..cec9997d 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -39,14 +39,14 @@ class Zone { uint16_t getMinDistance() const; ROI *roi = new ROI(); Threshold *threshold = new Threshold(); + void set_max_samples(uint8_t max) { max_samples = max; }; protected: int getOptimizedValues(int *values, int sum, int size); - uint16_t distance; - int *Distances; - uint8_t sample_size; - uint8_t samples; + uint16_t last_distance; uint16_t min_distance; + std::vector samples; + uint8_t max_samples; }; } // namespace roode } // namespace esphome From 19c0e62a03efc1f504ca86df2113233182312e61 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 15:58:00 -0600 Subject: [PATCH 37/54] Have Roode auto load VL53L1X component and let roode's sensor id default to first found. This lets the entire VL53L1X component be omitted from config if using defaults --- components/roode/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index d27b18bf..9a034ec8 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -11,7 +11,7 @@ from ..vl53l1x import NullableSchema, VL53L1X DEPENDENCIES = ["vl53l1x"] -AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor", "number"] +AUTO_LOAD = ["vl53l1x", "sensor", "binary_sensor", "text_sensor", "number"] MULTI_CONF = True CONF_ROODE_ID = "roode_id" @@ -68,7 +68,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Roode), - cv.Required(CONF_SENSOR): cv.use_id(VL53L1X), + cv.GenerateID(CONF_SENSOR): cv.use_id(VL53L1X), cv.Optional(CONF_ORIENTATION, default="parallel"): cv.enum(ORIENTATION_VALUES), cv.Optional(CONF_SAMPLING, default=2): cv.All(cv.uint8_t, cv.Range(min=1)), cv.Optional(CONF_ROI, default={}): ROI_SCHEMA, From 543e0dae4713e33f977ed033df22989e2e9cff79 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 19:35:21 -0600 Subject: [PATCH 38/54] Create Zone.roi_override that config writes to separate from current ROI. This allows ROI to be recalibrated later without losing what the config overrides are. --- components/roode/__init__.py | 2 +- components/roode/roode.cpp | 16 +++++----------- components/roode/roode.h | 1 - components/roode/zone.cpp | 24 ++++++++++++++++-------- components/roode/zone.h | 2 ++ 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 9a034ec8..14dbd393 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -102,7 +102,7 @@ def setup_zone(name: str, config: Dict, roode: cg.Pvariable): zone_config = config[CONF_ZONES][name] zone_var = cg.MockObj(f"{roode}->{name}", "->") - roi_var = cg.MockObj(f"{zone_var}->roi", "->") + roi_var = cg.MockObj(f"{zone_var}->roi_override", "->") setup_roi(roi_var, zone_config.get(CONF_ROI, {}), config.get(CONF_ROI, {})) threshold_var = cg.MockObj(f"{zone_var}->threshold", "->") diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index aa9f614a..24748db0 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -12,8 +12,7 @@ void Roode::setup() { version_sensor->publish_state(VERSION); } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); - ESP_LOGI(SETUP, "Creating entry and exit zones."); - createEntryAndExitZone(); + calibrateZones(); } @@ -60,15 +59,6 @@ bool Roode::handleSensorStatus() { return check_status; } -void Roode::createEntryAndExitZone() { - if (!entry->roi->center) { - entry->roi->center = orientation_ == Parallel ? 167 : 195; - } - if (!exit->roi->center) { - exit->roi->center = orientation_ == Parallel ? 231 : 60; - } -} - VL53L1_Error Roode::getAlternatingZoneDistances() { this->current_zone->readDistance(distanceSensor); App.feed_wdt(); @@ -254,6 +244,10 @@ const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_dista void Roode::calibrateZones() { ESP_LOGI(SETUP, "Calibrating sensor zones"); + + entry->reset_roi(orientation_ == Parallel ? 167 : 195); + exit->reset_roi(orientation_ == Parallel ? 231 : 60); + calibrateDistance(); entry->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); diff --git a/components/roode/roode.h b/components/roode/roode.h index 1758f0a9..090cdf43 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -124,7 +124,6 @@ class Roode : public PollingComponent { VL53L1_Error getAlternatingZoneDistances(); void doPathTracking(Zone *zone); bool handleSensorStatus(); - void createEntryAndExitZone(); void calibrateDistance(); void calibrateZones(); const RangingMode *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index 687a84d9..b246ae99 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -20,8 +20,18 @@ VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { return sensor_status; } +/** + * This sets the ROI for the zone to the given overrides or the standard default. + * This is needed to do initial calibration of thresholds & ROI. + */ +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; +} + void Zone::calibrateThreshold(TofSensor *distanceSensor, int number_attempts) { - ESP_LOGI(CALIBRATION, "Beginning. zoneId: %d", id); + ESP_LOGD(CALIBRATION, "Beginning. zoneId: %d", id); int *zone_distances = new int[number_attempts]; int sum = 0; for (int i = 0; i < number_attempts; i++) { @@ -48,13 +58,11 @@ void Zone::roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Or // center of the two zones int function_of_the_distance = 16 * (1 - (0.15 * 2) / (0.34 * (min(entry_threshold, exit_threshold) / 1000))); int ROI_size = min(8, max(4, function_of_the_distance)); - if (!this->roi->width) { - this->roi->width = ROI_size; - } - if (!this->roi->height) { - this->roi->height = ROI_size * 2; - } - if (!this->roi->center) { + this->roi->width = this->roi_override->width ?: ROI_size; + this->roi->height = this->roi_override->height ?: ROI_size * 2; + if (this->roi_override->center) { + this->roi->center = this->roi_override->center; + } else { // now we set the position of the center of the two zones if (orientation == Parallel) { switch (ROI_size) { diff --git a/components/roode/zone.h b/components/roode/zone.h index cec9997d..0563717a 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -32,12 +32,14 @@ class Zone { public: explicit Zone(uint8_t id) : id{id} {}; VL53L1_Error readDistance(TofSensor *distanceSensor); + void reset_roi(uint8_t default_center); void calibrateThreshold(TofSensor *distanceSensor, int number_attempts); void roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Orientation orientation); const uint8_t id; uint16_t getDistance() const; uint16_t getMinDistance() const; ROI *roi = new ROI(); + ROI *roi_override = new ROI(); Threshold *threshold = new Threshold(); void set_max_samples(uint8_t max) { max_samples = max; }; From fa0a273691a9d79f6bd329b856565695e92311f9 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 19:49:17 -0600 Subject: [PATCH 39/54] Use ranging mode from config if given when determining idle distances. Don't set ranging mode again if it hasn't changed. --- components/roode/roode.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 24748db0..dde2a9fe 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -218,11 +218,7 @@ void Roode::updateCounter(int delta) { void Roode::recalibration() { calibrateZones(); } const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_distance, - uint16_t average_exit_zone_distance) { - if (this->distanceSensor->get_ranging_mode_override().has_value()) { - return this->distanceSensor->get_ranging_mode_override().value(); - } - + uint16_t average_exit_zone_distance) { uint16_t min = average_entry_zone_distance < average_exit_zone_distance ? average_entry_zone_distance : average_exit_zone_distance; uint16_t max = average_entry_zone_distance > average_exit_zone_distance ? average_entry_zone_distance @@ -262,13 +258,17 @@ void Roode::calibrateZones() { } void Roode::calibrateDistance() { - distanceSensor->set_ranging_mode(Ranging::Medium); + distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value_or(Ranging::Medium)); entry->calibrateThreshold(distanceSensor, number_attempts); exit->calibrateThreshold(distanceSensor, number_attempts); - auto *mode = determineRangingMode(entry->threshold->idle, exit->threshold->idle); - distanceSensor->set_ranging_mode(mode); + if (distanceSensor->get_ranging_mode_override().has_value()) { + auto *mode = determineRangingMode(entry->threshold->idle, exit->threshold->idle); + if (mode != Ranging::Medium) { // already set above + distanceSensor->set_ranging_mode(mode); + } + } } void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { From 99b59d690968030a7d8789c85723f616ec45d02c Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 19:59:52 -0600 Subject: [PATCH 40/54] Add status code to ranging setters error log --- components/vl53l1x/vl53l1x.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index 9d361207..c1b18166 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -54,17 +54,17 @@ void VL53L1X::set_ranging_mode(const RangingMode *mode) { auto status = this->sensor.SetDistanceMode(mode->mode); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set distance mode. mode: %d", mode->mode); + ESP_LOGE(TAG, "Could not set distance mode: %d, error code: %d", mode->mode, status); } status = this->sensor.SetTimingBudgetInMs(mode->timing_budget); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set timing budget. timing_budget: %d ms", mode->timing_budget); + ESP_LOGE(TAG, "Could not set timing budget: %d ms, error code: %d", mode->timing_budget, status); } status = this->sensor.SetInterMeasurementInMs(mode->delay_between_measurements); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set measurement delay. %d ms", mode->delay_between_measurements); + ESP_LOGE(TAG, "Could not set measurement delay: %d ms, error code: %d", mode->delay_between_measurements, status); } ESP_LOGI(TAG, "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, From 2d8a6899101752c6e1fa9a1db33f59d2e1077c52 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 20:54:22 -0600 Subject: [PATCH 41/54] Change threshold absolute values to use esphome's distance field --- components/roode/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 14dbd393..92f044cb 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -39,7 +39,6 @@ roi_range = cv.int_range(min=4, max=16) - ROI_SCHEMA = cv.Any( NullableSchema( { @@ -51,10 +50,23 @@ cv.one_of(CONF_AUTO), ) + +def distance_as_mm(): + dval = cv.distance + + def validator(value): + meters = dval(value) + return int(meters * 1000) + + return cv.All(validator, cv.Range(min=0, max=65535)) + + +threshold = cv.Any(cv.percentage, distance_as_mm()) + THRESHOLDS_SCHEMA = NullableSchema( { - cv.Optional(CONF_MIN): cv.Any(cv.uint16_t, cv.percentage), - cv.Optional(CONF_MAX): cv.Any(cv.uint16_t, cv.percentage), + cv.Optional(CONF_MIN): threshold, + cv.Optional(CONF_MAX): threshold, } ) From 6fdaaf14e394d3b9fd87b6a1b35b9efcdbf471d6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Thu, 13 Jan 2022 22:40:26 -0600 Subject: [PATCH 42/54] Tweak pin config --- components/vl53l1x/__init__.py | 4 ++-- components/vl53l1x/vl53l1x.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 414a9b6e..3f20b04e 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -60,8 +60,8 @@ def none_to_empty(value): cv.GenerateID(): cv.declare_id(VL53L1X), cv.Optional(CONF_PINS, default={}): NullableSchema( { - cv.Optional(CONF_XSHUT): pins.gpio_input_pin_schema, - cv.Optional(CONF_INTERRUPT): pins.gpio_output_pin_schema, + cv.Optional(CONF_XSHUT): pins.gpio_output_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.gpio_input_pin_schema, } ), cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h index f7d47ea5..3d62394b 100644 --- a/components/vl53l1x/vl53l1x.h +++ b/components/vl53l1x/vl53l1x.h @@ -28,7 +28,7 @@ class VL53L1X : public i2c::I2CDevice, public Component { optional read_distance(ROI *roi, VL53L1_Error &error); void set_ranging_mode(const RangingMode *mode); - void set_xshut_pin(InternalGPIOPin *pin) { this->xshut_pin = pin; } + void set_xshut_pin(GPIOPin *pin) { this->xshut_pin = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin = pin; } optional get_ranging_mode_override() { return this->ranging_mode_override; } void set_ranging_mode_override(const RangingMode *mode) { this->ranging_mode_override = {mode}; } @@ -37,7 +37,7 @@ class VL53L1X : public i2c::I2CDevice, public Component { protected: VL53L1X_ULD sensor; - optional xshut_pin{}; + optional xshut_pin{}; optional interrupt_pin{}; /** Mode from user config, which can be get/set independently of current mode */ optional ranging_mode_override{}; From b20f8d920a1111a65052a69babedc32fb4155ac8 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Fri, 14 Jan 2022 15:49:32 -0600 Subject: [PATCH 43/54] Mark interrupt pin as requiring an internal one which supports interrupts --- components/vl53l1x/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 3f20b04e..0abbfd73 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -61,7 +61,7 @@ def none_to_empty(value): cv.Optional(CONF_PINS, default={}): NullableSchema( { cv.Optional(CONF_XSHUT): pins.gpio_output_pin_schema, - cv.Optional(CONF_INTERRUPT): pins.gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.internal_gpio_input_pin_schema, } ), cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( From 6d937038f24ebbbd13f676524105fa8c5dafbd5b Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sat, 15 Jan 2022 13:12:44 +0100 Subject: [PATCH 44/54] Fix default sensor address --- components/vl53l1x/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 0abbfd73..1ef336f5 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -74,7 +74,7 @@ def none_to_empty(value): } ), } -).extend(i2c.i2c_device_schema(0x52)) +).extend(i2c.i2c_device_schema(0x29)) async def to_code(config: Dict): From 4ee6492808e59c105f992491afb489ef2338253a Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sat, 15 Jan 2022 15:07:51 +0100 Subject: [PATCH 45/54] Cleanup and fixes --- README.md | 56 +++++++++++++--------- components/roode/configuration.cpp | 76 ------------------------------ components/roode/configuration.h | 31 ------------ components/roode/roode.cpp | 76 +++++++++--------------------- components/roode/roode.h | 17 ++++--- components/roode/zone.h | 3 +- components/vl53l1x/vl53l1x.cpp | 2 + peopleCounter32.yaml | 42 ++++++++--------- peopleCounter32Dev.yaml | 49 +++++++++++-------- peopleCounter8266.yaml | 41 ++++++++-------- peopleCounter8266Dev.yaml | 55 +++++++++++---------- platformio.ini | 4 +- 12 files changed, 169 insertions(+), 283 deletions(-) delete mode 100644 components/roode/configuration.cpp delete mode 100644 components/roode/configuration.h diff --git a/README.md b/README.md index ac8c16ce..5b46ca32 100644 --- a/README.md +++ b/README.md @@ -70,27 +70,38 @@ Roode is provided as an external_component which means it is easy to setup in an Example configuration -``` +```yml +# Sensor is configured separately from Roode people counting algorithm +vl53l1x: + calibration: + # ranging: longest # defaults to "auto" for formerly "calibration: true" + # offset: 8 + # xtalk: 53406 + # These pins are not yet implemented but they are passed into class now + pins: + # xshut: 3 # shutdown pin to change address or multiple sensors + # interrupt: 1 # hardware callback when measurement is ready roode: id: roode_platform - i2c_address: 0x29 - update_interval: 200ms - calibration: - max_threshold_percentage: 85 - min_threshold_percentage: 5 - roi_calibration: true - # manual: - # sensor_mode: 2 - # roi_height: 16 - # roi_width: 6 - # manual_threshold: 1300 - # timing_budget: 100 - invert_direction: true - -number: - - platform: roode - people_counter: - name: People Count + sampling: 2 + # defaults for both zones + roi: + # height: 14 + # width: 6 + detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. + # min: 234mm # absolute distance + max: 85% # percent based on idle distance + zones: + invert: true + entry: + roi: auto # defaults to auto + exit: + roi: + # height: 8 + # width: 4 + # center: 124 + detection_thresholds: + max: 70% # override max for exit zone ``` ### Configuration variables @@ -110,8 +121,8 @@ number: - **timing_budget (optional, int)**: The timing budget for the sensor. Increasing this slows down detection but increases accuracy. Min: `10ms` Max: `1000s`. Defaults to `10ms`. - **invert_direction (Optional, bool)**: Inverts the counting direction. Switch to `true` if the movement count appears backwards. Defaults to `false`. - **advised_sensor_orientation(Optional, bool)**: Advised orientation has the two sensor pads parallel to the entryway. - So `false` means the pads are perpendicular to the entryway. - Defaults to `true`. + So `false` means the pads are perpendicular to the entryway. + Defaults to `true`. ### Sensor @@ -290,6 +301,7 @@ lower right. 4. Bad connections ## Sponsors + Thank you very much for you sponsorship! -* sunshine-hass +- sunshine-hass diff --git a/components/roode/configuration.cpp b/components/roode/configuration.cpp deleted file mode 100644 index 33d068bf..00000000 --- a/components/roode/configuration.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "configuration.h" - -#include "roode.h" -/* -This Object is used to store the sensor configratuin like calibration data, -sensor mode, ROI settings etc. -*/ -namespace esphome { -namespace roode { -void Configuration::setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int new_timing_budget) { - distanceSensor.StopRanging(); - switch (sensor_mode) { - case 0: // short mode - time_budget_in_ms = time_budget_in_ms_short; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Short); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Short); - } - ESP_LOGI(SETUP, "Set short mode. timing_budget: %d", time_budget_in_ms); - break; - case 1: // medium mode - time_budget_in_ms = time_budget_in_ms_medium; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium mode. timing_budget: %d", time_budget_in_ms); - break; - case 2: // medium_long mode - time_budget_in_ms = time_budget_in_ms_medium_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set medium long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 3: // long mode - time_budget_in_ms = time_budget_in_ms_long; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set long range mode. timing_budget: %d", time_budget_in_ms); - break; - case 4: // max mode - time_budget_in_ms = time_budget_in_ms_max; - delay_between_measurements = time_budget_in_ms + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Set max range mode. timing_budget: %d", time_budget_in_ms); - break; - case 5: // custom mode - time_budget_in_ms = new_timing_budget; - delay_between_measurements = new_timing_budget + 5; - sensor_status = distanceSensor.SetDistanceMode(Long); - if (sensor_status != VL53L1_ERROR_NONE) { - ESP_LOGE(SETUP, "Could not set distance mode. mode: %d", Long); - } - ESP_LOGI(SETUP, "Manually set custom range mode. timing_budget: %d", time_budget_in_ms); - break; - default: - break; - } - sensor_status = distanceSensor.SetTimingBudgetInMs(time_budget_in_ms); - if (sensor_status != 0) { - ESP_LOGE(SETUP, "Could not set timing budget. timing_budget: %d ms", time_budget_in_ms); - } -} -} // namespace roode -} // namespace esphome \ No newline at end of file diff --git a/components/roode/configuration.h b/components/roode/configuration.h deleted file mode 100644 index fbe447d8..00000000 --- a/components/roode/configuration.h +++ /dev/null @@ -1,31 +0,0 @@ - -#pragma once -#include - -#include "VL53L1X_ULD.h" -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/i2c/i2c.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/text_sensor/text_sensor.h" -#include "esphome/core/application.h" -#include "esphome/core/component.h" -#include "zone.h" - -static VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; -static VL53L1_Error sensor_status = VL53L1_ERROR_NONE; -namespace esphome { -namespace roode { -class Configuration { - public: - void setSensorMode(VL53L1X_ULD distanceSensor, int sensor_mode, int timing_budget = 0); - int getTimingBudget(); - - protected: - VL53L1X_ULD distanceSensor; - VL53L1_Error sensor_status = VL53L1_ERROR_NONE; - void setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax); - void setCorrectDistanceSettings(float average_entry_zone_distance, float average_exit_zone_distance); -}; -} // namespace roode -} // namespace esphome diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index dde2a9fe..dc52098d 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -13,7 +13,7 @@ void Roode::setup() { } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); - calibrateZones(); + calibrate_zones(); } void Roode::update() { @@ -27,10 +27,10 @@ void Roode::update() { void Roode::loop() { // unsigned long start = micros(); - getAlternatingZoneDistances(); + get_alternating_zone_distances(); // uint16_t samplingDistance = sampling(this->current_zone); - doPathTracking(this->current_zone); - handleSensorStatus(); + path_tracking(this->current_zone); + handle_sensor_status(); this->current_zone = this->current_zone == this->entry ? this->exit : this->entry; // ESP_LOGI("Experimental", "Entry zone: %d, exit zone: %d", // entry->getDistance(Roode::distanceSensor, Roode::sensor_status), @@ -39,7 +39,7 @@ void Roode::loop() { // loop", "loop took %lu microseconds", delta); } -bool Roode::handleSensorStatus() { +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) { @@ -59,13 +59,13 @@ bool Roode::handleSensorStatus() { return check_status; } -VL53L1_Error Roode::getAlternatingZoneDistances() { +VL53L1_Error Roode::get_alternating_zone_distances() { this->current_zone->readDistance(distanceSensor); App.feed_wdt(); return sensor_status; } -void Roode::doPathTracking(Zone *zone) { +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 // where nobody is any of the zones @@ -75,32 +75,8 @@ void Roode::doPathTracking(Zone *zone) { int AllZonesCurrentStatus = 0; int AnEventHasOccured = 0; - uint16_t Distances[2][samples]; - uint16_t MinDistance; - uint8_t i; - uint8_t zoneId = zone == this->entry ? 0 : 1; - auto zoneName = zone == this->entry ? "entry" : "exit "; - if (DistancesTableSize[zoneId] < samples) { - ESP_LOGD(TAG, "Distances[%d][DistancesTableSize[zone]] = %d", zoneId, zone->getDistance()); - Distances[zoneId][DistancesTableSize[zoneId]] = zone->getDistance(); - DistancesTableSize[zoneId]++; - } else { - for (i = 1; i < samples; i++) - Distances[zoneId][i - 1] = Distances[zoneId][i]; - Distances[zoneId][samples - 1] = zone->getDistance(); - ESP_LOGD(SETUP, "Distance[%s] = %d", zoneName, Distances[zoneId][samples - 1]); - } - // pick up the min distance - MinDistance = Distances[zoneId][0]; - if (DistancesTableSize[zoneId] >= 2) { - for (i = 1; i < DistancesTableSize[zoneId]; i++) { - if (Distances[zoneId][i] < MinDistance) - MinDistance = Distances[zoneId][i]; - } - } - // PathTrack algorithm - if (MinDistance < zone->threshold->max && MinDistance > zone->threshold->min) { + if (zone->getMinDistance() < zone->threshold->max && zone->getMinDistance() > zone->threshold->min) { // Someone is in the sensing area CurrentZoneStatus = SOMEONE; if (presence_sensor != nullptr) { @@ -146,7 +122,6 @@ void Roode::doPathTracking(Zone *zone) { // if an event has occured if (AnEventHasOccured) { - delay(1); ESP_LOGD(TAG, "Event has occured, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); if (PathTrackFillingSize < 4) { PathTrackFillingSize++; @@ -154,7 +129,6 @@ void Roode::doPathTracking(Zone *zone) { // if nobody anywhere lets check if an exit or entry has happened if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { - delay(1); ESP_LOGD(TAG, "Nobody anywhere, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); // check exit or entry only if PathTrackFillingSize is 4 (for example 0 1 // 3 2) and last event is 0 (nobobdy anywhere) @@ -165,8 +139,7 @@ void Roode::doPathTracking(Zone *zone) { if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) { // This an exit ESP_LOGI("Roode pathTracking", "Exit detected."); - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; + this->updateCounter(-1); if (entry_exit_event_sensor != nullptr) { entry_exit_event_sensor->publish_state("Exit"); @@ -178,12 +151,6 @@ void Roode::doPathTracking(Zone *zone) { if (entry_exit_event_sensor != nullptr) { entry_exit_event_sensor->publish_state("Entry"); } - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; - } else { - // reset the table filling size also in case of unexpected path - DistancesTableSize[0] = 0; - DistancesTableSize[1] = 0; } } @@ -215,10 +182,10 @@ void Roode::updateCounter(int delta) { ESP_LOGI(TAG, "Updating people count: %d", (int) next); this->people_counter->set(next); } -void Roode::recalibration() { calibrateZones(); } +void Roode::recalibration() { calibrate_zones(); } -const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_distance, - uint16_t average_exit_zone_distance) { +const RangingMode *Roode::determine_raning_mode(uint16_t average_entry_zone_distance, + uint16_t average_exit_zone_distance) { uint16_t min = average_entry_zone_distance < average_exit_zone_distance ? average_entry_zone_distance : average_exit_zone_distance; uint16_t max = average_entry_zone_distance > average_exit_zone_distance ? average_entry_zone_distance @@ -238,7 +205,7 @@ const RangingMode *Roode::determineRangingMode(uint16_t average_entry_zone_dista return Ranging::Longest; } -void Roode::calibrateZones() { +void Roode::calibrate_zones() { ESP_LOGI(SETUP, "Calibrating sensor zones"); entry->reset_roi(orientation_ == Parallel ? 167 : 195); @@ -251,27 +218,30 @@ void Roode::calibrateZones() { exit->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); exit->calibrateThreshold(distanceSensor, number_attempts); - publishSensorConfiguration(entry, exit, true); + publish_sensor_configuration(entry, exit, true); App.feed_wdt(); - publishSensorConfiguration(entry, exit, false); + publish_sensor_configuration(entry, exit, false); ESP_LOGI(SETUP, "Finished calibrating sensor zones"); } void Roode::calibrateDistance() { - distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value_or(Ranging::Medium)); + distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value_or(Ranging::Longest)); entry->calibrateThreshold(distanceSensor, number_attempts); exit->calibrateThreshold(distanceSensor, number_attempts); - if (distanceSensor->get_ranging_mode_override().has_value()) { - auto *mode = determineRangingMode(entry->threshold->idle, exit->threshold->idle); - if (mode != Ranging::Medium) { // already set above + auto *mode = determine_raning_mode(entry->threshold->idle, exit->threshold->idle); + + if (mode->timing_budget != Ranging::Longest->timing_budget) { // already set above + if (distanceSensor->get_ranging_mode_override().has_value()) { + distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value()); + } else { distanceSensor->set_ranging_mode(mode); } } } -void Roode::publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax) { +void Roode::publish_sensor_configuration(Zone *entry, Zone *exit, bool isMax) { if (isMax) { if (max_threshold_entry_sensor != nullptr) { max_threshold_entry_sensor->publish_state(entry->threshold->max); diff --git a/components/roode/roode.h b/components/roode/roode.h index 090cdf43..b4a969f5 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -1,7 +1,6 @@ #pragma once #include -#include "configuration.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -53,8 +52,6 @@ static int time_budget_in_ms_medium_long = 50; static int time_budget_in_ms_long = 100; static int time_budget_in_ms_max = 200; // max range: 4m -static uint8_t DistancesTableSize[2] = {0, 0}; - class Roode : public PollingComponent { public: void setup() override; @@ -121,13 +118,15 @@ class Roode : public PollingComponent { text_sensor::TextSensor *version_sensor; text_sensor::TextSensor *entry_exit_event_sensor; - VL53L1_Error getAlternatingZoneDistances(); - void doPathTracking(Zone *zone); - bool handleSensorStatus(); + 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); + bool handle_sensor_status(); void calibrateDistance(); - void calibrateZones(); - const RangingMode *determineRangingMode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); - void publishSensorConfiguration(Zone *entry, Zone *exit, bool isMax); + void calibrate_zones(); + const RangingMode *determine_raning_mode(uint16_t average_entry_zone_distance, uint16_t average_exit_zone_distance); + void publish_sensor_configuration(Zone *entry, Zone *exit, bool isMax); void updateCounter(int delta); Orientation orientation_{Parallel}; uint8_t samples{2}; diff --git a/components/roode/zone.h b/components/roode/zone.h index 0563717a..0f9d6439 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -6,7 +6,6 @@ #include "esphome/core/optional.h" #include "../vl53l1x/vl53l1x.h" #include "orientation.h" -#include "configuration.h" using TofSensor = esphome::vl53l1x::VL53L1X; using esphome::vl53l1x::ROI; @@ -45,6 +44,8 @@ class Zone { protected: int getOptimizedValues(int *values, int sum, int size); + VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; + VL53L1_Error sensor_status = VL53L1_ERROR_NONE; uint16_t last_distance; uint16_t min_distance; std::vector samples; diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index c1b18166..3718dffa 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -9,10 +9,12 @@ void VL53L1X::dump_config() { } void VL53L1X::setup() { +#ifdef USE_ESP32 auto speed = Wire.getClock(); if (speed < 400000) { ESP_LOGW(TAG, "Slow clock speed, speed: %d", speed); } +#endif // TODO use xshut_pin, if given, to change address auto status = this->sensor.Begin(this->address_); diff --git a/peopleCounter32.yaml b/peopleCounter32.yaml index 71957ae9..43f45fff 100644 --- a/peopleCounter32.yaml +++ b/peopleCounter32.yaml @@ -63,32 +63,28 @@ i2c: sda: 21 scl: 22 +# Sensor is configured separately from Roode people counting algorithm +vl53l1x: + calibration: + # ranging: longest # defaults to "auto" for formerly "calibration: true" roode: id: roode_platform - i2c_address: 0x29 - update_interval: 10ms - calibration: - max_threshold_percentage: 85 - min_threshold_percentage: 5 - roi_calibration: true - # sensor_offset_calibration: 8 - # sensor_xtalk_calibration: 53406 - # manual: - # sensor_mode: 3 - # manual_threshold: 1280 - # timing_budget: 200 - # sampling: 3 + # I removed the { size: 1 } option here since it was redundant. + # Can always add back later if we have more sampling paramaters. + sampling: 2 + # defaults for both zones + roi: + # height: 14 + # width: 6 + detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. + # min: 234mm # absolute distance + max: 85% # percent based on idle distance zones: - invert_direction: true - # entry: - # roi: - # roi_height: 12 - # roi_width: 6 - # exit: - # roi: - # roi_height: 12 - # roi_width: 6 - + invert: true + entry: + roi: auto + exit: + roi: auto button: - platform: restart name: $friendly_name Restart diff --git a/peopleCounter32Dev.yaml b/peopleCounter32Dev.yaml index 073136c8..565d95a3 100644 --- a/peopleCounter32Dev.yaml +++ b/peopleCounter32Dev.yaml @@ -47,33 +47,42 @@ logger: level: INFO i2c: - sda: 4 - scl: 5 + sda: 21 + scl: 22 +# Sensor is configured separately from Roode people counting algorithm +vl53l1x: + calibration: + # ranging: longest # defaults to "auto" for formerly "calibration: true" + # offset: 8 + # xtalk: 53406 + # These pins are not yet implemented but they are passed into class now + pins: + # xshut: 3 # shutdown pin to change address or multiple sensors + # interrupt: 1 # hardware callback when measurement is ready roode: id: roode_platform - i2c_address: 0x29 - calibration: - sensor_mode: 3 - timing_budget: 200 - offset: 8 - xtalk: 53406 - sampling: 1 - detection_thresholds: - max: 85% - min: 5% + # I removed the { size: 1 } option here since it was redundant. + # Can always add back later if we have more sampling paramaters. + sampling: 2 + # defaults for both zones roi: - height: 8 - width: 8 + # height: 14 + # width: 6 + detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. + # min: 234mm # absolute distance + max: 85% # percent based on idle distance zones: invert: true entry: - roi: - height: 4 - width: 4 + roi: auto exit: - roi: - width: 6 + # roi: + # height: 4 + # width: 4 + # center: 124 + detection_thresholds: + max: 70% # override max for exit zone button: - platform: restart @@ -173,7 +182,7 @@ text_sensor: font: - file: "fonts/Roboto-Regular.ttf" id: my_font - size: 20 + size: 18 display: - platform: ssd1306_i2c model: "SSD1306 128x64" diff --git a/peopleCounter8266.yaml b/peopleCounter8266.yaml index f0771a6b..a35d9e8d 100644 --- a/peopleCounter8266.yaml +++ b/peopleCounter8266.yaml @@ -50,31 +50,28 @@ i2c: sda: 4 scl: 5 +# Sensor is configured separately from Roode people counting algorithm +vl53l1x: + calibration: + # ranging: longest # defaults to "auto" for formerly "calibration: true" roode: id: roode_platform - i2c_address: 0x29 - update_interval: 10ms - calibration: - max_threshold_percentage: 85 - min_threshold_percentage: 5 - roi_calibration: true - # sensor_offset_calibration: 8 - # sensor_xtalk_calibration: 53406 - # manual: - # sensor_mode: 3 - # manual_threshold: 1280 - # timing_budget: 200 - # sampling: 3 + # I removed the { size: 1 } option here since it was redundant. + # Can always add back later if we have more sampling paramaters. + sampling: 2 + # defaults for both zones + roi: + # height: 14 + # width: 6 + detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. + # min: 234mm # absolute distance + max: 85% # percent based on idle distance zones: - invert_direction: true - # entry: - # roi: - # roi_height: 12 - # roi_width: 6 - # exit: - # roi: - # roi_height: 12 - # roi_width: 6 + invert: true + entry: + roi: auto + exit: + roi: auto button: - platform: restart diff --git a/peopleCounter8266Dev.yaml b/peopleCounter8266Dev.yaml index f427f64f..c265cc69 100644 --- a/peopleCounter8266Dev.yaml +++ b/peopleCounter8266Dev.yaml @@ -46,32 +46,39 @@ logger: i2c: sda: 4 scl: 5 - +# Sensor is configured separately from Roode people counting algorithm +vl53l1x: + calibration: + # ranging: longest # defaults to "auto" for formerly "calibration: true" + # offset: 8 + # xtalk: 53406 + # These pins are not yet implemented but they are passed into class now + pins: + # xshut: 3 # shutdown pin to change address or multiple sensors + # interrupt: 1 # hardware callback when measurement is ready roode: id: roode_platform - i2c_address: 0x29 - update_interval: 100ms - calibration: - max_threshold_percentage: 85 - min_threshold_percentage: 5 - roi_calibration: true - # sensor_offset_calibration: 8 - # sensor_xtalk_calibration: 53406 - # manual: - # sensor_mode: 3 - # manual_threshold: 1280 - # timing_budget: 200 - # sampling: 3 + # I removed the { size: 1 } option here since it was redundant. + # Can always add back later if we have more sampling paramaters. + sampling: 1 + # defaults for both zones + roi: + # height: 14 + # width: 6 + detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. + # min: 234mm # absolute distance + max: 85% # percent based on idle distance zones: - invert_direction: true - # entry: - # roi: - # roi_height: 12 - # roi_width: 6 - # exit: - # roi: - # roi_height: 12 - # roi_width: 6 + invert: true + entry: + roi: auto + exit: + # roi: + # height: 4 + # width: 4 + # center: 124 + # detection_thresholds: + # max: 70% # override max for exit zone button: - platform: restart @@ -171,7 +178,7 @@ text_sensor: font: - file: "fonts/Roboto-Regular.ttf" id: my_font - size: 14 + size: 18 display: - platform: ssd1306_i2c model: "SSD1306 128x64" diff --git a/platformio.ini b/platformio.ini index c7b71855..06fdb780 100644 --- a/platformio.ini +++ b/platformio.ini @@ -3,8 +3,8 @@ [platformio] src_dir = components -include_dir = .esphome/build/roodedev/src -lib_dir = .esphome/build/roodedev/.piolibdeps/roodedev +include_dir = .esphome/build/roode32dev/src +lib_dir = .esphome/build/roode32dev/.piolibdeps/roode32dev [env:roode] framework = arduino From 6f6e00fad34d3d6a3469f2f3c05ccd11ebc0d7d8 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sat, 15 Jan 2022 15:50:16 +0100 Subject: [PATCH 46/54] Compare objects instead of timing_budget --- components/roode/roode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index dc52098d..827205dc 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -231,8 +231,8 @@ void Roode::calibrateDistance() { exit->calibrateThreshold(distanceSensor, number_attempts); auto *mode = determine_raning_mode(entry->threshold->idle, exit->threshold->idle); - - if (mode->timing_budget != Ranging::Longest->timing_budget) { // already set above + if (mode != Ranging::Longest && + distanceSensor->get_ranging_mode_override().value() != Ranging::Longest) { // already set above if (distanceSensor->get_ranging_mode_override().has_value()) { distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value()); } else { From 93a51dd52716bf2cc3e98ae58a79a73e3d322ea4 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 08:51:42 -0600 Subject: [PATCH 47/54] Move I2C frequency warning to python, to not break ESPHome's I2C interface --- components/vl53l1x/__init__.py | 8 +++++++- components/vl53l1x/vl53l1x.cpp | 7 ------- components/vl53l1x/vl53l1x.h | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 1ef336f5..33fc3c4f 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -1,3 +1,4 @@ +import logging from typing import Dict, Any from esphome.components import i2c from esphome.core import CORE @@ -14,6 +15,8 @@ ) import esphome.pins as pins +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["i2c"] AUTO_LOAD = ["i2c"] MULTI_CONF = False # TODO enable when we support multiple addresses @@ -89,9 +92,12 @@ async def to_code(config: Dict): i2c_config = next( entry for entry in CORE.config[CONF_I2C] if entry[CONF_ID] == i2c_id ) - if i2c_config[CONF_FREQUENCY] == 50000: # default + frequency = i2c_config[CONF_FREQUENCY] + if frequency == 50000: # default i2c_var = await cg.get_variable(i2c_id) cg.add(i2c_var.set_frequency(400000)) + elif frequency < 400000: + _LOGGER.warning("Recommended I2C frequency for VL53L1X is 400kHz. Currently: %dkHz", frequency / 1000) await setup_hardware(vl53l1x, config) await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index 3718dffa..21c2258b 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -9,13 +9,6 @@ void VL53L1X::dump_config() { } void VL53L1X::setup() { -#ifdef USE_ESP32 - auto speed = Wire.getClock(); - if (speed < 400000) { - ESP_LOGW(TAG, "Slow clock speed, speed: %d", speed); - } -#endif - // TODO use xshut_pin, if given, to change address auto status = this->sensor.Begin(this->address_); if (status != VL53L1_ERROR_NONE) { diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h index 3d62394b..8a77e57c 100644 --- a/components/vl53l1x/vl53l1x.h +++ b/components/vl53l1x/vl53l1x.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "VL53L1X_ULD.h" #include "esphome/components/i2c/i2c.h" From 8c6333b80dbe31c56d30270318361449c783be3a Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 09:10:43 -0600 Subject: [PATCH 48/54] Simplify setting calibrated ranging mode --- components/roode/roode.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index 827205dc..c0c53470 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -225,19 +225,15 @@ void Roode::calibrate_zones() { } void Roode::calibrateDistance() { - distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value_or(Ranging::Longest)); + auto *const initial = distanceSensor->get_ranging_mode_override().value_or(Ranging::Longest); + distanceSensor->set_ranging_mode(initial); entry->calibrateThreshold(distanceSensor, number_attempts); exit->calibrateThreshold(distanceSensor, number_attempts); auto *mode = determine_raning_mode(entry->threshold->idle, exit->threshold->idle); - if (mode != Ranging::Longest && - distanceSensor->get_ranging_mode_override().value() != Ranging::Longest) { // already set above - if (distanceSensor->get_ranging_mode_override().has_value()) { - distanceSensor->set_ranging_mode(distanceSensor->get_ranging_mode_override().value()); - } else { - distanceSensor->set_ranging_mode(mode); - } + if (mode != initial) { + distanceSensor->set_ranging_mode(mode); } } From fe957a637f21e926ca547b46801477debffdff30 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 10:11:13 -0600 Subject: [PATCH 49/54] Add more config logging and log ranging mode by name --- components/roode/roode.cpp | 2 +- components/vl53l1x/ranging.h | 17 +++++++++-------- components/vl53l1x/vl53l1x.cpp | 19 +++++++++++++++---- components/vl53l1x/vl53l1x.h | 1 + 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index c0c53470..f2547d98 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -3,7 +3,7 @@ namespace esphome { namespace roode { void Roode::dump_config() { - ESP_LOGCONFIG(TAG, "dump config:"); + ESP_LOGCONFIG(TAG, "Roode:"); LOG_UPDATE_INTERVAL(this); } void Roode::setup() { diff --git a/components/vl53l1x/ranging.h b/components/vl53l1x/ranging.h index 8201f402..6f22f469 100644 --- a/components/vl53l1x/ranging.h +++ b/components/vl53l1x/ranging.h @@ -5,9 +5,10 @@ namespace esphome { namespace vl53l1x { struct RangingMode { - explicit RangingMode(uint16_t timing_budget, EDistanceMode mode = EDistanceMode::Long) - : timing_budget{timing_budget}, mode{mode} {} + explicit RangingMode(const char * name, uint16_t timing_budget, EDistanceMode mode = EDistanceMode::Long) + : name{name}, timing_budget{timing_budget}, mode{mode} {} + const char *name; uint16_t const timing_budget; uint16_t const delay_between_measurements = timing_budget + 5; EDistanceMode const mode; @@ -15,12 +16,12 @@ struct RangingMode { namespace Ranging { // NOLINTBEGIN(cert-err58-cpp) -__attribute__((unused)) static const RangingMode *Shortest = new RangingMode(15, Short); -__attribute__((unused)) static const RangingMode *Short = new RangingMode(20); -__attribute__((unused)) static const RangingMode *Medium = new RangingMode(33); -__attribute__((unused)) static const RangingMode *Long = new RangingMode(50); -__attribute__((unused)) static const RangingMode *Longer = new RangingMode(100); -__attribute__((unused)) static const RangingMode *Longest = new RangingMode(200); +__attribute__((unused)) static const RangingMode *Shortest = new RangingMode("Shortest", 15, Short); +__attribute__((unused)) static const RangingMode *Short = new RangingMode("Short", 20); +__attribute__((unused)) static const RangingMode *Medium = new RangingMode("Medium", 33); +__attribute__((unused)) static const RangingMode *Long = new RangingMode("Long", 50); +__attribute__((unused)) static const RangingMode *Longer = new RangingMode("Longer", 100); +__attribute__((unused)) static const RangingMode *Longest = new RangingMode("Longest", 200); // NOLINTEND(cert-err58-cpp) } // namespace Ranging diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index 21c2258b..9706fdea 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -4,8 +4,19 @@ namespace esphome { namespace vl53l1x { void VL53L1X::dump_config() { - ESP_LOGCONFIG(TAG, "dump config:"); - LOG_I2C_DEVICE(this) + ESP_LOGCONFIG(TAG, "VL53L1X:"); + LOG_I2C_DEVICE(this); + if (this->ranging_mode != nullptr) { + ESP_LOGCONFIG(TAG, " Ranging: %s", this->ranging_mode->name); + } + if (offset.has_value()) { + ESP_LOGCONFIG(TAG, " Offset: %dmm", this->offset.value()); + } + if (xtalk.has_value()) { + ESP_LOGCONFIG(TAG, " XTalk: %dcps", this->xtalk.value()); + } + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin.value()); + LOG_PIN(" XShut Pin: ", this->xshut_pin.value()); } void VL53L1X::setup() { @@ -62,8 +73,8 @@ void VL53L1X::set_ranging_mode(const RangingMode *mode) { ESP_LOGE(TAG, "Could not set measurement delay: %d ms, error code: %d", mode->delay_between_measurements, status); } - ESP_LOGI(TAG, "Set ranging mode. timing_budget: %d, delay: %d, distance_mode: %d", mode->timing_budget, - mode->delay_between_measurements, mode->mode); + this->ranging_mode = mode; + ESP_LOGI(TAG, "Set ranging mode: %s", mode->name); } optional VL53L1X::read_distance(ROI *roi, VL53L1_Error &status) { diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h index 8a77e57c..70fb0ea8 100644 --- a/components/vl53l1x/vl53l1x.h +++ b/components/vl53l1x/vl53l1x.h @@ -38,6 +38,7 @@ class VL53L1X : public i2c::I2CDevice, public Component { VL53L1X_ULD sensor; optional xshut_pin{}; optional interrupt_pin{}; + const RangingMode * ranging_mode{}; /** Mode from user config, which can be get/set independently of current mode */ optional ranging_mode_override{}; optional offset{}; From 01d5c8fd67af078df9692ee87cb7ca413dd85b23 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 10:22:47 -0600 Subject: [PATCH 50/54] Ensure address stored/logged is the one the sensor is using --- components/vl53l1x/vl53l1x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index 9706fdea..f1afa940 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -28,6 +28,7 @@ void VL53L1X::setup() { this->mark_failed(); return; } + this->address_ = sensor.GetI2CAddress(); if (this->offset.has_value()) { ESP_LOGI(TAG, "Setting sensor offset calibration to %d", this->offset.value()); From bc4a0e5bcd4705a7fc506301c434fa6b94b4139a Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 10:47:00 -0600 Subject: [PATCH 51/54] Change xtalk & offset to have units --- components/roode/__init__.py | 15 ++------------- components/vl53l1x/__init__.py | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/components/roode/__init__.py b/components/roode/__init__.py index 92f044cb..bcc738d5 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -8,7 +8,7 @@ CONF_SENSOR, CONF_WIDTH, ) -from ..vl53l1x import NullableSchema, VL53L1X +from ..vl53l1x import distance_as_mm, NullableSchema, VL53L1X DEPENDENCIES = ["vl53l1x"] AUTO_LOAD = ["vl53l1x", "sensor", "binary_sensor", "text_sensor", "number"] @@ -50,18 +50,7 @@ cv.one_of(CONF_AUTO), ) - -def distance_as_mm(): - dval = cv.distance - - def validator(value): - meters = dval(value) - return int(meters * 1000) - - return cv.All(validator, cv.Range(min=0, max=65535)) - - -threshold = cv.Any(cv.percentage, distance_as_mm()) +threshold = cv.Any(cv.percentage, cv.All(distance_as_mm, cv.uint16_t)) THRESHOLDS_SCHEMA = NullableSchema( { diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 33fc3c4f..46cb249e 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -44,6 +44,20 @@ int16_t = cv.int_range(min=-32768, max=32768) # signed +def distance_as_mm(value): + meters = cv.distance(value) + return int(meters * 1000) + + +def int_with_unit(*args, **kwargs): + validator = cv.float_with_unit(*args, **kwargs) + + def int_validator(val): + return int(validator(val)) + + return int_validator + + def NullableSchema(*args, default: Any = None, **kwargs): """ Same as Schema but will convert nulls to empty objects. Useful when all the schema keys are optional. @@ -72,8 +86,10 @@ def none_to_empty(value): cv.Optional(CONF_RANGING_MODE, default=CONF_AUTO): cv.enum( RANGING_MODES ), - cv.Optional(CONF_XTALK): cv.uint16_t, - cv.Optional(CONF_OFFSET): int16_t, + cv.Optional(CONF_XTALK): cv.All( + int_with_unit("correction value", "(cps)"), cv.uint16_t + ), + cv.Optional(CONF_OFFSET): cv.All(distance_as_mm, int16_t), } ), } @@ -97,7 +113,10 @@ async def to_code(config: Dict): i2c_var = await cg.get_variable(i2c_id) cg.add(i2c_var.set_frequency(400000)) elif frequency < 400000: - _LOGGER.warning("Recommended I2C frequency for VL53L1X is 400kHz. Currently: %dkHz", frequency / 1000) + _LOGGER.warning( + "Recommended I2C frequency for VL53L1X is 400kHz. Currently: %dkHz", + frequency / 1000, + ) await setup_hardware(vl53l1x, config) await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) From 52623456ff918544ccff312d2e0959616587ce18 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 11:03:04 -0600 Subject: [PATCH 52/54] Tweak xtalk name and validation description --- components/vl53l1x/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 46cb249e..9a8b25b9 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -28,7 +28,7 @@ CONF_CALIBRATION = "calibration" CONF_RANGING_MODE = "ranging" CONF_XSHUT = "xshut" -CONF_XTALK = "xtalk" +CONF_XTALK = "crosstalk" Ranging = vl53l1x_ns.namespace("Ranging") RANGING_MODES = { @@ -87,7 +87,7 @@ def none_to_empty(value): RANGING_MODES ), cv.Optional(CONF_XTALK): cv.All( - int_with_unit("correction value", "(cps)"), cv.uint16_t + 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), } From 5952226a0009c8665719e33801911be5a203709e Mon Sep 17 00:00:00 2001 From: Carson Full Date: Sat, 15 Jan 2022 12:29:14 -0600 Subject: [PATCH 53/54] Update README to fully reflect new config --- README.md | 182 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 5b46ca32..0816541b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ People counter working with any smart home system which supports ESPHome and the - [ESP32](#esp32) - [ESP8266](#esp8266) - [Configuration](#configuration) - - [Configuration variables](#configuration-variables) - - [Sensor](#sensor) + - [Platform Setup](#platform-setup) + - [Sensors](#sensors) - [Threshold distance](#threshold-distance) - [Algorithm](#algorithm) - [FAQ/Troubleshoot](#faqtroubleshoot) @@ -66,72 +66,123 @@ Ps=0 (when connected to GND): In the IIC mode, the user can operate the chip by ## Configuration +## Platform Setup + Roode is provided as an external_component which means it is easy to setup in any ESPHome sensor configuration file. -Example configuration +Other than base ESPHome configuration the only config that's needed for Roode is +```yaml +external_components: + - source: github://Lyr3x/Roode + refresh: always + +roode: +``` +This uses the recommended default configuration. + +However, we offer a lot of flexibility. Here's the full configuration spelled out. ```yml -# Sensor is configured separately from Roode people counting algorithm +external_components: + - source: github://Lyr3x/Roode + refresh: always + +# VL53L1X sensor configuration is separate from Roode people counting algorithm vl53l1x: + # A non-standard I2C address + address: + + # Sensor calibration options calibration: - # ranging: longest # defaults to "auto" for formerly "calibration: true" - # offset: 8 - # xtalk: 53406 - # These pins are not yet implemented but they are passed into class now + # The ranging mode is different based on how long the distance is that the sensor need to measure. + # The longer the distance, the more time the sensor needs to take a measurement. + # Available options are: auto, shortest, short, medium, long, longer, longest + ranging: auto + # The offset correction distance. See calibration section (WIP) for more details. + offset: 8mm + # The corrected photon count in counts per second. See calibration section (WIP) for more details. + crosstalk: 53406cps + + # Hardware pins pins: - # xshut: 3 # shutdown pin to change address or multiple sensors - # interrupt: 1 # hardware callback when measurement is ready + # Shutdown/Enable pin, which is needed to change the I2C address. Required with multiple sensors. + xshut: GPIO3 + # Interrupt pin. Use to notify us when a measurement is ready. This feature is WIP. + # This needs to be an internal pin. + interrupt: GPIO1 + +# Roode people counting algorithm roode: - id: roode_platform + # Smooth out measurements by using the minimum distance from this number of readings sampling: 2 - # defaults for both zones - roi: - # height: 14 - # width: 6 - detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. - # min: 234mm # absolute distance - max: 85% # percent based on idle distance + + # The orientation of the two sensor pads in relation to the entryway being tracked. + # The advised orientation is parallel, but if needed this can be changed to perpendicular. + orientation: parallel + + # This controls the size of the Region of Interest the sensor should take readings in. + # The current default is + roi: { height: 16, width: 6 } + # We have an experiential automatic mode that can be enabled with + roi: auto + # or only automatic for one dimension + 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. + detection_thresholds: + min: 0% # default minimum is any distance + max: 85% # default maximum is 85% + # an example of absolute units + 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. zones: - invert: true + # 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. + # If omitted, they use the options configured above. entry: - roi: auto # defaults to auto + # Entry zone will automatically configure ROI, regardless of ROI above. + roi: auto exit: roi: - # height: 8 - # width: 4 - # center: 124 + # 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. + center: 124 + detection_thresholds: - max: 70% # override max for exit zone + # Exit zone's min detection threshold will be 5% of idle/resting distance, regardless of setting above. + min: 5% + # Exit zone's max detection threshold will be 70% of idle/resting distance, regardless of setting above. + max: 70% ``` -### Configuration variables - -- **i2c_address (Optional, integer)**: The I²C address of the sensor. Defaults to `0x29`. -- **update_interval (Optional, Time)**: The interval to check the sensor. Defaults to `100ms`. -- **calibration (Optional, exclusive-mode)**: Enables automatic zone calibration: - - **max_threshold_percentage (Optional, int)**: The maxium threshold in % which needs to be reached to detect a person. Min: `50` Max: `100`. Defaults to `85`. - - **min_threshold_percentage (Optional, int)**: The minimum threshold in % which needs to be reached to detect a person. Min: `0` Max: `100`. Defaults to `0`. - - **roi_calibration (Optional, bool)**: Enables automatic ROI calibration (experimental). Defaults to `false`. -- **manual (Optional, exclusiv-modee)**: Enables manual sensor setup: - - **manual_threshold (required, int)**: The threshold for both zones. Min: `40` Max: `4000`. Defaults to `2000`. - - **roi_height (required, int)**: The height of the ROI zones. Min: `4` Max: `16`. Defaults to `16`. - - **roi_width (required, int)**: The height of the ROI zones. Min: `4` Max: `16`. Defaults to `6`. - - **sensor_mode(required, int)**: Sets the distance mode of the sensor if `calibration=false`. - - Options: `0=short`, `1=long`, `2=max`. Defaults to `true`. - - **timing_budget (optional, int)**: The timing budget for the sensor. Increasing this slows down detection but increases accuracy. Min: `10ms` Max: `1000s`. Defaults to `10ms`. -- **invert_direction (Optional, bool)**: Inverts the counting direction. Switch to `true` if the movement count appears backwards. Defaults to `false`. -- **advised_sensor_orientation(Optional, bool)**: Advised orientation has the two sensor pads parallel to the entryway. - So `false` means the pads are perpendicular to the entryway. - Defaults to `true`. - -### Sensor - -Example Sensor setup to use all available features: +### 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. + +#### Other sensors available + +```yaml binary_sensor: - - platform: status - name: $friendly_name Status - platform: roode presence_sensor: name: $friendly_name presence @@ -152,34 +203,6 @@ sensor: roi_width: name: $friendly_name ROI width - - platform: wifi_signal - name: $friendly_name RSSI - update_interval: 60s - - - platform: uptime - name: Uptime Sensor - id: uptime_sensor - update_interval: 120s - internal: true - on_raw_value: - then: - - text_sensor.template.publish: - id: uptime_human - state: !lambda |- - int seconds = round(id(uptime_sensor).raw_state); - int days = seconds / (24 * 3600); - seconds = seconds % (24 * 3600); - int hours = seconds / 3600; - seconds = seconds % 3600; - int minutes = seconds / 60; - seconds = seconds % 60; - return ( - (days ? String(days) + "d " : "") + - (hours ? String(hours) + "h " : "") + - (minutes ? String(minutes) + "m " : "") + - (String(seconds) + "s") - ).c_str(); - text_sensor: - platform: roode version: @@ -187,11 +210,6 @@ text_sensor: - platform: roode entry_exit_event: name: $friendly_name last direction - - - platform: template - name: $friendly_name Uptime Human Readable - id: uptime_human - icon: mdi:clock-start ``` ### Threshold distance From 8062f84a7ffde8b50a3fb3699b55ea790283da30 Mon Sep 17 00:00:00 2001 From: Kai Bepperling Date: Sat, 15 Jan 2022 20:19:46 +0100 Subject: [PATCH 54/54] Update example configs for ESP8266 and ESP32 --- peopleCounter32.yaml | 28 ++++++++++++++++------------ peopleCounter8266.yaml | 27 +++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/peopleCounter32.yaml b/peopleCounter32.yaml index 43f45fff..03b85344 100644 --- a/peopleCounter32.yaml +++ b/peopleCounter32.yaml @@ -63,28 +63,32 @@ i2c: sda: 21 scl: 22 -# Sensor is configured separately from Roode people counting algorithm +# VL53L1X sensor configuration is separate from Roode people counting algorithm vl53l1x: calibration: - # ranging: longest # defaults to "auto" for formerly "calibration: true" + # The ranging mode is different based on how long the distance is that the sensor need to measure. + # The longer the distance, the more time the sensor needs to take a measurement. + # Available options are: auto, shortest, short, medium, long, longer, longest + ranging: auto + roode: id: roode_platform - # I removed the { size: 1 } option here since it was redundant. - # Can always add back later if we have more sampling paramaters. + # Smooth out measurements by using the minimum distance from this number of readings sampling: 2 - # defaults for both zones - roi: - # height: 14 - # width: 6 + # This controls the size of the Region of Interest the sensor should take readings in. + roi: 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. detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. # min: 234mm # absolute distance max: 85% # percent based on idle distance + # 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. zones: invert: true - entry: - roi: auto - exit: - roi: auto + button: - platform: restart name: $friendly_name Restart diff --git a/peopleCounter8266.yaml b/peopleCounter8266.yaml index a35d9e8d..a125d387 100644 --- a/peopleCounter8266.yaml +++ b/peopleCounter8266.yaml @@ -50,28 +50,31 @@ i2c: sda: 4 scl: 5 -# Sensor is configured separately from Roode people counting algorithm +# VL53L1X sensor configuration is separate from Roode people counting algorithm vl53l1x: calibration: - # ranging: longest # defaults to "auto" for formerly "calibration: true" + # The ranging mode is different based on how long the distance is that the sensor need to measure. + # The longer the distance, the more time the sensor needs to take a measurement. + # Available options are: auto, shortest, short, medium, long, longer, longest + ranging: auto + roode: id: roode_platform - # I removed the { size: 1 } option here since it was redundant. - # Can always add back later if we have more sampling paramaters. + # Smooth out measurements by using the minimum distance from this number of readings sampling: 2 - # defaults for both zones - roi: - # height: 14 - # width: 6 + # This controls the size of the Region of Interest the sensor should take readings in. + roi: 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. detection_thresholds: # defaults for both zones. These also default to 0% & 85% as previously done. # min: 234mm # absolute distance max: 85% # percent based on idle distance + # 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. zones: invert: true - entry: - roi: auto - exit: - roi: auto button: - platform: restart