Help understanding structure behaviior in esp now example

Hello Everybody. I am new.

Can anyone help explain? I am baffled by a section of the code found at Getting Started with ESP-NOW (ESP8266 NodeMCU with Arduino IDE) | Random Nerd Tutorials The code is run without modification, but not as expected. Just need to enter the mac address and compile.

typedef struct struct_message {
  char a[32];
  int b;
  float c;
  String d;
  bool e;
} struct_message;

It defines the above structure. Inside that structure is a String type. a) In use the structure seems to stay the same size, 56 bytes, despite having a dynamically sized member. b) The string may be filled with any number (tried up to 200) of chars and returned with a serial.print(). c) the message is sent as expected and printed by the receiver, only up to 10 chars. 11 or more breaks the receiver and an empty string is printed, even though it was fine on the sender. d) it does not "overrun" the bool after it in the structure.

so, I ask:

  1. How does this even work? Wouldn't the structures on both sides need to have defined lengths to line up and unpack properly at the other end?

  2. How does the String member get packed into the structure if it is dynamically allocated and expandable? I expanded it with the loop that added a character with every iteration. It works on the sender and can be printed to serial. I can fill it with hundreds of chars and still get 56 bytes from sizeof(myData).

  3. As stated, when I access myData.d directly I get all the data. When I send the entire structure (56 bytes??) all data remains intact except for myData.d which behaves properly until 11 chars. At 11 chars it is received blank Try the examples, make the string "12345678901" and it fails to receive. Why? and Why 10 chars specifically? Data placed after it in the structure is unaffected.

I have been at this for to days, so I am reaching out. Hello by the way. It's my first post.

Rick.

Welcome to the forum

In the struct d is an object of the String class. Rather than being the text that holds, d is a container for it and that text is not actually in the struct. As a result the struct itself has a fixed size

Please post your 2 sketches, using code tags when you do

what gets packed in the struct is the pointer to the dynamic area (on the heap) where the text is kept. Your structure is therefore not flat anymore.

When you send the structure, you just send the pointer address and not the content, and obviously the receiver can't do anything with that pointer.

Hello UKHeliBob.

The code is from:

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp-now-esp8266-nodemcu-arduino-ide/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

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

// REPLACE WITH RECEIVER MAC Address
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Structure example to send data
// Must match the receiver structure
typedef struct struct_message {
  char a[32];
  int b;
  float c;
  String d;
  bool e;
} struct_message;

// Create a struct_message called myData
struct_message myData;

unsigned long lastTime = 0;  
unsigned long timerDelay = 2000;  // send readings timer

// 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");
  }
}
 
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() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer
  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}
 
void loop() {
  if ((millis() - lastTime) > timerDelay) {
    // Set values to send
    strcpy(myData.a, "THIS IS A CHAR");
    myData.b = random(1,20);
    myData.c = 1.2;
    myData.d = "Hello";
    myData.e = false;

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

    lastTime = millis();
  }
}

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp-now-esp8266-nodemcu-arduino-ide/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

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

// REPLACE WITH RECEIVER MAC Address
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Structure example to send data
// Must match the receiver structure
typedef struct struct_message {
  char a[32];
  int b;
  float c;
  String d;
  bool e;
} struct_message;

// Create a struct_message called myData
struct_message myData;

unsigned long lastTime = 0;  
unsigned long timerDelay = 2000;  // send readings timer

// 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");
  }
}
 
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() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer
  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}
 
void loop() {
  if ((millis() - lastTime) > timerDelay) {
    // Set values to send
    strcpy(myData.a, "THIS IS A CHAR");
    myData.b = random(1,20);
    myData.c = 1.2;
    myData.d = "Hello";
    myData.e = false;

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

    lastTime = millis();
  }
}

I can't see how that code is supposed to work, the only way I can see it did in their test is down to luck.

I guess what they did is load the sender sketch, and then load the receiver sketch. Since both sketches use the same variables, the memory allocation is the same for both sketches. So when the receiver sketch de-references the String pointer, it will point to the memory that was initialized when the sender sketch was loaded.

Exactly. As I am understanding it, the purpose of the struct is to pack several pieces of data into a formatted "packet" that can be disassembled on the other side using the same struct definition. For this to work don't all elements in the structure have to be of fixed size?

Could the author of this code simply missed that and gotten lucky that the code works as published? I broke it when I tried to send a bigger string, then went down a rabbit hole with many tunnels.

I just want to understand the code and why it behaves the way it does. Why does it work perfectly up to 10 chars? It packs and sends and unpacks on the other side, till I send 11.

I am new to cpp and the learning curve is steep.

I did what I wanted by making an oversized char string, putting it at the end of the structure, and added a length member.

Ok, That's what I was expecting too, but I'm a newbie. It did send the actual string data, not a pointer, up until 11 chars was too much. Then nothing in the string.

I though i was doing something wrong or missing something. Thank you.

Yes, and they are all of a fixed size. However, as has been pointed out the String is held as a pointer to the data and so is more than likely not consistent at both ends of the link

An array of chars is a much better solution even if t0eh String worked

That's what I expected when I read it too, but the actual data was passed.

Anyway. Thank you all.

did you mean struct or String?

a struct is commonly used to handle IP, ethernet and other types of messages composed of various members and lengths.

it's common to pass the pointer/address of a struct along with a size to a send() which copies the memory contents at that address.

i think it's the XINU book where i might have see something like

struct Eth {
    uint8_t  dst  [6];
    uint8_t  src  [6];
    uint8_t  tag  [4];
    uint8_t  tpye [2];
    uint8_t  data [];
};

yes, data [] which ethernet buffers would be cast to to access fields and use data [] to locate the contents of the frame/packet

I was unclear. I obviously meant the value of the pointer to the cstring that is held as part of the String class within the struct

if you have

typedef struct struct_message {
  char a[32];
  int b;
  float c;
  String d;
  bool e;
} struct_message;

d is a String instance (on a UNO sizeof(String) is 6)

the memory representation is something like this (assuming packed)

and so the first entry is actually a pointer (the address of some other region in memory) to the Heap where the data actually lives.


There was something else at play that tricked you into believing it was working.

in ESP-NOW test I used a structure

uint8_t broadcastAddress[] = {0x24,0x62,0xAB,0xE0,0x64,0x54}; //{ 0x84, 0xCC, 0xA8, 0x7A, 0x56, 0x6C };  //{0x24,0x62,0xAB,0xE0,0x64,0x54};

esp_now_peer_info_t peerInfo;

// test structure
struct __attribute__((packed)) Data {
  int16_t seq;  // sequence number
  int32_t distance;
  float voltage;
  char text[50];
} data = { 0, 56, 3.14159, "hello test" };  // sample data

which was transmitted using

  esp_now_send(broadcastAddress, (uint8_t *)&data, sizeof(data));

this is flat in memory (consecutive bytes) so there is no problem
the issue is when you have a non trivial data type with the actual values not being stored in the same place.
Sending such structures requires serialising and unserializing the data

reminds me of transmitting java objects over TCP/IP

@rick3939

are you interested in examining all the detailsabout ESP-NOW and what datatypes can be used in a struct / can not be used in a struct
or

do you want to realise some project that exchanges data over ESP-NOW?

If you want to realise a project simply don't use String but instead use arrays of chars
(another name for them is "c_string" ) or use the library SafeString.h

best regards Stefan

Show us how you would create and use a struct holding 2 "SafeStrings"?

Also the SafeString class has the same issue as the String class when it comes to not being flat. The class holds a pointer to the underlying (fixed size) c-string.

not a good solution to this

1 Like

SafeString would be used for easy string-manpulation
and offers this
https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html#working

Show us how it’s flat…. It’s not…so the string manipulation is not the issue there. Also the constructors of the class are meant to be called through cumbersome macros, making it thus more difficult than necessary to use that type as part of a struct.
Try it.

No.

In the code I would use SafeStrings for string-manipulation and when it comes to send the ESP-NOW-Data I would copy the SafeString to a classical char-array.

In this way it will be flat

No time at the moment to write the code

Sounds like a great use of CPU and RAM.

:wink: