Как разделить входящую строку?

Я отправляю список позиций сервоприводов через последовательное соединение на Arduino в следующем формате

1:90&2:80&3:180

Что будет проанализировано как:

servoId : Позиция & servoId : Позиция & servoId: позиция

Как мне разделить эти значения и преобразовать их в целое число?

, 👍71

Обсуждение

у меня раб (arduino uno) отправляет строку через последовательный порт 30; 12.4; 1 и 1 основная (esp8266) строка получения я хочу, чтобы в мастере были отдельные данные, например 30 12,4 1 и сохранить на микро сд карту, @majid mahmoudi


12 ответов


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

97

В отличие от других ответов, я бы предпочел держаться подальше от String по следующим причинам:

  • использование динамической памяти (что может быстро привести к фрагментации кучи и исчерпанию памяти)
  • довольно медленный из-за операторов построения/уничтожения/присваивания

Во встроенной среде, такой как Arduino (даже для Mega с большим объемом SRAM), я бы предпочел использовать стандартные функции C:

  • strchr(): поиск символа в строке C ( т.е. char *)
  • strtok(): разбивает строку C на подстроки на основе символ-разделитель
  • atoi(): преобразует строку C в интервал

Это приведет к следующему примеру кода:

// Расчет на основе максимального размера ввода, ожидаемого для одной команды
#define INPUT_SIZE 30
...

// Получить следующую команду из Serial (добавьте 1 для конечного 0)
char input[INPUT_SIZE + 1];
byte size = Serial.readBytes(input, INPUT_SIZE);
// Добавляем последний 0 в конце строки C
input[size] = 0;

// Чтение каждой пары команд
char* command = strtok(input, "&");
while (command != 0)
{
    // Разделить команду на два значения
    char* separator = strchr(command, ':');
    if (separator != 0)
    {
        // На самом деле разделить строку на 2: заменить ':' на 0
        *separator = 0;
        int servoId = atoi(command);
        ++separator;
        int position = atoi(separator);

        // Делаем что-то с servoId и позицией
    }
    // Находим следующую команду во входной строке
    command = strtok(0, "&");
}

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

,

Не думал о проблеме с памятью. отлично., @ValrikRobot

Отличный. Мой ответ был очень основан на «arduino» и использовал типичные функции arduino SDK, к которым новый пользователь мог бы быть более привычным, но этот ответ — это то, что следует сделать для «производственных» систем. В общем, постарайтесь избежать динамического выделения памяти во встраиваемых системах., @drodri


9

Вы можете использовать Stream.readStringUntil(terminator), передавая разные разделители для каждой части. .

В каждой части вы затем вызываете String.toInt

,

13

Вы можете сделать что-то вроде следующего, но примите во внимание несколько моментов:

Если вы используете readStringUntil(), он будет ждать, пока не получит символ или время ожидания. Таким образом, с вашей текущей строкой последняя позиция будет длиться немного дольше, так как она должна ждать. Вы можете добавить завершающий &, чтобы избежать этого тайм-аута. Вы можете легко проверить это поведение на своем мониторе, попробуйте отправить строку с дополнительным & и без него, и вы увидите такую задержку времени ожидания.

На самом деле вам не нужен индекс сервопривода, вы можете просто отправить свою строку позиций и получить индекс сервопривода по позиции значения в строке, например: 90&80&180& . Если вы используете сервоиндекс, возможно, вы захотите проверить его (преобразовать в int, а затем сопоставить индекс цикла i), чтобы убедиться, что с вашим сообщением все в порядке.

Вы должны убедиться, что строка, возвращаемая из readStringUntil, не пуста. Если время ожидания функции истекло, вы не получили достаточно данных, и поэтому любая попытка извлечь ваши значения int приведет к странным результатам.

void setup() {
    Serial.begin(9600);
}

void loop() {
    for(int i=1; i<=3; i++) {
        String servo = Serial.readStringUntil(':');
        if(servo != ""){
            //здесь вы можете проверить номер сервопривода
            String pos = Serial.readStringUntil('&');
            int int_pos=pos.toInt();
            Serial.println("Pos");
            Serial.println(int_pos);
        }
    }
}
,

Это кажется очень хорошим решением, спасибо. Пример прекрасно все объясняет, @ValrikRobot

Что, если бы у нас было неопределенное количество входов для сервоприводов? в моем примере их было 3. А что, если иногда их было больше или меньше. Можете ли вы предложить какое-либо предложение для обработки такого сценария, @ValrikRobot

Конечно: есть две возможности. 1. Отправьте сначала количество сервоприводов: 3:val1&val2&val3&, прочитайте этот номер перед запуском цикла. 2. Используйте другой терминатор, чтобы указать, что у вас больше нет сервоприводов, повторяйте, пока не найдете его: например, val1&val2&val3&#., @drodri

Рад, что это решение помогло вам, @ValrikRobot, не могли бы вы подтвердить ответ, если он был полезен?, @drodri

или вы можете просто удалить for, и тогда код будет работать каждый раз, когда вы отправляете команду., @Lesto


45

Эту функцию можно использовать для разделения строки на части в зависимости от символа-разделителя.

String xval = getValue(myString, ':', 0);
String yval = getValue(myString, ':', 1);

Serial.println("Y:" + yval);
Serial.print("X:" + xval);

Преобразовать строку в целое число

int xvalue = xvalue.toInt(xval);
int yvalue = yvalue.toInt(yval);

Этот фрагмент кода берет строку, разделяет ее на основе заданного символа и возвращает Элемент между разделительным символом

String getValue(String data, char separator, int index)
{
    int found = 0;
    int strIndex[] = { 0, -1 };
    int maxIndex = data.length() - 1;

    for (int i = 0; i <= maxIndex && found <= index; i++) {
        if (data.charAt(i) == separator || i == maxIndex) {
            found++;
            strIndex[0] = strIndex[1] + 1;
            strIndex[1] = (i == maxIndex) ? i+1 : i;
        }
    }
    return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
}
,

4
String getValue(String data, char separator, int index)
{
    int maxIndex = data.length() - 1;
    int j = 0;
    String chunkVal = "";

    for (int i = 0; i <= maxIndex && j <= index; i++)
    {
        chunkVal.concat(data[i]);

        if (data[i] == separator)
        {
            j++;

            if (j > index)
            {
                chunkVal.trim();
                return chunkVal;
            }

            chunkVal = "";
        }
        else if ((i == maxIndex) && (j < index)) {
            chunkVal = "";
            return chunkVal;
        }
    }   
}
,

Это было хорошо. Это было идеально!!!, @Arthur Alunts


4

См. пример: https://github.com/BenTommyE/Arduino_getStringPartByNr

// разбивает строку и возвращает индекс номера части, разделенный разделителем
String getStringPartByNr(String data, char separator, int index) {
    int stringData = 0;        //переменная для подсчета части данных nr
    String dataPart = "";      //переменная для удаления возвращаемого текста

    for(int i = 0; i<data.length()-1; i++) {    //Проходим по тексту по одной букве за раз
        if(data[i]==separator) {
            //Подсчитываем, сколько раз символ-разделитель появляется в тексте
            stringData++;
        } else if(stringData==index) {
            //получаем текст, если разделитель правильный
            dataPart.concat(data[i]);
        } else if(stringData>index) {
            // возвращаем текст и останавливаемся, если появляется следующий разделитель - для экономии процессорного времени
            return dataPart;
            break;
        }
    }
    // возвращаем текст, если это последняя часть
    return dataPart;
}
,

7

Самое простое решение — использовать sscanf().

  int id1, id2, id3;
  int pos1, pos2, pos3;
  char* buf = "1:90&2:80&3:180";
  int n = sscanf(buf, "%d:%d&%d:%d&%d:%d", &id1, &pos1, &id2, &pos2, &id3, &pos3);
  Serial.print(F("n="));
  Serial.println(n);
  Serial.print(F("id1="));
  Serial.print(id1);
  Serial.print(F(", pos1="));
  Serial.println(pos1);
  Serial.print(F("id2="));
  Serial.print(id2);
  Serial.print(F(", pos2="));
  Serial.println(pos2);
  Serial.print(F("id3="));
  Serial.print(id3);
  Serial.print(F(", pos3="));
  Serial.println(pos3);

Это даст следующий результат:

n=6
id1=1, pos1=90
id2=2, pos2=80
id3=3, pos3=180

Ура!

,

Он не работает для serial.read()... есть идеи, почему? Я получаю следующую ошибку: неверное преобразование из 'int' в 'char*' [-fpermissive], @Alvaro


1

Вот метод Arduino для разделения строки в качестве ответа на вопрос "Как разделить строку на подстроку?" объявлен как дубликат текущего вопроса.

Целью решения является анализ ряда положений GPS, зарегистрированных в файле SD-карты. Вместо получения строки из Serial строка считывается из файла.

Функция StringSplit() анализирует строку sLine = "1.12345,4.56789,hello" до 3 строк sParams[0]="1.12345", sParams[1]="4.56789" & sParams[2]="привет".

  1. String sInput: строки ввода для анализа,
  2. char cDelim: символ-разделитель между параметрами,
  3. String sParams[]: выходной массив параметров,
  4. int iMaxParams: максимальное количество параметров,
  5. Вывод int: количество проанализированных параметров,

Эта функция основана на String::indexOf() и String::substring() :

int StringSplit(String sInput, char cDelim, String sParams[], int iMaxParams)
{
    int iParamCount = 0;
    int iPosDelim, iPosStart = 0;

    do {
        // Поиск разделителя с помощью indexOf()
        iPosDelim = sInput.indexOf(cDelim,iPosStart);
        if (iPosDelim > (iPosStart+1)) {
            // Добавление нового параметра с помощью substring()
            sParams[iParamCount] = sInput.substring(iPosStart,iPosDelim-1);
            iParamCount++;
            // Проверка количества параметров
            if (iParamCount >= iMaxParams) {
                return (iParamCount);
            }
            iPosStart = iPosDelim + 1;
        }
    } while (iPosDelim >= 0);
    if (iParamCount < iMaxParams) {
        // Добавляем последний параметр в конец строки
        sParams[iParamCount] = sInput.substring(iPosStart);
        iParamCount++;
    }

    return (iParamCount);
}

И использование очень простое:

String sParams[3];
int iCount, i;
String sLine;

// чтение строки из файла
sLine = readLine();
// анализировать, только если существует
if (sLine.length() > 0) {
    // разбираем строку
    iCount = StringSplit(sLine,',',sParams,3);
    // печатаем извлеченные параметры
    for(i=0;i<iCount;i++) {
        Serial.print(sParams[i]);
    }
    Serial.println("");
}
,

1
char str[] = "1:90&2:80&3:180";     // тестовый образец последовательного ввода от сервопривода
int servoId;
int position;

char* p = str;
while (sscanf(p, "%d:%d", &servoId, &position) == 2)
{
    // обрабатываем servoId, позиция здесь
    //
    while (*p && *p++ != '&');   // к следующей паре id/pos
}
,

3

jfpoilpret предоставил отличный ответ для синтаксического анализа последовательной команды на Arduino. Однако у Attiny85 нет двунаправленного последовательного порта — необходимо использовать SoftwareSerial. Вот как вы портируете тот же код для Attiny85

#include <SoftwareSerial.h>

// Расчет на основе максимального размера ввода, ожидаемого для одной команды
#define INPUT_SIZE 30

// Инициализировать SoftwareSerial
SoftwareSerial mySerial(3, 4); // RX=PB3, TX=PB4

// Параметр для получения последовательной команды (добавьте 1 для конечного 0)
char input[INPUT_SIZE + 1];

void setup() {
  mySerial.begin(9600);
}

void loop() {
  // Нам нужен этот счетчик для имитации Serial.readBytes, которого нет в SoftwareSerial
  int key = 0;

  // начинаем получать команду от Serial
  while (mySerial.available()) {
    delay(3);  // Задержка для заполнения буфера, без этого код на Attiny85 почему-то становится нестабильным
    // Не читать больше символов, чем определено
    if (key < INPUT_SIZE && mySerial.available()) {
      input[key] = mySerial.read();
      key += 1;
    }
  }

  if (key > 0) {
    // Добавляем последний 0 в конце строки C
    input[key] = 0;

    // Чтение каждой пары команд
    char* command = strtok(input, "&");
    while (command != 0)
    {
      // Разделить команду на два значения
      char* separator = strchr(command, ':');
      if (separator != 0)
      {
        // На самом деле разбить строку на 2: заменить ':' на 0
        *separator = 0;
        int servoId = atoi(command);
        ++separator;
        int position = atoi(separator);
      }
      // Находим следующую команду во входной строке
      command = strtok(0, "&");
    }
  }
}

Схема Attiny85 для номеров контактов

Скетч компилируется в:

Sketch uses 2244 bytes (27%) of program storage space. Maximum is 8192 bytes.
Global variables use 161 bytes (31%) of dynamic memory, leaving 351 bytes for local variables. Maximum is 512 bytes.

Так что для остального кода достаточно места и памяти

,

Как читать из серийного номера на ATtiny85, на самом деле не является частью вопроса., @gre_gor

Извините за отклонение от вопроса, но сообщество и ресурсы, доступные для Attiny, намного меньше, чем для Arduino. Такие люди, как я, ищущие ответы, используют ключевое слово «Arduino» и иногда попадают в очень сложные ситуации, поскольку реализация кода Arduino на Attiny не всегда тривиальна. Пришлось преобразовать исходный код для работы на Attiny, проверил его работу и решил поделиться им., @goodevil

Этот сайт в формате вопросов и ответов. Ответы должны отвечать на вопрос. Ваш просто добавляет что-то, что не связано с ним., @gre_gor


0
void setup() {
Serial.begin(9600);
char str[] ="1:90&2:80";
char * pch;
pch = strtok(str,"&");
printf ("%s\n",pch);

pch = strtok(NULL,"&"); //pch=следующее значение
printf ("%s\n",pch);
}
void loop(){}
,

0

Это не ответ на ваш вопрос, но может быть кому-то полезен. Если ваша строка имеет определенный префикс, вы можете просто использовать startsWith и substring. Например

void loop ()
  if(Serial.available()){
    command = Serial.readStringUntil('\n');
    Serial.println("Received command: " + command);
    if(command.startsWith("height")) {
      Serial.println(command.substring(7));
    }
  }
}

А затем отправьте height 10, что позволит извлечь 10.

,