Timing / scheduling problem - ESP Now

I am running Esp NOW with sequential board communication.
The problem is sequencing board events upon the receipt of a message.

It is a simple timeline and illustrated below:

  1. Receive message
  2. Turn on Led
  3. Wait 100ms
  4. Replicate and send incoming message to next unit
    5 Wait 400ms (or whatever balance time for Led on)
  5. Turn Led off
    Wait until new message received and start again (approx 1500ms intervals)

I have tried a number of approaches with millis , case and even delay.
I cant get one to work consistently.
the common problem (when it does work) is that about 20% of the Led time on periods appear to be interfered with and inconsistent.(abrupt flash only)
I am sure there is a simple solution but just can’t see the forest for the Trees at the moment.

relevant part of code:

void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
memcpy(&myData, incomingData, sizeof(myData));
Serial.print("Bytes received: ");
Serial.print("Pin Status is : ");
Serial.println(myData.pinStatus);
Serial.println();
digitalWrite (ledPin, myData.pinStatus);

// upon receipt - digital write as above
// delay 100ms and send myData to next peer
// further delay to turn Led off (total Led on time say 500ms)
// wait for next incoming message
}

Pointers appreciated.
Thanks

How do you know this is the relevant part?
Don’t post snippets (Snippets R Us!)

—-
please edit your post, select the code part and press the </> icon in the tool bar to mark it as code. It’s painful to read as it stands. (also make sure you indented the code in the IDE before copying, that’s done by pressing ctrlT on a PC or cmdT on a Mac)

Apologies

Please find attached full (receiving) sketch:

I suspect the issue is more to do with managing the incoming data to be acted upon initially.

In this case a simple “0” .

Separating it out to another variable and acting on that with if .. does not work either.

/*
  ESP NOw 8266
  Receive data and act on it
  Havent got it to work with Scheduler or millis
  works with delay sort of
  Forwarding-on bits removed until receiving actions sorted.
*/

#include <ESP8266WiFi.h>
#include <espnow.h>
#include <SchedTask.h> //scheduling elements removed

int ledPin = 2;

uint8_t pinStatus = 1;


// Structure of receive data
typedef struct message {
  uint8_t pinStatus;
} message;

message myData;   //  message called myData


// Callback function that will be executed when data is received

void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
  memcpy(&myData, incomingData, sizeof(myData));

  Serial.print("Bytes received: ");
  Serial.print (len);
  Serial.println ();
  Serial.print("Pin Status is : ");
  Serial.println(myData.pinStatus);

  digitalWrite (ledPin, myData.pinStatus);  //turns Led on - works (but cant turn off)

  //Turning Led off  and subsequent operations to follow

}

void setup() {

  Serial.begin(115200);
  pinMode (ledPin, OUTPUT);

  WiFi.mode(WIFI_STA);  // Set device as a Wi-Fi Station
  WiFi.disconnect();

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // get recv packer info
  esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
  esp_now_register_recv_cb(OnDataRecv);
}


void loop() {

  //using delay and off-Led here sort of works not properly & stuffs up onforwarding

}

The code yu have posted does just receive some data and that's it.
No non-blocking timing no sending.

if you want to receive ESP-NOW-Data you have to avoid delay()
The receiver can not predict when a new ESP-NOW-message arrives

Delay() blocks the complete processor. So the unit is unable to receive anything as long as the processor is executing a delay().

Your code does include a library taskScheduler but your code does not use it.

I have never used TaskScheduler. I always use non-blocking timing based on millis(). To make timing based on millis() really non-blocking it has to be done in a certain way.

as an allday example with easy to follow numbers
delay() is blocking. As long as the delay is "delaying" nothing else of the code can be executed.
Now there is a technique of non-blocking timing.
The basic principle of non-blocking timing is fundamental different from using delay()
You have to understand the difference first and then look into the code.
otherwise you might try to "see" a "delay-analog-thing" in the millis()-code which it really isn't
Trying to see a "delay-analog-thing" in millis() makes it hard to understand millis()
Having understood the basic principle of non-blocking timing based on millis() makes it easy to understand.

imagine baking a frosted pizza
the cover says for preparation heat up oven to 200°C
then put pizza in.
Baking time 10 minutes

You are estimating heating up needs 3 minutes
You take a look onto your watch it is 13:02 (snapshot of time)
You start reading the newspaper and from time to time looking onto your watch
watch shows 13:02. 13:02 - 13:02 = 0 minutes passed by not yet time
watch shows 13:03. 13:03 - 13:02 = 1 minute passed by not yet time
watch shows 13:04. 13:04 - 13:02 = 2 minutes passed by not yet time

watch shows 13:05 when did I start 13:02? OK 13:05 - 13:02 = 3 minutes time to put pizza into the oven

New basetime 13:05 (the snapshot of time)
watch 13:06 not yet time
watch 13:07 not yet time
watch 13:08 not yet time (13:08 - 13:05 = 3 minutes is less than 10 minutes
watch 13:09 not yet time
watch 13:10 not yet time
watch 13:11 not yet time
watch 13:12 not yet time
watch 13:13 not yet time
watch 13:14 not yet time (13:14 - 13:05 = 9 minutes is less than 10 minutes
watch 13:15 when did I start 13:05 OK 13:15 - 13:05 = 10 minutes time to eat pizza (yum yum)

You did a repeated comparing how much time has passed by
This is what non-blocking timing does

In the code looking at "How much time has passed by" is done

currentTime - startTime >= bakingTime

bakingTime is 10 minutes

13:06 - 13:05 = 1 minute >= bakingTime is false
13:07 - 13:05 = 2 minutes >= bakingTime is false
...
13:14 - 13:05 = 9 minutes >= bakingTime is false
13:15 - 13:05 = 10 minutes >= bakingTime is TRUE time for timed action!!

So your loop() is doing

void loop()
  // doing all kinds of stuff like reading the newspaper
  
  if (currentTime - previousTime >= period) {
    previousTime = currentTime; // first thing to do is updating the snapshot of time 
    // time for timed action
  }

it has to be coded exactly this way because in this way it manages the rollover from max back to zero
of the function millis() automatically

baldengineer.com has a very good tutorial about timing with function millis() too .

There is one paragraph that nails down the difference between function delay() and millis() down to the point:

The millis() function is one of the most powerful functions of the Arduino library. This function returns the number of milliseconds the current sketch has been running since the last reset. At first, you might be thinking, well that’s not every useful! But consider how you tell time during the day. Effectively, you look at how many minutes have elapsed since midnight. That’s the idea behind millis()!

Instead of “waiting a certain amount of time” like you do with delay(), you can use millis() to ask “how much time has passed”?

best regards Stefan

Stefan,
Thank you for your time and the effort of your response.
(I have read number of your posts).
I just have a mental block on this one
I am familiar with the millis() timing and have tried it with this sketch both in the main loop and as a subroutine
No joy.

The timing is sorted on the ‘Master’ sketch (using a scheduler) which works well and the current permutation is sending timed instruction for both led on and off to the slave. So just now trying to send the data onward with a delay before doing so. The intent is a cascade effect for subsequent units.

My issue is perhaps not so much a timing problem but setting the threshold that subsequent instructions act upon once the data is received at the slave.

Attached is the latest slave sketch. I have tried various placements of a forwarding instruction and methods to address it but without success, so something basic is wrong.

The message is forwarded but the timing aspect appears to be ignored and the data is sent immediately upon receipt. (I tried different timings in case of inbuilt latency).
I intially had the same problem trying to control the led locally (but resolved that via another method).

Fix that and I suspect the rest will just follow, so any pointers on that will be appreciated.

/*
  ESP 8266 ESP NOW station 2 sending to 3
  Data recieving works
  send works but 'delay' does not
*/

#include <ESP8266WiFi.h>
#include <espnow.h>

int ledPin = 2;

void Send();
unsigned long lastTime = 0;
unsigned long Wait = 100;


// RECEIVER MAC Address
uint8_t broadcastAddress[] = {0x8C, 0xAA, 0xB5, 0x7B, 0x4D, 0x1E}; //   No 3 address


// Structure of data
typedef struct message {
  int pinStatus;
} message;

// message
message myData;


// Callback when data is sent
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0) {
    Serial.println("Delivery success");
  }
  else {
    Serial.println("Delivery fail");
  }
}

// Callback function that will be executed when data is received

void OnDataRecv(uint8_t * mac, uint8_t * incomingData, uint8_t len) {
  memcpy(&myData, incomingData, sizeof(myData));
  digitalWrite (ledPin, myData.pinStatus);  //change pin status
  Serial.print("Bytes received: ");
  Serial.print (len);
  Serial.println ();
  Serial.print("Pin Status is : ");
  Serial.println(myData.pinStatus);  //audit incoming data
  Send();


}

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  pinMode (ledPin, OUTPUT);
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_set_self_role(ESP_NOW_ROLE_COMBO);

  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_COMBO, 1, NULL, 0); // Register peer
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
}

//..................................................................

void loop() {

}

//..................................................................

void Send () {


  if ((millis() - lastTime) > Wait) {

    // Send message
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

    lastTime = millis();
  }

}

In OnDataRecv You should just

  • turn on the LED
  • copy the data
  • memorize millis
  • set a flag that you have an upcoming sending action

In the loop() you

1/ check if the sending action flag is set And if the delay has elapsed, in which case you

  • send the data
  • memorize millis (if the wait time after sending is always the same)
  • set a flag that you have an upcoming ledoff action
  • reset the sending action flag.

2/ check if the ledoff flag is set And if the delay has elapsed, in which case you

  • turn the led off
  • reset the ledoff action flag.

It’s a small state machine. That will work only if the total delay is less than the pace at which you receive data

I assume the scheduler takes action at a fixed intervall.
This means if you use a a scheduler inside the slaves that acts on a fixed time-interval the slaves will drift away over time.

Your slaves should act not on a purely "fixed-timed" condition but on a condition like "command LED on received" This command will start the wait-time

The state-machine that J-M-LJackson has described will do the job.

I looked into your code

The function void OnDataRecv is called only once per receved message
you call your function Send() inside OnDataRecv

This means you call the function Send() only once
and never again
but your construction of function Send()

if ((millis() - lastTime) > Wait) {

    // Send message
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

    lastTime = millis();
  }

needs to be called again and again to finally see the condition to become true

This would only work if you would send a lot of messages always with the command LED on or LED off in a fast sequence to keep the checking of
if ((millis() - lastTime) > Wait) going.

As a general hint:
Add serial-debug-output to your code and watch the serial output

If you would have added serial output inside your Send()-function
you would have seen in the serial-monitor Send() gets called only once
and this would have guided you to the place to look at.

best regards Stefan

I do not use a scheduler in the slave.
I have adjusted the set up to the sequence as indicated by J-L-M. It hasnt change any outcome so far.
I will add a debugger to the loop and check that but suspect it will show what you have indicated.

The 'Send' function is suppose to only operate once upon each message receipt - it does this but the delay period part doesn't work .

i understand the dilemma of needing the timing part to be in a continuing loop for the millis() to work.
the logic of a state machine i also understand and agree it is approriate but perhaps more complex than needed here.

As the Send fn timing and base line isnt working;
how do i do the recomemded:
memorise millis
set the flag for next activity.

This is where i am failing and stuck.

void OnDataRecv(uint8_t * mac, uint8_t * incomingData, uint8_t len) {
  digitalWrite (ledPin, myData.pinStatus);  //change pin status
  memcpy(&myData, incomingData, sizeof(myData));
  Send();
  Serial.print("Bytes received: ");
  Serial.print (len);
  Serial.println ();
  Serial.print("Pin Status is : ");
  Serial.println(myData.pinStatus);  //audit incoming data
void Send () {


  if ((millis() - lastTime) > Wait) {

    // Send message
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

    lastTime = millis();
  }

}

thanks for time so far

to state the very obvious:

The function setup gets executed only once after power on.

The function OnDataRecv gets executed only once per received message.

So all the rest has to be inside function loop which is empty in the code you have posted above.

You have to modify your function OnDataRecv to just copy the received message into a data-variable and set a flag-variable true

all the rest is done inside function loop because loop is looping infinetely as the name "loop" already says.

In loop you check if flag-variable is set true
and do all the timing

best regards Stefan

I tried that on one of my earlier iterations and it also failed.
However perhaps incorrectly put together so i will revisit again.
thanks

I had something like this in mind, typed here from my tablet, so fully untested, might have typos etc...

#include <ESP8266WiFi.h>
#include <espnow.h>

const byte ledPin = 2;
const unsigned long waitingTimeSend = 100;
const unsigned long waitingTimeLed = 100;
unsigned long lastTime = 0;

enum t_state : byte {WAITING_DATA, WAITING_SEND, WAITING_LEDOFF} state = WAITING_DATA;


// RECEIVER MAC Address
uint8_t broadcastAddress[] = {0x8C, 0xAA, 0xB5, 0x7B, 0x4D, 0x1E}; //   No 3 address


// Structure of data
struct __attribute__((packed)) t_message {
  int pinStatus;
} myData;


// Callback when data is sent
void OnDataSent(uint8_t __attribute__ ((unused)) *mac_addr, uint8_t sendStatus) {
  if (sendStatus == 0) Serial.println("Delivery success");
  else    Serial.println("Delivery fail");
}

// Callback function that will be executed when data is received
void OnDataRecv(uint8_t __attribute__ ((unused)) *mac, uint8_t * incomingData, uint8_t len) {
  if (state == WAITING_DATA) {
    if (len == sizeof myData) {
      lastTime = millis();
      memcpy(&myData, incomingData, sizeof(myData));
      digitalWrite (ledPin, HIGH);
      Serial.print(F("Bytes received: ")); Serial.println (len);
      Serial.print(F("Pin Status is : ")); Serial.println(myData.pinStatus);  //audit incoming data
      state = WAITING_SEND;
    } else Serial.println(F("Message with wrong length"));
  }
}

void setup() {
  // initilize pin's state
  pinMode (ledPin, OUTPUT);

  // Initialize Serial Monitor
  Serial.begin(115200);

  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    while (true) yield();
  }

  esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_COMBO, 1, NULL, 0); // Register peer
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
}

//..................................................................

void loop() {
  switch (state) {
    case WAITING_SEND:
      if (millis() - lastTime >= waitingTimeSend) {
        esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof myData);
        state = WAITING_LEDOFF;
        lastTime = millis();
      }
      break;

    case WAITING_LEDOFF:
      if (millis() - lastTime >= waitingTimeLed) {
        digitalWrite (ledPin, LOW);
        state = WAITING_DATA;
      }
      break;

    default: break;
  }
}

Thank you...
it looks perfect
A very welcome relief after many frustrating days.
i should be able to sort any hickups (if any) from here
I really appreciate the assitance and time you have (both ) given.