Clean way to send data struct from python to arduino?

Question:

I’m working on a robot and I’d like to somehow send a command using pySerial to the arduino.
The command would look like {MOVE, 60, 70} or {REQUEST_DATA}, where I’d have the arduino read in the first value, if it’s "MOVE" then it drives some motors with speed 60 and 70, and if it’s "REQUEST_DATA" it would respond with some data like battery status, gps location etc.

Sending this as a string of characters and then parsing is really a huge pain! I’ve tried days (!frustration!) without it working properly. Is there a way to serialize a data structure like {‘MOVE’, 70, 40}, send the bytes to the arduino and reconstruct into a struct there? (Using struct.pack() maybe? But I don’t yet know how to "unpack" in the arduino).

I’ve looked at serial communication on arduino and people seem to just do it the ‘frustrating’ way – sending single chars. Plus all talk about sending struct from arduino to python, and not the other way round.

Asked By: Dinh Trung Che

||

Answers:

There are a number of ways to tackle this problem, and the best solution depends on exactly what data you’re sending back and forth.

The simplest solution is to represent commands a single bytes (e.g., M for MOVE or R for REQUEST_DATA), because this way you only need to read a single byte on the arduino side to determine the command. Once you know that, you should know how much additional data you need to read in order to get the necessary parameters.

For example, here’s a simple program that understands two commands:

  • A command to move to a given position
  • A command to turn the built-in LED on or off

The code looks like this:

#define CMD_MOVE 'M'
#define CMD_LED 'L'

struct Position {
  int8_t xpos, ypos;
};

struct LEDState {
  byte state;
};

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);

  // We need this so our Python code knows when the arduino is
  // ready to receive data.
  Serial.println("READY");
}

void loop() {
  char cmd;
  size_t nb;

  if (Serial.available()) {
    cmd = Serial.read();
    switch (cmd) {
      case CMD_MOVE:
        struct Position pos;
        nb = Serial.readBytes((char *)&pos, sizeof(struct Position));
        Serial.print("Moving to position ");
        Serial.print(pos.xpos);
        Serial.print(",");
        Serial.println(pos.ypos);
        break;
      case CMD_LED:
        struct LEDState led;
        nb = Serial.readBytes((char *)&led, sizeof(struct LEDState));
        if (led.state) {
          digitalWrite(LED_BUILTIN, HIGH);
        } else {
          digitalWrite(LED_BUILTIN, LOW);
        }
        Serial.print("LED is ");
        Serial.println(led.state ? "on" : "off");

        break;
    }
  }
}

A fragment of Python code that interacts with the above might look like this (assuming that port is a serial.Serial object):

print("waiting for arduino...")
line=b""
while not b"READY" in line:
    line = port.readline()

port.write(struct.pack('bbb', ord('M'), 10, -10))
res = port.readline()
print(res)

for i in range(10):
    port.write(struct.pack('bb', ord('L'), i%2))
    res = port.readline()
    print(res)
    time.sleep(0.5)

port.write(struct.pack('bbb', ord('M'), -10, 10))
res = port.readline()
print(res)

Running the above Python code, with the Arduino code loaded on my Uno, produces:

waiting for arduino...
b'Moving to position -10,10rn'
b'LED is offrn'
b'LED is onrn'
b'LED is offrn'
b'LED is onrn'
b'LED is offrn'
b'LED is onrn'
b'LED is offrn'
b'LED is onrn'
b'LED is offrn'
b'LED is onrn'
b'Moving to position 10,-10rn'

This is simple to implement and doesn’t require much in the way of decoding on the Arduino side.

For more complex situations, you may want to investigate more complex serialization solutions: for example, you can send JSON to the arduino and use something like https://arduinojson.org/ to deserialize it on the Arduino side, but that’s going to be a much more complex solution.


In most cases, the speed at which this works is going to be limited by the speed of the serial port: the default speed of 9600bps is relatively slow, and you’re going to notice that with larger amounts of data. Using higher serial port speeds will make things noticeably faster: I’m too lazy to look up the max. speed supported by the Arduino, but my UNO works at least as fast as 115200bps.

Answered By: larsks