Монитор порта


Как мы с вами знаем из урока “О платформе“, на платах Ардуино стоит USB-TTL конвертер, позволяющий микроконтроллеру в текстовом режиме “консоли” общаться с компьютером по последовательному интерфейсу, Serial. На компьютере создаётся виртуальный COM порт, к которому можно подключиться при помощи программ-терминалов порта, и принимать-отправлять текстовые данные. Через этот же порт загружается прошивка, т.к. поддержка Serial является встроенной в микроконтроллер на “железном” уровне, и USB-TTL преобразователь подключен именно к этим выводам микроконтроллера. На плате Arduino Nano это кстати пины D0 и D1.

Именно поэтому пины D0 и D1 на платах Nano/Uno нельзя занимать датчиками или подтягивать к питанию/земле на момент прошивки: микроконтроллер не сможет получить данные и вы получите ошибку загрузки

К этим же пинам можно подключаться при помощи отдельных плат “программаторов”, например на чипах CP2102 или том же CH340 с целью загрузки прошивки или просто общения с платой.

В самой Arduino IDE тоже есть встроенная “консоль” – монитор порта, кнопка с иконкой лупы в правом верхнем углу программы. Нажав на эту кнопку мы откроем сам монитор порта, в котором будут настройки:

Окно монитора порта

Если с отправкой, автопрокруткой, отметками времени и кнопкой “очистить вывод” всё понятно, то конец строки и скорость мы рассмотрим подробнее:

  • Конец строки: тут есть несколько вариантов на выбор, чуть позже вы поймёте, на что они влияют. Лучше поставить нет конца строки, так как это позволит избежать непонятных ошибок на первых этапах знакомства с Ардуино.
    • Нет конца строки – никаких дополнительных символов в конце введённых символов после нажатия на кнопку отправка/Enter
    • NL – символ переноса строки в конце отправленных данных
    • CR – символ возврата каретки в конце отправленных данных
    • NL+CR – и то и то
  • Скорость – тут на выбор нам даётся целый список скоростей, т.к. общение по Serial может осуществляться на разных скоростях, измеряемых в бод (baud), и если скорости приёма и отправки не совпадают – данные будут получены некорректно. По умолчанию скорость стоит 9600, её и оставим.
  • Очистить вывод – тут всё понятно, очищает вывод

Объект Serial


Начнём знакомство с одной из самых полезных штук Arduino-разработчика – Serial. Serial это объект класса Stream, позволяющий как просто принимать/отправлять данные через последовательный порт, так и наследует из класса Stream кучу интересных возможностей и фишек, давайте сразу их все рассмотрим, а потом перейдём к конкретным примерам.

Serial.begin(speed)

Запустить связь по Serial на скорости speed (baud rate, бит в секунду). Скорость можно поставить любую, но есть несколько “стандартных”. Список скоростей для монитора порта Arduino IDE:

  • 300
  • 1200
  • 2400
  • 4800
  • 9600 чаще всего используется, можно назвать стандартной для большинства устройств с связью через TTL
  • 19200
  • 38400
  • 57600
  • 115200 тоже часто встречается
  • 230400
  • 250000
  • 500000
  • 1000000
  • 2000000
Serial.end()
Прекратить связь по Serial. Для УНО/НАНО (ATmega328) это позволяет освободить пины 0 и 1 для обычных целей (чтение/запись).
Serial.available()
Возвращает количество байт, хранящихся в буфере (объём буфера 64 байта) и доступных для чтения.
Serial.availableForWrite()
Возвращает количество байт, которые можно записать в буфер последовательного порта, не блокируя при этом функцию записи.
Serial.write(val), Serial.write(buf, len)
Отправляет в порт val численное значение или строку, или отправляет количество len байт из буфера buf. Важно! Отправляет данные как байт (см. таблицу ASCII), то есть отправив 88 вы получите букву X: Serial.write(88); выведет букву X.
Serial.print(val), Serial.print(val, format)

Отправляет в порт значение val – число или строку. В отличие от write выводит именно символы, т.е. отправив 88 вы получите 88: Serial.print(88); выведет 88. Также метод print/println имеет несколько настроек для разных данных, что делает его очень удобным инструментом отладки:

Serial.print(78);        // выведет 78
Serial.print(1.23456);   // 1.23 (по умолч. 2 знака)
Serial.print('N');       // выведет N
Serial.print("Hello world."); // Hello world.

// можно сделать форматированный вывод в стиле
Serial.print("i have " + String(50) + " apples");
// выведет строку i have 50 apples

// вместо чисел можно пихать переменные
byte appls = 50;
Serial.print("i have " + String(appls) + " apples");
// выведет то же самое

format позволяет настраивать вывод данных: BIN, OCT, DEC, HEX выведут число в соответствующей системе исчисления, а цифра после вывода float позволяет настраивать выводимое количество знаков после точки

Serial.print(78, BIN);    // вывод "1001110"
Serial.print(78, OCT);    // вывод "116"
Serial.print(78, DEC);    // вывод "78"
Serial.print(78, HEX);    // вывод "4E"
Serial.print(1.23456, 0); // вывод "1"
Serial.print(1.23456, 2); // вывод "1.23"
Serial.print(1.23456, 4); // вывод "1.2345"
Serial.println(), Serial.println(val), Serial.println(val, format)

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

Serial.flush()
Ожидает окончания передачи данных
Serial.peek()
Возвращает текущий байт с края буфера, не убирая его из буфера. При вызове Serial.read() будет считан тот же байт, но из буфера уже уберётся
Serial.read()
Читает и возвращает байт как код символа из таблицы ASCII. Если нужно вернуть цифру, делаем Serial.read() – ‘0’;
Serial.setTimeout(time)
Устанавливает time (миллисекунды) таймаут ожидания приёма данных для следующих ниже функций. По умолчанию равен 1000 мс (1 секунда)
Serial.find(target), Serial.find(target, length)

Читает данные из буфера и ищет набор символов target (тип char), опционально можно указать длину length. Возвращает true, если находит указанные символы. Ожидает передачу по таймауту

// будем искать слово hello
char target[] = "hello";

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

void loop() {
  if (Serial.available() > 0) {
    if (Serial.find(target))
      Serial.println("found");
    // вывести found, если было послано
  }
}
Serial.findUntil(target, terminal)
Читает данные из буфера и ищет набор символов target (тип char) либо терминальную строку terminal. Ожидает окончания передачи по таймауту, либо завершает приём после чтения terminal
Serial.readBytes(buffer, length)
Читает данные из порта и закидывает их в буфер buffer (массив char [] или byte []), также указывается количество байт, который нужно записать – length (чтобы не переполнить буфер)
Serial.readBytesUntil(character, buffer, length)
Читает данные из порта и закидывает их в буфер buffer (массив char [] или byte []), также указывается количество байт, который нужно записать – length (чтобы не переполнить буфер) и терминальный символ character. Окончание приёма в buffer происходит при достижении заданного количества length, при приёме терминального символа character (он в буфер не идёт) или по таймауту
Serial.readString()
Читает буфер порта и формирует из данных строку String, которую возвращает. Заканчивает работу по таймауту
Serial.readStringUntil(terminator)
Читает буфер порта и формирует из данных строку String, которую возвращает. Заканчивает работу по таймауту или после приёма символа terminator (символ char)
Serial.parseInt(), Serial.parseInt(skipChar)
Читает целочисленное значение из буфера порта и возвращает его (тип long). Заканчивает работу по таймауту. Останавливает чтение на всех знаках, кроме знака – (минус). Можно также отдельно указать символ skipChar, который нужно пропустить, например кавычку-разделитель тысяч (10’325’685), чтобы принять такое число
Serial.parseFloat()
Читает значение с плавающей точкой из буфера порта и возвращает его. Заканчивает работу по таймауту

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


Рассмотрим самый классический пример для всех языков программирования: Hello World!

Отправка данных в порт не должна вызывать трудностей и вопросов, потому что всё понятно/очевидно, да и чуть выше в описании метода print мы рассмотрели все варианты вывода. Отправка в порт позволяет узнать значение переменной в нужном месте программы, этот процесс называется отладка. Когда код работает не так, как нужно, начинаем смотреть, где какие переменные какие значения принимают. Или выводим текст из разных мест программы, чтобы наблюдать за правильностью (порядком) её работы. Давайте вспомним урок циклы и массивы и выведем в порт массив:

void setup() {
  Serial.begin(9600); // открыть порт для связи

  byte arr[] = {0, 50, 68, 85, 15, 214, 63, 254};
  for (byte i = 0; i < 8; i++) {
    Serial.print(arr[i]);
    Serial.print(" ");
  }
  Serial.println();
}

void loop() {

}

Вывод: 0 50 68 85 15 214 63 254 – элементы массива, разделённые пробелами!

Проблемы возникают при попытке принять данные в порт. Дело в том, что метод read() читает один символ, даже если вы отправите длинное число – программа получит его по одной цифре, и составлять число из цифр придётся вручную. Для принятия одиночных чисел у нас есть готовый метод – parseInt/parseFloat – для целочисленных и рациональных чисел соответственно. Процесс приёма и расшифровки данных называется парсинг (parsing). Давайте примем в порт число 1234, используя готовый метод парсинга.

void setup() {
  Serial.begin(9600); // открыть порт для связи
  Serial.println("Hello World!"); // отправить
}

void loop() {
  if (Serial.available()) {       // есть что на вход?
    int buff = Serial.parseInt(); // принять в переменную buff
    if (buff == 1234) {           // если приняли 1234
      Serial.println("OK");       // успех
    } else {
      Serial.println("error");    // ошибка 
    }
  }
}

Итак, мы используем конструкцию if (Serial.available()) {} чтобы опрашивать порт только в том случае, если в него что-то пришло. Отправив в порт число 1234 мы получим ответ ОК, отправив любое другое – error. Также вы заметите, что после отправки проходит секунда, прежде чем плата ответит. Эта секунда спрятана внутри метода parseInt, программа ждёт секунду после принятия данных, чтобы все данные успели прийти. Секунда это очень много, достаточно было ждать, скажем, 50 миллисекунд. Это можно сделать при помощи метода setTimeout.

void setup() {
  Serial.begin(9600); // открыть порт для связи
  Serial.setTimeout(50); // установить таймаут
  Serial.println("Hello World!"); // отправить
}

void loop() {
  if (Serial.available()) {       // есть что на вход?
    int buff = Serial.parseInt(); // принять в переменную buff
    if (buff == 1234) {           // если приняли 1234
      Serial.println("OK");       // успех
    } else {
      Serial.println("error");    // ошибка
    }
  }
}

Теперь после отправки цифры программа будет ждать всего 50 мс, и сразу же вам ответит.

В реальном устройстве часто требуется передавать несколько параметров, например у нас Bluetooth танк. Мы ему должны отправить например скорость правой гусеницы, скорость левой гусеницы, положение башни, состояние подсветки, команду на выстрел… Да что угодно. Как быть в таком случае? Тут начинается настоящий парсинг, и появляются варианты, нам придётся придумывать собственный протокол связи.

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

Как реализовать данные способы парсинга я очень подробно разбирал в примерах в сборнике полезных алгоритмов Arduino, раздел “Работа с 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 чисел через сериал
// десятичный разделитель - . (точка)
// разделитель - ; (семиколон)
// пример посылки: 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 пачку значений, и раскидать
их в целочисленный массив. Использовать можно банально для управления
ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
Либо управлять через Serial с какой-то программы с ПК
Как использовать:
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] = '\0';
} 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 пачку значений, и раскидать
их в целочисленный массив. Использовать можно банально для управления
ЧЕМ УГОДНО через 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 пачку значений, и раскидать
их в целочисленный массив. Использовать можно банально для управления
ЧЕМ УГОДНО через 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();
}
}

/*
Парсинг разных пакетов по ключевым "префиксам", построено на строках.
Префикс состоит из двух символов (в этом примере 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();
}
}

/*
Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
Префикс состоит из одного буквенного символа (в этом примере 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();
}
}
}

/*
Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
Режим работы полностью "прозрачный" (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;
}
}

/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через условия и встроенные методы работы со строками.
Принятие в порт блокирующее!!! 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);
}
}
}

/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через 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;                           // запоминаем номер команды
}
}
}
}

/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через 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;
}
}

Управляющие символы


Существуют так называемые управляющие символы, позволяющие форматировать вывод. Их около десятка, но вот самые полезные из них

  • \n – новая строка
  • \r – возврат каретки
  • \v – вертикальная табуляция
  • \t – горизонтальная табуляция

Как использовать? Просто писать в вывод. Например комбинация \r\n переведёт строку и вернёт курсор в левое положение:

Serial.print("Hello, World!\r\nArduino Forever");
// выведет
// Hello, World!
// Arduino Forever
// (на разных строках!)

Именно так кстати и работает функция println(), она просто добавляет вывод \r\n после print() =)

Символы табуляции позволят удобно отправлять данные для последующей вставки в excel или другие таблицы. Например выведем несколько степеней двойки в виде таблицы, используя символ табуляции \t:

for (byte i = 0; i < 16; i++) { // i от 0 до 16
Serial.print(i);      // степень
Serial.print("\t");   // табуляция
Serial.println(round(pow(2, i))); // 2 в степени i
}

Результат скопируем и вставим в excel

Удобно!

Видео


Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Монитор порта, отладка
4 (80%) 1 vote[s]
Последнее обновление Июль 06, 2019
2019-07-06T17:40:29+03:00