Отправка и парсинг Serial

Общение по Serial


Данный урок переехал из урока о мониторе порта, рекомендуется сначала изучить его. Стандартные инструменты “библиотеки” Serial позволяют отправлять и принимать данные по интерфейсу UART. Здесь мы рассмотрим некоторые алгоритмы и протоколы связи, чтобы наладить управление программой через монитор порта в ручном режиме, или использовать для этого специальные программы, а также приложение на смартфоне и Bluetooth-UART модули. Между двумя Ардуинами, или между Ардуино и другим МК (esp8266, STM32) можно общаться по Serial. Для этого нужно соединить их следующим образом:

  • Соединить GND, ибо сигнал не ходит по одному проводу.
  • Для односторонней связи соединить дата-пины у отправителя -> приёмника как TX -> RX.
    • Сериал может быть как аппаратный (пины подписаны на плате), так и программный, например встроенная библиотека SoftwareSerial.h. У неё пины указываются вручную.
  • Для двухсторонней связи нужно соединить также RX -> TX.
    • Внимание! Если прошивка загружается через аппаратный юарт, например TX RX на Arduino Nano, то пин RX нужно освободить, иначе прошивка не загрузится.

Для передачи данных между платами можно использовать разобранные выше функции и разобранные ниже алгоритмы, но я хочу показать вам один особенно удобный способ передачи структур данных при помощи стандартных средств ядра Ардуино. Напомню, структура представляет собой набор данных из любых типов, что очень удобно для передачи разных данных. Отправка данных осуществляется при помощи Serial.write(байтовый буфер, размер), а приём – при помощи Serial.readBytes(байтовый буфер, размер). Минус readBytes заключается в том, что она блокирующая: выполнение кода не идёт дальше, пока функция не примет указанное количество байт или не завершит работу по таймауту, про таймаут написано выше в этом уроке. Обе функции принимают байтовый буфер, но мы с вами знаем про указатели и их типы, поэтому можем обманом (byte*) заманить структуру в отправку и чтение. Ниже показываю примеры как отправить и принять структуру с одной Ардуины на другую при помощи SoftwareSerial, таким же образом можно использовать обычный аппаратный Serial. Также поделюсь примером отправки и чтения с контролем целостности данных – CRC, который сильно повышает надёжность передачи: позволит распознать ошибку в пакете, если хоть один бит был передан или принят неправильно. Примеры будут работать на любых Ардуино-совместимых платах, в том числе на базе esp8266. С ней есть некоторые особенности, о них расскажу ниже.

Отправка
// Пример отправки и приёма структуры через Serial
// ОТПРАВИТЕЛЬ
// Ардуины соединены так:
// отправитель D11 -> приёмник D10
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX

struct Str {
  byte val_b;
  int val_i;
  long val_l;
  float val_f;
};

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

void loop() {
  // буфер на отправку
  Str buf;

  // заполняем
  buf.val_b = 123;
  buf.val_i = 12345;
  buf.val_l = 123456;
  buf.val_f = 123.456;

  // отправляем родным write()
  // указываем ему буфер-структуру, но приводим тип к byte*
  // размер можно указать через sizeof()
  mySerial.write((byte*)&buf, sizeof(buf));
  delay(2000);
}
Чтение
// Пример отправки и приёма структуры через Serial
// ПРИЁМНИК
// Ардуины соединены так:
// приёмник D10 -> отправитель D11
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX

// структура для приёма
// должна соответствовать отпраляемой
struct Str {
  byte val_b;
  int val_i;
  long val_l;
  float val_f;
};

// создаём саму структуру
Str buf;

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

void loop() {
  // читаем родным методом readBytes()
  // указываем ему буфер-структуру, но приводим тип к byte*
  // размер можно указать через sizeof()
  if (mySerial.readBytes((byte*)&buf, sizeof(buf))) {
    Serial.println(buf.val_b);
    Serial.println(buf.val_i);
    Serial.println(buf.val_l);
    Serial.println(buf.val_f);
  }
}
Отправка с CRC
// Пример отправки и приёма структуры через Serial
// с контролем целостности данных
// ОТПРАВИТЕЛЬ
// Ардуины соединены так:
// отправитель D11 -> приёмник D10
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX

struct Str {
  byte val_b;
  int val_i;
  long val_l;
  float val_f;
  byte crc;
};

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

void loop() {
  // буфер на отправку
  Str buf;

  // заполняем
  buf.val_b = 123;
  buf.val_i = 12345;
  buf.val_l = 123456;
  buf.val_f = 123.456;

  // последний байт - crc. Считаем crc всех байт кроме последнего, то есть кроме самого crc!!! (размер-1)
  buf.crc = crc8_bytes((byte*)&buf, sizeof(buf) - 1);

  // если отправить какой-то случайный crc, приёмник будет считать данные повреждёнными
  //buf.crc = 5;

  // отправляем родным write()
  // указываем ему буфер-структуру, но приводим тип к byte*
  // размер можно указать через sizeof()
  mySerial.write((byte*)&buf, sizeof(buf));
  delay(2000);
}

// функция для расчёта crc
byte crc8_bytes(byte *buffer, byte size) {
  byte crc = 0;
  for (byte i = 0; i < size; i++) {
    byte data = buffer[i];
    for (int j = 8; j > 0; j--) {
      crc = ((crc ^ data) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      data >>= 1;
    }
  }
  return crc;
}
Чтение с CRC
// Пример отправки и приёма структуры через Serial
// с контролем целостности данных
// ПРИЁМНИК
// Ардуины соединены так:
// приёмник D10 -> отправитель D11
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX

// структура для приёма
// должна соответствовать отправляемой
struct Str {
  byte val_b;
  int val_i;
  long val_l;
  float val_f;
  byte crc;
};

// создаём саму структуру
Str buf;

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

void loop() {
  // читаем родным методом readBytes()
  // указываем ему буфер-структуру, но приводим тип к byte*
  // размер можно указать через sizeof()
  if (mySerial.readBytes((byte*)&buf, sizeof(buf))) {
    // считаем crc пакета:
    // передаём буфер, преобразовав его к (byte*)
    // а также его ПОЛНЫЙ размер, включая байт crc
    byte crc = crc8_bytes((byte*)&buf, sizeof(buf));

    // если crc равен 0, данные верны (такой у него алгоритм расчёта)
    if (crc == 0) {
    Serial.println(buf.val_b);
    Serial.println(buf.val_i);
    Serial.println(buf.val_l);
    Serial.println(buf.val_f);
    } else {
      Serial.println("data is damaged");
    }
  }
}

// функция для расчёта crc
byte crc8_bytes(byte *buffer, byte size) {
  byte crc = 0;
  for (byte i = 0; i < size; i++) {
    byte data = buffer[i];
    for (int j = 8; j > 0; j--) {
      crc = ((crc ^ data) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      data >>= 1;
    }
  }
  return crc;
}
Особенности 32-битных МК (esp8266 и проч)

При использовании описанного выше способа для отправки данных на esp или обратно нужно помнить о некоторых особенностях памяти в esp:

  • Тип данных int занимает на esp 4 байта, то есть Ардуина должна принимать этот тип как long.
  • Компилятор “выравнивает” структуру по 4 байтам, поэтому однобайтные типы данных нужно размещать в конце структуры.  Либо использовать принудительное выравнивание по 1 байту (как на AVR) при помощи команды компилятору #pragma pack(push, 1)

Располагаем данные по убыванию размера

struct myStruct {
  float val_f;
  float val_f2;
  int val_i;
  long val_l;
  byte val_b;
};

Или используем принудительное выравнивание

#pragma pack(push, 1)
struct myStruct {
  float val_f;
  float val_f2;
  int val_i;
  long val_l;
  byte val_b;
};
#pragma pack(pop)

 

Другие алгоритмы парсинга


В реальном устройстве часто требуется передавать несколько параметров, например у нас Bluetooth танк. Мы ему должны отправить например скорость правой гусеницы, скорость левой гусеницы, положение башни, состояние подсветки, команду на выстрел… Да что угодно. Как быть в таком случае? Тут начинается настоящий парсинг, и появляются варианты, нам придётся придумывать собственный протокол связи. Есть два базовых варианта: отправка пакета всех-всех данных и его парсинг, или отправка отдельно каждого параметра с уникальным “ключом” у каждого. Как это понимать: суть первого варианта состоит в принятии пакета данных, которые разделены разделителем. Также правильно будет выделить начало и конец посылки. Пример: $120 80 180 1; – начальный символ $, разделитель ” ” (пробел) и завершающий символ ; . Наличие начального и завершающего символа повышает скорость работы и помехозащищённость связи. Второй вариант – посылки вида MOT1_120, содержащие ключ и значение, соответствующее этому ключу. Как реализовать данные способы парсинга я очень подробно разбирал в примерах в сборнике полезных алгоритмов Arduino, раздел “Работа с Serial”. Но давайте я оставлю их также и здесь, пользуйтесь!

Принимаем текст из Serial в строку
/*
  Данный код позволяет принять данные, идущие из порта, в строку (String) без "обрывов"
*/

String strData = "";
boolean recievedFlag;

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

void loop() {
  while (Serial.available() > 0) {         // ПОКА есть что то на вход    
    strData += (char)Serial.read();        // забиваем строку принятыми данными
    recievedFlag = true;                   // поднять флаг что получили данные
    delay(2);                              // ЗАДЕРЖКА. Без неё работает некорректно!
  }

  if (recievedFlag) {                      // если данные получены
    Serial.println(strData);               // вывести
    strData = "";                          // очистить
    recievedFlag = false;                  // опустить флаг
  }
}
Примем-ка два float числа
// приём двух float чисел через сериал
// десятичный разделитель - . (точка)
// разделитель - ; (семиколон)
// пример посылки: 5.326;-3.589

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(50);    // таймаут шоб не ждать (по умолч. секунда)
}

void loop() {
  if (Serial.available() > 0) {
    String bufString = Serial.readString();       // читаем как строку
    byte dividerIndex = bufString.indexOf(';');   // ищем индекс разделителя

    String buf_1 = bufString.substring(0, dividerIndex);    // создаём строку с первым числом
    String buf_2 = bufString.substring(dividerIndex + 1);   // создаём строку со вторым числом

    float val_1 = buf_1.toFloat();    // преобразуем во флоат
    float val_2 = buf_2.toFloat();    // ...

    Serial.println(val_1);  // проверка
    Serial.println(val_2);  // ...

    Serial.println(val_1, 5);  // вывод с 5 знаками после запятой
    Serial.println(val_2, 7);  // вывод с 7 знаками
  }
}
Парсинг Serial потоковый. Вариант 1 (strtok_r, без задержек)
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК.
   ВНИМАНИЕ! Парсинг работает через тяжёлую функцию strtok_r. Рекомендуется использовать
   другие алгоритмы из этого урока или сборника алгоритмов!
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Есть массив inputData, его размер задаётся дефайном INPUT_AMOUNT
   Вот это значение должно быть больше или равно числу СИМВОЛОВ, которое будет в пакете
   То есть в с чёт идут пробелы, цифры и заголовок и хвост
   Пример: пакет $110 25 600 920; содержит 16 символов, таким образом INPUT_AMOUNT должен быть НЕ МЕНЬШЕ!
   3) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
*/
#define PARSE_AMOUNT 5       // число значений в массиве, который хотим получить
#define INPUT_AMOUNT 80      // максимальное количество символов в пакете, который идёт в сериал
char inputData[INPUT_AMOUNT];  // массив входных значений (СИМВОЛЫ)
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга
boolean recievedFlag;
boolean getStarted;
byte index;
String string_convert;

void parsing() {
  while (Serial.available() > 0) {
    char incomingByte = Serial.read();      // обязательно ЧИТАЕМ входящий символ
    if (incomingByte == '$') {              // если это $
      getStarted = true;                    // поднимаем флаг, что можно парсить
    } else if (incomingByte != ';' && getStarted) { // пока это не ;
      // в общем происходит всякая магия, парсинг осуществляется функцией strtok_r
      inputData[index] = incomingByte;
      index++;
      inputData[index] = NULL;
    } else {
      if (getStarted) {
        char *p = inputData;
        char *str;
        index = 0;
        String value = "";
        while ((str = strtok_r(p, " ", & p)) != NULL) {
          string_convert = str;
          intData[index] = string_convert.toInt();
          index++;
        }
        index = 0;
      }
    }
    if (incomingByte == ';') {        // если таки приняли ; - конец парсинга
      getStarted = false;
      recievedFlag = true;
    }
  }
}

void setup() {
  Serial.begin(9600);
}
void loop() {
  parsing();       // функция парсинга
  if (recievedFlag) {                           // если получены данные
    recievedFlag = false;
    for (byte i = 0; i < PARSE_AMOUNT; i++) { // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    }
    Serial.println();
  }
}
Парсинг Serial потоковый. Вариант 2 (вручную, без задержек)
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
   Отличие от предыдущего примера: написан мной, не используя никаких хитрых функций. Предельно просто и понятно работает
*/
#define PARSE_AMOUNT 5         // число значений в массиве, который хотим получить
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга
boolean recievedFlag;
boolean getStarted;
byte index;
String string_convert = "";
void parsing() {
  if (Serial.available() > 0) {
    char incomingByte = Serial.read();        // обязательно ЧИТАЕМ входящий символ
    if (getStarted) {                         // если приняли начальный символ (парсинг разрешён)
      if (incomingByte != ' ' && incomingByte != ';') {   // если это не пробел И не конец
        string_convert += incomingByte;       // складываем в строку
      } else {                                // если это пробел или ; конец пакета
        intData[index] = string_convert.toInt();  // преобразуем строку в int и кладём в массив
        string_convert = "";                  // очищаем строку
        index++;                              // переходим к парсингу следующего элемента массива
      }
    }
    if (incomingByte == '$') {                // если это $
      getStarted = true;                      // поднимаем флаг, что можно парсить
      index = 0;                              // сбрасываем индекс
      string_convert = "";                    // очищаем строку
    }
    if (incomingByte == ';') {                // если таки приняли ; - конец парсинга
      getStarted = false;                     // сброс
      recievedFlag = true;                    // флаг на принятие
    }
  }
}
void setup() {
  Serial.begin(9600);
}
void loop() {
  parsing();       // функция парсинга
  if (recievedFlag) {                           // если получены данные
    recievedFlag = false;
    for (byte i = 0; i < PARSE_AMOUNT; i++) { // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    } Serial.println();
  }
}
Парсинг Serial потоковый. Вариант 2 (библиотека)
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК.
   Парсинг не блокирующий, не содержит while и delay, не мешает работе основного скетча
   НО ОН ЧИТАЕТ ВЕСЬ ТРАФИК ИЗ ПОРТА!
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
   Отличие от предыдущего примера: алгоритм запрятан в GParsingStream.h, входит в пак GyverHacks
   Доступны и подсвечиваются функции parsingStream и dataReady
   Скачать можно здесь: https://github.com/AlexGyver/GyverLibs
*/

#define PARSE_AMOUNT 5         // число значений в массиве, который хотим получить
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга

#include "GParsingStream.h"

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

void loop() {
  parsingStream((int*)&intData);               // функция парсинга, парсит в массив!

  if (dataReady()) {                           // если получены данные
    for (byte i = 0; i < PARSE_AMOUNT; i++) {  // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    }
    Serial.println();
  }
}
Парсинг Serial раздельный. Вариант 1 (с задержкой)
/*
  Парсинг разных пакетов по ключевым "префиксам", построено на строках.
  Префикс состоит из двух символов (в этом примере m1, b2...)
  Таким образом пакет имеет вид: m1950 - переменной motor1 будет присвоено 950
  b21 - переменной button2 будет присвоено число 1
*/

char thisChar;
byte availableBytes;
String strData = "";
boolean recievedFlag;

int motor1, motor2;
int button1, button2;

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

void loop() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    strData = "";                               // очистить строку
    while (Serial.available() > 0) {            // пока идут данные
      strData += (char)Serial.read();           // забиваем строку принятыми данными
      delay(2);                                 // обязательно задержка, иначе вылетим из цикла раньше времени
    }
    recievedFlag = true;                        // поднять флаг что получили данные
  }

  if (recievedFlag) {                           // если есть принятые данные
    int intVal = strData.substring(2).toInt();  // перевести в int всю строку кроме первых двух символов!
    String header = strData.substring(0, 2);    // создать мини строку, содержащую первые два символа

    if (strData.startsWith("m1"))               // если строка начинается с m1
      motor1 = intVal;
    if (strData.startsWith("m2"))               // если строка начинается с m2
      motor2 = intVal;
    if (strData.startsWith("b1"))               // если строка начинается с b1
      button1 = intVal;
    if (strData.startsWith("b2"))               // если строка начинается с b2
      button2 = intVal;

    recievedFlag = false;                       // данные приняты, мой капитан!

    // выводим в порт для отладки
    Serial.print(motor1); Serial.print(" ");
    Serial.print(motor2); Serial.print(" ");
    Serial.print(button1); Serial.print(" ");
    Serial.print(button2); Serial.println();
  }
}
Парсинг Serial раздельный. Вариант 2 (с задержкой)
/*
  Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
  Префикс состоит из одного буквенного символа (в этом примере a, b, c...)
  Таким образом пакет имеет вид: a950 - переменной motor1 будет присвоено 950
  d1 - переменной button2 будет присвоено число 1
*/

int motor1, motor2;
int button1, button2;

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

void loop() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    char header = Serial.read();                // мы ждём префикс
    if (isAlpha(header)) {                      // если префикс БУКВА
      int intValue = 0;                         // обнуляем
      delay(2);
      while (Serial.available() > 0) {          // пока идут данные
        char thisChar = Serial.read();          // читаем
        if (! isDigit(thisChar)) break;         // если приходит НЕ ЦИФРА, покидаем парсинг
        intValue = intValue * 10 + (thisChar - '0');  // с каждым принятым число растёт слева направо
        delay(2);                                 // обязательно задержка, иначе вылетим из цикла раньше времени
      }

      switch (header) {             // раскидываем по переменным
        case 'a':                   // если заголовок а
          motor1 = intValue;
          break;
        case 'b':                   // если заголовок b, и так далее
          motor2 = intValue;
          break;
        case 'c':
          button1 = intValue;
          break;
        case 'd':
          button2 = intValue;
          break;
      }

      // выводим в порт для отладки
      Serial.print(motor1); Serial.print(" ");
      Serial.print(motor2); Serial.print(" ");
      Serial.print(button1); Serial.print(" ");
      Serial.print(button2); Serial.println();
    }
  }
}
Парсинг Serial раздельный. Вариант 3 (однобуквенный, без задержки)
/*
  Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
  Режим работы полностью "прозрачный" (not blocking), также сделан таймаут
  Префикс состоит из одного буквенного символа (в этом примере a, b, c...)
  Таким образом пакет имеет вид: a950 - переменной motor1 будет присвоено 950
  d1 - переменной button2 будет присвоено число 1
*/
#define TIMEOUT 100     // таймаут в миллисекундах на отработку неправильно посланных данных

int motor1, motor2;
int button1, button2;

int intValue;
char header;
boolean recievedFlag, startParse;
unsigned long parseTime;

void parsing() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    char thisChar = Serial.read();              // принимаем байт
    if (startParse) {                           // если парсим
      if (! isDigit(thisChar)) {                // если приходит НЕ ЦИФРА
        switch (header) {                       // раскидываем по переменным
          case 'a':                             // если заголовок а
            motor1 = intValue;
            break;
          case 'b':                             // если заголовок b, и так далее
            motor2 = intValue;
            break;
          case 'c':
            button1 = intValue;
            break;
          case 'd':
            button2 = intValue;
            break;
        }
        recievedFlag = true;                  // данные приняты
        startParse = false;                   // парсинг завершён
      } else {                                // если принятый байт всё таки цифра
        intValue = intValue * 10 + (thisChar - '0');  // с каждым принятым число растёт слева направо
      }
    }
    if (isAlpha(thisChar) && !startParse) {     // если префикс БУКВА и парсинг не идёт
      header = thisChar;                        // запоминаем префикс
      intValue = 0;                             // обнуляем
      startParse = true;                        // флаг на парсинг
      parseTime = millis();                     // запоминаем таймер
    }
  }
  if (startParse && (millis() - parseTime > TIMEOUT)) {
    startParse = false;                   // парсинг завершён по причине таймаута
  }
}

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

  if (recievedFlag) {
    // выводим в порт для отладки
    Serial.print(motor1); Serial.print(" ");
    Serial.print(motor2); Serial.print(" ");
    Serial.print(button1); Serial.print(" ");
    Serial.print(button2); Serial.println();
    recievedFlag = false;
  }

}
Парсинг Serial раздельный. Вариант 4 (текстовый интерфейс, readString)
/*
	Пример текстового интерфейса через монитор порта. Парсинг осуществляется
	через условия и встроенные методы работы со строками.
	Принятие в порт блокирующее!!! readString()
	Пример отправки: keyword 5 - ключевое слово keyword, значение 5
	Между ключевым словом и значением стоит ПРОБЕЛ
	Символа окончания не нужно, используется readString() со своим таймаутом
*/

// строки текстовых команд
String help_s = "help";
String sayHello_s = "say hello";
String getValue_s = "get value";
String setValue_s = "set value";

// переменная для теста
int value = 0;

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(100);   // установка таймаута для readString (мс) (по умолчанию слишком длинный)
  help();                   // вывод текстового меню
}

void loop() {
  serialTick();     // обработка команд из порта
}

void help() {
  // макрос F сохраняет текст во флеш память
  Serial.println(F("***********************************************************************"));
  Serial.println(F("You can use serial commands:"));
  Serial.println(F("         - say hello"));
  Serial.println(F("         - get value of value"));
  Serial.println(F("   - set value to VALUE (example: set value 500)"));
  Serial.println(F("              - print this instruction again"));
  Serial.println(F("***********************************************************************"));
}

// опросчик и парсер сериал
void serialTick() {
  if (Serial.available() > 0) {           // проверка данных на вход
    String buf = Serial.readString();     // читаем как string
    // механизм такой: командой startsWith сравниваем начало строки
    // если совпадает - делаем какие то действия
    // для приёма значений используется такой механизм:
    // строка обрезается командой substring до длины команды .substring(команда.length())
    // оставшееся число преобразуется в число командой .toInt()
    
    if (buf.startsWith(help_s)) {
      help();
    }
    else if (buf.startsWith(sayHello_s)) {
      Serial.println("Hello!");
    }
    else if (buf.startsWith(getValue_s)) {
      Serial.print("Value is ");
      Serial.println(value);
    }
    else if (buf.startsWith(setValue_s)) {
      value = buf.substring(setValue_s.length()).toInt();
      Serial.print("Value set to ");
      Serial.println(value);
    }
  }
}
Парсинг Serial раздельный. Вариант 5 (удобный, readString)
/*
	Пример текстового интерфейса через монитор порта. Парсинг осуществляется
	через switch и встроенные методы работы со строками.
	Принятие в порт блокирующее!!! Читаем методом readString()
	Пример отправки: keyword 5 - ключевое слово keyword, значение 5
	Между ключевым словом и значением стоит ПРОБЕЛ
	Символа окончания не нужно, используется readString() со своим таймаутом
*/

// строки текстовых команд
const char *headers[]  = {
  "mtr1",   // 0
  "mtr2",   // 1
  "mtr3",   // 2
  "mtr4",   // 3
  "set1",   // 4
  "set2",   // 5
};

// соответствующие им названия для commandsTick
enum names {
  MOTOR1,   // 0
  MOTOR2,   // 1
  MOTOR3,   // 2
  MOTOR4,   // 3
  SETT1,    // 4
  SETT2,    // 5
};

int prsValue = 0;
boolean recievedFlag;
names thisName;
byte comm_amount = sizeof(headers) / 2;

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(100);   // установка таймаута для readString (мс) (по умолчанию слишком длинный)
}

void loop() {
  serialTick();     // обработка команд из порта
  commandsTick();   // отработка полученных команд
}

void commandsTick() {
  if (recievedFlag) {
    recievedFlag = false;
    switch (thisName) {
      case MOTOR1:
        Serial.print("motor1 ");
        Serial.println(prsValue);
        break;
      case MOTOR2:
        Serial.print("motor2 ");
        Serial.println(prsValue);
        break;
      case MOTOR3:
        Serial.print("motor3 ");
        Serial.println(prsValue);
        break;
      case MOTOR4:
        Serial.print("motor4 ");
        Serial.println(prsValue);
        break;
      case SETT1:
        Serial.print("sett1 ");
        Serial.println(prsValue);
        break;
      case SETT2:
        Serial.print("sett2 ");
        Serial.println(prsValue);
        break;
    }
  }
}

// опросчик и парсер сериал
void serialTick() {
  if (Serial.available() > 0) {                 // проверка данных на вход
    String buf = Serial.readString();           // читаем как string
    for (byte i = 0; i < comm_amount; i++) {    // пробегаемся по всем именам
      if (buf.startsWith(headers[i])) {         // если совпадаем по названию
        String thisHeader = headers[i];         // костыль
        prsValue = buf.substring(thisHeader.length()).toInt();  // перевод в int
        recievedFlag = true;                    // флаг
        thisName = i;                           // запоминаем номер команды
      }
    }
  }
}
Парсинг Serial раздельный. Вариант 6 (удобный, оптимальный)
/*
	Пример текстового интерфейса через монитор порта. Парсинг осуществляется
	через switch и встроенные методы работы со строками.
	Принятие в порт НЕ блокируещее!!! Читаем посимвольно
	Пример отправки: keyword 5; - ключевое слово keyword, значение 5
	Между ключевым словом и значением стоит divider (можно настроить)
	Посылка завершается символом ending (можно настроить)
	Парсинг работает со включенным и выключенным концом строки, т.е. универсально
*/

char divider = ' ';
char ending = ';';

const char *headers[]  = {
  "asd1",   // 0
  "asd2",   // 1
  "asd3",   // 2
  "asd4",   // 3
  "qw1",    // 4
  "qw2",    // 5
};
enum names {
  MOTOR1,   // 0
  MOTOR2,   // 1
  MOTOR3,   // 2
  MOTOR4,   // 3
  SETT1,    // 4
  SETT2,    // 5
};

names thisName;
byte headers_am = sizeof(headers) / 2;
uint32_t prsTimer;
String prsValue = "";
String prsHeader = "";
enum stages {WAIT, HEADER, GOT_HEADER, VALUE, SUCCESS};
stages parseStage = WAIT;
boolean recievedFlag;

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

void loop() {
  parsingSeparate();
  if (recievedFlag) {
    recievedFlag = false;
    switch (thisName) {
      case MOTOR1: Serial.print("motor1 "); Serial.println(prsValue);
        break;
      case MOTOR2: Serial.print("motor2 "); Serial.println(prsValue);
        break;
      case MOTOR3: Serial.print("motor3 "); Serial.println(prsValue);
        break;
      case MOTOR4: Serial.print("motor4 "); Serial.println(prsValue);
        break;
      case SETT1: Serial.print("sett1 "); Serial.println(prsValue);
        break;
      case SETT2: Serial.print("sett2 "); Serial.println(prsValue);
        break;
    }
  }
}

void parsingSeparate() {
  if (Serial.available() > 0) {
    if (parseStage == WAIT) {
      parseStage = HEADER;
      prsHeader = "";
      prsValue = "";
    }
    if (parseStage == GOT_HEADER)
      parseStage = VALUE;

    char incoming = (char)Serial.read();
    if (incoming == divider) {
      parseStage = GOT_HEADER;
    }
    else if (incoming == ending) {
      parseStage = SUCCESS;
    }
    if (parseStage == HEADER)
      prsHeader += incoming;
    else if (parseStage == VALUE)
      prsValue += incoming;
    prsTimer = millis();
  }
  if (parseStage == SUCCESS) {
    for (byte i = 0; i < headers_am; i++) { if (prsHeader == headers[i]) { thisName = i; } } recievedFlag = true; parseStage = WAIT; } if ((millis() - prsTimer > 10) && (parseStage != WAIT)) {		// таймаут
    parseStage = WAIT;
  }
}
Делаем свой оптимальный протокол (ЛУЧШЕЕ)
// Делаем свой протокол для общения с устройствами по UART
// Протокол: $ дата_1 дата_2 дата_3 ;
// Пробелов между байтами нет! Просто передаём потоком
// Стартовый байт $ (значение 36)
// Конечный байт ; (значение 59)
// Первый байт даты - номер команды
// Остальные байты - данные
// Принимаем "вручную"

// ВНИМАНИЕ! Если отправляет print (отправка символа) - ASCII_CONVERT должен быть '0'
// (переводит символы даты в цифры)
// Если отправляет write (отправка чистого байта) - ASCII_CONVERT должен быть просто 0
#define ASCII_CONVERT '0'

byte buffer[5];
void setup() {
  Serial.begin(9600);
}

void loop() {
  if (parsing()) {
    switch (buffer[0]) {  // согласно коду команды
      case 0:   // тут можно читать данные из buffer согласно коду команды
        break;
      case 1:   // тут можно читать данные из buffer согласно коду команды
        break;
      case 2:   // тут можно читать данные из buffer согласно коду команды
        break;
      case 3:   // тут можно читать данные из buffer согласно коду команды
        break;
    }
  }
}

// парсер. Возвращает количество принятых байтов даты
int parsing() {
  static bool parseStart = false;
  static byte counter = 0;

  if (Serial.available()) {
    char in = Serial.read();    
    if (in == '\n' || in == '\r') return 0; // игнорируем перевод строки    
    if (in == ';') {        // завершение пакета
      parseStart = false;
      return counter;
    }    
    if (in == '$') {        // начало пакета
      parseStart = true;
      counter = 0;
      return 0;
    }    
    if (parseStart) {       // чтение пакета
      // - '0' это перевод в число (если отправитель print)
      buffer[counter] = in - ASCII_CONVERT;
      counter++;
    }
  }
  return 0;
}

Библиотеки


Для удобного получения данных по Serial и другим Stream-совместимым каналам связи можно использовать библиотеку AsyncStream. Она просто асинхронно и не блокируя программу собирает входящие данные в свой char буфер и сигналит о том, когда достигнут таймаут или символ окончания (EOL). Базовый пример выглядит так:

#include <AsyncStream.h>
AsyncStream<100> serial(&Serial, '\n'); // указали Stream-объект и символ конца

void setup() {
  Serial.begin(9600);
  // serial.setTimeout(100);    // установить другой таймаут
  // serial.setEOL(';');        // установить другой терминатор (EOL)
}

// строка для теста (отправь в сериал)
// 1234,3.14,hello,4567,lolkek,qwerty
void loop() {
  if (serial.available()) {     // если данные получены
    Serial.println(serial.buf); // выводим их (как char*)    
  }
}

Для того, чтобы разобрать буфер, можно использовать лёгкую библиотеку GParser. Библиотека работает с массивом  char, может разбить его на подстроки или перевести в числа. Основная фишка этих двух библиотек – не используется String, что сильно экономит память и процессорное время. Также GParser не дублирует данные в памяти. Пример парсинга при помощи GParser:

// тест парсера строк
#include <GParser.h>

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

  // строка для примера
  // данные разделены разделителем, например запятой
  // могут быть получены из Serial/UDP/TCP/MQTT итд
  char str[] = "1234,3.14,hello,4567,lolkek,qwerty";

  // кормим строку парсеру, указываем разделитель (умолч. запятая)
  GParser data(str, ',');

  // ВНИМАНИЕ! Операция "ломает" исходную строку, заменяя разделители на NULL
  int am = data.split();    // разделяем, получаем количество данных

  Serial.println(am); // выводим количество

  // можем обратиться к полученным строкам как data[i] или data.str[i]
  for (byte i = 0; i < am; i++) Serial.println(data[i]);

  // также можно получить их в виде int и float чисел
  // передав индекс строки
  Serial.println(data.getInt(0));
  Serial.println(data.getFloat(1));

  // можно сравнить со строкой (номер парс строки, строка для сравнения)
  if (data.equals(2, "hello")) Serial.println("true");
  else Serial.println("false");
}

void loop() {
}

Подробнее – на видео ниже и в описании библиотек на GitHub.

Парсим String


Также поговорим о String, эти строки часто применяются в библиотеках для esp8266 и логичнее использовать их инструменты. Рассмотрим такую задачу – иногда нужно разделить данные, переданные одной строкой и разделённые запятой, как в примерах выше. Можно использовать простенький инструмент для парсинга:

struct StringParser {
    void reset() {
        from = to = -1;    
    }
    bool update(String& s, char div = ',') {
        if (to == s.length()) return 0;
        from = to + 1;
        to = s.indexOf(div, from);
        if (to < 0) to = s.length();
        str = s.substring(from, to);
        return 1;
    }
    String str;
    int from = -1;
    int to = -1;
};

Работает следующим образом – передаём ему строку, и функция update() будет возвращать true до тех пор, пока не будет разобрана вся строка. Каждый вызов update() будет записывать во внутреннюю переменную str новый кусок строки. Таким образом можно взаимодействовать с каждым кусочком например в цикле:

String s = "123,hello,3.14,keks";   // строка для теста. Разделитель - запятая
StringParser pars;                  // можно локально создать парсер

while (pars.update(s)) {            // цикл обработки выглядит так
  Serial.println(pars.str);         // забираем кусок строки
}

// можно распарсить тем же экземпляром ещё одну строку
// для примера с другим разделителем - ;
String s2 = "test;str2;567";

// вызываем reset() перед новым циклом парсинга!
pars.reset();

// передаём строку и разделитель
while (pars.update(s2, ';')) Serial.println(pars.str);

Видео


Полезные страницы


4.9/5 - (9 голосов)
Подписаться
Уведомить о
guest

17 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии