Каков наилучший (самый быстрый и надежный) способ отправки сообщений между Python на ПК и Arduino через последовательный порт?

Я пытаюсь установить связь между ПК с Python, используя PySerial, и Arduino. Сам Arduino имеет щит CAN и отвечает за взаимодействие с двигателем. Моя цель состоит в том, чтобы ПК сконструировал нужный кадр CAN (8 байт), отправил его по последовательному порту на Arduino, который затем сам использует библиотеку CAN для связи с двигателем. Затем мотор отправляет кадр возврата того же размера, который Arduino копирует в массив, а также выводит в последовательный порт.

В некоторых случаях это работает, хотя я изо всех сил пытаюсь сделать его надежным. Например, я управляю двигателем в режиме управления крутящим моментом (команда 37 в это техническое описание). Для произвольного прямого крутящего момента «50» отправляющий кадр должен быть:

[161, 0, 0, 0, 50, 0, 0, 0]

А для команды '-50' кадр должен быть:

[161, 0, 0, 0, 206, 255, 255, 255]

Я не понимал, что это может быть проблемой, но я предполагаю, что, поскольку для этого на самом деле требуется больше символов, отправка занимает больше времени. Положительные команды крутящего момента отправляются без проблем, в то время как отрицательные вызывают пропуски или дрожание двигателя. Я вижу, что иногда Arduino на самом деле не получает правильную команду, см. ниже (слева — кадр, отправленный из Python, а справа — то, что Arduino получает в ответ).

Слева желаемый и отправленный кадр, справа то, что получает Arduino.

Я пробовал:

  1. Очистка последовательных буферов до и после отправки сообщений на обоих устройствах
  2. Добавление задержек, чтобы попытаться замедлить связь.
  3. Изменение скорости передачи
  4. Попытка использовать Serial.write() вместо Serial.print(), поскольку я предполагал, что это может быть быстрее (?)

Я прикрепил свой код ниже, но в целом, как лучше всего и быстрее обмениваться данными между Python и Arduino по последовательному порту? Даже если мое текущее приложение будет проигнорировано, как могут выглядеть идеальные сценарии для каждого из них?

Питон:

    def serial_begin(self, baud, com):
        self.ser = serial.Serial(com, baud)
        self.ser.flushInput()
        self.ser.flushOutput()
        print("Connected to Serial Port " + com)
        t.sleep(1)

    def send_cam_frame(self, frame):

        self.send_frame = frame
        string_to_send = "<" + str(int(frame[0])) + "," + \
                         str(int(frame[1])) + "," + \
                         str(int(frame[2])) + "," + \
                         str(int(frame[3])) + "," + \
                         str(int(frame[4])) + "," + \
                         str(int(frame[5])) + "," + \
                         str(int(frame[6])) + "," + \
                         str(int(frame[7])) + ">"

        self.ser.write(string_to_send.encode('UTF-8'))
        self.receive_can_frame()

    def receive_can_frame(self):
        get_data = self.ser.readline().decode('UTF-8', errors='ignore')[0:][:-2]
        self.receive_frame = np.fromstring(get_data, dtype='int', count=8, sep=' ')

    def set_torque(self, torque):

        self.command = self.command_list["SET_TORQUE"]

        torque = int(self.constrain(torque * 2000 / 32, -2000, 2000))
        frame = [self.command, 0, 0, 0, torque & 0xFF, (torque >> 8) & 0xFF, (torque >> 16) & 0xFF,
                 (torque >> 24) & 0xFF]
        self.send_cam_frame(frame)

def main():

    arduino_port = "COM5"  # Default COM port
    baud_rate = 57600  # Default Baud Rate

    rmd = Motor()
    rmd.serial_begin(baud=baud_rate, com=arduino_port)

    set_torque = 0

    while True:
        try:

            if keyboard.is_pressed('w'):
                set_torque += 0.001
            if keyboard.is_pressed('s'):
                set_torque -= 0.001

            rmd.set_torque(set_torque)

        except KeyboardInterrupt:
            rmd.disable_motor()
            print("Keyboard Interrupt")

Ардуино:


byte recvFrame[8] = {0, 0, 0, 0, 0, 0, 0, 0};
byte sendFrame[8] = {0, 0, 0, 0, 0, 0, 0, 0};

// Serial Read Parameters
const byte numChars = 32;  // Length of received char array from Serial
char receivedChars[numChars];  // Received char array from Serial
char tempChars[numChars];        // Temporary array for use when parsing
bool newData = false;  // Flag to check if newData has been received


void setup() {
  Serial.begin(57600);
  delay(1000);
  
}

void loop() {

  CANMessage frame;

  frame.id = 0x140 + 1;
  frame.len = 8;

  // Do not touch this section
  recvWithStartEndMarkers();  // Check Serial and receive data if there is newData
  if (newData == true) {
      strcpy(tempChars, receivedChars);  // Copy variables to prevent them being altered
      parseData();  // Parse data (split where there are commas)
      
      for (int i = 0; i  < 8; i++){
        frame.data[i] = sendFrame[i];
      }

      can.tryToSend(frame);

      newData = false;  // Set to false
  }
  
  if (can.available()){
    can.receive(frame);
  }
  
  for (int i = 0; i < 8; i++){
    recvFrame[i] = frame.data[i];
  }

  printFrame();

}


// Do not touch this function
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;
        }
    }
}

// Only touch this function if more data is being sent from Python code
void parseData() {      // split the data into its parts

    char * strtokIndx; // this is used by strtok() as an index

    strtokIndx = strtok(tempChars,",");
    sendFrame[0] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[1] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[2] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[3] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[4] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[5] = atoi(strtokIndx);

    strtokIndx = strtok(NULL,",");
    sendFrame[6] = atoi(strtokIndx);
 
    strtokIndx = strtok(NULL, ",");
    sendFrame[7] = atoi(strtokIndx);


    // How to add a new variable
    // Currently, data is sent as <0, 0, 0, 0>
    // If a fourth parameter was to be sent (<0, 0, 0, 0, 1>), the following lines need to be added

    // strtokIndx = strtok(NULL, ", "); This reads the string, from where it was previously cut, up until the next comma
    // newVariableName = atoi(strtokIndx); atoi is 'to integer'. If newVariable is a float, atof is needed etc


}


void printFrame(){

  for (int i = 0; i < 8; i++){
    Serial.print(sendFrame[i]);
    Serial.print(" ");
  }
  Serial.println();
  //Serial.write(sendFrame, 8);
  //delay(3);
  
}

Примечание. Для ясности я удалил некоторые ненужные функции из обоих скриптов (например, остальную часть класса Motor для Python и настройку CAN Shield для Arduino).

, 👍1

Обсуждение

вопросы о надежном последовательном протоколе не относятся к Arduino, @jsotola

Просить «лучшее» решение — плохой подход к дизайну. Четко сформулируйте свои требования, и у кого-то может быть **достаточно хорошее** решение., @Elliot Alderson

Вы проверили передачу данных без кода CAN и двигателя и без каких-либо других подключений? И в настоящее время вы отправляете обратно уже проанализированный кадр. Пожалуйста, попробуйте вместо этого отправить буфер символов и сообщить о результатах. И я не вижу, где вы печатали отправленные и полученные данные в коде Python. Пожалуйста, включите фактический код, который создал этот вывод. Это может быть актуально для отладки кода, @chrisl

Вам может понравиться https://github.com/nanopb/nanopb, @chicks


1 ответ


Лучший ответ:

2

Нет лучшего способа, но есть способ лучше (и более эффективный).

Для произвольного прямого крутящего момента '50' отправляющий кадр должен быть:

[161, 0, 0, 0, 50, 0, 0, 0]

А для команды '-50' кадр должен быть:

[161, 0, 0, 0, 206, 255, 255, 255]

Большинство программистов, которые плохо знакомы со встроенными программами, подготавливают свои данные для отправки по каналу связи в виде строки вместо отправки необработанных двоичных данных. Проблема заключается в том, что а) для отправки значения 215 вы отправляете 3 байта ASCII, в то время как данные могут фактически отправляться одним байтом. б) как целое число 256 (0x0100), так и 32766 (0x7FFE) занимают всего два байта, но когда вы кодируете его как ASCII, вы имеете дело с различной длиной от 3 символов до 5 символов. Это не только требует больше байтов для отправки данных, но и затрудняет анализ данных, если у вас нет разделителя для разделения данных, например ('256,32766`, разделенных запятой).

Глядя на ваш набор данных и таблицу данных, становится ясно, что каждая точка данных представлена 4-байтовыми подписанными данными с небольшим порядком байтов. (то есть младший байт отправляется первым), поэтому десятичный 50 равен 0x00000032, он хранится в памяти как 0x32, 0x00, 0x00, 0x00 с обратным порядком байтов.

Отрицательное значение представлено дополнением до двух положительного значения, поэтому -50 в десятичном виде равен 0xFFFFFFCE и сохраняется как 0xCE, 0xFF, 0xFF, 0xFF.

В python библиотека struct позволяет выполнять преобразования между значениями Python и структурами C, представленными в виде Python. байты объектов.

form struct import *

torque = -50

data = pack('<i', torque) # pack torque as a 4-byte integer with little endian
ser.write(torque)         # send over serial as bytes

На стороне Arduino считайте данные из Serial как byte (т.е. uint8_t) и сохраните их в массиве. Затем вы можете преобразовать его обратно в int32.

// предположим, что вы прочитали эти 4 байта данных из Serial и добавили их в массив
uint8_t data[4]{0xCE, 0xFF, 0xFF, 0xFF};

// Преобразование полученных байтов данных в int32_t
int32_t torque = static_cast<int32_t> (data[3]<< 24 | data[2] << 16 | data[1] << 8 | data[0]);

// Это вернет результат -50
Serial.println(torque);

Это должно сократить объем кода и значительно повысить эффективность отправки данных. Однако код не повышает надежность, одно из преимуществ отправки данных в виде строки заключается в том, что вы можете определить конец потока данных, обнаружив терминатор \0 в конце строки. При отправке необработанных двоичных файлов в идеале вам нужен механизм для обозначения количества байтов данных байтов, которые вы собираетесь отправить.

,