Непредсказуемое поведение при синтаксическом анализе ввода с разделителями "ключ-значение" из серийного номера
У меня проблемы с "простым" парсером команд; Я предполагаю, что делаю что-то не так с распределением памяти - ценю любые указатели :)
Для контекста: я использую 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'; } недействительный цикл () { ЧитатьСерийный(); }
Спасибо!
@David W, 👍0
Обсуждение2 ответа
Лучший ответ:
Добро пожаловать в клуб «Я забыл очистить свой буфер» (я потерял несколько дней, пытаясь найти похожую проблему)
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
Проблема заключалась в том, что в переменных, которые я хотел сохранить (например, текст в приведенном ниже коде), я присваивал значение указателя на проанализированную строку, которая повторно используется при следующем обнаружении команды, а не копирование строки.
Исправленный код ниже — см. комментарий к рис. 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 за помощь.
- Как разделить входящую строку?
- Как вывести несколько переменных в строке?
- форматирование строк в Arduino для вывода
- Очень простая операция Arduino Uno Serial.readString()
- Arduino Преобразование std:string в String
- Выделение строковой памяти Arduino
- Как прочитать входящие ШЕСТНАДЦАТИРИЧНОЕ значение из serial метод read ()?
- Как исправить код утечки памяти в ESP8266/NodeMCU, вызванный концентрацией строк?
Являются ли ваши «ключи» предопределенными значениями? Или это предназначено для того, чтобы в качестве «ключа» можно было вводить абсолютно все?, @Majenko
В настоящее время они определены таким образом, чтобы в качестве ключа можно было вводить что угодно., @David W
Это действительно то, чего вы хотите? Лично я обычно определяю структуру для получения данных и сопоставления строк с ключом, чтобы определить, какой записи в структуре присвоить значение. Вы также анализируете значение в соответствии с его типом, поэтому числа хранятся как целые числа и т. д., @Majenko
То, как вы используете malloc и изменение указателя динамически созданной переменной, вызовет утечку памяти. Что касается проблемы, вы забираете содержимое памяти, оставленное предыдущим буфером., @hcheung