Этюды для начинающих: интерфейс Printable
- Войдите на сайт для отправки комментариев
Коллеги, хочу по мере «нечем заняться» публиковать некую серию этюдов, для тех из вас, кто только осваивается с программированием. Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.
Ловите первый этюд.
Часто в кодах, публикуемых на данном форуме, используются структуры для хранения каких-то наборов значений. Для примера в данном этюде я буду рассматривать некую структуру значений датчиков, хотя, всё, что будет сказано, никак к ней не привязано и может применяться с любыми другими структурами.
Итак, наша модельная структура:
struct SSensorValue { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. };
Как видите, обычная структура, каких миллион в кодах, встречающихся здесь.
Теперь, допустим, в процессе работы нам надо иногда печатать значение этой структуры (всех полей) в тот же Serial (например, для отладки или ещё для чего). Пробуем:
struct SSensorValue { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. }; void setup() { Serial.begin(115200); Serial.println("Fun begins!"); // // Объявляем структуру и заполняем какими-то значениями // SSensorValue sv; sv.timeStamp = millis(); sv.temperture = 36.6; sv.humidity = 93.2; sv.pressure = 755; // // Печатаем структуру // Serial.print(sv); } void loop() {}
Какая боль! Компилятор сказал, что понятия не имеет как именно мы собираемся печатать значение переменной типа SSensorValue :(
В общем-то, это понятно, откуда системе знать что у нас там за структура и как именно мы собираемся её печатать?
Вопрос, что делать?
Как всегда, есть 100500 путей!
Путь первый: вместо Serial.print(sv); расписать поэлементную печать. Что-то вроде такого:
struct SSensorValue { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. }; void setup() { Serial.begin(115200); Serial.println("Fun begins!"); // // Объявляем структуру и заполняем какими-то значениями // SSensorValue sv; sv.timeStamp = millis(); sv.temperture = 36.6; sv.humidity = 93.2; sv.pressure = 755; // // Печатаем структуру // Serial.print("SENSOR VALUE: Time:"); Serial.print(sv.timeStamp); Serial.print("ms; T:"); Serial.print(sv.temperture); Serial.print("C; H:"); Serial.print(sv.humidity); Serial.print("%; P:"); Serial.print(sv.pressure); Serial.println("mm"); } void loop() {}
Но такой путь хорош, если печатать нам надо только в одном месте программы. А если в нескольких? Везде расписывать эту кучу строк? А, главное, если приспичит что-то в печати поменять, не забывать менять везде? Нет, это большая головная боль.
Можно пойти другим путём - написать функцию печати и вызывать её вместо Serial.print(sv); Примерно так:
struct SSensorValue { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. }; void printSensorValue(const void * vv) { const SSensorValue * sv = (const SSensorValue *) vv; Serial.print("SENSOR VALUE: Time:"); Serial.print(sv->timeStamp); Serial.print("ms; T:"); Serial.print(sv->temperture); Serial.print("C; H:"); Serial.print(sv->humidity); Serial.print("%; P:"); Serial.print(sv->pressure); Serial.println("mm"); } void setup() { Serial.begin(115200); Serial.println("Fun begins!"); // // Объявляем структуру и заполняем какими-то значениями // SSensorValue sv; sv.timeStamp = millis(); sv.temperture = 36.6; sv.humidity = 93.2; sv.pressure = 755; // // Печатаем структуру // printSensorValue(&sv); } void loop() {}
Это уже гораздо лучше! Теперь, в каких бы местах нам не приходилось печатать нашу структуру, мы везде можем вызвать функцию, а если приспичит что-то поменять, так меняем в одном месте - в функции.
Такой подход был бы хорош в языке С, но для С++ он инороден. Недостаток этого подхода в том, что он несколько выбивается из идеологии объектно-ориентированного программирования, которая предполагает, что всё, что связано с обработкой данных некоторого класса (а структура - это класс), должно быть собрано (правильный термин "инкапсулировано") внутри класса, а не во внешних функциях. Класс должен быть самодостаточен.
Давайте попробуем сделать печать - собственным методом нашей структуры, чтобы она сама знала как ей печататься, а вместо вызова глобальной функции печати, будем вызывать метод переменной sv. Вот смотрите:
struct SSensorValue { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. void printIt() { Serial.print("SENSOR VALUE: Time:"); Serial.print(timeStamp); Serial.print("ms; T:"); Serial.print(temperture); Serial.print("C; H:"); Serial.print(humidity); Serial.print("%; P:"); Serial.print(pressure); Serial.println("mm"); } }; void setup() { Serial.begin(115200); Serial.println("Fun begins!"); // // Объявляем структуру и заполняем какими-то значениями // SSensorValue sv; sv.timeStamp = millis(); sv.temperture = 36.6; sv.humidity = 93.2; sv.pressure = 755; // // Печатаем структуру // sv.printIt(); // sv сама знает как её печататься! } void loop() {}
Пока понятно?
Это уже почти хорошо, но и здесь не всё слава Богу!
Во-первых, в нашем методе печати намертво забит Serial, а это не единственный в природе выводной поток и что нам делать, если захочется вывести куда-то ещё? Кроме того, всё же было бы гораздо лучше, если бы обучили сам Serial понимать наш метод печати и вернулись бы к нашей первоначальной идее писать просто Serial.print(sv), но чтобы при этом каким-то образом вызывался наш метод. Это было бы лучше и с точки зрения единообразности кода и с точки зрения других способов использования Serial (например, с операторами потокового ввода/вывода – я покажу как это делается в следующем этюде, если хоть кто-то заинтересуется этим).
Итак, мы всё же хотим писать просто и естественно: Serial.print(sv)
Обычно, в грамотно, профессионально сделанных системах печати, существует интерфейс Printable (печатабельно). Суть этого интерфейса в том, что ели некоторый класс объявлен Printable, он (класс) обязан содержать в себе метод печати собственных объектов. Причём метод строго стандартизованный. Тогда методы системы печати, могут просто вызывать этот метод печати класса и позволять ему распечатать себя, как он считает нужным.
К счастью, система печати в Arduino IDE сделана достаточно грамотно и такой интерфейс в ней содержится. Выглядит он вот так:
class Printable { public: virtual size_t printTo(Print& p) const = 0; };
То есть, для того чтобы наша структура была Printable (печатабельной), она обязана содержать метод с именем printTo и параметром "ссылка на Print" (кстати, наш любимый Serial - объект типа Print). Если наша структура будет содержать такой метод, то система печати просто будет сама вызывать его, когда ей нужно отпечатать нашу структуру!
Давайте это напишем:
struct SSensorValue : public Printable { unsigned long timeStamp; // Время съёма показаний (мс от запуска МК) float temperture; // температура в градусах С float humidity; // влажность в % float pressure; // давление в мм.рт.ст. size_t printTo(Print& p) const { // в качестве p нам передадут, например, Serial size_t res = p.print("SENSOR VALUE: Time:"); res += p.print(timeStamp); res += p.print("ms; T:"); res += p.print(temperture); res += p.print("C; H:"); res += p.print(humidity); res += p.print("%; P:"); res += p.print(pressure); return res + p.println("mm"); } }; void setup() { Serial.begin(115200); Serial.println("Fun begins!"); // // Объявляем структуру и заполняем какими-то значениями // SSensorValue sv; sv.timeStamp = millis(); sv.temperture = 36.6; sv.humidity = 93.2; sv.pressure = 755; // // Печатаем структуру // Serial.print(sv); } void loop() {}
Вот так! Обратите внимание: в строке 1 мы указали, что наша структура поддерживает интерфейс Printable, а в строках 7-17 мы реализовали необходимый для этого интерфейса метод printTo. Теперь, мы можем просто писать Serial.print(sv) (строка 34) и система поймёт, чтов этот момент надо просто вызвать наш метод, а он всё. что нужно, напечатает. Причём напечатает не тупо в Serial, а именно в тот поток, для которого он был вызван.
Если Вы запустите этот скетч, то он спокойно скомпилируется и в сериале Вы увидите:
Fun begins! SENSOR VALUE: Time:0ms; T:36.60C; H:93.20%; P:755.00mm
Вот , примерно так делаются "печатабельные" структуры и классы. Такой подход естественнен для С++. Следствием такой естественности будет, например, то, что это безо всяких исземений будет работать с любыми потоками, но потоки - тема следующего этюда.
Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.
Да, пишите обязательно. Прочитал с интересом. Не могу сказать, что я это тут же где-нибудь применю, но зато буду знать, что такое возможно и, если понадобиться, знать, где искать. Спасибо.
как вовремя.
Благодарю.
Но вот это надо пояснить: Причём напечатает не тупо в Serial, а именно в тот поток, для которого он был вызван.
Допустим, вывести надо еще и на LCD. Но код LCD.print(sv); противоречит сказанному выше, это уже не Serial.
Или под потоком понимается что-то иное?
Чтобы стандарт работал, он должен поддерживаться с обеих сторон: как со стороны организации типа данных, так и со стороны устройства потока вывода.
Т.е. если класс, объектом которого является LCD, реализован правильно, это будет работать.
Кстати, в этой теме, мне кажется, было бы уместно опубликовать варианты решения и с этой стороны - со стороны выходного потока.
Но код LCD.print(sv); противоречит сказанному выше, это уже не Serial.
Или под потоком понимается что-то иное?
LCD - не поток, конечно. Поток, это нечто, унаследованное от класса Stream.
Serial имеет тип HardwareSerial, который описан как
Если в программе имеются другие объекты, унаследованные от Stream, то с ними всё описанное будет работать.
Кстати, если быть уж совсем точным, то на самом деле всё это будет работать не только с потоками, а с любыми объектами унаследованными от Print, т.к. вся необходимая функциональность находится там.
Кстати, добавить такую функциональность в класс LCD нетрудно.
спасибо тебе, добрый человек!
очень интересен был бы этюд на тему - как в Ардуино IDE сделать библиотеку и загнать в нее свои функции как методы класса.
потому что получается через раз. (((
http://arduino.ua/ru/prog/LibraryTutorial
Очень интересно и красиво.
СПАСИБО ВАМ!
Спасибо Понравилось!!! Ждем продолжения.
Полезная тема! Cпасибo!
Модераторы, неплохо бы закрепить в начале ветки, чтобы не искать.
Не понравилось.
Во-первых, автор, похоже, вообще не различает библиотеки и ООП, хотя в действительности это ортогональные понятия.
Ну и насчет предметной области.
Увы, многие из начинающих программистов уверены, что для того, чтобы написать хорошую программу, нужно уметь программировать. Это заблуждение. Для того, чтобы написать хорошую программу в первую очередь нужно хорошо разбираться в той предметной области, которой посвящена программа.
И автор статьи, похоже относится именно к упомянутому выше типу программистов. По крайней мере, его библиотека, названная Morse, не имеет ни какого отношения ни к азбуке морзе, ни к Сэмюэлю Морзе.
тогда просьба, чтобы не учиться на заведомо неправильном - если будет время - покажите правильный подход, но для новичков, т.е. чтобы было понятно, как в той ссылке.
Логичное замечание.
Как, на мой взгляд, следовало бы отредактировать статью следующим образом:
1. Переименовать из Morse в SOS.
2. Структурно разделить перевод из С в С++ и преобразование фрагмента скетча в библиотеку.
Ну и еще от себя.
На мой взгляд, почти вся документация по Ардуино (а также код в стандартных библиотеках) составлены людьми, не слишком хорошо разбирающимися в программировании. Ну, то есть, где-то на уровне студентов специализированного ВУЗа. И вообще-то, по-хорошему, там нужно переписывать все с начала. Причем, начинать с arduino.cc, т.к. материалы на arduino.ru в основном являются переводами, содержащими те же оштибки, что и оригинал.
Поэтому начинать с данной статьи, мне кажется вдвойне нерационально: во-первых, создание библиотеки не так уж сильно востребовано - большинство довольствуется написанем простых скетчей с использованием либо стандартных, либо где-нибудь скачанных библиотек; а во-вторых, думаю, большинство из тех, кто доходит до самостоятельного создания библиотек, способны сами разобраться, что принимать на веру из написанного, а что - нет.
Возвращаясь к "во-первых": меня, например, гораздо больше смущает код в статье blink without delay. И, судя по всему, не только меня. По крайней мере, мне попадались статьи, где содержится критика этого кода и говорится, как этот код улучшить. Вот только на "родном" arduino.ru от этого ничего не меняется и новичкам по-прежнему предлагается код, неоднократно подвергшийся критике.
Поэтому, Tomasina, хотя по существу Вы правы, подобные предложения без возможности внести изменения в документацию на профильном сайте arduino.ru, мне кажутся неконструктивными (оговорка: в данном контексте, в отличие от русскоязычной традиции трактовать частицу "не" как "анти", у меня именно "неконструктивными" без оттенка "антиконструктивными").
Шикарный этюд -))
Для себя почерпнул кое что новое. Утянул в свою записную книжку.
Автору СПАСИБО.
... Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.
...
Я - новичок. То, что мне в руки два года назад попала ардуина, сподвигло почитать книги по языкам, программированию и тп. Но этого явно мало.
За Вашей статьёй видна ШКОЛА, которую никакими RTFMами не постигнуть. Многим такая школа не доступна, по тем или иным причинам, а в Ваших статьях объясняются как раз те тонкости, которые в книгах не описаны.
Хотел бы подписаться на весь цикл "этюдов", но не знаю как это сделать ))
АВТОРУ РЕСПЕКТ И УВАЖУХА
мне как чайнику в программировании самое то
a споры кодеров это для других форумов
Отличное изложение материала, понятно даже беглым прочтением )))
Я бы создал отдельный раздел "Образование" и прибил туда золотыми гвоздями все этюды, почистив лишнее. И сделать их только для чтения, для всех, кроме автора, а обсуждать и задавать вопросы по ним можно открыв новую тему для обсуждения. Иначе уйдёт в пучину форума, как в пясок, будет жаль, новички и колеблющиеся многое потеряют
Пока раздела нет, я бы попросил Евгения, если нетрудно, запостить в песочницу ссылки на все его этюды, особенно утонувшие, как этот. Третьим, заключительным, постом в "ПЕСОЧНИЦЕ" я планирую сделать список полезных тем и для Евгения там отведено козырное место. И искать легче будет.
Пока что-то непишется, задора нет, но закончу обязательно.
Пока что-то непишется, задора нет, но закончу обязательно.
А что надо чтобы раззадорить )))
А что надо чтобы раззадорить )))
Да Вам моя графомания, уж точно, без надобности.)))
Я бы создал отдельный раздел "Образование" и прибил туда золотыми гвоздями все этюды, почистив лишнее. И сделать их только для чтения, для всех, кроме автора, а обсуждать и задавать вопросы по ним можно открыв новую тему для обсуждения. Иначе уйдёт в пучину форума, как в пясок, будет жаль, новички и колеблющиеся многое потеряют
поддерживаю двумя руками....
Евгений, спасибо за прекрасный этюд!
Более всего мне в нем понравились рассуждение и логика действий после "затыка" (там где обычно в форуме "ничегонеполучается!!!"). Новичкам программирования зачитывать до дыр!
Хорошо написано. Некоторые примеры для меня были лишними, но совсем новичкам может они и пригодятся.
Как раз и хотелось нечто такое забабахать к себе в проект)))
Где Вы преподаете? Хотел бы у Вас учиться!!
Теперь, мы можем просто писать Serial.print(sv)
Если Вы запустите этот скетч, то он спокойно скомпилируется
Спасибо, супер! Осталось модифицировать ( если это возможно) структуру так, чтобы
не кушал драгоценную память =)