Esphome stopwatch - HC-SR04

Hello !
I’ve made an esphome stopwatch and i want to share my code.
I’ts not perfect but it works :slight_smile:
Limitations :

  • The HC-SR04 sensor can only be fire up every 50ms, so the stopwatch have a sensitivity of 0.05s
  • The stopwatch is synchronysing on a ntp server. That’s how you can use two stopwatch. Someone told me that it could have issues, but after few hours, i didn’t found any. I will make more tests.
  • I use it with nodered so i prefer to use mqtt protocol (but it’s working with esphome api)

Configuration :

  • You can adjust the detection distance in the “Detection distance (cm)” number. Default is 100 cm, so, if HC-SR04 “see” something minus 100 cm, it starts

Running :
You can see the state of the stopwatch with the onboard led (pin 2 on my nodemcuV2, it’s hardcoded in my stopwatch_custom_sensor.h, you can change it).

  • When the led pin blink every 1000ms (1 second), the stopwatch is waiting for button_start_detection (Press it on homeassistant or send PRESS in paypload in nodered)
  • When the led pin blink every 100ms (0.1 second), the stopwatch is waiting for someone (or something) in front of him to start
  • When the led pin blink 10 times 25ms (0.025 second), the stopwatch have detect someone (or something), it send the timestamp with a precision of 0.01s (remember that the sensor have a precision of 0.05s)

Here is the .yaml code :

substitutions:
  stopwatch_number: "1"

esphome:
  name: stopwatch-${stopwatch_number}
  friendly_name: stopwatch-${stopwatch_number}
  includes:
    - stopwatch_custom_sensor.h

esp8266:
  board: nodemcuv2

# Enable logging
# logger:

# Example configuration entry
mqtt:
  broker: mymosquitto.com
  username: admin
  password: !secret mqtt_password

# Enable Home Assistant API
#api:
#  encryption:
#    key: "B5Fa94LAON5+ePby6NzUhKOZ+FkzefoVgZcyysWHJMc="

ota:
  password: "0d18357050485c325b5435acc4a76705"

wifi:
  networks:
    - ssid: !secret wifi_ssid_cyp
      password: !secret wifi_password_cyp
    - ssid: !secret wifi_ssid
      password: !secret wifi_password

  ap:
    ssid: stopwatch-${stopwatch_number}-Hotspot
    password: "8uu4d7gYav0s"

captive_portal:

# Web server
web_server:
  port: 80

# Example configuration entry
time:
  - platform: sntp
    id: sntp_time

# Add number template
number:
  - platform: template
    name: "Detection distance (cm)"
    id: number_detection_distance
    optimistic: true
    min_value: 10
    max_value: 200
    initial_value: 100
    step: 10
    on_value: 
      lambda: |-
        ((ChronoCustomSensor*)chrono_custom_id)->distanceDetection = x;
      
        

text_sensor:
- platform: custom
  lambda: |-
    auto chrono_custom = new ChronoCustomSensor();
    App.register_component(chrono_custom);
    return {chrono_custom};
  text_sensors:
      name: "Chrono Custom Sensor"
      id: chrono_custom_id

# Add button template
button:
  - platform: template
    name: "Start detection"
    id: button_start_detection
    on_press:
      - logger.log: Button Start detection Pressed
      - lambda: !lambda |-
          ESP_LOGD("main", "Button Start detection", "pressed !");
          if (((ChronoCustomSensor*)chrono_custom_id)->currentState == ((ChronoCustomSensor*)chrono_custom_id)->State::Wait) {
            ((ChronoCustomSensor*)chrono_custom_id)->currentState = ((ChronoCustomSensor*)chrono_custom_id)->State::Chrono;
            // ((ChronoCustomSensor*)chrono_custom_id)->publish_state("0");
          }

And here is stopwatch_custom_sensor.h (put it in /config/esphome/stopwatch_custom_sensor.h in home assistant)

#include "esphome.h"

class ChronoCustomSensor: public Component, public TextSensor {
  // code from https://randomnerdtutorials.com/esp8266-nodemcu-hc-sr04-ultrasonic-arduino/
  // Should be better to use esphome ultrasonic ??
  public: 
    float get_setup_priority() const override { return esphome::setup_priority::AFTER_WIFI; }
    unsigned long previousMillis = 0; // will store last time LED was updated
    int ledState = LOW; // ledState used to set the LED
    int blinkInterval = 1000;
    int shortInterval = 100;
    const int trigPin = 12;
    const int echoPin = 14;
    //define sound velocity in cm/uS
    #define SOUND_VELOCITY 0.034
    #define CM_TO_INCH 0.393701

    long duration;
    float distanceCm;
    int distanceDetection = 100;
    int detectionRetry = 0; // Number of time under the distanceDetection

    enum class State {
      Wait,
      Chrono,
      Detect
    };

    State currentState = State::Wait;
    struct timeval tv;

  void setup() override {
    pinMode(2, OUTPUT); // LED pin as output.
    pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
    pinMode(echoPin, INPUT); // Sets the echoPin as an Input    
  }

  void loop() override {
    unsigned long currentMillis = millis();
    switch (currentState) {

      case State::Wait :
        // Blink Led Slow
        if (currentMillis - previousMillis >= blinkInterval) {
          // save the last time you blinked the LED
          previousMillis = currentMillis;
          // if the LED is off turn it on and vice-versa:
          if (ledState == LOW) {
            blinkInterval = 1500;
            ledState = HIGH;
          } else {
            blinkInterval = 100;
            ledState = LOW;
          }
          digitalWrite(2, ledState);
        }
        break;

      case State::Chrono :
        // Blink Led quick
        if (currentMillis - previousMillis >= shortInterval) {
          // save the last time you blinked the LED
          previousMillis = currentMillis;
          // if the LED is off turn it on and vice-versa:
          if (ledState == LOW) {
            ledState = HIGH;
          } else {
            ledState = LOW;
          }
          digitalWrite(2, ledState);
          // ESP_LOGD("main", "distance cm %d", (int)distanceCm);
          // ESP_LOGD("main", "distance detection %d", (int)distanceDetection);          
        }

        // Distance measure
        // Clears the trigPin
        digitalWrite(trigPin, LOW);
        delayMicroseconds(2);
        // Sets the trigPin on HIGH state for 10 micro seconds
        digitalWrite(trigPin, HIGH);
        delayMicroseconds(10);
        digitalWrite(trigPin, LOW);

        // Reads the echoPin, returns the sound wave travel time in microseconds
        duration = pulseIn(echoPin, HIGH);
        
        // Calculate the distance
        distanceCm = duration * 0.034/2;  


        if ((int)distanceCm < (int)distanceDetection) {
          detectionRetry++;
          if (detectionRetry == 2) {
            detectionRetry = 0;
            currentState = State::Detect;
          }
          if (detectionRetry == 0) {
            gettimeofday(&tv, NULL); // get the first detect
          }
        } else {
          detectionRetry = 0;
        }
        
        break;

      case State::Detect :
        // Sending time
        ESP_LOGD("main", "tv_sec %d", tv.tv_sec);
        ESP_LOGD("main", "tv_usec %d", tv.tv_usec);
        int msec = tv.tv_usec / 10000;
        ESP_LOGD("main", "tv_usec %d", msec);

        for (int i=0; i < 10; i++) {
          digitalWrite(2, LOW);
          delay(25);
          digitalWrite(2, HIGH);
          delay(25);
        }
        char buffer [128];
        unsigned long long int mylong = tv.tv_sec * 100 + msec;
        int ret = snprintf(buffer, sizeof(buffer), "%llu", mylong);
        publish_state(buffer);

        currentState = State::Wait;
        break;
    }
  }
};

Feel free to improve my work !

1 Like

Nice! :cowboy_hat_face:

I especially like you’ve made it into a custom component.

I just might have to locate the HC-SR04 sensor, I have somewhere, and try it out. :stopwatch:

Edit: I think I recognize some ‘Blink Without Delay’ code there :slight_smile:

Yes, just use delay for the last blink, it’s very quick, but it’s not optimized…

I recognized it because I’ve recently employed it myself in a sketch that simulates the pulsing led, which some electricity meters have. Seems though I’ve lost the HC-SR04 sensor, so can’t even test it out :frog:

Hope you could get one and make me a feedback :slight_smile:

I’ll definitely put one in the basket next time I’m at my local Arduino store. I seem to remember not finding it very useful, but I think that was because I was trying to use it wrong. A stopwatch seem like a more suitable use for the sensor.