Pause until variable value stops changing?

I need a macro that detects when a variable value has stopped changing for a period of time greater than a time interval I define (3 seconds in this particular case). I'm currently using a Repeat action set to assign a "before" variable to the current value at the beginning of the macro, wait for 3 seconds, then assign an "after" variable to the current value at that point in time. The idea is to then compare the value of the "before" variable to the value of the "after" variable and, if they are still the same after the aforementioned 3 second interval, drop out of the Repeat loop or, if the "before" and "after" variables are still different after the aforementioned 3 second interval, Retry the loop.

This seems to work for what I'm wanting, but I'm curious if there is a better or more simple way to do this? I was kind of hoping that there was a way to the use the Pause Until action to do this?

I'm sure there are many possible solutions to this problem. I've solved this problem myself, more than one way.

Fundamentally, in a pure math sense, there is NO WAY to know when a variable changes by "polling" it. If you check its value at two separate times, it may have changed 0, 1, or 100 times between those two checks. Of course, if the value of the variable is always strictly increasing, (like a counter) then you would know! But you didn't say that your variable was always increasing. The solution (a truely reliable solution) to this is to make sure that you don't change the variable without validating each time if the variable has changed. And I'm going to show you one way to do that, below.

By the way, another fundamental problem is that quite often a variable gets "changed" into the exact same value. Does that constitute a "change"? I don't know, it depends on your definition of a change. This is a particular problem if the numbers you are storing are "real" values. For example, if the first time you check a value it contains "14.0" and the next time you check it's "13.9999999999", is that a "change"? You didn't say if the variables you are checking are integers, fractions, real numbers or strings. I think we need to know that. There are pitfalls if you are testing real number values.

One way to address all this is to create a subroutine that you always use to change a variable. The subroutine will itself detect if it is changing the variable. For example, I've used macros like this:

This macro will allow you to pass (a) the name of a variable, and (b) a new expression to assign to that variable. If the new value is different from the old value, this macro will say "Changing value" but if the value is not changing it will say "No Change." You can of course replace that with your own code.

I tend to prefer solutions like this to "polling" solutions such as what you are describing. Your approach might be valid for your particular problem, but I prefer solutions that work with all possible problems.

You might say, "but your solution doesn't let me check if 3 seconds have passed." But indeed it does let you check that. My code above simply warns you if the value has changed, true, but you can modify it to also record the time of the last change, which is something you can then use to see if it has changed in X seconds. I made my code above work with ANY variable, not just a single variable like you were requesting, so to make my record the time of ANY variable, it's probably best to use a KM dictionary to record the time of the change. For example, if you replaced my action which says "Changing Value" with the following action:

image

Then your dictionary would contain the last time change of every variable that you are changing, as long as you use my subroutine to change your values. Then you can test if a variable has truly changed in the last three seconds like this:

image

Then that will test if the time of "MyVar's" last change was more than 3 seconds ago. It sounds like a solution to your problem.

This approach is quite robust, and I use it often to solve problems like yours. I do things like this all the time, so I probably didn't make a typo above, but if I did, I'll answer any issues you have with it.

I'd usually do an "infinite loop" rather than your Repeat/Retry (mainly because that's how my brain works), but the theory would be the same -- note that you don't need an "after" variable, you can do the comparison directly:

But it does depend on how "tight" you want this to be -- a 3 second pause to see if your variable has been unchanged for 3 seconds means that, on average, your macro will resume 4.5 seconds after the variable changes. If you want more granular than that it might be easier to update a time stamp every time you detect a change, then do a "if still the same and timestamp more than 3 minutes ago..."

To answer some of the questions, and further define what I'm doing, it's actually pretty simple. I have eight buttons on my Stream Deck which are dedicated to a particular kind of action. Each of these eight buttons, upon triggering their respective KM macro, sends out a MIDI signal (32, 33,...39). So these values can only ever be one of those eight numbers. There are not other numbers to consider. The only thing that would be considered a "change" is for the value to change from 32 to 37, 35 to 33, etc.

Among other things, I have a different macro which is set to refresh the values displayed on these eight buttons. I want this refresh action to continue running in the background until I haven't pushed another one of these eight buttons for at least three seconds. This is why I've created a macro to keep polling in the background, looking for periods where the button value hasn't changed for at least three seconds.

So the polling action I've created (using the Repeat action) seems to work, but I am curious what other options might be out there that would work better or faster.

I don't care so much about how many times the variable might be changing during the specified time interval. I just need to know when it's STOPPED changing for longer than three seconds. In any case, the only reason the variable would even be changing is because I've intentionally pushed a button on my Stream Deck which has caused the change to occur, so that aspect remains pretty simple and knowable.

I will look at some of your example macros and get back to you once I've had some more time to absorb this. Thanks

Yeah, I think we're more or less doing the same thing, with the only real difference being that I'm using the Repeat action and you're using the While action.

I've not messed with the While action much. Is the only real difference between Repeat and While that Repeat loops back a specified number of times, whereas While loops back infinitely?

What causes a refresh? It sounds like it's a button press -- in which case, why not include the refresh as part of the macro the buttons trigger? Then it would be pretty much instant, no background macro to kill, and no need to poll the variable.

I have it set up the way that I do because the refresh takes a few seconds to complete, and I don't want a secondary user to have to understand that they must wait for the refresh to complete before pushing the next button they wish to act on. Right now, I have semaphores on a different set of sub-macros which do other things as well. There's a lot going on at the same time, but not necessarily on the same schedule, so this avoids any conflicts which may occur.

I've separated the two flows so that they operate independently, and without requiring a higher level of understanding from the user. They just quickly push the buttons they want to push, and then the semaphores will take over and run the execution instances in series, all while the Refresh macro continues to run independently in the background and update the displayed values on the SD buttons. The two different flows are not perfectly in sync, but that's okay.

If near enough is good enough and what you have now is doing what you want -- stick with it! "Working" is more important than "optimised" :wink:

If you did want to tweak, consider posting a timestamp when you change the variable (since that change seems to be under your/user control rather than under external influence) -- then you can use the variable for whatever while your "shall I quit?" decision only cares about the difference between NOW() and the last-change timestamp.

I've experimented with the time stamp thing, using the SECONDS() token, and it works in principle, but it would time out before all of the semaphores got done executing, so I tried to tie it to something that fires a little later in the sequence, that way the refresh macro keeps running until the end.

I'm at the point where I'm trying to optimize things, so that's why I'm looking to see what other options are out there, or if the polling loop thing that I've been doing is about as good as it gets.

I've not messed with the While action much. Is the only real difference between Repeat and While that Repeat loops back a specified number of times, whereas While loops back infinitely?

I'm confused -- what's the difference between

  1. Waiting 3 seconds after the last change in variable
  2. Waiting 3 seconds after a timestamp that was set at the last change in variable

It doesn't really make a difference, but I think you might find it easier to separate the value of the variable from the time it was last changed -- if only so you don't run into trouble when the variable is "changed" to the same value it had before!

Yes and no.

In KM a "Repeat" action loops a certain number of times -- although that number can be the result of a calculation, you don't have to fix it when you write the macro.

The "While" action loops "while" its condition(s) evaluate to true (or false depending on the options you set) -- the above is a particular form of that because we are saying WHILE 1 is true and KM consider anything "not zero" to be true. It's a way of saying "keep looping forever" -- so remember to include a "Break" condition!

Aha! In this case you have a simpler solution. Inside the KM action which detects the trigger, you can set a variable called TimeOfChange to the value "SECONDS()" which ensures that you always know when the last change was (you may want an IF statement around it, if you don't want a button pressed twice in a row to constitute a "change.") Then you can simply use polling in a loop to check the time like this: "IF SECONDS()-TimeOfChange>3".

However I have two entirely different ideas now for a solution:

ALTERNATE IDEA #1:

If the time is not 3 seconds, but is a multiple of 60 seconds, (which may happen for some people, but this will not help your case) there's a much easier way. You can simply use the IDLE trigger in a KM macro, which will work if the MIDI trigger emulates a harmless keyboard or mouse action whenever the Stream Deck presses a key. I recommend using the KM action "Simulate Scroll Wheel" which does nothing harmless for most active apps. This is a great solution, but only if your silence time is 60 seconds.

ALTERNATE IDEAS #2a and #2b:

In every idea we've discussed so far, you still have to use "polling" in a loop to check if 3 seconds have passed. However I can think of a couple of ways to solve this problem without any polling. I.e., you will not need an "IF" statement and it will still detect and take action when a period of no change occurs.

The first way is, instead of polling, create a KM macro with a "cron trigger" that is set to trigger 3 seconds in the future. Then each time a Stream Deck key is pressed, use ML to adjust the cron time to 3 more seconds into the future. The problem with this solution is that there is a noticeable delay between changes the KM Editor makes and the time the KM Engine receives copies of the changes, so 3 seconds is just too quick to be practical here.

The second way is not to use a KM macro as a cron trigger, but use macOS itself as the cron trigger. You can set the macOS cron trigger using an Execute Shell Script command, and the cron trigger's own shell script can call a KM macro when it is triggered, like this:

osascript -e 'tell application "Keyboard Maestro Engine" to do script "inactivity3seconds"'

That is a really cool way to trigger actions based on inactivity. I might have to fully cook this and post it on its own thread.

Okay, and since your approach won't "notice" a few changes that happen quickly, you might fail to detect an actual change, resulting in your macro triggering when there were still some changes happening. I've had this happen myself, which is why I invented my solution above that cannot fail.

Anyway, if you are happy with your approach, that's good enough for me. I'm just here to have fun offering other solutions.