По мотивам «Обрабатываем строки на Arduino. Объект String в Arduino и команды через последовательный порт

Я пришел к программированию в целом, и программированию Arduino в частности, полным нулем, около двух месяцев назад. Сейчас же, по ходу своей текущей деятельности я ощутил необходимость в освоении обработки строк на ардуино. Обычный поход в Google за информацией не обрадовал статьей, где все просто и понятно для чайников написано. И поэтому я здесь для того, чтобы рассказать о том, каким образом был реализован парсинг строк из последовательного порта и какие были встречены на пути подводные камни. Интересующихся прошу под кат.

Итак. Вот примерный алгоритм, которому я следовал:

  1. Идем на arduino.ru и высматриваем в колонке типов все, связанное с символами.
  2. Решаем, какую форму представления будем использовать (Я остановился на классе String, т.к. имел неприятный опыт с месивом массивом).
  3. Судорожно пытаемся написать свою функцию с преферансом и профурсетками
  4. Ищем класса.
  5. Ищем нужные операторы.
  6. Пишем!
А алгоритм работы основного тела программы прост:
  1. Циклично проверяем, есть ли в буфере com порта доступный для чтения байт, если есть, читаем.
  2. Если принятый байт - символ переноса строки ("\n"), то вызываем самописную функцию парсинга, если же нет, то добавляем принятый байт в созданную переменную типа String.
  3. Парсим, наконец, строку.

    На самом деле, т.к. практическое использование парсинга строк у меня не горит, то данная функция носит всего лишь демонстративный характер, сравнивая полученную строку с уже записанной в памяти константной строкой. Итак, используя оператор equals сравниваем принятую строку с записанной, и, если оператор возвращает true, выходим из функции обработчика, если же нет, сравниваем со следующей. Если строки эквивалентны, то опять же, выходим из функции-обработчика, возвращая результат. Ну а уж если и это условие не сработает, то все равно выйдем из функции и скажем, что что строка не верна.

  4. В зависимости от принятого результата с помощью switch case выбираем нужный.
  5. Обнуляем принятую строку, чтобы потом начать собирать ее заново.

А вот, наконец-таки, код:

#define led 13 String input_string = ""; const String Led_off = "switch led off"; const String Led_on = "switch led on"; bool led_running; void setup() { Serial.begin(9600); } void loop() { while (Serial.available() > 0) { char c = Serial.read(); if (c == "\n") { Serial.print("Input_string is: "); Serial.println(input_string); switch (parse(input_string, Led_off, Led_on)) { case 10: led_running=false; Serial.println("Switching off is done"); break; case 11: led_running=true; Serial.println("Switching on is done"); break; case 0: Serial.println("invalid String"); break; } input_string = ""; digitalWrite(led, led_running); } else { input_string += c; } } } byte parse(String input_string, const String Led_off, const String Led_on) { if (input_string.equals(Led_off) == true) { return 10; } else if (input_string.equals(Led_on) == true) { return 11; } else return 0; }


Так, я не понял, что за дела? Почему не загорается светодиод? Ах да, как же это я запамятовал, в void setup нужно добавить:

PinMode(led, OUTPUT);

P.S.: Немаловажно установить монитор com порта в Arduino IDE в режим «Новая строка», т.к. в любом другом посылаемая строка не будет сопровождаться символом ее окончания "\n".

P.P.S.: В холиваре по поводу того, что ардуино нинужна - участвовать не собираюсь, изучая основы программирования и алгоритмизации я ничего дурного не сделал.

P.P.P.S.: Если статья будет принята адекватно, напишу следующую о том, что у меня вышло с улучшением функционала функции парсинга. Ну, с богом! .

Сколько ни изучаю Arduino, она не перестаёт удивлять меня своей простотой. К примеру, собирая и тестируя систему "умного дома", я думал, что подача команд с компьютера будет самой сложной частью - это же надо принимать строку из последовательного порта, распознавать её, следить, чтобы не возникало ошибок... Однако оказалось достаточно почитать сайт Arduino.cc да потестить пару примеров, как стало ясно - разработчики постарались, чтобы оградить нас от написания длинного и унылого кода. К слову сказать, с задачей в итоге я справился за вечер, под конец даже подумывая: "а какую бы ещё команду прикрутить?.."

Итак, предположим, вы уже умеете программировать Arduino и можете разобраться в своём или чужом коде. Одним из основных понятий являются переменные и их типы. Ну-ка навскидку? byte, int, long, char, string... Два последних - по сути одно и то же, ибо string - массив переменных типа char (Кто-нибудь сейчас должен возразить, что char представляется в виде байтового числа, но речь не об этом). Итак, всё, что принимается из последовательного порта, следует читать, как char:

Char inChar = " "; byte z = 0; while (Serial.available()) { inChar[z] = Serial.read(); z++; }

Это первый пример, который может придти в голову . Создаём пустую строку, затем, если есть, что читать из последовательного порта, посимвольно её заполняем. Функция Serial.available() возвращает количество байт, доступных для чтения, а если там пусто - то 0, очевидно. Этим можно пользоваться, чтобы узнать длину поданной команды, хотя мы и так её узнаем в приведённом примере - это величина переменной z на выходе из цикла. Да, строка из пробелов (ASCII код пробела - не ноль!) - это терпимо, но всё-таки не очень хорошо, по возможности избегайте этого. А догадливый читатель сможет похвалить себя, если сразу догадается, что стоит исправить в вышеприведённом коде. Для тех, кто не догадался - подсказка: char inchar - строка длиной 6 символов. Если строке присваивается значение, компилятор позволяет не указывать явно её длину, поэтому в примере квадратные скобки пустые.

Кстати, не стоит забывать прописать в setup()

Serial.begin(9600);

А во встроенном мониторе порта, в который, собственно, и будут отправляться команды, указать скорость - 9600 бод, иначе увидите крякозябры.

Далее, что делать с полученной строкой? Те, кто предложат сравнивать побайтово строку с известными значениями (а такая мысль наверняка кому-то может придти в голову) после прочтения статьи переместятся вперёд во времени лет на 20. Из прошлого, я имею в виду:)

Поиск по документации Arduino IDE даёт два варианта, что такое string. Это сам string как строка char"ов, и String, являющийся объектом. Что такое объект? Согласно википедии, это "некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеющая заданные значения свойств (атрибутов) и операций над ними (методов)". Другими словами - переменная со встроенными функциями, делающими что-то с этой переменной. Чтобы начать работать с этим объектом, напишем что-нибудь такого вида:

String input = ""; while (Serial.available()) { input += Serial.read(); }

Здесь к input будут добавляться всё новые символы, пока буфер не иссякнет. Тогда можно будет анализировать полученную строку, например, так:

Input.toLowerCase(); if(input.startsWith("pause")) { String toWait = input.substring(5); toWait.trim(); int delaytime = toWait.toInt(); if(delaytime>0) { if(delaytime<10000) { delay(delaytime); } } }

Код использует очень удобные функции, встроенные в объект String. Это startsWith(), которая возвращает единицу, если строка начинается с того, что записано в скобках, substring(), возвращающая кусок строки, начинающийся в данном случае с 5-го символа (считается, начиная с нуля), trim(), отбрасывающий всё лишнее по краям строки, ну и toInt(), превращающий то, что осталось, в число типа Int. Это число неплохо ещё и проверить на предмет попадания в рамки ожидаемого. В итоге, если дать команду "PauSe 567 ", то МК подождёт ровно 567 миллисекунд.

Про trim() стоит написать отдельно. Он нужен не только для того, чтобы отбросить пробел в начале получившейся строки, но в первую очередь - чтобы избавиться от символов в её конце. Это служебные символы, добавляющиеся при отправке сообщения - NL (новая строка) и CR (возврат каретки). Они нужны как раз для того, чтобы сигнализировать о конце команды, но могут и помешать. Поэтому, несмотря на то, что в мониторе порта можно выбрать, какие из этих символов посылать или не посылать ничего, лучше перестраховаться. Тем более, что делается это в одну строчку кода.

А вот и список функций (методов) объекта String.

    charAt() - возвращает символ, стоящий на указанном месте

    concat() - функция конкатенации, т.е слияния двух строк в одну. Правда string1 = string1 + string2 это то же самое, что и string1.concat(string1, string2), а записывается проще и понятнее.

    equals() - возвращает единицу, если строка посимвольно равна тому, что написано в скобках. Есть ещё equalsIgnoreCase(), который игнорирует регистр (верхний или нижний)

    endsWith() - который работает аналогично startsWith()

    indexOf() - возвращающий место в строке символа(или строки) в скобках. Ищет с конца и возвращает -1, если не найдено.

    length() - выдающий длину строки

    setCharAt() - требующий место и символ, который надо поставить на это место, например: string1.setCharAt(3, "d") поставит d третьим символом в строке взамен того, что там стояло

  • И ещё несколько других, которые вряд ли вам понадобятся, если вы не в силах залезть на arduino.cc и прочитать о них:)

Вот и всё, что хотел рассказать. Надеюсь, эта статья поможет не бояться ООП и научить вашего домашнего робота на Arduino повиноваться сигналам с компа


String text = "Temp: " + tempC + " C";

Увы, в C этот прием не работает. В данном случае сообщение можно вывести несколькими инструкциями print, как показано далее:

lcd.print("Temp: "); lcd.print(tempC); lcd.print(" C");

Этот подход устраняет необходимость закулисного копирования данных в процессе конкатенации (объединения) строк, как происходит в других современных языках.

Аналогичный подход с применением нескольких инструкций вывода можно использовать при работе с монитором последовательного порта и инструкциями Serial.print. В подобных случаях последней в строке обычно используется команда println, добавляющая в конец символ перевода строки.

Форматирование строк с помощью sprintf

Стандартная библиотека строковых функций для языка C (не путайте с библиотекой Arduino String Object, которая обсуждается в следующем разделе) включает очень удобную функцию sprintf, выполняющую форматирование массивов символов. Она вставляет значения переменных в строку шаблона, как показано в следующем примере:

sprint(line1, "Temp: %d C", tempC);

Массив символов line1 - это строковый буфер, содержащий форматированный текст. Как указано в примере, он имеет емкость 17 символов, включая дополнительный нулевой символ в конце. Имя line1 я выбрал потому, что собираюсь показать, как сформировать содержимое верхней строки для жидкокристаллического дисплея с двумя строками по 16 символов в каждой.

В первом параметре команде sprintf передается массив символов, в который должен быть записан результат. Следующий аргумент - строка формата, содержащая смесь простого текста, такого как Temp:, и команд форматирования, например %d. В данном случае %d означает «десятичное целое со знаком». Остальные параметры будут подставлены в строку формата в порядке их следования на место команд форматирования.

Чтобы во вторую строку на жидкокристаллическом дисплее вывести время, его можно сформировать из отдельных значений часов, минут и секунд, как показано далее:

sprintf(line2, "Time: %2d:%02d:%02d", h, m, s);

Если попробовать вывести строку line2 в монитор последовательного порта или на экран жидкокристаллического дисплея, вы увидите текст

Команда sprintf не только подставила числа в нужные места, но и добавила ведущий ноль перед цифрой 5. В примере между символами: находятся команды форматирования трех компонентов времени. Часам соответствует команда %2d, которая выводит двузначное десятичное число. Команды форматирования для минут и секунд немного отличаются (%02d). Эти команды также выводят двузначные десятичные числа, но добавляют ведущий ноль, если это необходимо.

Однако имейте в виду, что этот прием предназначен для значений типа int. К сожалению, разработчики Arduino не реализовали в стандартной библиотеке C поддержку других типов, таких как float.

Определение длины строки

Так как строки, хранящиеся в массивах символов, часто оказываются короче самих массивов, в библиотеке предусмотрена удобная функция с именем strlen. Эта функция подсчитывает число символов в массиве, предшествующих нулевому символу, отмечающему конец строки.

Функция принимает массив символов в своем единственном параметре и возвращает размер строки (исключая пустой символ), хранящейся в нем, например, команда

вернет число 3.

Библиотека Arduino String Object

В Arduino IDE, начиная с версии 019, вышедшей несколько лет тому назад, включается библиотека String, более понятная и дружественная разработчикам, использующим Java, Ruby, Python и другие языки, где конкатенацию строк допускается выполнять простым оператором +. Эта библиотека также предлагает массу вспомогательных функций для работы со строками.

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

Эта библиотека удивительно проста в использовании, и, если вам приходилось работать со строками в Java, благодаря библиотеке Arduino String Object вы будете чувствовать себя как дома.

Создание строк

Создать строку можно из массива элементов типа char, а также из значения типа int или float, как показано в следующем примере:

String message = "Temp: ";

String temp = String(123);

Конкатенация строк

Строки типа String можно объединять друг с другом и с данными других типов с помощью оператора +. Попробуйте добавить следующий код в функцию setup пустого скетча:

Serial.begin(9600);

String message = "Temp: ";

String temp = String(123);

Serial.println(message + temp + " C");

Обратите внимание на то, что последнее значение, добавляемое в строку, в действительности является массивом символов. Если первый элемент в последовательности значений между операторами + является строкой, остальные элементы автоматически будут преобразованы в строки перед объединением.

Другие строковые функции

В табл. 6.1 перечислены еще несколько удобных функций из библиотеки String. Полный список доступных функций можно найти по адресу http://arduino.cc/en/Reference/StringObject.

Таблица 6.1. Некоторые полезные функции в библиотеке String

Функция

Пример

Описание

char ch = String("abc")

Переменная ch получит значение "a"

String s = " abc ";

Удалит пробелы с обеих сторон от группы символов abc. Переменная s получит значение "abc"

String s = "123";

int x = s.toInt();

Преобразует строковое представление числа в значение типа int или long

String s = "abcdefg";

String s2 = s.substring(1, 3);

Возвращает фрагмент исходной строки. Переменная s2 получит значение "bc". В параметрах передаются: индекс первого символа фрагмента и индекс символа, следующего за последним символом фрагмента

String s = "abcdefg";

s.replace("de", "DE");

Заменит все вхождения "de" в строке на "DE". Переменная s2 получит значение "abcDEfg"

Использование ЭСППЗУ

Содержимое всех переменных, используемых в скетче Arduino, теряется при выключении питания или выполнении сброса. Чтобы сохранить значения, их нужно записать байт за байтом в память ЭСППЗУ. В Arduino Uno имеется 1 Кбайт памяти ЭСППЗУ.

ПРИМЕЧАНИЕ

Это не относится к плате Arduino Due, не имеющей ЭСППЗУ. В этой модели данные следует сохранять на карту microSD.

Для чтения и записи данных в ЭСППЗУ требуется использовать библиотеку, входящую в состав Arduino IDE. Следующий пример демонстрирует, как записать единственный байт в ЭСППЗУ, в данном случае операция выполняется в функции setup:

#include

byte valueToSave = 123

EEPROM.write(0, valueToSave);

В первом аргументе функции write передается адрес в ЭСППЗУ, куда должен быть записан байт данных, а во втором - значение для записи в этот адрес.

Доброе время суток! Продолжаем освоение платы ардиуно уно. В прошлой статье мы научились выводить информационные сообщения на экран компьютера/ноутбука.

Если вы помните, у нас с вами осталось незавершённое дело. А именно способ задание входных параметров функций Serial. print () и Serial. println () . Мы задавали параметры этих функций непосредственно в строках, где они вызывались, вместо того, чтобы задавать параметры, через переменные.

Для начала снова соберём схему с двумя светодиодами. После чего открываем программу, что мы написали в 5 уроке.

Как вы можете видеть, я дополнил второй необходимыми строками. Надеюсь, вы также выполнили это задание 🙂 Если же нет, тогда дополните вашу программу.

Не стоит копировать строки кода из текста статьи. Например, кавычки в вердовском документе могут не восприниматься за «кавычки» в программной среде Arduino IDE. Еще хочу сказать следующее – если с первой попытки загрузить программу в МК не удалось, стоит повторить попытку (несколько раз).

В одной из предыдущих статей я уже затрагивал вопросы касающееся использования . Повторюсь. Намного практичнее вносить изменения в необходимые параметры через изменения значений переменных, что находятся вначале программы или в отдельном файле, чем рыскать в поисках нужных значений по всему коду.

До сегодняшнего момента, мы использовали переменные, что имели тип целого числа. Такими числами удобно вести подсчёт или указывать номер вывода микроконтроллера. Кроме того, данный тип занимает гораздо меньший объём памяти микроконтроллера в сравнении с другими типами. Однако при считывании данных с датчиков (например, датчик температуры) мы будем получать значения, которые состоят из целой и дробной части. Для того, чтобы работать с такими числами нам потребуется тип float (числа с плавающей точкой). Данный тип будет описан отдельно, сейчас же поговорим о строковом типе данных String .

Переменным типа String присваиваются данные заключенные в двойные кавычки.

Объявим переменные типа String .

  • String greenLedText = “”;
  • String redLedText = “”;

В коде программы заменим текстовые сообщения на имена переменных. Как вы можете видеть, при записи переменных в качестве входных параметров функций двойные кавычки не нужны. Но что же будет, если мы запишем имя одной из переменных в двойных кавычках? А вот сейчас мы и проверим…

Загрузим программу и посмотрим на результат. Одна строка отображается корректно, а вторая вместо того, чтобы выводить на экран необходимую информацию выводит имя переменной. Всему виной двойные кавычки.

Правило следующее: хотите вывести строку непосредственно с круглых скобок ставьте двойные кавычки, нужно вывести информацию, что присвоена строковой переменной – кавычки не ставим.

Создадим локальную переменную «приветствие» в void setup . Почему именно локальную? Во-первых, она занимает значительно меньше места в памяти микроконтроллера. Во-вторых, что мы зря что-ли обсуждали вопросы локальных и глобальных переменных, нужно теорию применять на практике. А почему создаём «локалку» именно в void setup , а всё потому, что он выполняется всего один раз.

Объявляем и присваиваем значение переменной.

String first_ message = “ Hello”;

Одного «hello» будет мало, нужно еще что-то. Давайте напишем что-то вроде… 🙂 и разобьём это на два сообщения.

  • String second_message = “I am Migalka”;
  • String third_message = “I want blinky”;

Вызов функций следует выполнять после строки включения (инициализации) последовательного порта.

  • Serial.println (first_message);
  • Serial.println (second_message);


Загрузим программу и посмотрим на результат. Как и планировалось, приветствие выводится один раз. Но как-то не красиво согласны? 🙂 Для того, чтобы второе и третье сообщение были на одной строке есть несколько вариантов:

  • Заменить функцию println (second_message) на Serial.print (second_message) . Для того, чтобы третье сообщение не наплывало на второе после … «Migalka » поставьте пробел.
  • Соединить вместе второе и третье сообщение. sum_ message = second_ message + third_ message;

Выбираем второй вариант. Предварительно объявим новую строковую переменную String sum_ message;

Заменяем имя « second_ message » на « sum_ message ». Если вы заменяли функцию Serial. println (second_ message) на Serial. print ( second_message) , верните все назад, чтобы новое сообщение начиналось с новой строки, а строчку «Serial.println (third_message); » удаляем по причине ненадобности.

Загрузим программу и смотрим на результат.

Я пришел к программированию в целом, и программированию Arduino в частности, полным нулем, около двух месяцев назад. Сейчас же, по ходу своей текущей деятельности я ощутил необходимость в освоении обработки строк на ардуино. Обычный поход в Google за информацией не обрадовал статьей, где все просто и понятно для чайников написано. И поэтому я здесь для того, чтобы рассказать о том, каким образом был реализован парсинг строк из последовательного порта и какие были встречены на пути подводные камни. Интересующихся прошу под кат.

Итак. Вот примерный алгоритм, которому я следовал:

  1. Идем на arduino.ru и высматриваем в колонке типов все, связанное с символами.
  2. Решаем, какую форму представления будем использовать (Я остановился на классе String, т.к. имел неприятный опыт с месивом массивом).
  3. Судорожно пытаемся написать свою функцию с преферансом и профурсетками
  4. Ищем класса.
  5. Ищем нужные операторы.
  6. Пишем!

А алгоритм работы основного тела программы прост:

  1. Циклично проверяем, есть ли в буфере com порта доступный для чтения байт, если есть, читаем.
  2. Если принятый байт - символ переноса строки ("n"), то вызываем самописную функцию парсинга, если же нет, то добавляем принятый байт в созданную переменную типа String.
  3. Парсим, наконец, строку.

    На самом деле, т.к. практическое использование парсинга строк у меня не горит, то данная функция носит всего лишь демонстративный характер, сравнивая полученную строку с уже записанной в памяти константной строкой. Итак, используя оператор equals сравниваем принятую строку с записанной, и, если оператор возвращает true, выходим из функции обработчика, если же нет, сравниваем со следующей. Если строки эквивалентны, то опять же, выходим из функции-обработчика, возвращая результат. Ну а уж если и это условие не сработает, то все равно выйдем из функции и скажем, что что строка не верна.

  4. В зависимости от принятого результата с помощью switch case выбираем нужный.
  5. Обнуляем принятую строку, чтобы потом начать собирать ее заново.

А вот, наконец-таки, код:

#define led 13 String input_string = ""; const String Led_off = "switch led off"; const String Led_on = "switch led on"; bool led_running; void setup() { Serial.begin(9600); } void loop() { while (Serial.available() > 0) { char c = Serial.read(); if (c == "n") { Serial.print("Input_string is: "); Serial.println(input_string); switch (parse(input_string, Led_off, Led_on)) { case 10: led_running=false; Serial.println("Switching off is done"); break; case 11: led_running=true; Serial.println("Switching on is done"); break; case 0: Serial.println("invalid String"); break; } input_string = ""; digitalWrite(led, led_running); } else { input_string += c; } } } byte parse(String input_string, const String Led_off, const String Led_on) { if (input_string.equals(Led_off) == true) { return 10; } else if (input_string.equals(Led_on) == true) { return 11; } else return 0; }

Так, я не понял, что за дела? Почему не загорается светодиод? Ах да, как же это я запамятовал, в void setup нужно добавить:

PinMode(led, OUTPUT);

P.S.: Немаловажно установить монитор com порта в Arduino IDE в режим «Новая строка», т.к. в любом другом посылаемая строка не будет сопровождаться символом ее окончания "n".

P.P.S.: В холиваре по поводу того, что ардуино нинужна - участвовать не собираюсь, изучая основы программирования и алгоритмизации я ничего дурного не сделал.

P.P.P.S.: Если статья будет принята адекватно, напишу следующую о том, что у меня вышло с улучшением функционала функции парсинга. Ну, с богом! .



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: