Await non-blockingly to send an MQTT message

My use-case: a remote for shutters/roller blinds. Code is here: Somfy_Remote/main.cpp at main · afoeder/Somfy_Remote · GitHub

I can only send commands to the blinds, i.e. open, close, and stop. When opening or closing, I would like to set a delay/timeout and after this timeout, another MQTT should be sent saying “closed” or “opened” respectively. So I’d like to assume the state.
I would also like to keep access to the pointer in order to probably cancel the timeout if I stop on my own or the direction is changed.
Can you give me a pointer, ideally some example code somewhere, where this is achieved? I’ve already read about std::async etc. but I don’t really understand it resp. I don’t even know if I could use it with PIO…

Thanks!

The basics for this are e.g. explained in the Blink without Delay tutorial. It non-blockingly / asynchronously blinks an LED. The general working principle is simple: Using millis() we can check the system time and thus when an event occurs we can calculate the next time we should do something as a millis timestamp in the future. We can repeatedly poll the millis() value in our loop() to check if the future timestamp has now arrived, and then act on it.

So, this is basically a software timer implementation. Calculating when something should be done as a timestamp, then repeadedly checking if the time has come.

So in the simplest way, after you send your MQTT acknowledgement

you can set a global variable to be

/* global var */
unsigned long send_closed_message_time = 0;  

[..]
  /* after starting to close / open the blinds..  */
  send_done_message_time = millis() + 1000;
[..]

/* in loop() */
//check one-shot event
if ( millis() >= send_done_message_time && send_done_message_time  != 0) {
   send_done_message_time = 0; //reset time so that we don't do it again
   //send done message via MQTT
   //..
}

Then you would also have to remove or decrease the delay() in your loop if you want better accuracy (otherwise the check only happens every 100ms)

There are of course libraries which abstract away from this but under the hood do that exact same thing. Some of them might get fancier and use hardware timers for scheduling or integrate themselves with the RTOS if one is running (like FreeRTOS in the ESP32 Arduino core).

I can e.g. recommend the TaskManagerIO library.

You repeadedly call into the scheduler so that it can check its tasks in the loop()

And then at any point you can give it tasks to do, like periodic tasks or one-shot tasks.

And then the given function will be executed and you can do whatever.

You can get a lot more in-depth here and not use this software-timer approach but a hardware-timer approach. Utilizing the timer hardware peripheral of the ESP8266 or ESP32, you can schedule an interrupt to occur and then execute code in it, without the code having to check millis() explicitly. See e.g. Arduino/libraries/Ticker at master · esp8266/Arduino · GitHub or arduino-esp32/libraries/Ticker at master · espressif/arduino-esp32 · GitHub. However, care must be taken here, as your code will then run in an interrupt (ISR) context, you can only do very minimal stuff as opposed to running in threaded mode. But this is probably very overkill of your application.

I’d say if you use a simple task scheduler library like the one above, it has all the features you need and works cross-platform.

1 Like

woah what an in-depth, detailed explanation so thoroughly written! thank you! I need some time now to ingest everything; thanks again!!

(Update: it seems this currently doesn’t work by design: Look into the scheduling of lamba function with captures as another option · Issue #24 · davetcc/TaskManagerIO · GitHub)

So, allow me an additional question please, also if it doesn’t quite fit into the scope of “PlatformIO”, but basic C++ I guess.

I couldn’t help but going down the refactoring rabbit hole because I didn’t like the lacking cohesion of the code base and I wanted the struct REMOTE be less anemic like I know as a Domain-Driven Design aficionado.

So besides many other things my REMOTE got methods like moveUp, down etc. so it’s in the control and responsibility of the REMOTE to handly anything related to the particular intention: i.e. as well publishing the MQTT state[^1].

Eventually I stumbled upon a code line (L122 in particular) that doesn’t compile:

(github) /afoeder/Somfy_Remote/blob/2d0a154fd3dbc646dfd8dcc6693c7388357a66cf/src/main.cpp#L113-L123 (this discourse apparently doesn’t allow me to post a link to GH)

It tells me src/main.cpp:123:22: error: no matching function for call to 'TaskManager::scheduleOnce(uint32_t&, REMOTE::moveDown(PubSubClient&, const char*, const char*)::__lambda0)'.

I’ve trial&error’d a bit with an empty function body and the above error occurs exactly as soon as I introduce this in the lambda capture group [this]. Using [&] with an empty func body compiles; but as soon as I reintroduce an invokation on this like so [&] { this->dummy(); } the error occurs again.

Won’t that maybe work by design? Can’t I schedule a task with a reference to this because the environment could lose/garbage-collect the instance?

Sorry for the long text but I wanted to go sure there’s all information and intent.

Thanks again and have a happy easter!

[^1]: granted, I wouldn’t see MQTT being the native task of the remote but that’s some other step for further refactoring. I might have some domain helper that ties together MQTT listening, remote sending, and MQTT republishing.

Lambdas which capture other variables are not functions, but objects with a call operator (because that captured information has to be passed in somehow) - see this stackoverflow discussion for some more details. One (ugly) workaround out is to pass the extra info via global (or static) variables.

2 Likes

The problem there is that the scheduleOnce etc function accepts a pure void (*TimerFn)(); function pointer, so, a function with return type void and no arguments. If you reference a member function of some class, it implicitly has the function pointer this as the first argument (even if it’s not visible to you in the argument list), so it cannot be a void x(); function pointer. I’ve made a comment in the referenced issue by which these cases are allowed by using generic std::function objects.

Otherwise there are as said workarounds. Defining a global void xyz(); function that calls into a object pointer with the needed argument that were somehow prepared earlier (as e.g. global variables) is one way.