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/.github/workflows/main.yml b/.github/workflows/main.yml index fbb895cd..a9f3b68d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,16 +10,13 @@ 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 - - name: Validate ESP8266 Config - run: esphome config peopleCounter.yaml - - name: Build ESP8266 - run: esphome compile peopleCounter.yaml + pip install -U pillow - name: Validate ESP32 Config - run: esphome config peopleCounter32.yaml + run: esphome config peopleCounter32Dev.yaml - name: Build ESP32 - run: esphome compile peopleCounter32.yaml + run: esphome compile peopleCounter32Dev.yaml 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 diff --git a/Documents/diagram.puml b/Documents/diagram.puml new file mode 100644 index 00000000..0b35c940 --- /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_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) +} + +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/README.md b/README.md index 6889c2b5..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,61 +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: - id: roode_platform - i2c_address: 0x29 - update_interval: 200ms +``` +This uses the recommended default configuration. + +However, we offer a lot of flexibility. Here's the full configuration spelled out. + +```yml +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: - 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 + # 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: + # 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: + # Smooth out measurements by using the minimum distance from this number of readings + sampling: 2 + + # 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: + # 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: + # Entry zone will automatically configure ROI, regardless of ROI above. + roi: auto + exit: + roi: + # Exit zone will have a height of 8 and a width of number set above or default or auto + height: 8 + # Additionally, zones can manually set their center point. + # Usually though, this is left for Roode to automatically determine. + center: 124 + + detection_thresholds: + # 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% +``` + +### 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. -### 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: +#### Other sensors available -``` +```yaml binary_sensor: - - platform: status - name: $friendly_name Status - platform: roode presence_sensor: name: $friendly_name presence @@ -132,43 +194,15 @@ 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 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: @@ -176,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 @@ -290,6 +319,7 @@ lower right. 4. Bad connections ## Sponsors + Thank you very much for you sponsorship! -* sunshine-hass +- sunshine-hass diff --git a/components/roode/__init__.py b/components/roode/__init__.py index e6954753..bcc738d5 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -1,13 +1,17 @@ -from re import I +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_INVERT, + CONF_SENSOR, + CONF_WIDTH, ) +from ..vl53l1x import distance_as_mm, NullableSchema, VL53L1X - -# DEPENDENCIES = ["i2c"] -AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor", "number"] +DEPENDENCIES = ["vl53l1x"] +AUTO_LOAD = ["vl53l1x", "sensor", "binary_sensor", "text_sensor", "number"] MULTI_CONF = True CONF_ROODE_ID = "roode_id" @@ -15,159 +19,130 @@ 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_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_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_AUTO = "auto" +CONF_ORIENTATION = "orientation" +CONF_DETECTION_THRESHOLDS = "detection_thresholds" +CONF_ENTRY_ZONE = "entry" +CONF_EXIT_ZONE = "exit" +CONF_CENTER = "center" +CONF_MAX = "max" +CONF_MIN = "min" CONF_ROI = "roi" -CONF_ROI_ACTIVE = "roi_active" -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, -] +CONF_SAMPLING = "sampling" +CONF_ZONES = "zones" + +Orientation = roode_ns.enum("Orientation") +ORIENTATION_VALUES = { + "parallel": Orientation.Parallel, + "perpendicular": Orientation.Perpendicular, +} + +roi_range = cv.int_range(min=4, max=16) + +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), +) + +threshold = cv.Any(cv.percentage, cv.All(distance_as_mm, cv.uint16_t)) + +THRESHOLDS_SCHEMA = NullableSchema( + { + cv.Optional(CONF_MIN): threshold, + cv.Optional(CONF_MAX): threshold, + } +) + +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_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( - 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_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.Exclusive( - CONF_MANUAL, - "mode", - f"Only one mode, {CONF_MANUAL} or {CONF_CALIBRATION} is usable", - ): cv.Schema( - { - 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.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.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, + cv.Optional(CONF_DETECTION_THRESHOLDS, default={}): THRESHOLDS_SCHEMA, + cv.Optional(CONF_ZONES, default={}): NullableSchema( { - 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_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_ROI) - 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: - raise cv.Invalid( - "ROI calibration cannot be used with manual ROI width and height" - ) - if roi_calibration == False and roi == None: - raise cv.Invalid("You need to set the ROI manually or use ROI calibration") - if manual != None and roi == 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)) +async def to_code(config: Dict): + roode = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(roode, config) + + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(roode.set_tof_sensor(sens)) + + 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_override", "->") + 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 != CONF_AUTO + else {CONF_HEIGHT: CONF_AUTO, CONF_WIDTH: CONF_AUTO} + ) + fallback: Dict = ( + 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 != CONF_AUTO: + cg.add(var.set_height(height)) + if width != CONF_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: - for key in sampling: - cg.add(getattr(hub, f"set_sampling_{CONF_SAMPLING_SIZE}")(sampling[key])) - - -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) - - 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) + 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/roode.cpp b/components/roode/roode.cpp index ff7d43ae..f2547d98 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -1,698 +1,273 @@ -#include "esphome/core/log.h" #include "roode.h" -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:"); - 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", DISTANCES_ARRAY_SIZE); - - if (invert_direction_) - { - ESP_LOGI(SETUP, "Inverting direction"); - LEFT = 1; - RIGHT = 0; - } - if (calibration_active_) - { - ESP_LOGI(SETUP, "Calibrating sensor"); - calibration(distanceSensor); - App.feed_wdt(); - } - if (manual_active_) - { - ESP_LOGI(SETUP, "Manual sensor configuration"); - center[0] = 167; - center[1] = 231; - distanceSensor.SetROI(Roode::roi_width_, Roode::roi_height_); - setSensorMode(sensor_mode, timing_budget_); - DIST_THRESHOLD_MAX[0] = Roode::manual_threshold_; - DIST_THRESHOLD_MAX[1] = Roode::manual_threshold_; - publishSensorConfiguration(DIST_THRESHOLD_MAX, true); - } - distanceSensor.SetInterMeasurementInMs(delay_between_measurements); - distanceSensor.StartRanging(); - } - - void Roode::update() - { - if (distance_sensor != nullptr) - { - distance_sensor->publish_state(distance); - } - } - - void Roode::loop() - { - // unsigned long start = micros(); - getZoneDistance(); - zone++; - zone = zone % 2; - App.feed_wdt(); - // unsigned long end = micros(); - // unsigned long delta = end - start; - // ESP_LOGI("Roode loop", "loop took %lu microseconds", delta); - } - - 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; - } - - last_sensor_status = sensor_status; - sensor_status = VL53L1_ERROR_NONE; - return check_status; - } - - uint16_t Roode::getDistance() - { - // 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 - uint16_t distance; - 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 - distanceSensor.ClearInterrupt(); - - return distance; - } - - void Roode::getZoneDistance() - { - 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; - static uint8_t DistancesTableSize[2] = {0, 0}; - 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) - { - Distances[zone][DistancesTableSize[zone]] = distance; - DistancesTableSize[zone]++; - ESP_LOGD(SETUP, "Distances[%d][DistancesTableSize[zone]] = %d", zone, Distances[zone][DistancesTableSize[zone]]); - } - 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]); - } - ESP_LOGD(SETUP, "Distances[%d][0]] = %d", zone, Distances[zone][0]); - ESP_LOGD(SETUP, "Distances[%d][1]] = %d", zone, Distances[zone][1]); - // pick up the min distance - MinDistance = Distances[zone][0]; - if (DistancesTableSize[zone] >= 2) - { - for (i = 1; i < DistancesTableSize[zone]; i++) - { - if (Distances[zone][i] < MinDistance) - MinDistance = Distances[zone][i]; - } - } - distance = MinDistance; - - // PathTrack algorithm - if (distance < DIST_THRESHOLD_MAX[zone] && distance > DIST_THRESHOLD_MIN[zone]) - { - // Someone is in the sensing area - CurrentZoneStatus = SOMEONE; - if (presence_sensor != nullptr) - { - presence_sensor->publish_state(true); - } - } - - // left zone - if (zone == LEFT) - { - if (CurrentZoneStatus != LeftPreviousStatus) - { - // event in left zone has occured - AnEventHasOccured = 1; - - 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 an event has occured - if (AnEventHasOccured) - { - if (PathTrackFillingSize < 4) - { - PathTrackFillingSize++; - } - - // 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) - { - // check exit or entry. no need to check PathTrack[0] == 0 , it is always the case - - if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) - { - // This an exit - ESP_LOGD("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_LOGD("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; - } - } - - 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() - { - 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)); - Roode::roi_width_ = ROI_size; - Roode::roi_height_ = 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; - break; - case 5: - center[0] = 159; - center[1] = 239; - break; - case 6: - center[0] = 159; - center[1] = 239; - break; - case 7: - center[0] = 167; - center[1] = 231; - break; - case 8: - center[0] = 167; - center[1] = 231; - break; - } - } - else - { - switch (ROI_size) - { - case 4: - center[0] = 193; - center[1] = 58; - break; - case 5: - center[0] = 194; - center[1] = 59; - break; - case 6: - center[0] = 194; - center[1] = 59; - break; - case 7: - center[0] = 195; - center[1] = 60; - break; - case 8: - center[0] = 195; - center[1] = 60; - break; - } - } - // 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.SetInterMeasurementInMs(delay_between_measurements); - sensor_status += distanceSensor.StartRanging(); - if (sensor_status != VL53L1_ERROR_NONE) - { - ESP_LOGE(SETUP, "Error in calibration: %d", sensor_status); - } - - 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; - } - 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); - } - void Roode::setSensorMode(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); - } - } - - void Roode::setCorrectDistanceSettings(float average_zone_0, float average_zone_1) - { - if (average_zone_0 <= short_distance_threshold || average_zone_1 <= 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)) - { - 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)) - { - 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)) - { - setSensorMode(3); - } - if (average_zone_0 > long_distance_threshold || average_zone_1 > long_distance_threshold) - { - setSensorMode(4); - } - } - - int Roode::getSum(int *array, int size) - { - 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) - { - distanceSensor.StopRanging(); - // the sensor does 100 measurements for each zone (zones are predefined) - 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 (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); - } - 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_; - } - - 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_); - 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; - } - - // after we have computed the sum for each zone, we can compute the average distance of each zone +namespace esphome { +namespace roode { +void Roode::dump_config() { + ESP_LOGCONFIG(TAG, "Roode:"); + LOG_UPDATE_INTERVAL(this); +} +void Roode::setup() { + ESP_LOGI(SETUP, "Booting Roode %s", VERSION); + if (version_sensor != nullptr) { + version_sensor->publish_state(VERSION); + } + ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); + + calibrate_zones(); +} + +void Roode::update() { + if (distance_entry != nullptr) { + distance_entry->publish_state(entry->getDistance()); + } + if (distance_exit != nullptr) { + distance_exit->publish_state(exit->getDistance()); + } +} + +void Roode::loop() { + // unsigned long start = micros(); + get_alternating_zone_distances(); + // uint16_t samplingDistance = sampling(this->current_zone); + 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), + // 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); +} + +bool Roode::handle_sensor_status() { + ESP_LOGV(TAG, "Sensor status: %d, Last sensor status: %d", sensor_status, last_sensor_status); + bool check_status = false; + if (last_sensor_status != sensor_status && sensor_status == VL53L1_ERROR_NONE) { + if (status_sensor != nullptr) { + 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; + } + + last_sensor_status = sensor_status; + sensor_status = VL53L1_ERROR_NONE; + return check_status; +} + +VL53L1_Error Roode::get_alternating_zone_distances() { + this->current_zone->readDistance(distanceSensor); + App.feed_wdt(); + return sensor_status; +} + +void Roode::path_tracking(Zone *zone) { + static int PathTrack[] = {0, 0, 0, 0}; + static int PathTrackFillingSize = 1; // init this to 1 as we start from state + // 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 + if (zone->getMinDistance() < zone->threshold->max && zone->getMinDistance() > zone->threshold->min) { + // Someone is in the sensing area + CurrentZoneStatus = SOMEONE; + if (presence_sensor != nullptr) { + presence_sensor->publish_state(true); + } + } + + // left zone + if (zone == (this->invert_direction_ ? this->exit : this->entry)) { + if (CurrentZoneStatus != LeftPreviousStatus) { + // event in left zone has occured + AnEventHasOccured = 1; + + 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; + } + } - 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); - if (roi_calibration_) - { - roi_calibration(distanceSensor, optimized_zone_0, optimized_zone_1); - } + // if an event has occured + if (AnEventHasOccured) { + ESP_LOGD(TAG, "Event has occured, AllZonesCurrentStatus: %d", AllZonesCurrentStatus); + if (PathTrackFillingSize < 4) { + PathTrackFillingSize++; + } - 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); - 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); - } - distanceSensor.StopRanging(); + // if nobody anywhere lets check if an exit or entry has happened + if ((LeftPreviousStatus == NOBODY) && (RightPreviousStatus == NOBODY)) { + 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) { + // check exit or entry. no need to check PathTrack[0] == 0 , it is + // always the case + + if ((PathTrack[1] == 1) && (PathTrack[2] == 3) && (PathTrack[3] == 2)) { + // This an exit + ESP_LOGI("Roode pathTracking", "Exit detected."); + + 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"); + } } + } + + 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() { calibrate_zones(); } + +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 + : average_exit_zone_distance; + if (min <= short_distance_threshold) { + return Ranging::Short; + } + if (max > short_distance_threshold && min <= medium_distance_threshold) { + return Ranging::Medium; + } + if (max > medium_distance_threshold && min <= medium_long_distance_threshold) { + return Ranging::Long; + } + if (max > medium_long_distance_threshold && min <= long_distance_threshold) { + return Ranging::Longer; + } + return Ranging::Longest; +} + +void Roode::calibrate_zones() { + 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_); + entry->calibrateThreshold(distanceSensor, number_attempts); + exit->roi_calibration(entry->threshold->idle, exit->threshold->idle, orientation_); + exit->calibrateThreshold(distanceSensor, number_attempts); + + publish_sensor_configuration(entry, exit, true); + App.feed_wdt(); + publish_sensor_configuration(entry, exit, false); + ESP_LOGI(SETUP, "Finished calibrating sensor zones"); +} + +void Roode::calibrateDistance() { + 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 != initial) { + distanceSensor->set_ranging_mode(mode); + } +} + +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); + } - void Roode::publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], bool isMax) - { - 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) - { - max_threshold_zone0_sensor->publish_state(DIST_THRESHOLD_ARR[0]); - } - - if (max_threshold_zone1_sensor != nullptr) - { - max_threshold_zone1_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) - { - min_threshold_zone0_sensor->publish_state(DIST_THRESHOLD_ARR[0]); - } - if (min_threshold_zone1_sensor != nullptr) - { - min_threshold_zone1_sensor->publish_state(DIST_THRESHOLD_ARR[1]); - } - } - - if (roi_height_sensor != nullptr) - { - roi_height_sensor->publish_state(roi_height_); - } - if (roi_width_sensor != nullptr) - { - roi_width_sensor->publish_state(roi_width_); - } - } + if (max_threshold_exit_sensor != nullptr) { + max_threshold_exit_sensor->publish_state(exit->threshold->max); + } + } else { + if (min_threshold_entry_sensor != nullptr) { + min_threshold_entry_sensor->publish_state(entry->threshold->min); + } + if (min_threshold_exit_sensor != nullptr) { + min_threshold_exit_sensor->publish_state(exit->threshold->min); } -} \ No newline at end of file + } + + if (entry_roi_height_sensor != nullptr) { + entry_roi_height_sensor->publish_state(entry->roi->height); + } + if (entry_roi_width_sensor != nullptr) { + entry_roi_width_sensor->publish_state(entry->roi->width); + } + + if (exit_roi_height_sensor != nullptr) { + exit_roi_height_sensor->publish_state(exit->roi->height); + } + if (exit_roi_width_sensor != nullptr) { + exit_roi_width_sensor->publish_state(exit->roi->width); + } +} +} // namespace roode +} // namespace esphome \ No newline at end of file diff --git a/components/roode/roode.h b/components/roode/roode.h index 8b62db12..b4a969f5 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -1,157 +1,142 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" +#include + #include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" -#include "esphome/components/i2c/i2c.h" #include "esphome/core/application.h" -#include "VL53L1X_ULD.h" -#include +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "../vl53l1x/vl53l1x.h" +#include "orientation.h" +#include "zone.h" + +using namespace esphome::vl53l1x; +using TofSensor = esphome::vl53l1x::VL53L1X; -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 int LEFT = 0; - static int RIGHT = 1; - - /* - ##### 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, - 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 const char *const TAG = "Roode"; +static const char *const SETUP = "Setup"; +static const char *const CALIBRATION = "Sensor Calibration"; - static int delay_between_measurements = 0; - static int time_budget_in_ms = 0; +/* +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. +*/ - /* - 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 int delay_between_measurements = 0; +static int time_budget_in_ms = 0; - class Roode : public PollingComponent - { - public: - void setup() override; - void update() override; - void loop() override; - void dump_config() override; +/* +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 - 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_roi_height(int height) { roi_height_ = height; } - void set_roi_width(int width) { 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_sensor(sensor::Sensor *distance_sensor_) { distance_sensor = distance_sensor_; } - 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_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_; } - void getZoneDistance(); - void recalibration(); - bool handleSensorStatus(); - uint16_t getDistance(); +class Roode : public PollingComponent { + public: + void setup() override; + void update() override; + void loop() override; + void dump_config() override; - 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}; + 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; + 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; } + 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 recalibration(); + Zone *entry = new Zone(0); + Zone *exit = new Zone(1); - protected: - VL53L1X_ULD distanceSensor; - sensor::Sensor *distance_sensor; - 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 *status_sensor; - binary_sensor::BinarySensor *presence_sensor; - text_sensor::TextSensor *version_sensor; - text_sensor::TextSensor *entry_exit_event_sensor; + protected: + TofSensor *distanceSensor; + Zone *current_zone = entry; + 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 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 setSensorMode(int sensor_mode, int timing_budget = 0); - void publishSensorConfiguration(int DIST_THRESHOLD_ARR[2], 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}; - 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 DISTANCES_ARRAY_SIZE{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 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; - }; + 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 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}; + bool invert_direction_{false}; + int number_attempts = 20; // TO DO: make this configurable + int short_distance_threshold = 1300; + int medium_distance_threshold = 2000; + int medium_long_distance_threshold = 2700; + int long_distance_threshold = 3400; +}; - } // namespace roode -} // namespace esphome +} // namespace roode +} // namespace esphome diff --git a/components/roode/sensor.py b/components/roode/sensor.py index 8badda26..cb93b2c6 100644 --- a/components/roode/sensor.py +++ b/components/roode/sensor.py @@ -13,60 +13,84 @@ DEPENDENCIES = ["roode"] -CONF_DISTANCE = "distance_sensor" -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): 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_MAX_THRESHOLD_ZONE0): 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_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_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): sensor.sensor_schema( + cv.Optional(CONF_ROI_WIDTH_exit): sensor.sensor_schema( icon="mdi:table-column-width", unit_of_measurement="px", accuracy_decimals=0, @@ -85,27 +109,36 @@ 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_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/components/roode/zone.cpp b/components/roode/zone.cpp new file mode 100644 index 00000000..b246ae99 --- /dev/null +++ b/components/roode/zone.cpp @@ -0,0 +1,121 @@ +#include "zone.h" + +namespace esphome { +namespace roode { +VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { + last_sensor_status = sensor_status; + + auto result = distanceSensor->read_distance(roi, sensor_status); + if (!result.has_value()) { + return sensor_status; + } + + 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; +} + +/** + * 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_LOGD(CALIBRATION, "Beginning. zoneId: %d", id); + 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]; + }; + 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(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->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) { + case 4: + this->roi->center = this->id == 0U ? 150 : 247; + break; + case 5: + case 6: + this->roi->center = this->id == 0U ? 159 : 239; + break; + case 7: + case 8: + this->roi->center = this->id == 0U ? 167 : 231; + break; + } + } else { + switch (ROI_size) { + case 4: + this->roi->center = this->id == 0U ? 193 : 58; + break; + case 5: + case 6: + this->roi->center = this->id == 0U ? 194 : 59; + break; + case 7: + case 8: + this->roi->center = this->id == 0U ? 195 : 60; + break; + } + } + } + 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; + 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() 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 new file mode 100644 index 00000000..0f9d6439 --- /dev/null +++ b/components/roode/zone.h @@ -0,0 +1,55 @@ +#pragma once +#include + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/optional.h" +#include "../vl53l1x/vl53l1x.h" +#include "orientation.h" + +using TofSensor = esphome::vl53l1x::VL53L1X; +using esphome::vl53l1x::ROI; + +static const char *const TAG = "Zone"; +static const char *const CALIBRATION = "Zone calibration"; +namespace esphome { +namespace roode { +struct Threshold { + /** Automatically determined idling distance (average of several measurements) */ + uint16_t idle; + uint16_t min; + optional min_percentage{}; + uint16_t max; + 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: + 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; }; + + 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; + uint8_t max_samples; +}; +} // namespace roode +} // namespace esphome diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py new file mode 100644 index 00000000..9a8b25b9 --- /dev/null +++ b/components/vl53l1x/__init__.py @@ -0,0 +1,141 @@ +import logging +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, +) +import esphome.pins as pins + +_LOGGER = logging.getLogger(__name__) + +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) + +CONF_AUTO = "auto" +CONF_CALIBRATION = "calibration" +CONF_RANGING_MODE = "ranging" +CONF_XSHUT = "xshut" +CONF_XTALK = "crosstalk" + +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 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. + 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_PINS, default={}): NullableSchema( + { + cv.Optional(CONF_XSHUT): pins.gpio_output_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.internal_gpio_input_pin_schema, + } + ), + cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( + { + cv.Optional(CONF_RANGING_MODE, default=CONF_AUTO): cv.enum( + RANGING_MODES + ), + cv.Optional(CONF_XTALK): cv.All( + int_with_unit("corrected photon count as cps (counts per second)", "(cps)"), cv.uint16_t + ), + cv.Optional(CONF_OFFSET): cv.All(distance_as_mm, int16_t), + } + ), + } +).extend(i2c.i2c_device_schema(0x29)) + + +async def to_code(config: Dict): + 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) + + # 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 + ) + 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]) + + +async def setup_hardware(vl53l1x: cg.Pvariable, config: Dict): + 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/vl53l1x/ranging.h b/components/vl53l1x/ranging.h new file mode 100644 index 00000000..6f22f469 --- /dev/null +++ b/components/vl53l1x/ranging.h @@ -0,0 +1,29 @@ +#pragma once +#include "VL53L1X_ULD.h" + +namespace esphome { +namespace vl53l1x { + +struct RangingMode { + 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; +}; + +namespace Ranging { +// NOLINTBEGIN(cert-err58-cpp) +__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 + +} // namespace vl53l1x +} // namespace esphome diff --git a/components/vl53l1x/roi.h b/components/vl53l1x/roi.h new file mode 100644 index 00000000..c5c6ad4f --- /dev/null +++ b/components/vl53l1x/roi.h @@ -0,0 +1,16 @@ +#pragma once + +namespace esphome { +namespace vl53l1x { + +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 vl53l1x +} // namespace esphome diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp new file mode 100644 index 00000000..f1afa940 --- /dev/null +++ b/components/vl53l1x/vl53l1x.cpp @@ -0,0 +1,131 @@ +#include "vl53l1x.h" + +namespace esphome { +namespace vl53l1x { + +void VL53L1X::dump_config() { + 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() { + // TODO use xshut_pin, if given, to change address + 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); + 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()); + status = this->sensor.SetOffsetInMm(this->offset.value()); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not set sensor offset calibration, error code: %d", status); + this->mark_failed(); + return; + } + } + + if (this->xtalk.has_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(TAG, "Could not set sensor offset calibration, error code: %d", status); + this->mark_failed(); + return; + } + } + + ESP_LOGI(TAG, "Setup complete"); +} + +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(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: %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, error code: %d", mode->delay_between_measurements, status); + } + + this->ranging_mode = mode; + ESP_LOGI(TAG, "Set ranging mode: %s", mode->name); +} + +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_LOGE(TAG, "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_LOGE(TAG, "Failed to check if data is ready, error code: %d", status); + return {}; + } + delay(1); + } + + // Get the results + uint16_t distance; + status += this->sensor.GetDistanceInMm(&distance); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "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_LOGE(TAG, "Could not stop ranging, error code: %d", status); + return {}; + } + + ESP_LOGV(TAG, "Finished distance read"); + return {distance}; +} + +} // namespace vl53l1x +} // namespace esphome diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h new file mode 100644 index 00000000..70fb0ea8 --- /dev/null +++ b/components/vl53l1x/vl53l1x.h @@ -0,0 +1,49 @@ +#pragma once +#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 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 VL53L1X : public i2c::I2CDevice, 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_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}; } + void set_offset(int16_t val) { this->offset = val; } + void set_xtalk(uint16_t val) { this->xtalk = val; } + + protected: + 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{}; + optional xtalk{}; +}; + +} // namespace vl53l1x +} // namespace esphome diff --git a/fonts/Roboto-Regular.ttf b/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..7d9a6c4c Binary files /dev/null and b/fonts/Roboto-Regular.ttf differ diff --git a/peopleCounter32.yaml b/peopleCounter32.yaml index 8dcf5aaf..03b85344 100644 --- a/peopleCounter32.yaml +++ b/peopleCounter32.yaml @@ -63,64 +63,83 @@ i2c: sda: 21 scl: 22 +# VL53L1X sensor configuration is separate from Roode people counting algorithm +vl53l1x: + calibration: + # 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 - i2c_address: 0x29 - update_interval: 10ms - # roi: - # roi_height: 16 - # roi_width: 6 - 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: - size: 3 - invert_direction: true + # Smooth out measurements by using the minimum distance from this number of readings + sampling: 2 + # 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 + +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 - max_threshold_zone0: + - delta: 100 + 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 - - platform: wifi_signal name: $friendly_name RSSI update_interval: 60s diff --git a/peopleCounter32Dev.yaml b/peopleCounter32Dev.yaml new file mode 100644 index 00000000..565d95a3 --- /dev/null +++ b/peopleCounter32Dev.yaml @@ -0,0 +1,196 @@ +substitutions: + devicename: roode32dev + friendly_name: $devicename + +external_components: + refresh: always + source: components + +esphome: + name: $devicename + +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: 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: 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 + # 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: true + entry: + roi: auto + exit: + # roi: + # height: 4 + # width: 4 + # center: 124 + detection_thresholds: + max: 70% # override max for exit zone + +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: peopleCounter + 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 + id: entryDist + filters: + - delta: 100 + distance_exit: + name: $friendly_name distance zone 1 + id: exitDist + 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: 18 +display: + - platform: ssd1306_i2c + model: "SSD1306 128x64" + id: screen + reset_pin: D0 + address: 0x3C + update_interval: 500ms + lambda: |- + 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/peopleCounter.yaml b/peopleCounter8266.yaml similarity index 53% rename from peopleCounter.yaml rename to peopleCounter8266.yaml index 37e79865..a125d387 100644 --- a/peopleCounter.yaml +++ b/peopleCounter8266.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 @@ -60,61 +50,82 @@ i2c: sda: 4 scl: 5 +# VL53L1X sensor configuration is separate from Roode people counting algorithm +vl53l1x: + calibration: + # 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 - i2c_address: 0x29 - update_interval: 10ms - # roi: - # roi_height: 16 - # roi_width: 6 - 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: - size: 3 - invert_direction: true + # Smooth out measurements by using the minimum distance from this number of readings + sampling: 2 + # 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 + +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: 100 + distance_exit: + name: $friendly_name distance zone 1 filters: - - delta: 10.0 - max_threshold_zone0: + - delta: 100 + 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 @@ -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/peopleCounter8266Dev.yaml b/peopleCounter8266Dev.yaml new file mode 100644 index 00000000..c265cc69 --- /dev/null +++ b/peopleCounter8266Dev.yaml @@ -0,0 +1,192 @@ +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 +# 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 + # 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: true + entry: + roi: auto + exit: + # roi: + # height: 4 + # width: 4 + # center: 124 + # detection_thresholds: + # max: 70% # override max for exit zone + +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: peopleCounter + 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 + id: entryDist + filters: + - delta: 100 + distance_exit: + name: $friendly_name distance zone 1 + id: exitDist + 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: 18 +display: + - platform: ssd1306_i2c + model: "SSD1306 128x64" + id: screen + reset_pin: D0 + address: 0x3C + update_interval: 500ms + lambda: |- + 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/peopleCounterDev.yaml b/peopleCounterDev.yaml deleted file mode 100644 index 5e5513c7..00000000 --- a/peopleCounterDev.yaml +++ /dev/null @@ -1,145 +0,0 @@ -substitutions: - devicename: roodedev - friendly_name: $devicename - -external_components: - refresh: always - source: components - -esphome: - name: $devicename - -esp32: - board: wemos_d1_mini32 - framework: - type: arduino - -wifi: - networks: - - ssid: !secret ssid1 - password: !secret ssid1_password - fast_connect: True - power_save_mode: light - domain: .shieldnet - -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: DEBUG - -i2c: - sda: 21 - scl: 22 - -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 - roi_calibration: true - # sensor_offset_calibration: 8 - # sensor_xtalk_calibration: 53406 - # manual: - # sensor_mode: 3 - # manual_threshold: 1280 - # timing_budget: 200 - # sampling: 3 - invert_direction: true - -number: - - platform: roode - people_counter: - name: $friendly_name people counter - -switch: - - platform: restart - name: $friendly_name Restart -binary_sensor: - - platform: 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 - max_threshold_zone0: - name: $friendly_name max zone 0 - max_threshold_zone1: - name: $friendly_name max zone 1 - min_threshold_zone0: - name: $friendly_name min zone 0 - min_threshold_zone1: - name: $friendly_name min zone 1 - roi_height: - name: $friendly_name ROI height - roi_width: - name: $friendly_name ROI width - 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 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