Proper way to wait a while in a lambda?

I know how to use the delay action. AFAICT, I can use it to wait arbitrarily long amounts of time. I’m trying to figure out how to do the same thing from within a lambda. The backstory is that I have a lambda doing some things in a loop, and I want a delay between iterations of the loop.

I first tried the standard C/C++ sleep() method, but if I sleep more than 4-5 seconds, the ESP32 reboots. I imagine that’s some kind of watchdog timer reset. I’ve also tried splitting it up into a bunch of sleeps of a 1 ms with an esphome::yield call between each of them. Same reboot result. I’m now trying kookier and kookier things to abuse ESPHome APIs, but I think maybe I might be overlooking something simple.

How could I implement the equivalent of “sleep(N)” for arbitrary N in a lambda?

since a lambda is translated into c code (as far is i know), it should be possible to use delay(x) in lambda’s (x in milliseconds). and the “native” delay function should not trigger the WDT

That was my thinking, too, but it didn’t work. I got an ESP32 reboot when I delayed more than 4-5 seconds. It’s possible that I didn’t do it correctly since I was trying a lot of different things in rapid succession. It seems like it should work.

I found a clunky workaround by converting the loop in my lambda into an ESPHome while loop, which means I have a handful of static variables in the lambda to keep track of state and an ESPHome global variable to communicate out of the lambda. This isn’t a style of coding I would recommend to anyone, but it does at least work for this scenario.

globals:
  - id: NEXT_DELAY
    type: float

script:
  - id: loop_with_dynamic_delays
    then:
      - globals.set: {id: NEXT_DELAY, value: '0'}
      - while:
          condition: {lambda: "return id(NEXT_DELAY) >= 0;"}
          then:
            - logger.log: {level: DEBUG, format: "DELAY %f sec", args: [id(NEXT_DELAY)]}
            # With no suffix, delays are milliseconds
            - delay: !lambda 'return 1000 * id(NEXT_DELAY);'
            - lambda: |-
                // some static state variables
                if (/* done looping */) {
                  id(NEXT_DELAY) = -1;
                } else {
                  id(NEXT_DELAY) = // ...
                }

Did you try the esphome::delay() functions in your testing?

  • esphome::delay ( uint32_t ms )
  • esphome::delay_microseconds_safe ( uint32_t us )

You might be able to use the interval component or the component with the lambda to use set_update_interval (uint32_t update_interval_ms) to dynamically change the trigger interval to get a dynamic delay. Something like this…

next_delay = ...
id(my_interval_component).set_update_interval(next_delay);

In the generated code, there is a #define so that delay() is the same as esphome::delay. I can’t remember if I tried esphome::delay_microseconds_safe, but I remember coming across it. I also saw the use of DelayAction instances in the generated code. That’s probably what I really needed to use instead of the delay function.

I don’t want to stray too far into undocumented territory since a monthly ESPHome release can clobber me. I was really hoping that there was some well-known way to do this that I was just missing. Unless I find that well-known way, I’ll probably stick with the while loop workaround. At least everything there is documented and so should be stable.

I think they idea of changing the value of an interval is probably just a different way of doing that I did in the while loop since it would still mean multiple invocations of my lambda and some static variables inside it to keep track of state.