Непредсказуемое поведение при синтаксическом анализе ввода с разделителями "ключ-значение" из серийного номера

У меня проблемы с "простым" парсером команд; Я предполагаю, что делаю что-то не так с распределением памяти - ценю любые указатели :)

Для контекста: я использую Arduino Uno (если это имеет значение). Ожидаемые команды имеют вид <key1=value1;key2=value2;key3=value3>. Для простоты здесь (а также для моих собственных неудачных попыток отладки) я удалил все постороннее, так что этот код просто выводит обратно на серийный монитор независимо от текущего значения, связанного с ключом «текст», каждый раз, когда команда заключен в < > встречается.

Кажется, это работает правильно до тех пор, пока ключ не будет найден в первый раз; то ведет себя несколько непредсказуемо - пока не удалось разглядеть закономерность. Но, например, если я отправлю:

<a=5>
-> EMPTY
<b=7;text=here;d=9>
-> here
<d=abc>
-> here
<d=ghi;e=afds>
-> afds

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

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

const int MAX_KEY_VALUE_PAIRS = 3; // максимальное количество пар ключ-значение в сообщении
const int MAX_ELEMENT_CHARS = 25; // максимальное количество символов (+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; // максимальный размер сообщения

получено_символов[MAX_MESSAGE_CHARS]; // <- РЕДАКТИРОВАТЬ №1
char полученные_символы[30] ={'\0'};
логические новые_данные = ложь;

символ *текст; // значение, определяемое ключевым словом "текст", т. е. в <text=abc;time=814a> текстом будет abc.

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

pch = strtok(str, "=");

в то время как (пч != NULL)
{
strcpy(config[число_пар][0], pch);
pch = strtok(NULL, ";");
если (pch == NULL) перерыв;

strcpy(config[число_пар][1], pch);
пар_счетчик++;
pch = strtok(NULL, "=");
если (pch == NULL) перерыв;
}

for(int i=0;i<pairs_count;i++) {
if (strcmp(config[i][0], "text")==0) text = config[i][1];
}
Serial.println(текст);
}

недействительным ReadSerial () {
// После вызова ReceiveText для очистки буфера, если была найдена полная команда,
// разбираем эту команду.
новые_данные = ПолучитьТекст();
если (новые_данные == истина) {
char temp_chars[MAX_MESSAGE_CHARS]; // временный массив для использования при разборе
st rcpy (temp_chars, полученные_символы);
полученные_символы[0] = '\0'; // <- РЕДАКТИРОВАТЬ №2
ParseData(temp_chars);
}
}

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

логическое новое_данные = ложь;
в то время как (Serial.available() > 0 && new_data == false) {
rc = Serial.read();

если (recv_in_progress == истина) {
если (rc != END_MARKER) {
полученные_символы[ndx] = rc;
ндх++;
если (ndx >= MAX_MESSAGE_CHARS) {
ndx = MAX_MESSAGE_CHARS — 1;
}
} еще {
полученные_символы[ndx] = '\0'; // завершаем строку
recv_in_progress = ложь;
ндх = 0;
новые_данные = истина;
}
} иначе если (rc == START_MARKER) {
recv_in_progress = истина;
}
}
вернуть новые_данные;
}

недействительная установка () {
Серийный.начать(9600);
текст = (char*)malloc(25);
strcpy(текст, "ПУСТОЙ");
полученные_символы[0] = '\0';
}

недействительный цикл () {
ЧитатьСерийный();
}

Спасибо!

, 👍0

Обсуждение

Являются ли ваши «ключи» предопределенными значениями? Или это предназначено для того, чтобы в качестве «ключа» можно было вводить абсолютно все?, @Majenko

В настоящее время они определены таким образом, чтобы в качестве ключа можно было вводить что угодно., @David W

Это действительно то, чего вы хотите? Лично я обычно определяю структуру для получения данных и сопоставления строк с ключом, чтобы определить, какой записи в структуре присвоить значение. Вы также анализируете значение в соответствии с его типом, поэтому числа хранятся как целые числа и т. д., @Majenko

То, как вы используете malloc и изменение указателя динамически созданной переменной, вызовет утечку памяти. Что касается проблемы, вы забираете содержимое памяти, оставленное предыдущим буфером., @hcheung


2 ответа


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

0

Добро пожаловать в клуб «Я забыл очистить свой буфер» (я потерял несколько дней, пытаясь найти похожую проблему)

 strcpy(temp_chars, received_chars)  // strcpy очищает temp_chars и добавляет полученные_chars

но у вас нет (для меня) видимой процедуры для очистки Received_chars
Сделайте это, установив

received_chars[0] ='\0'; // устанавливаем указатель на первый индекс

или

 received_chars ="0";

или

strcpy(received_chars,"0");

после того, как вы использовали содержимое буфера. С помощью

static byte ndx = 0;

вы просто устанавливаете указатель, а затем начинаете писать, в качестве защиты вы должны сначала сбросить конечный терминатор, а затем начать запись. Вероятно, это причина того, что вы видите символы, даже если ни одна клавиша не нажата из-за все еще «полного» буфера символов.
Чтобы предотвратить повреждение кучи, определите свои полученные_символы как фиксированный массив перед настройкой:

char received_chars[128] ={'\0'}; // 127 символов + терминатор

если вы используете malloc, вы должны использовать free, но это с большим количеством динамических данных приводит к разрыву кучи -> сбой в конце

ОБНОВЛЕНО
Теперь программа делает следующее

  • игнорирует все символы, не заключенные в <>
  • проверить текст и отобразить все после = и до;
  • игнорирует все после ; если это не text= еще раз
  • если его text= снова игнорирует первый и отображает только второй параметр

Здесь вам не нужен strtok, так как он удаляет символы. Я использую модифицированный маршрут подстроки для массивов символов. Вот код, работающий, как описано выше:

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

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

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

//char Received_chars[MAX_MESSAGE_CHARS] = {'\0'};// <- EDIT#1
char received_chars[256] = {'\0'};
bool new_data = false;
char text[25] =  {'\0'};      // значение, определяемое ключевым словом "текст", т. е. в <text=abc;time=814a> текстом будет abc.

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 (uint8_t i = 0; i < pairs_count; i++) {
    if (strcmp(config[i][0], "text") == 0) strcpy (text,config[i][1]);
  }
  Serial.println(text);
}

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';  // <- РЕДАКТИРОВАТЬ №2
    ParseData(temp_chars);
  }
}

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

  bool 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(115200);
   strcpy (text, "EMPTY"); 
 // текст = (char*)malloc(25);
// полученные_символы[0] = '\0';
}

void loop() {
  ReadSerial();
}

Если бы вы могли описать, чего вы действительно хотите в конечном итоге, я уверен, что мы сможем помочь с некоторыми основными процедурами.

,

Спасибо за идею - я внес эти изменения как для инициализации в настройках, так и сразу после strcopy, с предложением received_chars[0] = '\0';. Увы, я все еще получаю такое же плохое поведение. Но, возможно, я неправильно понял ваше предложение., @David W

Пожалуйста, отредактируйте свой вопрос с обновленным кодом, @Codebreaker007

Готово - да, это, вероятно, поможет вам увидеть, что именно я сделал (неправильно): P, @David W

Спасибо - следуя вашему коду, я выделил свои ошибочные пути. В частности, когда я нашел ключевое слово «текст», я использовал text = config[i][1];, тогда как мне следовало использовать strcopy(text, config[i][1];. помощь!, @David W

Пожалуйста, отметьте как решенное / принятое - спасибо, @Codebreaker007


0

Проблема заключалась в том, что в переменных, которые я хотел сохранить (например, текст в приведенном ниже коде), я присваивал значение указателя на проанализированную строку, которая повторно используется при следующем обнаружении команды, а не копирование строки.

Исправленный код ниже — см. комментарий к рис. 1. Существует также второе исправление, хотя оно не вызывало проблемного поведения.

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

const int MAX_KEY_VALUE_PAIRS = 3;  // максимальное количество пар ключ-значение в сообщении
const int MAX_ELEMENT_CHARS = 25;  // максимальное количество символов (+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 *text;  // значение, определяемое ключевым словом "текст", т. е. в <text=abc;time=814a> текстом будет abc.

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]);  // Исправление №1
  }
  Serial.println(text);
}

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';  // Исправление № 2 — не вызывало этой проблемы, но все равно было неправильным
    ParseData(temp_chars);
  }
}

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);
  strcpy(text, "EMPTY");
  received_chars[0] = '\0';
}

void loop() {
  ReadSerial();
}

Спасибо Codebreaker007 за помощь.

,