Latency between arduino and python

I’m trying to track the position, along one axes, of an object using a motion tracker and send to arduino to be displayed using a linear motor.
In other words if the object is moved up the probe of the linear motor will be extended, in the other case will be retracted.
The problem is that when I integrate the serial communication of arduino in the tracking program that read the position from the motion tracker, the latency of the communication is way to high.
For some preliminary test i would like to have the latency to be, at least, less than a second.

The code in python is related to another function called NatNetClient.py that setup the communication with the motive tracker system.

If someone is familiar with this kind of program for tracking could kindly suggest me the best way to integrate a communication with arduino ?

I’m using an arduino Uno.

I couldn’t attach the entire python program, so here is a snippet of the communication (based on the one provided by Robin2 here )

def receiveRigidBodyFrame( id, position, rotation ):
   
    values=MapPointMotionTracker(position) #mapping the values from the motion tracker to a scale 0-50 for the linear motor
    
    Values.x_t1=values[1]*Values.alpha+(1-Values.alpha)*Values.x_tp #exponential filtering of the data
    
    pos=[]
    Values.x_tp=Values.x_t1
    if (Values.x_t1>=50):
        pos.append("<Linear,"+str(50)+">")
    elif (Values.x_t1<=0):
        pos.append("<Linear,"+str(0)+">")
    else:
        pos.append("<Linear,"+str(int(round(Values.x_t1)))+">")    


    print( "POSITION FROM THE MOTION TRACKER : ", position[1], "POSITION SMOOTHED", Values.x_t1, "VALUE NOT SMOOTHED", values[1])   
    
    if(round(values[1])==Values.positionOld):
        runTest(pos)

Control_Interface.ino (8.07 KB)

I wrote a newer and simpler Python-Arduino example here. I am not aware of any latency in it - nor do I recall any from my earlier examples.

I have a project that uses essentially the same code and Python receives messages instantly - and lots of them if I let my Arduino program run fast.

You need to measure the time in different parts of your program to determine where the sloth is. IMHO you should not have any calculations in the function that receives messages. Do the calculations after the message has been received.

...R

One little question about your replyToPC().

Why do you use a Serial.println(">") and not a Serial.print(">")? The former just sends extra stuff that should be ignored by the Python side.

sterretje: Why do you use a Serial.println(">") and not a Serial.print(">")? The former just sends extra stuff that should be ignored by the Python side.

i agree, it does not matter to Python. However if, for debugging purposes, you show the output on the Arduino Serial Monitor the linefeed makes things tidier.

Choose whichever you prefer.

...R

Robin2: I have a project that uses essentially the same code and Python receives messages instantly - and lots of them if I let my Arduino program run fast.

Thank you for your prompt answer and for providing a new example. I will integrate it in the program and see if something changes. Probably the latency is due to the way the program of the motion tracking call the communication with arduino.

The python function that I shared is called continuously, and creates a "pos" list every time for a single position and calls "runTest". It is possible that using your program in this way (instead of creating only a list of multiple values) can generate some delay?

Also when testing I noticed that the movement that arduino does in the setup, are overshoot when is reset from the python script. I doesn't make much sense to me, in your knowledge is it possible ?

Robin2: IMHO you should not have any calculations in the function that receives messages. Do the calculations after the message has been received.

Are you talking about the arduino script or in the python one ?

sterretje: One little question about your replyToPC().

Why do you use a Serial.println(">") and not a Serial.print(">")? The former just sends extra stuff that should be ignored by the Python side.

When I will make the finished program I will try to optimize, thank you for your advice!

berto906:
Also when testing I noticed that the movement that arduino does in the setup, are overshoot when is reset from the python script. I doesn’t make much sense to me, in your knowledge is it possible ?

Not sure what you’re trying to say.

Seeing the word reset, the Uno is reset when you open the serial port. If you open the port continuously, you will have plenty resets of the Arduino. In general, it can be prevented by not touching DTR when you open the port; no idea how to do so in Python.

sterretje: Not sure what you're trying to say.

Seeing the word reset, the Uno is reset when you open the serial port. If you open the port continuously, you will have plenty resets of the Arduino. In general, it can be prevented by not touching DTR when you open the port; no idea how to do so in Python.

Sorry, my bad, I wasn't clear enough. When I run the python script it resets the arduino, the same way if I pushed the reset button. Anyway this does not matter anymore, it didn't shown that behavior again. Probably I changed accidentally the value.

berto906: The python function that I shared is called continuously, and creates a "pos" list every time for a single position and calls "runTest". It is possible that using your program in this way (instead of creating only a list of multiple values) can generate some delay?

I don't understand. I thought the purpose of the Python code was to receive data from the Arduino and. if so, I would only expect it to update when a new message is received.

Also when testing I noticed that the movement that arduino does in the setup, are overshoot when is reset from the python script. I doesn't make much sense to me, in your knowledge is it possible ?

When a PC program opens the serial port it normally causes the Arduino to reset. Your program should allow time for that and then keep the serial port open.

Are you talking about the arduino script or in the python one ?

Both, if appropriate. A "receive" function should do nothing but receive.

...R

Robin2: I don't understand. I thought the purpose of the Python code was to receive data from the Arduino and. if so, I would only expect it to update when a new message is received.

The objective is to send to arduino a number that correspond with the position of the object tracked with the motion tracker.

During a bit of troubleshooting today I found out that neither the program to retrieve the position or the communication with arduino created delay by itself. Although togheter they caused the position to be read with a big delay.

But using the script you shared on the second answer, to send the position to arduino solved the problem. Now I have no delay in the communication

Thank you so much!

berto906: The objective is to send to arduino a number that correspond with the position of the object tracked with the motion tracker.

Sorry, I had things back-to-front.

But using the script you shared on the second answer, to send the position to arduino solved the problem. Now I have no delay in the communication

Good to hear

...R

I seem to be late to the party, but it's honestly faster and more reliable to interface Python with your Arduino using compatible libraries such as pySerialTransfer and SerialTransfer.h.

pySerialTransfer is pip-installable and cross-platform compatible. SerialTransfer.h runs on the Arduino platform and can be installed through the Arduino IDE's Libraries Manager.

Both of these libraries have highly efficient and robust packetizing/parsing algorithms with easy to use APIs.

Example Python Script: Note that while this script may seem daunting, it's meant to show the flexibility of the library while your particular script doesn't need to be as complex.

import time
from pySerialTransfer import pySerialTransfer as txfer


if __name__ == '__main__':
    try:
        link = txfer.SerialTransfer('COM17')
        
        link.open()
        time.sleep(2) # allow some time for the Arduino to completely reset
        
        while True:
            send_size = 0
            
            ###################################################################
            # Send a list
            ###################################################################
            list_ = [1, 3]
            list_size = link.tx_obj(list_)
            send_size += list_size
            
            ###################################################################
            # Send a string
            ###################################################################
            str_ = 'hello'
            str_size = link.tx_obj(str_, send_size) - send_size
            send_size += str_size
            
            ###################################################################
            # Send a float
            ###################################################################
            float_ = 5.234
            float_size = link.tx_obj(float_, send_size) - send_size
            send_size += float_size
            
            ###################################################################
            # Transmit all the data to send in a single packet
            ###################################################################
            link.send(send_size)
            
            ###################################################################
            # Wait for a response and report any errors while receiving packets
            ###################################################################
            while not link.available():
                if link.status < 0:
                    if link.status == -1:
                        print('ERROR: CRC_ERROR')
                    elif link.status == -2:
                        print('ERROR: PAYLOAD_ERROR')
                    elif link.status == -3:
                        print('ERROR: STOP_BYTE_ERROR')
            
            ###################################################################
            # Parse response list
            ###################################################################
            rec_list_  = link.rx_obj(obj_type=type(list_),
                                     obj_byte_size=list_size,
                                     list_format='i')
            
            ###################################################################
            # Parse response string
            ###################################################################
            rec_str_   = link.rx_obj(obj_type=type(str_),
                                     obj_byte_size=str_size,
                                     start_pos=list_size)
            
            ###################################################################
            # Parse response float
            ###################################################################
            rec_float_ = link.rx_obj(obj_type=type(float_),
                                     obj_byte_size=float_size,
                                     start_pos=(list_size + str_size))
            
            ###################################################################
            # Display the received data
            ###################################################################
            print('SENT: {} {} {}'.format(list_, str_, float_))
            print('RCVD: {} {} {}'.format(rec_list_, rec_str_, rec_float_))
            print(' ')
    
    except KeyboardInterrupt:
        link.close()
    
    except:
        import traceback
        traceback.print_exc()
        
        link.close()

Example Arduino Sketch:

#include "SerialTransfer.h"


SerialTransfer myTransfer;


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


void loop()
{
  if(myTransfer.available())
  {
    // send all received data back to Python
    for(uint16_t i=0; i < myTransfer.bytesRead; i++)
      myTransfer.txBuff[i] = myTransfer.rxBuff[i];
    
    myTransfer.sendData(myTransfer.bytesRead);
  }
}

On the Arduino side, you can use myTransfer.txObj() and myTransfer.rxObj() to copy values to the library's RX buffer and parse multi-byte variables out of the library's TX buffer.

For theory behind robust serial communication, check out the tutorials Serial Input Basics and Serial Input Advanced.

Power_Broker:
I seem to be late to the party, but it’s honestly faster and more reliable to interface Python with your Arduino using compatible libraries such as […]

Never too late! I was just trying to improve the program. Now works really good for my need, but after 20 seconds or so, it starts slowing down and flooding the data all together, every now and then.

I need just a script to send a number (int) to arduino continuously with an interval as little as possible. I’m not that used to parsing or with the library you have mentioned. There is some more material I could use to implement it in my program? Especially for the arduino side?

berto906: Now works really good for my need, but after 20 seconds or so, it starts slowing down and flooding the data all together, every now and then.

I need just a script to send a number (int) to arduino continuously with an interval as little as possible.

It sounds as if you are sending data more frequently than the Arduino is capable of handling. You need to match the rate at which you send data with the speed at which the receiver can process the incoming data.

Please post the latest version of your Arduino program (the complete program) and tell us how often the Python program sends messages.

...R

Robin2:
It sounds as if you are sending data more frequently than the Arduino is capable of handling. You need to match the rate at which you send data with the speed at which the receiver can process the incoming data.

Please post the latest version of your Arduino program (the complete program) and tell us how often the Python program sends messages.

…R

The python program send data every 0.07 seconds, and send a string. Arduino then convert this string in to an int and move a linear actuator into the position.

All the millis along the program is due to the fact that the linear motor has no encoder, is a way to keep track of the position of the probe.

Power_Broker:
I seem to be late to the party, but it’s honestly faster and more reliable to interface Python with your Arduino using compatible libraries such as […]

I have tried the examples you sent, but on the python terminal nothing is printed. It doesn’t output any errors, but it doesn’t work either.

Control_Interface2.ino (7.8 KB)

berto906: The python program send data every 0.07 seconds, and send a string. Arduino then convert this string in to an int and move a linear actuator into the position.

How long does the Arduino require to do that?

Give an example of the string that Python sends.

...R

Robin2: Give an example of the string that Python sends.

Python sends something like sendToArduino(str(50))

Robin2: How long does the Arduino require to do that?

I will check tomorrow, but for now I solved deleting the answer sent from arduino to the python program.

berto906:
Python sends something like sendToArduino(str(50))

I would like to see one or two examples of what is actually sent.

By the way, please include short programs in a Post so we don’t have to download them.

…R

@berto906's Arduino code attachement

#include 
#include 
#include 

Adafruit_ADS1115 ads1115(0x48);

Servo Squeezer; //name of the servo motor

//unsigned long initMillis; //time at which the linear actuator has started moving 
unsigned long posMillis; //millis for the start of the movement of the linear actuator
unsigned long initMillis; //time at which the linear actuator has started moving 
unsigned long currentMillis;
unsigned long stepMillis; //time of the precedent counted movement of the linear actuator

boolean newposition=false; //bool for a new position
boolean moving=false; //bool for the motion of the probe
boolean extend=false;
boolean retract=false;

float posC=0; // the actual position of the actuator 
int flag =0; //variable for LinearActuation Function
int oldPos=0; //variable to store the position
int position=0; //required position

//int PWM1=3; //Pin to control the DC motor
int PWML1=5;//PWM OF THE LINEAR MOTOR (RED)
int PWML2=6;//GND OF THE LINEAR MOTOR (BLACK)
int Spin=9;//squeezer pin 

int NewMotorValue= 0;  

int16_t results; //variable for the force sensor

//variable needed for communication 
const byte numChars = 64;
char receivedChars[numChars];

boolean newData = false;

byte ledPin = 13;   // the onboard LED



const unsigned long period =138; //number in millisecond corresponding to 1 mm
boolean toend=false;

//===========

void setup() {
  Serial.begin(115200);
  //pinMode(PWM1,OUTPUT);

  pinMode(PWML1,OUTPUT);
  pinMode(PWML2,OUTPUT);
  
  ads1115.begin();

  Squeezer.attach(9); //the pin the servo motor is attached to has to be PWM
  
    // flash LEDs so we know we are alive
  //for (byte n = 0; n < numLEDs; n++) {
  //   pinMode(ledPin[n], OUTPUT);
  //   digitalWrite(ledPin[n], HIGH);
  //}
  digitalWrite(PWML2,HIGH);
  digitalWrite(PWML1,LOW);
  Serial.println("");
  delay(8000); // delay() is OK in setup as it only happens once
  digitalWrite(PWML2,LOW);
  
  posC=25; // i position the probe to the middle point to avoid initial overshoot
  Serial.println("");
  digitalWrite(PWML1,HIGH);
  currentMillis=millis();
  while (millis()-currentMillis<=25*period){
    digitalWrite(PWML1,HIGH);
    }
  digitalWrite(PWML1,LOW);

   

  Serial.println("");
  
  initMillis=millis();
  
  
}


//==========
void loop() {
  currentMillis = millis();
  //Position();
  recvWithStartEndMarkers();

  updateMotorSpeed(); //this order is important, this has to stay between this two functions 
  
  replyToPython();

   
  readForceSensor();
  Squeezing();
  
  updateLinear();     //has to be called continuously to stop the probe 
  UpdatePosLin();
}


//=============

void Position(){ //used for debugging 
  if (currentMillis-posMillis>100)
  {
    Serial.print("POSITION REQUIRED : ");
    //receivedChars =random(0,50);
    //postion=random(0,50)
    Serial.println(position);
    newposition=true;
    posMillis=millis();
  }
}
//=============

void recvWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    char startMarker = '<';
    char endMarker = '>';
    char rc;

    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();

        if (recvInProgress == true) {
            if (rc != endMarker) {
                receivedChars[ndx] = rc;
                ndx++;
                if (ndx >= numChars) {
                    ndx = numChars - 1;
                }
            }
            else {
                receivedChars[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                ndx = 0;
                newData = true;
            }
        }

        else if (rc == startMarker) {
            recvInProgress = true;
        }
    }
}


//=============

void replyToPython() {
    if (newData == true) {
        Serial.print("');
            // change the state of the LED everytime a reply is sent
        //digitalWrite(ledPin, ! digitalRead(ledPin));
        newData = false;
    }
  /*if (newData==true){
    newData=false;
    Serial.println("It'a all false now");
  }*/
}


//=============

void updateMotorSpeed() {

   // this illustrates using different inputs to call different functions
  //if (strcmp(messageFromPC, "Motor1") == 0) {
   //  updateMotor1();
  //}
  //Serial.println("inside the control of motor");
  
  if (newData == true) {
    position=atoi(receivedChars);
    newposition=true;
    updateLinear();
  
  }
}

//=============

void updateLinear(){ // given a position (receivedChars) from 0 to 50 have to move the linear motor accordingly 
  if((int(posC)!=position)&&(newposition)){
    int buf=posC-position;
    //oldPos=position;//store the position that has to be reached 
    //Serial.println("There is a new position received ");
    
    //Serial.print("LENGTH OF MOVEMENT : ");
    //Serial.println(buf);
    if(buf<0){
      
      digitalWrite(PWML2,LOW);
      digitalWrite(PWML1,HIGH);
      
      //Serial.println("MI ESTENDO");
      
      initMillis=currentMillis;//initMillis can actually be deleted 
      stepMillis=initMillis;
      //endMillis=initMillis+period*abs(buf); //time at which the probe has to stop used in the previous version, can be used to doulbe check
      
      newposition=false;
      moving=true;
      extend=true;
      retract=false;
    }
    if(buf>0){
      digitalWrite(PWML2,HIGH);
      digitalWrite(PWML1,LOW);
      
      //Serial.println("MI ACCORCIO");
      
      initMillis=currentMillis;
      stepMillis=initMillis;
     
      //endMillis=initMillis+period*abs(buf); //time at which the probe has to stop
      
      newposition=false;
      
      moving=true;
      retract=true;
      extend=false;
    }
  }
  //if((currentMillis>=endMillis)&&(moving)){
  if(int(posC)==position){
      digitalWrite(PWML2,LOW);
      digitalWrite(PWML1,LOW);
      newposition=false;
      moving=false;
      extend=false;
      retract=false;
      //pos=oldPos;
        
      //Serial.print("STOPPED THE PROBE, THE POSITION IS:  ");
      //Serial.println(posC);
    }
}
//============
void UpdatePosLin(){
  currentMillis=millis();// to test what changes to have it here instead of before the function call in the loop
  float increment;
  if((int(posC)!=position)&&(moving)){ //moving condition is sufficient probably
        if(extend){
          increment= ((float)currentMillis-(float)stepMillis)/period;
          posC=posC+increment; // the position is going toward 50
          stepMillis=currentMillis;
        }
        if(retract){
          increment= ((float)currentMillis-(float)stepMillis)/period;
          posC=posC-increment; //the position is going toward 0 
          stepMillis=currentMillis;
        }
        //Serial.print("AGGIORNO LA POSIZIONE CON IL CALCOLOLO: ");
        //Serial.println(posC);
    //Serial.print("L'incremento vale : ");
    //Serial.println(increment);
    }
}


//============

void readForceSensor(){
  
  results = ads1115.readADC_Differential_0_1()/5.33;//the fraction is used for calibration purposes (same number on the amplifier and the arduino)
  //Serial.print("Differential: "); Serial.print(results); Serial.print("("); Serial.print(results * 3); Serial.println("mV)");
  
}

void Squeezing(){ //given the value of the force sensor map the value on the range of the servo and moves it

  int val = map(results, 0, 600, 0, 180); //the first two numbers should be the range of the force sensor 
  Squeezer.write(val);
}

Robin2: I would like to see one or two examples of what is actually sent.

It is not clear to me what do you mean by "what is actually sent". The function that compute the position of the object looks like this :

if(time.time()-Values.prevTime>0.05): #this regulates the delay beetween two data trasmission to arduino
        Values.x_tp=Values.x_t1
        Values.prevTime=time.time()
        if (Values.x_t1>=50)and(50!=Values.positionOld):
            sendToArduino(str(50))
            Values.positionOld=50  
        elif (Values.x_t1<=0)and(0!=Values.positionOld): #to guarantee not to enter here twice 
            sendToArduino(str(0))
            Values.positionOld=0 
        else:
            if(round(values[1],8)!=round(Values.positionOld,8)): 
                sendToArduino(str(int(round(Values.x_t1))))    
                Values.positionOld=(round(Values.x_t1,6))# update the position values for the new iteration

Values.x_t1 is a float number ranging (usually) from 0 to 50, but with the two if condition inside I ensure that only 0-50 int values are sent.

The function sendToArduino is the same as in the example you shared:

 def sendToArduino(stringToSend):
    
        # this adds the start- and end-markers before sending
    global startMarker, endMarker, serialPort
    
    stringWithMarkers = (startMarker)
    stringWithMarkers += stringToSend
    stringWithMarkers += (endMarker)

    serialPort.write(stringWithMarkers.encode('utf-8')) # encode needed for Python3