Sending floats over serial question

I am wanting to send floats over serial from my PC to an Arduino Nano and then send them back again to verify that the correct value has been sent & decoded correctly by the Nano.

The Python script on the PC side,

import serial
import struct
import numpy as np
import time

usbport = 'COM3'
ser     = serial.Serial(usbport, 115200, timeout=1)

xyz = [1.2, -3.4, 0.0]

ser.write(struct.pack("f", xyz[0]))

# Sanity check
xyz_bin = struct.pack("f", xyz[0])
print(xyz_bin)
print(struct.unpack("f", xyz_bin)[0])

t_end = time.time() + 10
while time.time() < t_end:
    
    p = ser.read(4)
    if p:
        p_flt = struct.unpack('f', p)
        print(p_flt, type(p_flt))

ser.close()

I'me expectig this to write four bytes out onto the serial line and then wait for a response 4 bytes long. I've put a second timeout, so the code waits for a second then tries again until the end time is met. If I leave out the timeout in the serial object coonstructor then the ser.read(4) acts as a blocking statement.

Either way I get nothing from the Nano. The Nano side code,

union 
{
 uint8_t b[4];
 float f;
} x;


void setup() 
{
  Serial.begin(115200);
}

void loop() 
{
  if(Serial.available() > 3)
  {
    for(int i = 0; i < 4; i++)
    {
      x.b[i] = Serial.read();
    }

    Serial.println(x.f);
  }

  // Serial.println(x.f); // endless print what has been received

}

So I think I'm reading in 4 bytes that have been put into the serial buffer by the PC. converting via a union to a float, then printing the recieved float onto the serial line.

So yeah, I dont get anything back.

I tried sending the data & closing the PC connection & just have the Nano printing the received data to the serial monitor in the Arduino IDE but I just get 0.00 printed endlessly whilst I'm expecting 1.2.

What am I missing here?

shouldn’t that be less than instead of greater than??

should not use a union for type pruning..

good luck.. ~q

Type punning using a union is not permitted in C++ (it is in C).

Also, your code makes no attempt at synchronization. How do you know which byte of which float value is being received at any given time?

i starts as zero, which is not greater than 3, so that's the end of your for loop.

Really you are asking for trouble just sending 4 bytes and expecting to receive them properly. Ideally convert to ASCII (print) send as text with some kind of start (and possibly end) marker, then convert back to a float.

There's a really good tutorial about serial communication, which I will post a link to shortly...

Edit:
See next reply, thank you @jremington

Serial Input Basics tutorial

  1. Float formats and interpretations must be identical at source and destination. Google IEEE-754, etc. and then open the reference material for the languages used at both ends.

  2. Are you sure your floats even have the same number of bytes, source and destination?

  3. Do you know that the storage order is the same, source and destination?

Lotta assumptions, there. Outside of the obvious framing, synchronization, etc. issues. All in all, better to use an ASCII formatted string in all but the most desperate circumstances.

As other said, the issue is that the Arduino code was not reading bytes correctly due to a wrong for loop condition and using Serial.println, which sends text instead of raw bytes.

A safe way to transfer floats is to accumulate 4 bytes in a buffer, use memcpy to convert to a float, and send back the bytes. This is how you do it it C++, not with a union.

Try (typed here - mind mistakes )

uint8_t buf[4];

void setup() {
  Serial.begin(115200);
}

void loop() {
  static uint8_t idx = 0;
  while (Serial.available()) {
    buf[idx++] = Serial.read();
    if (idx >= 4) {
      float f;
      memcpy(&f, buf, 4);
      Serial.write(buf, 4);
      idx = 0;
    }
  }
}

Python side for asynchronous reading and verification:

import serial
import struct
import time

ser = serial.Serial('COM3', 115200, timeout=0.1)
xyz = [1.2, -3.4, 0.0]

for val in xyz:
  ser.write(struct.pack('f', val))
  start = time.time()
  buf = b''
  while time.time() - start < 1:
    buf += ser.read(4 - len(buf))
    if len(buf) == 4:
      received = struct.unpack('f', buf)[0]
      print('Sent:', val, 'Received:', received)
      break

ser.close()

Of course you need to ensure the binary representation of the float is compatible.


If you are interested in a n ASCII communication, I’ve written a small tutorial on interfacing with Python. See Two ways communication between Python3 and Arduino (same idea could be used for binary payloads with caveats on endianness and start or end markers)

How many decimal places? Does the float ever go negative?

If it helps, you can test the Arduino side by itself, even if you choose -- against advice -- to receive raw IEEE-754 float32 values. The Serial Monitor can easily send bytes in the [0x20,0x7F) range, since that is (mostly) visible ASCII text. Adjusting the example @J-M-L posted above

uint8_t buf[4];

void setup() {
  Serial.begin(115200);
}

void loop() {
  static uint8_t idx = 0;
  while (Serial.available()) {
    buf[idx++] = Serial.read();
    if (idx >= 4) {
      float f;
      memcpy(&f, buf, 4);
      Serial.println(f, 7);  // print the resulting float
      idx = 0;
    }
  }
}

In the Serial Monitor, switch from "New Line" to "No Line Ending". If you type @BCE and press Enter, you can see those four bytes encode, as shown at float.exposed/0x45434240, exactly 3124.140625 (This value was chosen specifically as a comprehensible number.)

Note that the values are little-endian: the last byte (E) is the high byte (0x45). This is the case for most, but certainly not all, CPUs nowadays. If it helps to remember, the sign bit is in the same place for a 32-bit int and a 32-bit float.

Yes, quite right. I was messing around with something else & for got to change it back. Have edited the OP. The problem persists.

Float32, yes it can go negative.

Thanks, yes the loop counter was a typo & I've edited the OP accordingly.

This seems tantalizingly close to what I'm after. The code only returns the last value of xyz though.

This is likely because the Arduino reboots when you open the serial line and possibly it waits 2s (depending on boot loader) before really starting - so may be the first few bytes sent are not seen as your code does not run yet on the Arduino.

You can confirm this by adding a short delay at the start of the Python script (for instance time.sleep(2.5)) before sending.

(If you read my tutorial that’s why I suggested a handshake at boot)

Yes the loop counter was atypo, fixed in the OP edit now thanks. The issue persists though.

Yes that was it. Going through your tutorial now, thankyou.

Glad you got it to work

Have fun.