Como eu divido uma string de entrada?

51

Estou enviando uma lista de posições servo através da conexão serial ao arduino no seguinte formato

1:90&2:80&3:180

Qual seria analisado como:

servoId : Position & servoId : Position & servoId : Position

Como eu dividir esses valores e convertê-los em um número inteiro?

ValrikRobot
fonte
eu tenho escravo (uno arduino) seqüência de envio via serial 30; 12,4; 1 e 1 master (esp8266) Cadeia recive eu quero em master ter separado de dados como 30 12,4 1 e salve-o em cartão micro SD
majid Mahmoudi

Respostas:

72

Ao contrário de outras respostas, prefiro ficar longe Stringpelas seguintes razões:

  • uso dinâmico de memória (que pode levar rapidamente à fragmentação do heap e ao esgotamento da memória )
  • bastante lento devido a operadores de construção / destruição / atribuição

Em um ambiente incorporado como o Arduino (mesmo para um Mega com mais SRAM), prefiro usar as funções C padrão :

  • strchr(): procura um caractere em uma string C (ou seja char *)
  • strtok(): divide uma string C em substrings, com base em um caractere separador
  • atoi(): converte uma string C em um int

Isso levaria ao seguinte exemplo de código:

// Calculate based on max input size expected for one command
#define INPUT_SIZE 30
...

// Get next command from Serial (add 1 for final 0)
char input[INPUT_SIZE + 1];
byte size = Serial.readBytes(input, INPUT_SIZE);
// Add the final 0 to end the C string
input[size] = 0;

// Read each command pair 
char* command = strtok(input, "&");
while (command != 0)
{
    // Split the command in two values
    char* separator = strchr(command, ':');
    if (separator != 0)
    {
        // Actually split the string in 2: replace ':' with 0
        *separator = 0;
        int servoId = atoi(command);
        ++separator;
        int position = atoi(separator);

        // Do something with servoId and position
    }
    // Find the next command in input string
    command = strtok(0, "&");
}

A vantagem aqui é que não ocorre alocação dinâmica de memória; você pode até declarar inputcomo uma variável local dentro de uma função que lê os comandos e os executa; Depois que a função é retornada, o tamanho ocupado por input(na pilha) é recuperado.

jfpoilpret
fonte
Não tinha pensado no problema de memória. isso é ótimo.
ValrikRobot 31/03
4
Excelente. Minha resposta foi muito baseada no "arduino" e usando funções típicas do SDK do arduino com as quais um novo usuário poderia estar mais acostumado, mas essa resposta é o que deve ser feito para os sistemas de "produção". Em geral, tente escapar da alocação dinâmica de memória em sistemas incorporados.
drodri
22

Esta função pode ser usada para separar uma sequência em partes com base no que é o caractere de separação.

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

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

Converter String em int

int xvalue = stringToNumber(xval);
int yvalue = stringToNumber(yval);

Esse pedaço de código pega uma string e a separa com base em um determinado caractere e retorna O item entre o caractere de separação

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]) : "";
}
Odis Harkins
fonte
11
isso é uma resposta perfeita bonita! Muito obrigado !
Curnelious
11

Você pode fazer algo como o seguinte, mas leve em consideração várias coisas:

Se você usar readStringUntil(), ele esperará até receber o personagem ou os tempos limites. Assim, com sua string atual, a última posição durará um pouco mais, pois ela terá que esperar. Você pode adicionar um final &para evitar esse tempo limite. Você pode facilmente verificar esse comportamento no seu monitor, tentar enviar a string com e sem o extra &e verá esse atraso de tempo limite.

Você realmente não precisa o índice de servo, você pode simplesmente enviar sua seqüência de posições, e obter o índice servo pela posição de valor na cadeia, algo como: 90&80&180&. Se você usar o índice servo, talvez queira verificá-lo (converter para inte depois corresponder ao índice de loop i) para garantir que nada deu errado com sua mensagem.

Você deve verificar se a sequência de retorno de readStringUntilnão está vazia. Se a função atingir o tempo limite, você não recebeu dados suficientes e, portanto, qualquer tentativa de extrair seus intvalores produzirá resultados estranhos.

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

void loop() {
    for(int i=1; i<=3; i++) {
        String servo = Serial.readStringUntil(':');
        if(servo != ""){
            //here you could check the servo number
            String pos = Serial.readStringUntil('&');
            int int_pos=pos.toInt();
            Serial.println("Pos");
            Serial.println(int_pos);
        }
    }
}
drodri
fonte
Parece uma solução muito boa, obrigado. O exemplo limpa-lo perfeitamente
ValrikRobot
E se tivéssemos um número indefinido de entradas servo? no meu exemplo, havia 3. Mas e se às vezes fosse mais ou menos. Você pode oferecer alguma sugestão para lidar com esse cenário
ValrikRobot 31/03
11
Claro: existem duas possibilidades. 1. Envie primeiro o número de servos: 3: val1 e val2 e val3 e, leia esse número antes de iniciar o loop. 2. Use um terminador diferente para indicar que você não tem mais servos, faça um loop até encontrá-lo: val1 & val2 & val3 & #, por exemplo.
drodri
Ainda bem que esta solução ajudou você, @ValrikRobot, você poderia validar a resposta se ela foi útil?
drodri
11
ou você pode simplesmente remover o for, para que o código funcione sempre que você enviar um comando.
Lesto 31/03
4

A solução mais simples é usar 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);

Isso fornece a seguinte saída:

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

Felicidades!

Mikael Patel
fonte
Não está funcionando para serial.read () ... alguma idéia por que? Eu recebo o seguinte erro:invalid conversion from 'int' to 'char*' [-fpermissive]
Alvaro
4

Veja o exemplo em: https://github.com/BenTommyE/Arduino_getStringPartByNr

// splitting a string and return the part nr index split by separator
String getStringPartByNr(String data, char separator, int index) {
    int stringData = 0;        //variable to count data part nr 
    String dataPart = "";      //variable to hole the return text

    for(int i = 0; i<data.length()-1; i++) {    //Walk through the text one letter at a time
        if(data[i]==separator) {
            //Count the number of times separator character appears in the text
            stringData++;
        } else if(stringData==index) {
            //get the text when separator is the rignt one
            dataPart.concat(data[i]);
        } else if(stringData>index) {
            //return text and stop if the next separator appears - to save CPU-time
            return dataPart;
            break;
        }
    }
    //return text if this is the last part
    return dataPart;
}
Ben-Tommy Eriksen
fonte
3
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;
        }
    }   
}
Jam Ville
fonte
2

O jfpoilpret forneceu uma ótima resposta para analisar o comando serial no Arduino. No entanto, o Attiny85 não possui serial bidirecional - o SoftwareSerial deve ser usado. É assim que você porta o mesmo código para o Attiny85

#include <SoftwareSerial.h>

// Calculate based on max input size expected for one command
#define INPUT_SIZE 30

// Initialize SoftwareSerial
SoftwareSerial mySerial(3, 4); // RX=PB3, TX=PB4

// Parameter for receiving Serial command (add 1 for final 0)
char input[INPUT_SIZE + 1];

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

void loop() {
  // We need this counter to simulate Serial.readBytes which SoftwareSerial lacks
  int key = 0;

  // Start receiving command from Serial
  while (mySerial.available()) {
    delay(3);  // Delay to allow buffer to fill, code gets unstable on Attiny85 without this for some reason
    // Don't read more characters than defined
    if (key < INPUT_SIZE && mySerial.available()) {
      input[key] = mySerial.read();
      key += 1;
    }
  }

  if (key > 0) {
    // Add the final 0 to end the C string
    input[key] = 0;

    // Read each command pair
    char* command = strtok(input, "&");
    while (command != 0)
    {
      // Split the command in two values
      char* separator = strchr(command, ':');
      if (separator != 0)
      {
        // Actually split the string in 2: replace ':' with 0
        *separator = 0;
        int servoId = atoi(command);
        ++separator;
        int position = atoi(separator);
      }
      // Find the next command in input string
      command = strtok(0, "&");
    }
  }
}

Esquemas Attiny85 para números de pinos insira a descrição da imagem aqui

O esboço é compilado em:

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.

Portanto, há muito espaço e memória para o restante do código

goodevil
fonte
Como ler da série em um ATtiny85 não é realmente parte da questão.
gre_gor
Desculpe por divergir das perguntas, mas a comunidade e os recursos disponíveis para o Attiny são bem menores do que para o Arduino. Pessoas como eu, procurando respostas, usam Arduinopalavras-chave e às vezes entram em situações muito complicadas, pois a implementação do código do Arduino no Attiny nem sempre é trivial. Teve que converter o código original para o trabalho em ATTINY, testou-o a trabalhar e decidi compartilhá-lo
goodevil
Este site está no formato de perguntas e respostas. As respostas devem responder à pergunta. O seu apenas adiciona algo que não está relacionado a ele.
gre_gor
1
char str[] = "1:90&2:80&3:180";     // test sample serial input from servo
int servoId;
int position;

char* p = str;
while (sscanf(p, "%d:%d", &servoId, &position) == 2)
{
    // process servoId, position here
    //
    while (*p && *p++ != '&');   // to next id/pos pair
}
pakled
fonte
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=next value
printf ("%s\n",pch);
}
void loop(){}
Vitalicus
fonte
-1

Aqui está o método Arduino para dividir uma String como resposta à pergunta "Como dividir uma string em substring?" declarado como duplicado da presente questão.

O objetivo da solução é analisar uma série de posições de GPS registradas em um arquivo de cartão SD . Em vez de receber uma String de Serial, a String é lida do arquivo.

A função é StringSplit()analisar uma String sLine = "1.12345,4.56789,hello"para 3 Strings sParams[0]="1.12345", sParams[1]="4.56789"& sParams[2]="hello".

  1. String sInput: as linhas de entrada a serem analisadas,
  2. char cDelim: o caractere delimitador entre os parâmetros,
  3. String sParams[]: a matriz de saída dos parâmetros,
  4. int iMaxParams: o número máximo de parâmetros,
  5. Saída int: o número de parâmetros analisados,

A função é baseada em String::indexOf()e String::substring():

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

    do {
        // Searching the delimiter using indexOf()
        iPosDelim = sInput.indexOf(cDelim,iPosStart);
        if (iPosDelim > (iPosStart+1)) {
            // Adding a new parameter using substring() 
            sParams[iParamCount] = sInput.substring(iPosStart,iPosDelim-1);
            iParamCount++;
            // Checking the number of parameters
            if (iParamCount >= iMaxParams) {
                return (iParamCount);
            }
            iPosStart = iPosDelim + 1;
        }
    } while (iPosDelim >= 0);
    if (iParamCount < iMaxParams) {
        // Adding the last parameter as the end of the line
        sParams[iParamCount] = sInput.substring(iPosStart);
        iParamCount++;
    }

    return (iParamCount);
}

E o uso é realmente simples:

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

// reading the line from file
sLine = readLine();
// parse only if exists
if (sLine.length() > 0) {
    // parse the line
    iCount = StringSplit(sLine,',',sParams,3);
    // print the extracted paramters
    for(i=0;i<iCount;i++) {
        Serial.print(sParams[i]);
    }
    Serial.println("");
}
J. Piquard
fonte