Отладка различного поведения последовательного приема/отправки между Uno и Mega

У меня есть простой текстовый синтаксический анализатор (как компонент более крупного проекта), написанный кодом и вроде бы работающий правильно. Он анализирует команду с разделителями "ключ-значение", например: <key1=value1;key2=value2;key3=value3>.

В настоящее время функциональность очень глупа, просто чтобы проверить ее: когда Arduino находит ключ с именем «текст», он составляет ответ аналогичной формы, но только с одним ключом «текст_возврат» и тем же значением, и отправляет его обратно через серийный номер. То есть, когда отправляется команда <key1=value1;text=123;key3=value3>, я ожидаю от Arduino ответа <text_return=123>.

Затем я подключаю плату к своему ноутбуку через USB и проверяю ее как с помощью Serial Monitor в среде разработки Arduino, так и с помощью простой программы, написанной на Python.

Итак, вот запутанная часть:

  1. Последовательный монитор с Uno: работает
  2. Python с Uno: работает
  3. Последовательный монитор с Mega: работает
  4. Python с Mega: НЕ работает

В частности, под "не работает" я подразумеваю, что Mega, похоже, получает последовательный ввод от программы, но не выдает ответа.

Вот вывод терминала из теста № 2, отображаемый программой python, показывающий ответ на команду, который я ожидал (я проанализировал полученную команду в dict):

SENT: <text=1953>
SENT: <text=1954>
SENT: <text=1954>
SENT: <text=1954>
SENT: <text=1954>
SENT: <text=1954>
SENT: <text=1954>
SENT: <text=1954>
RECD: {'text_return': 1954}
SENT: <text=1954>
RECD: {'text_return': 1954}
SENT: <text=1954>
RECD: {'text_return': 1954}
SENT: <text=1954>
RECD: {'text_return': 1954}

А вот вывод терминала из неработающего теста № 4 — т.е. нет ответа.

SENT: <text=2001>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2002>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>
SENT: <text=2003>

Во всех случаях я использую скорость 9600 бод; единственное различие между двумя тестами заключается в том, что я выбираю плату Uno vs. Mega/Mega 2560 в Arduino IDE.

Плата Mega представляет собой Elegoo Mega2560 R3. Код может быть лишним, но в помощь:

Открытие порта на Arduino из Python

def FindArduino(baud=9600, timeout=0):
  initial_time = time.time()
  arduino_found = False
  attempted = False
  while not attempted or time.time() - initial_time < timeout and not arduino_found:
    attempted = True
    ports = serial.tools.list_ports.comports(include_links=False)
    for port in ports:
      manufacturer = port.manufacturer
      if manufacturer and 'arduino' in manufacturer.lower():
        arduino_port = port
        arduino_found = True

  if arduino_found:
    try:
      arduino = serial.Serial(arduino_port.device, baud, timeout=0)
      arduino.reset_input_buffer()
      arduino.reset_output_buffer()
    except serial.SerialException:
      arduino_found = False

  if arduino_found:
    return arduino
  else:
    return None

Отправка серийного номера на Python в Arduino

  cmd = '<text=%d>' % (time.time() - 1586218411)
  print('SENT: %s' %cmd)
  cmd = bytes(cmd, 'ascii')
  arduino.write(cmd)
  time.sleep(.1)

Получение серийного номера в python от Arduino

  cmds = []

  if (arduino.in_waiting>0):
    buffer += arduino.read(arduino.in_waiting).decode('ascii')
    while COMMAND_END_CHAR in buffer:
      end_char_pos = buffer.find(COMMAND_END_CHAR)
      potential_command = buffer[:end_char_pos]
      if COMMAND_START_CHAR in potential_command:
        cmds.append(potential_command[potential_command.find(COMMAND_START_CHAR)+1:])
      buffer = buffer[end_char_pos+1:]

  return (buffer, cmds)

И полный код самой Arduino

// индикаторы команды
char START_MARKER = '<';
char END_MARKER = '>';

const int MAX_KEY_VALUE_PAIRS = 3;  // максимальное количество пар ключ-значение в сообщении
const int MAX_ELEMENT_CHARS = 30;  // максимальное количество символов (+1 для терминатора) в ключе или значении

// формат сообщения: <key1=value1;key2=value2;key3=value3>
const int MAX_MESSAGE_CHARS = (MAX_KEY_VALUE_PAIRS * (MAX_ELEMENT_CHARS + 1)) * 2 + (MAX_KEY_VALUE_PAIRS - 1) + 2;  // максимальный размер сообщения

char received_chars[MAX_MESSAGE_CHARS];
bool new_data = false;
char written_chars[MAX_MESSAGE_CHARS];

char *text =  {'\0'};


void ParseData(char *str) {
  // Это выбирает пары ключ-значение с разделителями ; и присваивает их массиву с несколькими размерами
  char * pch;
  int pairs_count = 0;
  char config[MAX_KEY_VALUE_PAIRS][2][MAX_ELEMENT_CHARS];

  pch = strtok(str, "=");

  while (pch != NULL)
  {
    strcpy(config[pairs_count][0], pch);
    pch = strtok(NULL, ";");
    if (pch == NULL) break;

    strcpy(config[pairs_count][1], pch);
    pairs_count++;
    pch = strtok(NULL, "=");
    if (pch == NULL) break;
  }

  for(int i=0;i<pairs_count;i++) {
    if (strcmp(config[i][0], "text")==0) 
      strcpy(text, config[i][1]);
    WriteSerial();
  }
}

void ReadSerial(){
  // После вызова ReceiveText для очистки буфера, если была найдена полная команда,
  // разбираем эту команду.
  new_data = ReceiveText();
  if (new_data == true) {
    char temp_chars[MAX_MESSAGE_CHARS];  // временный массив для использования при разборе
    strcpy(temp_chars, received_chars);
    received_chars[0] = '\0';
    ParseData(temp_chars);
  }
}

void WriteSerial(){
  sprintf(written_chars, "<text_return=%s>\n", text);
  Serial.write(written_chars);
  written_chars[0] = '\0';
}

boolean ReceiveText() {
  // Это выгружает символы в буфере на данный момент Receive_chars, ища END_MARKER
  // по пути; если он находит его, он возвращается, чтобы найти START_MARKER; если и это найдется,
  // строка внутри представляет собой набор пар ключ-значение, разделенных символом ;
  static boolean recv_in_progress = false;
  static byte ndx = 0;
  char rc;

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

    if (recv_in_progress == true) {
      if (rc != END_MARKER) {
        received_chars[ndx] = rc;
        ndx++;
        if (ndx >= MAX_MESSAGE_CHARS) {
          ndx = MAX_MESSAGE_CHARS - 1;
        }
      } else {
        received_chars[ndx] = '\0'; // завершаем строку
        recv_in_progress = false;
        ndx = 0;
        new_data = true;
      }
    } else if (rc == START_MARKER) {
        recv_in_progress = true;
    }
  }
  return new_data;
}

void setup() {  
  Serial.begin(9600);
  text = (char*)malloc(25);

  received_chars[0] = '\0';

}

void loop() {
  ReadSerial();
}

ОБНОВЛЕНИЕ

Используя предложенные библиотеки, я обновил код. Связь в обоих направлениях для простых тестовых данных работает, хотя я не могу правильно преобразовать пакет, полученный Arduino, в строку. Вот вывод, который я вижу с моего терминала Python; Я ожидаю получить обратно первый символ того, что было отправлено (то есть: «0», затем «1»), а не просто инициализированное значение «S».

SENT: 0.20
RCVD: S
SENT: 0.41
RCVD: S
SENT: 0.61
RCVD: S
SENT: 0.82
RCVD: S
SENT: 1.02
RCVD: S
SENT: 1.23
RCVD: S
SENT: 1.43
RCVD: S
SENT: 1.64
RCVD: S

Код Python

import time
from pySerialTransfer import pySerialTransfer as txfer

if __name__ == '__main__':
  try:
    link = txfer.SerialTransfer('/dev/cu.usbmodem14201')

    link.open()
    time.sleep(2) # allow some time for the Arduino to completely reset
    base = time.time()

    while True:
      time.sleep(0.2)
      s = '%.2f' % (time.time() - base)
      l = len(s)
      for i in range(l):
        link.txBuff[i] = s[i]

      link.send(l)

      while not link.available():
        if link.status < 0:
          print('ERROR: {}'.format(link.status))


      response = ''
      for index in range(link.bytesRead):
        response += chr(link.rxBuff[index])

      print('SENT: %s' % s)
      print('RCVD: %s' % response)

  except KeyboardInterrupt:
    link.close()

Код Arduino

#include "SerialTransfer.h"

char str[100];

SerialTransfer myTransfer;
int LED = 13;

void blinkLED(int n)
{
  for (int i = 0; i < n; i++) {
    digitalWrite(LED, HIGH);
    delay(150);
    digitalWrite(LED, LOW);
    delay(150);
  }
} 


void setup()
{
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
  str[0] = 'S';
  str[1] = '\n';
  Serial.begin(115200);
  myTransfer.begin(Serial);
}

void loop()
{
  blinkLED(2);
  delay(500);

  // отправляем байты
  myTransfer.txBuff[0] = str[0];
  myTransfer.sendData(1);
  if(myTransfer.available())
  {
    blinkLED(60);

    // получаем байты
    byte bytes_to_read = myTransfer.bytesRead;
    for(byte i = 0; i < bytes_to_read; i++)
      strncpy(str + i, myTransfer.rxBuff[i], 1);

  }
  else if(myTransfer.status < 0)
  {
    Serial.print("ERROR: ");

    if(myTransfer.status == -1)
      Serial.println(F("CRC_ERROR"));
    else if(myTransfer.status == -2)
      Serial.println(F("PAYLOAD_ERROR"));
    else if(myTransfer.status == -3)
      Serial.println(F("STOP_BYTE_ERROR"));
  }
}

Спасибо!

, 👍1

Обсуждение

начните отладку, заменив ReadSerial(); в цикле() на WriteSerial() для отправки заранее определенных данных .... включите 5-секундную паузу, @jsotola

Хорошее предложение - только что попробовал delay(1000); WriteSerial(); в цикле. Начинаю определять - я не дросселирую свой питон (поскольку в настоящее время я моделирую конвейер данных, работая намного быстрее, чем с реальными данными). Таким образом, мое тестовое сообщение отправляется в Mega примерно 20 раз в секунду. Возможно, это удерживает его от ответа или перезаписывает память, где он мог бы ответить? Когда я ограничиваю код Python до одного раза в 1 секунду, проблема исчезает. Дросселирование не было необходимо для Uno. Я поэкспериментирую, чтобы найти необходимый уровень дросселирования, но есть мысли о том, зачем нужен дроссель или чем он отличается от Uno?, @David W


2 ответа


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

1

Было бы проще, если бы вы использовали библиотеки, совместимые с Arduino <--> Python, чтобы обеспечить надежную и надежную связь между ними. Примером таких библиотек могут быть pySerialTransfer и SerialTransfer. .ч.

pySerialTransfer устанавливается с помощью pip и совместим с разными платформами. SerialTransfer.h работает на платформе Arduino и может быть установлен через диспетчер библиотек Arduino IDE.

Обе эти библиотеки имеют высокоэффективные и надежные алгоритмы пакетирования/анализа с простыми в использовании API.

Пример скрипта Python:

from time import sleep
from pySerialTransfer import pySerialTransfer as txfer

if __name__ == '__main__':
    try:
        link = txfer.SerialTransfer('COM17')

        link.open()
        sleep(2) # allow some time for the Arduino to completely reset

        while True:
            link.txBuff[0] = 'h'
            link.txBuff[1] = 'i'
            link.txBuff[2] = '\n'

            link.send(3)

            while not link.available():
                if link.status < 0:
                    print('ERROR: {}'.format(link.status))

            print('Response received:')

            response = ''
            for index in range(link.bytesRead):
                response += chr(link.rxBuff[index])

            print(response)

    except KeyboardInterrupt:
        link.close()

Пример скетча Arduino:

#include "SerialTransfer.h"

SerialTransfer myTransfer;

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

void loop()
{
  myTransfer.txBuff[0] = 'h';
  myTransfer.txBuff[1] = 'i';
  myTransfer.txBuff[2] = '\n';

  myTransfer.sendData(3);
  delay(100);

  if(myTransfer.available())
  {
    // делаем что-то с полученными пакетными данными
  }
  else if(myTransfer.status < 0)
  {
    Serial.print("ERROR: ");

    if(myTransfer.status == -1)
      Serial.println(F("CRC_ERROR"));
    else if(myTransfer.status == -2)
      Serial.println(F("PAYLOAD_ERROR"));
    else if(myTransfer.status == -3)
      Serial.println(F("STOP_BYTE_ERROR"));
  }
}

Обратите внимание, что с помощью этих библиотек вы можете отправлять не только отдельные символы. С помощью библиотек можно передавать числа с плавающей запятой, целые числа, байты, массивы и даже структуры (или любую их комбинацию) в вашей программе! См. примеры в SerialTransfer.h для получения дополнительной информации

Теорию надежной последовательной связи см. в учебных пособиях Основы последовательного ввода и Расширенный последовательный ввод.

ИЗМЕНИТЬ:


Применяя приведенный выше пример к вашему случаю, вы можете использовать следующий код:

Питон:

import time
import struct
from pySerialTransfer import pySerialTransfer as txfer


def stuff_float(txfer_obj, val, start_pos=0):
    '''
    Description:
    ------------
    Insert a 32-bit floating point value into the (pySerialtxfer) TX
    buffer starting at the specified index

    :param txfer_obj: txfer - Transfer class instance to communicate over serial
    :param val:       float - value to be inserted into TX buffer
    :param start_pos: int   - index of TX buffer where the first byte of
                              the float is to be stored in

    :return start_pos: int - index of the last byte of the float in the TX
                             buffer + 1
    '''

    val_bytes = struct.pack('f', val)

    txfer_obj.txBuff[start_pos] = val_bytes[0]
    start_pos += 1
    txfer_obj.txBuff[start_pos] = val_bytes[1]
    start_pos += 1
    txfer_obj.txBuff[start_pos] = val_bytes[2]
    start_pos += 1
    txfer_obj.txBuff[start_pos] = val_bytes[3]
    start_pos += 1

    return start_pos


if __name__ == '__main__':
  try:
    link = txfer.SerialTransfer('COM17')

    link.open()
    time.sleep(2) # allow some time for the Arduino to completely reset
    base = time.time()

    while True:
      time.sleep(0.2)

      sent = time.time() - base
      stuff_float(link, sent)
      link.send(4)

      while not link.available():
        if link.status < 0:
          print('ERROR: {}'.format(link.status))

      response = ''
      for index in range(link.bytesRead):
        response += chr(link.rxBuff[index])

      print('SENT: {}'.format(sent))
      print('RCVD: {}'.format(response))
      print(' ')

  except KeyboardInterrupt:
    link.close()

Ардуино:

#include "SerialTransfer.h"


SerialTransfer myTransfer;


char buff[25];
char str[] = "I got this: %s%d.%04d";
float fromPython = 0;


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

void loop()
{
  if(myTransfer.available())
  {
    ////////////////////////////////////////////////
    // обработка вызова из Python
    myTransfer.rxObj(fromPython, sizeof(fromPython));

    char sign = (fromPython < 0) ? '-' : ' ';
    uint16_t integer = fromPython;
    float tmpFrac = fromPython - integer;
    uint16_t decimal = trunc(tmpFrac * 10000);
    sprintf(buff, str, sign, integer, decimal);
    ////////////////////////////////////////////////

    ////////////////////////////////////////////////
    // отправить ответ
    myTransfer.txObj(buff, sizeof(buff));
    myTransfer.sendData(sizeof(buff));
    ////////////////////////////////////////////////
  }
}

Проверенный вывод Python:

SENT: 0.20032477378845215
RCVD: I got this: 0.2003

SENT: 0.4077117443084717
RCVD: I got this: 0.4077

SENT: 0.61326003074646
RCVD: I got this: 0.6132

Еще одно редактирование:


Вдохновленный ответом, присланным OP, я обновил библиотеку (1.2.0), включив в нее функции-члены tx_obj() и rx_obj(). Это позволит вам автоматически отправлять и получать практически любые стандартные типы объектов Python (включая списки и словари).

Пример кода Python:

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()

Пример кода Arduino:

#include "SerialTransfer.h"


SerialTransfer myTransfer;


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


void loop()
{
  if(myTransfer.available())
  {
    // отправляем все полученные данные обратно в Python
    for(uint16_t i=0; i < myTransfer.bytesRead; i++)
      myTransfer.txBuff[i] = myTransfer.rxBuff[i];

    myTransfer.sendData(myTransfer.bytesRead);
  }
}
,

Спасибо, Люк. Действительно, Serial Inputs Basics были основой, с которой я написал то, с чего начал — похоже, что для Advanced нужно читать намного больше! Я использовал pyserial, но попробовал ваше предложение, перейдя на pySerialTransfer, точно используя код в этих примерах (конечно, кроме номера порта). Примерно через 2 секунды мигает индикатор Mega RX, но в остальном я не вижу обратной связи на терминале Python; ни ошибка, ни полученный ответ. Я, вероятно, делаю что-то не так, но на данный момент, по крайней мере, pyserial дал некоторое указание, что отправка и получение перемежаются. Ткс!, @David W

В примере предполагалось, что пользователь использует второй отдельный последовательный порт на Arduino для связи с Python, а последовательный порт — для отладочной печати. Я обновил код примера, и теперь он должен работать безупречно!, @P_B

Спасибо - обновленный код на основе вашего примера, похоже, идет по правильному пути, но все еще есть некоторые проблемы. Обновленный вопрос с новым кодом., @David W

Я добавил некоторое мигание светодиода на плате, чтобы определить, что, несмотря на то, что терминал python указывает, что он отправил некоторый текст, оператор if никогда не оценивается как истинный. Код обновлен, чтобы указать, что я тестировал; Короче говоря, я никогда не дохожу до инструкции blink(60;., @David W

@DavidW См. мой отредактированный ответ для рабочего примера, который должен работать для вашего конкретного варианта использования. Я успешно проверил точный код с помощью Python 3.7 и одного из моих Megas., @P_B

Красиво спасибо! Работает отлично. Я добавил немного больше в последующем ответе, чтобы упаковать другие типы объектов. Оцените вашу помощь здесь., @David W

@DavidW - я еще раз отредактировал свой ответ, лол. Я добавил в библиотеку несколько вспомогательных функций, вдохновленных вашим ответом. Обязательно обновите локальную копию pySerialTransfer, прежде чем пытаться использовать новые функции., @P_B


0

Опираясь на ответ P_B для упаковки дополнительных типов данных, я расширил python следующим образом. Конечно, принимающий код Arduino также должен принимать данные правильного типа.

def StuffInt(txfer_obj, int_to_send, start_pos=0):
  """Insert integer into pySerialtxfer TX buffer starting at the specified index."""
  return StuffObject(txfer_obj, int_to_send, 'i', 4, start_pos=0)


def StuffFloat(txfer_obj, float_to_send, start_pos=0):
  """Insert integer into pySerialtxfer TX buffer starting at the specified index."""
  return StuffObject(txfer_obj, float_to_send, 'f', 4, start_pos=0)


def StuffStr(txfer_obj, string_to_send, max_length=None, start_pos=0):
  """Insert string into pySerialtxfer TX buffer starting at the specified index.

  Args:
    txfer_obj: see StuffObject
    string_to_send: see StuffObject
    max_length: if provided, the string is truncated to the requested size; otherwise
      defaults to the length of the string
    object_byte_size: integer number of bytes of the object to pack
    start_pos: see StuffObject
  Returns:
    start_pos for next object
  """
  if max_length is None:
    max_length = len(string_to_send)
  format_string = '%ds' % max_length
  truncated_string = string_to_send[:max_length]
  truncated_string_b = bytes(truncated_string, 'ascii')
  print (truncated_string_b)
  return StuffObject(txfer_obj, truncated_string_b, format_string, max_length, start_pos=0)


def StuffObject(txfer_obj, val, format_string, object_byte_size, start_pos=0):
  """Insert an object into pySerialtxfer TX buffer starting at the specified index.

  Args:
    txfer_obj: txfer - Transfer class instance to communicate over serial
    val: value to be inserted into TX buffer
    format_string: string used with struct.pack to pack the val
    object_byte_size: integer number of bytes of the object to pack
    start_pos: index of the last byte of the float in the TX buffer + 1

  Returns:
    start_pos for next object
  """
  val_bytes = struct.pack(format_string, val)
  for index in range(object_byte_size):
    txfer_obj.txBuff[index + start_pos] = val_bytes[index]
  return object_byte_size + start_pos
,