Transmitting a square wave through ESP-NOW

Hello everyone,

I'm currently trying to use ESP-NOW to transmit a 100hz square wave. I'm converting the square wave into data using an ADC pin and attempting to achieve real-time transmission using ESP-NOW. The problem is that the receiver cannot perfectly receive the data sent by the transmitter. Does anyone have any suggestions for this?

Please post your transmitter and receiver sketches

A square wave has only 2 states, nothing what an ADC is required for.
Please explain.

This is the code I am using on the transmiter:

#include <esp_now.h>
#include <WiFi.h>

uint8_t broadcastAddress[] = {0xA0, 0xB7, 0x65, 0xF6, 0x23, 0xFC};


typedef struct struct_message {
    unsigned char id; 
    unsigned char data1;
  
} struct_message;


struct_message myData;

// Create peer interface
esp_now_peer_info_t peerInfo;

// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  
}
 
void setup() {
  // Init Serial Monitor
  Serial.begin(115200);

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

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

  
  esp_now_register_send_cb(OnDataSent);


  
  // Register peer
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
  // Add peer        
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }   
}
 
void loop() {
  // Set values to send
  myData.id = 1;

  myData.data1 =analogRead(36) ;
 

  // Send message via ESP-NOW
  
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

  if (result == ESP_OK) {
    Serial.println("Sent with success");
  }
  else {
    Serial.println("Error sending the data");
  }

 
}

This is the code for the receiver:

#include <esp_now.h>
#include <WiFi.h>



typedef struct struct_message {
  unsigned char id;
  unsigned char data1;

} struct_message; 


struct_message myData;


struct_message board1;

struct_message boardsStruct[1] = {board1};





void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
  char macStr[18];

  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  
  memcpy(&myData, incomingData, sizeof(myData));
 
  boardsStruct[myData.id - 1].data1 = myData.data1;
 
  
}





void setup() {

  Serial.begin(115200);


  WiFi.mode(WIFI_STA);


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

  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
    Serial.println(myData.data1);



}

This is a square wave on the oscilloscope:


This is data that has been read through an ADC and converted into a graph:

I am using an ARDUINO UNO to generate a square wave with the goal of reducing the load on the ESP32.

A square wave can be read from a digital input. If the Uno already creates the signal, what's the job of the ESP?

This is the structure.

I am saving the data read from the ADC as an Excel file,and plotting 500 data points into a graph.


Data received:

Do you mean that you want to use the ESP as a digital scope and the Uno produces a test signal?

I want to wireless transmit the square wave data from the transmitter's ADC PIN with two ESP32 .

The ESP can't simultaneously sample an input and transmit data, so the transmitted result is the input, but sampled slowly and unevenly.

Look up "sampling theorem" and "signal aliasing".

To get a reliable picture of the data, sample and store the result in buffer, using a sample rate at least 10x higher than the square wave frequency, then transmit the buffer.

  • You sample analog signals and transmit the result without controlling and checking the time between two consecutive measurements.
  • TCP is a protocol that - for good reasons - does not rely on synchronisation of sender/receiver and it is package based (sends a number of data in one package):

To correctly reconstruct the signal the receiver must have knowledge of the sample time(s) ...

Also see @jremington's post regarding sampling requirements and either use a fixed sampling rate known at the receiver side or you have to transmit a structure of sampling data and timing interval. With this you should be able to properly reconstruct the wave form.

[Edit]: I must correct that - while you are using ESP NOW - the TCP protocol does not apply.

ESP NOW provides a vendor-specific action frame with the format:

------------------------------------------------------------------------------------------------------------
| MAC Header | Category Code | Organization Identifier | Random Values | Vendor Specific Content |   FCS   |
------------------------------------------------------------------------------------------------------------
  24 bytes         1 byte              3 bytes               4 bytes             7~257 bytes        4 bytes

The vendor specific content is defined as

-------------------------------------------------------------------------------
| Element ID | Length | Organization Identifier | Type | Version |    Body    |
-------------------------------------------------------------------------------
    1 byte     1 byte            3 bytes         1 byte   1 byte   0~250 bytes

Each frame can transport a payload of 0 to 250 bytes (see the table above).

A complete frame consists of

  • in minimum (zero payload) -> 43 bytes
  • with payload of 2 bytes/characters -> 45 bytes -> 360 bits

There are still the following observations:

  • As radio transmission is depending on the availability of the used channel you cannot be sure that the timing of the transmission is synchronized with the sampling rate.
    • If you sample with a given rate the receiver can restore the signal after reception. Use a timer function (or at least every x microseconds in loop() ) to sample the data.
  • Single frames may not be received due to RF interference. The receiver cannot detect whether one or more frames/data are missing.
    • If you integrate a package counter into each frame the receiver can check whether data are missing of not. You have to decide on an error handling if that applies.
  • You transmit a frame of 45 bytes for a payload of 2 bytes.
    • It would be more efficient to collect e.g. 100 samples and transmit them with one frame.
  • You use one character for the data read by analogRead(). As analogRead() has as a standard 12-bit resolution with an ESP32 the data are 0 ... 4095 which requires two bytes.
    • Your sketch only works like it does at the moment because you are only interested in HIGH/LOW and not the real amplitude. If this is the case you could code the rectangle in binary data, each bit representing a certain time interval and the status HIGH or LOW. That would drop the required data from 1 byte per sample to 1 bit per sample (8 times reduction).

To add more to the sampling problem. The sampling MUST be more that twice the shortest time change in your signal. What time change will that be? That will be the rise time and the decay time for the edges of your square wave. What is that time? You will need a good oscilloscope to determine that.

Thank you for your responses, I will search for relevant information

In fact, I am currently studying this post, and if I can use the code in this article, I believe it can solve my problem. However, when I use his code, I cannot measure the waveform on the receivier's pin_DAC. Can someone help me take a look at the code in this article to see if there are any problem ? Thank you all."

Transmiter:

#define CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED 0
#define CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED 0

#include <esp_now.h>
#include <esp_wifi.h>
#include <driver/adc.h>
#include <driver/dac.h>

uint8_t broadcastAddress[] = {0xC0, 0x49, 0xEF, 0xCA, 0x3B, 0x00}; //

uint8_t IN_BUFF[250]={128, 131, 134, 138, 141, 144, 147, 150, 153, 157, 160, 163, 166, 169, 172, 175, 178, 181, 184, 187, 189, 192, 195, 198, 200, 203, 205, 208, 210, 213, 215, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237, 239, 240, 242, 243, 244, 246, 247, 248, 249, 250, 251, 252, 252, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 238, 236, 235, 233, 231, 229, 227, 225, 223, 221, 219, 216, 214, 212, 209, 207, 204, 202, 199, 196, 194, 191, 188, 185, 182, 179, 176, 173, 170, 167, 164, 161, 158, 155, 152, 149, 146, 142, 139, 136, 133, 130, 126, 123, 120, 117, 114, 110, 107, 104, 101, 98, 95, 92, 89, 86, 83, 80, 77, 74, 71, 68, 65, 62, 60, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23, 21, 20, 18, 16, 15, 14, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 16, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 41, 43, 46, 48, 51, 53, 56, 58, 61, 64, 67, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 103, 106, 109, 112, 115, 118, 122, 125, 128};
uint8_t OUT_BUFF[250];

volatile uint8_t i=0;
volatile uint8_t sent=1;
volatile uint8_t ack=1;
volatile uint8_t dataReady=0;
volatile uint8_t pin_state=0;

hw_timer_t *My_timer = NULL;

esp_now_peer_info_t peerInfo;

// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  if(status){
    Serial.println("PACKET ERROR!");
  }
  else{
    ack=1;
  }
}

void IRAM_ATTR sampleADC(){
  //Send a new sine wave byte to DAC1
  dac_output_voltage(DAC_CHANNEL_1, IN_BUFF[i++]);
  if(i==250){
    //If all 250 bytes have been sent, reset counter and prepare esp-now send if previous esp-now frame has been already sent out.
    i=0;
    if(sent){
      memcpy(OUT_BUFF, IN_BUFF, 250);
      dataReady=1;
      sent=0;
    }
  }
}
 
void setup() {
  Serial.begin(115200);
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_0);
  dac_output_enable(DAC_CHANNEL_1);

  // Set device as a Wi-Fi Station
  esp_netif_init();
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  esp_wifi_init(&cfg);
  esp_wifi_set_mode(WIFI_MODE_STA);
  esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT40);
  esp_wifi_set_storage(WIFI_STORAGE_RAM);
  esp_wifi_set_ps(WIFI_PS_NONE);
  esp_wifi_start();

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
  // Add peer        
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
  //Setting up a timer interrupt with freq. of 20kHz
  My_timer = timerBegin(0, 8, true);
  timerAttachInterrupt(My_timer, &sampleADC, true);
  timerAlarmWrite(My_timer, 500, true);

  uint8_t started=0;
  while(!started){
    if(Serial.available()>0){
      char c=Serial.read();
      if(c=='1'){
        started=1;
      }
    }
  }
  Serial.println("Entering loop...");
  timerAlarmEnable(My_timer);
}
 
void loop() {
  //If there is new data to send and previous frame was acknowledged by receiver
  if(dataReady && ack){
    esp_err_t result = esp_now_send(broadcastAddress, OUT_BUFF, sizeof(OUT_BUFF));
    sent=1;
    dataReady=0;
    ack=0;
  }
}

Receiver:

#define CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED 0
#define CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED 0

#include <esp_now.h>
#include <esp_wifi.h>
#include <driver/dac.h>

// Structure example to receive data
// Must match the sender structure
uint8_t IN_BUFF[250];
uint8_t OUT_BUFF[250];

volatile uint8_t i=0;
volatile uint8_t data4dac=0;

hw_timer_t *My_timer = NULL;

// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  //Copying received bytes to buffer and setting flag for new data for the DAC
  memcpy(IN_BUFF, incomingData, sizeof(IN_BUFF));
  data4dac=1;
}

void IRAM_ATTR writeDAC(){
  //If there is new data for the DAC
  if(data4dac){
    if(i==0){
      //If we finished sending previous 250 bytes, copy new ones from IN_BUFF to OUT_BUFF
      memcpy(OUT_BUFF, IN_BUFF, sizeof(OUT_BUFF));
    }
    dac_output_voltage(DAC_CHANNEL_1, OUT_BUFF[i++]);
    if(i==250){
        //If we reached end of 250 bytes, reset counter and data4dac flag
        i=0;
        data4dac=0;
    }
  }
}
 
void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  dac_output_enable(DAC_CHANNEL_1);
  
  // Set device as a Wi-Fi Station
  esp_netif_init();
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  esp_wifi_init(&cfg);
  esp_wifi_set_mode(WIFI_MODE_STA);
  esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT40);
  esp_wifi_set_storage(WIFI_STORAGE_RAM);
  esp_wifi_set_ps(WIFI_PS_NONE);
  esp_wifi_start();
  
  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);

  //Setting up a timer interrupt with a freq. of 20kHz
  My_timer = timerBegin(0, 8, true);
  timerAttachInterrupt(My_timer, &writeDAC, true);
  timerAlarmWrite(My_timer, 500, true);
  timerAlarmEnable(My_timer);
  
  esp_now_register_recv_cb(OnDataRecv);
}
 
void loop() {
  
}

The code you found solves a lot of the observations:

  • The replay rate at the receiver is identical to the sample rate at the transmitter.
  • The transmitter uses the full payload of one ESP NOW frame.

This routine in the transmitter sketch

void IRAM_ATTR sampleADC(){
  //Send a new sine wave byte to DAC1
  dac_output_voltage(DAC_CHANNEL_1, IN_BUFF[i++]);
  if(i==250){
    //If all 250 bytes have been sent, reset counter and prepare esp-now send if previous esp-now frame has been already sent out.
    i=0;
    if(sent){
      memcpy(OUT_BUFF, IN_BUFF, 250);
      dataReady=1;
      sent=0;
    }
  }
}

seems to be wrong as dac_output_voltage(DAC_CHANNEL_1, IN_BUFF[i++]); does not read from ADC but sets the output channel to IN_BUFF[i++].

Is it because both the transmitter and receiver use the same interrupt frequency to minimize data loss during transmission?

I believe the output from the transmitter's DAC should be fine because I can measure sine wave on the oscilloscope, but I cannot measure it at the receiver.

Yes and no ...

Yes: The same interrupt rate for sampling and replay is required to reconstruct the signal with correct timing.

No: It does not help in case of data loss. As all data are sent as independent frames you have to take care of that on your own.

(When you use the internet the TCP stack (software) takes care that packages arriving in a different order than they have been sent are rearranged in the correct sequence before handed over to the user application.)

I have not checked the complete sketch regarding its function, but on a first glance it looks as if this is done by the transmitter sketch:

  • IN_BUF[250] is preset with certain data

In the timer routine sampleADC() :

  • The byte IN_BUF[i]is replayed, the index i is incremented
  • If all 250 bytes have been replayed (i == 250)
    • i is set to zero
    • if the previous OUT_BUF was sent (sent ==1)
      • the data from IN_BUF are copied to OUT_BUF
      • the variable dataReady is set to 1

The variable "sent" makes sure that IN_BUF is only copied to OUT_BUF if the previous data have been sent.

The variable "dataReady" signalizes to the function in loop() that new data are ready for transmission.

If I do not miss anything in the code, IN_BUF is never changed and therefore always the same data as in the declaration of IN_BUF should be transmitted to the receiver and are replayed there via DAC:

uint8_t IN_BUFF[250]={128, 131, 134, 138, 141, 144, 147, 150, 153, 157, 160, 163, 166, 169, 172, 175, 178, 181, 184, 187, 189, 192, 195, 198, 200, 203, 205, 208, 210, 213, 215, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 237, 239, 240, 242, 243, 244, 246, 247, 248, 249, 250, 251, 252, 252, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 238, 236, 235, 233, 231, 229, 227, 225, 223, 221, 219, 216, 214, 212, 209, 207, 204, 202, 199, 196, 194, 191, 188, 185, 182, 179, 176, 173, 170, 167, 164, 161, 158, 155, 152, 149, 146, 142, 139, 136, 133, 130, 126, 123, 120, 117, 114, 110, 107, 104, 101, 98, 95, 92, 89, 86, 83, 80, 77, 74, 71, 68, 65, 62, 60, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23, 21, 20, 18, 16, 15, 14, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 16, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 41, 43, 46, 48, 51, 53, 56, 58, 61, 64, 67, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 103, 106, 109, 112, 115, 118, 122, 125, 128};

You may try this modified receiver sketch to print the block of the first 250 data received:

Modified Receiver Sketch
/*
  Modified receiver sketch from
  Forum: https://forum.arduino.cc/t/transmitting-a-square-wave-through-esp-now/1165489/17
  

  Should print the first 250 data received

*/

#define CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED 0
#define CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED 0

#include <esp_now.h>
#include <esp_wifi.h>
#include <driver/dac.h>

// Structure example to receive data
// Must match the sender structure
uint8_t IN_BUFF[250];
uint8_t OUT_BUFF[250];
uint8_t PRT_BUFF[250];

volatile uint8_t i=0;
volatile uint8_t data4dac=0;
volatile uint8_t data2print =0;


hw_timer_t *My_timer = NULL;

// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  //Copying received bytes to buffer and setting flag for new data for the DAC
  memcpy(IN_BUFF, incomingData, sizeof(IN_BUFF));
  data4dac=1;
  data2print=1;
}

void IRAM_ATTR writeDAC(){
  //If there is new data for the DAC
  if(data4dac){
    if(i==0){
      //If we finished sending previous 250 bytes, copy new ones from IN_BUFF to OUT_BUFF
      memcpy(OUT_BUFF, IN_BUFF, sizeof(OUT_BUFF));
    }
    dac_output_voltage(DAC_CHANNEL_1, OUT_BUFF[i++]);
    if(i==250){
        //If we reached end of 250 bytes, reset counter and data4dac flag
        i=0;
        data4dac=0;
    }
  }
}
 
void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  Serial.println("Setup()");
  dac_output_enable(DAC_CHANNEL_1);
  
  // Set device as a Wi-Fi Station
  esp_netif_init();
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  esp_wifi_init(&cfg);
  esp_wifi_set_mode(WIFI_MODE_STA);
  esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT40);
  esp_wifi_set_storage(WIFI_STORAGE_RAM);
  esp_wifi_set_ps(WIFI_PS_NONE);
  esp_wifi_start();
  
  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);

  //Setting up a timer interrupt with a freq. of 20kHz
  My_timer = timerBegin(0, 8, true);
  timerAttachInterrupt(My_timer, &writeDAC, true);
  timerAlarmWrite(My_timer, 500, true);
  timerAlarmEnable(My_timer);
  
  esp_now_register_recv_cb(OnDataRecv);
  Serial.println("Entering Loop()");
}
 
 uint8_t printDone = 1;

void loop() {
  if (data2print && printDone) {
    printDone = 0;
    data2print = 0;
    memcpy(PRT_BUFF, IN_BUFF, sizeof(PRT_BUFF));
  }
  prt();
}
void prt(){
  while (!printDone) {
    Serial.println("------------------------------------------------------------------");
    for (int i=0;i<sizeof(PRT_BUFF);i++){
      Serial.print(PRT_BUFF[i]);
      Serial.print('\t');
      if (!((i+1) % 8)) {
        Serial.println();
      };
    }
    Serial.println();
    Serial.println("------------------------------------------------------------------");
    printDone = 1;
  }
}

Expected output:

------------------------------------------------------------------
128	131	134	138	141	144	147	150	
153	157	160	163	166	169	172	175	
178	181	184	187	189	192	195	198	
200	203	205	208	210	213	215	218	
220	222	224	226	228	230	232	234	
236	237	239	240	242	243	244	246	
247	248	249	250	251	252	252	253	
253	254	254	255	255	255	255	255	
255	255	254	254	254	253	253	252	
251	250	249	248	247	246	245	244	
242	241	240	238	236	235	233	231	
229	227	225	223	221	219	216	214	
212	209	207	204	202	199	196	194	
191	188	185	182	179	176	173	170	
167	164	161	158	155	152	149	146	
142	139	136	133	130	126	123	120	
117	114	110	107	104	101	98	95	
92	89	86	83	80	77	74	71	
68	65	62	60	57	54	52	49	
47	44	42	40	37	35	33	31	
29	27	25	23	21	20	18	16	
15	14	12	11	10	9	8	7	
6	5	4	3	3	2	2	2	
1	1	1	1	1	1	1	2	
2	3	3	4	4	5	6	7	
8	9	10	12	13	14	16	17	
19	20	22	24	26	28	30	32	
34	36	38	41	43	46	48	51	
53	56	58	61	64	67	69	72	
75	78	81	84	87	90	93	96	
99	103	106	109	112	115	118	122	
125	128	
------------------------------------------------------------------

It is not solving the problems but helps to analyze what happens.

In addition:

There are interesting posts regarding fast ADC readings, here is one example:

https://www.esp32.com/viewtopic.php?t=2346

To sample from main, I used a task with IRAM_ATTR and the code below. With the CPU @ 80MHz (for other reasons..) and normal debugging it then achieves about 18.5 kHz with ADC1 and 21.0 kHz with ADC2 (both ADCs at 12bit).
Disabling the assertion/debug level and set the compiler optimizations it improves to 20.4 kHz on ADC1 and 23.3 kHz on ADC2. (sdkconfig: CONFIG_OPTIMIZATION_LEVEL_RELEASE=y and CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED=y ).

So it looks as if the 20KHz reading as done in your sketch is only achievable with optimizations.

I recommend to use this timer configuration for the transmitter:

  My_timer = timerBegin(0, 80, true);
  timerAttachInterrupt(My_timer, &sampleADC, true);
  timerAlarmWrite(My_timer, 1000, true);                 // 2000 => 500/sec; 1000 => 1000/sec; 500 => 2000/sec 
  timerAlarmEnable(My_timer);

With a value of 1000 im timerAlarmWrite() you get 1 kHz sample rate which is 10 times your intended 100 Hz signal.

The Timer routine to sample could be modified as follows:

constexpr byte analogPin = 36;

void IRAM_ATTR sampleADC(){
  IN_BUFF[i++] = byte(analogRead(analogPin) >> 4)  ; // To map from 4095 to 255
  if(i==250){
    //If all 250 bytes have been sent, reset counter and prepare esp-now send if previous esp-now frame has been already sent out.
    i=0;
    if(sent){
      memcpy(OUT_BUFF, IN_BUFF, 250);
      dataReady=1;
      sent=0;
    }
  }
}

You have to use the same configuration of course also for the receiver timer:

  My_timer = timerBegin(0, 80, true);
  timerAttachInterrupt(My_timer, &writeDAC, true);
  timerAlarmWrite(My_timer, 1000, true);
  timerAlarmEnable(My_timer);

Not tested, but you may give it a try ...

Good luck!

Have you considered something off the shelf that takes care of the timing, error detection and runs in the background without Arduino attention? If this sounds interesting look at CAN, I believe it will do exactly what you want without all the code overhead.