Help needed with sending serial data

I have 2 X ESP8266 and wish to transfer data from one to the other via the Tx and Rx pins.

The 'Transmit' chip has pin Tx connected to the Rx pin on the receive chip. Gnd is connected to both.

I can transmit a single character but wish to send data from this structure:-

char EpochTime[10];
float Temperature;
float Voltage;
int id;

Here is my sketch that sends a character:-

// send character to Tx serial pin

#define LED 2

void setup() {
  pinMode(LED,OUTPUT);
  Serial.begin(9600);
  while (!Serial); 
}

void loop() {
  digitalWrite(LED,HIGH);
  delay(1000);
  digitalWrite(LED,LOW);
  Serial.println('A'); 
  delay(10000); // Wait 10 second
}

And to receive:-


void setup() {
  // Initialize hardware serial at a standard baud rate (e.g., 115200 or 9600)
  Serial.begin(9600); 
}

void loop() {
  // Check if any character has been received
  if (Serial.available() > 0) {
    String incomingString = Serial.readString();
    
    Serial.print("Received character: ");

    Serial.println(incomingString);
  }
}

Any advice would be welcome.

Try sending a CSV string.

Serial.println("A,12,80.4,20.1");


A better approach would be to send a packet of data.

#pragma pack(push, 1) // Force the compiler to remove struct padding
struct MyData {
  char name[16]; // Fixed-size char array instead of a pointer
  uint8_t value; 
};
#pragma pack(pop)

MyData dataToSend;
MyData receivedData;

void setup() {
  Serial.begin(115200);
  while (!Serial); // Wait for Serial Monitor to open

  // Copy string safely into the fixed array
  strncpy(dataToSend.name, "Temperature", sizeof(dataToSend.name));
  dataToSend.value = 10;
  
  delay(500); // Give the serial port a moment to initialize
  
  // Cast the structure to a byte pointer and send the raw bytes
  Serial.write((uint8_t*)&dataToSend, sizeof(dataToSend));
}

void loop() {
   // Check if enough bytes have arrived to fill the struct
  if (Serial.available() >= sizeof(MyData)) {
    
    // Read raw bytes directly into the structure's memory space
    Serial.readBytes((char*)&receivedData, sizeof(MyData));
    
    // Print the unpacked data to verify it works
    Serial.print("Received Name: ");
    Serial.println(receivedData.name);
    Serial.print("Received Value: ");
    Serial.println(receivedData.value);
  }
}

The simplest way is probably to turn it into a string, send that string, and re-encode it on the other end. Sending binary data is harder because you have to define a protocol so you know when the packet starts and where it ends because otherwise you don't know what's an "end of data" marker vs what's a piece of data. Or you have to do as @HazardsMind did above and have a fixed data length. I prefer to send readable data: easier to debug.

char EpochTime[10];
float Temperature;
float Voltage;
int id;

char packet[100];
// Have to convert floats separately because of the brain-dead default
// Arduino implementation of sprintf
char TempStr[10];
dtostrf(Temperature, 3,1, TempStr);

char VoltStr[10];
dtostrf(Voltage, 3,1, VoltStr);
snprintf(packet, 100, "%s,%s,%s,%d",EpochTime, TempStr, VoltStr, id);
Serial.println(packet);

Did not test but this should work.

That would handle transmission. You can explore using strtok() to separate the received string into tokens using the comma separators, and decode the data from there on.

HTH

With start and stop markers as cedarlakeinstruments suggested.

#pragma pack(push, 1)
struct MyData {
  char name[16];
  uint8_t value;
};
#pragma pack(pop)

MyData dataToSend;
MyData receivedData;

// Unique framing markers
const char SOP = '<'; // Start of Packet
const char EOP = '>'; // End of Packet

void setup() {
  Serial.begin(115200);
  while (!Serial);

  strncpy(dataToSend.name, "Temperature", sizeof(dataToSend.name));
  dataToSend.value = 10;
  
  delay(500);

  // Send packet wrapped in markers
  Serial.write(SOP);
  Serial.write((uint8_t*)&dataToSend, sizeof(dataToSend));
  Serial.write(EOP);
}

void loop() {
  static bool recvInProgress = false;
  static byte byteCount = 0;
  static char tempBuffer[sizeof(MyData)]; // Temporary storage for incoming bytes

  while (Serial.available() > 0) {
    char rc = Serial.read();

    if (recvInProgress) {
      if (rc != EOP) {
        // Fill buffer if we haven't exceeded struct size
        if (byteCount < sizeof(MyData)) {
          tempBuffer[byteCount] = rc;
          byteCount++;
        } else {
          // Error: Packet grew too large without finding EOP
          recvInProgress = false;
          byteCount = 0;
        }
      } 
      else {
        // EOP found! Check if we got the exact number of expected bytes
        if (byteCount == sizeof(MyData)) {
          // Safely copy the raw bytes into our actual structure
          memcpy(&receivedData, tempBuffer, sizeof(MyData));
          
          // Process your successfully verified data here
          Serial.print("Received Name: ");
          Serial.println(receivedData.name);
          Serial.print("Received Value: ");
          Serial.println(receivedData.value);
        }
        
        // Reset state machine for next packet
        recvInProgress = false;
        byteCount = 0;
      }
    } 
    // Wait until we see the exact Start of Packet marker
    else if (rc == SOP) {
      recvInProgress = true;
      byteCount = 0;
    }
  }
}

you could use Serial.readByteUntil() where it waits for the linefeed, '\n' terminating the string.

unfortunately the arduino standard C printf() doesnt' support floats

look this over. a bit kluggy because of the limited float string processing

output

 serial tx/rx
rec:
    6/13 11:34
    1.00
    9.00
    87
send: 6/13 11:34,  1.00,  9.00,87


struct  {
    char  time [10];
    float temp;
    float volt;
    int   id;
} data;

char s [90];

// -----------------------------------------------------------------------------
void
rec (
    char *s )
{
    Serial.println ("rec:");

    // need to separate each comma delimited field
#define MaxTok  10
    char *toks [MaxTok];

    toks [0] = strtok (s, ",");
    for (int n = 1; (toks [n] = strtok (NULL, ",")) && MaxTok > n; n++) 
        ;

    // use atoi() and atof() to extract integer and floats
    strcpy (data.time, toks [0]);
    data.temp = atoi  (toks [1]);
    data.volt = atoi  (toks [2]);
    data.id   = atoi  (toks [3]);

    // dump values
    Serial.print ("    "); Serial.println (data.time);
    Serial.print ("    "); Serial.println (data.temp);
    Serial.print ("    "); Serial.println (data.volt);
    Serial.print ("    "); Serial.println (data.id);
}

// -----------------------------------------------------------------------------
void
send ()
{
    char s [90];
    char t [10];
    char v [10];

    // use d to str f to format floats into strings
    dtostrf (data.temp, 6, 2, t);
    dtostrf (data.volt, 6, 2, v);

    Serial.print ("send: ");

    sprintf (s, "%s,%s,%s,%d", data.time, t, v, data.id);
    Serial.println (s);
}

// -----------------------------------------------------------------------------
void loop ()
{
    if (Serial.available ())  {
        char buf [90];
        int n = Serial.readBytesUntil ('\n', buf, sizeof(buf)-1);
        buf [n] = '\0';

        rec (buf);
        send ();
    }
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);
    Serial.println (" serial tx/rx");
}

Wont time have "send :" in it or was that intentional?

Serial.print ("send: ");

sprintf (s, "%s,%s,%s,%d", data.time, t, v, data.id);
Serial.println (s);

The data is only checking for the newline so everything that comes before it will be read in and tokenized. So the final string will be "send : 6/13 11:34,1.00,9.00,87\n".

Then when tokenized it will be:

send : 6/13 11:34

1.00

9.00

87

if you use sscanf() then you can get away without the strtok parsing since you already know the string format. I forgot that in my first post. I've used this code to preset an RTC to the current system time:

  int s, h, m;
  if (sscanf(__TIME__, "%d:%d:%d", &h, &m, &s) != 3)
  {
    return;
  }

yes. it's there to indicate what function is being called. remove it in your code

sscanf() suffers the same floa processing limitations as printf()

Not an answer to your question, but are you aware that you can exchange data between the ESPs wirelessly using the ESP-NOW protocol?

Yeah. I forgot that part :frowning:

how far apart are the ESP8266 nodes?

if within 5 to 10 meters try ESP-NOW

if further than that you will probably need to use RS232 or some longer range wireless technology

Thanks Brazilino, my 'transmit' chip is receiving data from about 100ft away using NOW and then I want to forward the data using wifi. The esp8266 can't be used for both NOW and wifi. So I am using a second ESP8266 to forward the data by wifi.

Thank you everyone for the quick and helpful responses.

The essence of my problem is transmitting a character string via Tx/Rx rather than just a single character.

Somewhere in all these replies lies the answer!

I'll post again when I have the system working.

Thanks again.

Maybe in the mean time you can remove the solved tag, to stop giving a false notion that no further help is needed, and the problem is fully solved.

Sorry about that, can't un-mark the post now!

Why not? It only takes a click.

Because the 'Solution' button is grayed out.

Hi bill_lancaster,

If you really want to understand how serial communication and the USART peripheral work at the absolute hardware level, take a look at this.

Unfortunately, this code is written for the Arduino Uno / Nano (ATmega328P) and not for your ESP8266. However, it has a huge advantage: you can test and compile it directly in your browser using https://costycnc.github.io/avr-compiler-js/

video youtube

If you just want a quick fix for your ESP project, feel free to ignore this message. But if you love understanding what happens inside the chip registers under the hood, here is a complete layout. It shows exactly how the hardware handles the letter 'A' you are sending in your loop.

; ==============================================================================
; WHAT THIS ASSEMBLY (ASM) CODE DOES UNDER THE HOOD:
; ==============================================================================
; 1. It sets the built-in LED pin (D13) as an output.
; 2. It touches the hardware boxes (registers) to set the serial speed to 9600 baud.
; 3. It configures the frame size and turns on the data channels (RX and TX).
; 4. It turns on a hardware "alarm" (interrupt) that triggers automatically the exact millisecond a byte arrives.
; 5. While the board does nothing in the main infinite loop, it just waits.
; 6. As soon as you send a character from the PC (or another chip), the hardware alarm triggers:
;    * If the character is the letter "A" (ASCII number 65), the LED turns ON.
;    * If you send any other letter, the LED turns OFF.
; ==============================================================================

.org 0x0000
	jmp RESET

.org 0x0024
	jmp receive

.org 0x0060
RESET:
	sbi 0x04, 5

	ldi r16, 103       
	ldi r17, 0         
	sts 0xC4, r16      
	sts 0xC5, r17      

	ldi r16, 0b00000110
	sts 0xC2, r16

	ldi r16, 0b10011000
	sts 0xC1, r16

	sei                 

loop:
	rjmp loop           

receive:
	sbi 0x05, 5         
	lds r16, 0xC6       
	cpi r16, 65         
	breq next           
	cbi 0x05, 5         
next:
	reti                


; ==============================================================================
; PART 2: COMPLETELY COMMENTED CODE (FOR ABSOLUTE BEGINNERS) - Hidden from compiler
; ==============================================================================
; Think of a "register" as a box with 8 light switches.
; Each switch is a "bit" numbered from 7 to 0 (from left to right).
; Switch UP   (1) = electricity flows or a function turns ON.
; Switch DOWN (0) = no electricity or a function turns OFF.
;
; Box 0x04: controls whether wires D8 to D13 are INPUTS (0) or OUTPUTS (1).
; Box 0x05: gives POWER (1) or turns OFF (0) wires D8 to D13.
; Boxes 0xC4 / 0xC5: set the serial port speed (to talk to the PC).
; Boxes 0xC1 / 0xC2: turn ON the listening (RX) and sending (TX) channels.
; Box 0xC6: the mailbox where the message from the PC arrives.
; ----------------------------------------------
;
; .org 0x0000            Set code position to address 0x0000 in memory
; 	jmp RESET          Jump to the RESET section when the chip turns on
;
; .org 0x0024            Set code position to address 0x0024 (Serial message arrived address)
; 	jmp receive        Jump to the receive section as soon as a message arrives
;
; .org 0x0060            Set code position to address 0x0060 to start main program
; RESET:                 Label for the start of the setup configuration
; 	sbi 0x04, 5        Flip switch #5 in box 0x04 to ON to make wire D13 an OUTPUT
;
; 	ldi r16, 103       Load the number 103 into temporary register drawer r16
; 	ldi r17, 0         Load the number 0 into temporary register drawer r17
; 	sts 0xC4, r16      Copy number 103 from drawer r16 into speed box 0xC4
; 	sts 0xC5, r17      Copy number 0 from drawer r17 into speed box 0xC5
;
; 	We set the text size: 8 bits per character
; 	Switches:     7 6 5 4 3 2 1 0
; 	ldi r16,      0b00000110
; 	                        β”‚ β”‚
; 	                        β”‚ └── Switch #1 UP (UCSZ01 - Text size helper bit)
; 	                        └──── Switch #2 UP (UCSZ00 - Text size main bit)
; 	                        (When both #1 and #2 are UP, text size is 8-bit)
;
; 	sts 0xC2, r16      Copy these switches from drawer r16 into serial control box 0xC2
;
; 	We turn on communication and the alarm for incoming messages
; 	Switches:     7 6 5 4 3 2 1 0
; 	ldi r16,      0b10011000
; 	              β”‚   β”‚ β”‚
; 	              β”‚   β”‚ └── Switch #3 UP (TXEN0 - Turns ON data sending)
; 	              β”‚   └──── Switch #4 UP (RXEN0 - Turns ON data receiving)
; 	              └──────── Switch #7 UP (RXCIE0 - Turns ON alarm when message arrives)
;
; 	sts 0xC1, r16      Copy these switches from drawer r16 into serial control box 0xC1
;
; 	sei                Global Interrupt Enable: turn ON the main alarm switch
;
; loop:                  Label for the main infinite circle
; 	rjmp loop          Relative Jump back to loop: spin here and do nothing forever
;
; receive:               Label for the alarm code that runs when a message arrives
; 	sbi 0x05, 5        Flip switch #5 in box 0x05 to ON to give power to wire D13 (LED ON)
; 	
; 	lds r16, 0xC6      Load the message byte from mailbox box 0xC6 into drawer r16
; 	
; 	cpi r16, 65        Compare the byte in drawer r16 with number 65 (ASCII for 'A')
; 	
; 	breq next          Branch if Equal: if it is 'A', skip the next line and go to next
; 	
; 	cbi 0x05, 5        Clear Bit: flip switch #5 in box 0x05 to OFF to turn off wire D13 (LED OFF)
;
; next:                  Label used to skip the LED turning off
; 	reti               Return from Interrupt: turn the main alarm off and go back to the loop

If you compile this code and then disassemble the final binary file, you will see the EXACT SAME clean code you wrote.

There are no hidden instructions, no automatic libraries, and no magic shortcuts.

What you see is exactly what the CPU executes, giving you a 100% understanding!

But remember, this is only a teaching tool; it is not meant for production code.

Good luck with your ESP8266 structure transfer project!


To introduce strtok() into this discussion in unconscionable.

An experienced programmer who first encounters strtok() will spend some fair time with documentation and examples sussing out what, exactly, it does and how to exploit its features.

There is little hope than a beginner or early-stage hobbyist will.

But to use it like this

    toks [0] = strtok (s, ",");
    for (int n = 1; (toks [n] = strtok (NULL, ",")) && MaxTok > n; n++) 
        ;

Is not merely unconscionable, it is criminal. See how clever the for loop is! You are fired!

There is no reason to shoehorn a loop around strtok() into the parts afforded by a for loop.

There is no worse use of the for loop than when it replaces what would probably be just 8 or 9 lines of code just because you can.

A competent programmer, let’s say she has used strtok(), will marvel, then take many minutes convincing herself of the sheer genius of the compact (oh, and soooo efficient) for loop.

Also which beginners are not equipped to do.

Go to any competent doc page for strtok() and learn how it works; most use code examples that don’t require getting a PhD in for-loopology.

In the worst case, readers learn that writing compact, and nearly inscrutable for loops is a good thing: what β€œreal” programmers do all the time.

Coding like that was never a good thing, and in the 21st century there is no excuse for it.

a7