Этюды для начинающих: интерфейс Printable

ЕвгенийП
ЕвгенийП аватар
Онлайн
Зарегистрирован: 25.05.2015

Коллеги, хочу по мере «нечем заняться» публиковать некую серию этюдов, для тех из вас, кто только осваивается с программированием. Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.

Ловите первый этюд.

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

Итак, наша модельная структура:

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

Вот , примерно так делаются "печатабельные" структуры и классы. Такой подход естественнен для С++. Следствием такой естественности будет, например, то, что это безо всяких исземений будет работать с любыми потоками, но потоки - тема следующего этюда.

vk007
Offline
Зарегистрирован: 16.06.2015

ЕвгенийП пишет:

Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.

Да, пишите обязательно. Прочитал с интересом. Не могу сказать, что я это тут же где-нибудь применю, но зато буду знать, что такое возможно и, если понадобиться, знать, где искать. Спасибо.

Tomasina
Tomasina аватар
Offline
Зарегистрирован: 09.03.2013

как вовремя.
Благодарю.

Но вот это надо пояснить: Причём напечатает не тупо в Serial, а именно в тот поток, для которого он был вызван.
Допустим, вывести надо еще и на LCD. Но код LCD.print(sv); противоречит сказанному выше, это уже не Serial.
Или под потоком понимается что-то иное?

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

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

Т.е. если класс, объектом которого является LCD, реализован правильно, это будет работать.

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

ЕвгенийП
ЕвгенийП аватар
Онлайн
Зарегистрирован: 25.05.2015

Tomasina пишет:

Но код LCD.print(sv); противоречит сказанному выше, это уже не Serial.

Или под потоком понимается что-то иное?

LCD - не поток, конечно. Поток, это нечто, унаследованное от класса Stream.

Serial имеет тип HardwareSerial, который описан как

class HardwareSerial : public Stream {
...
};

// Кстати, в свою очередь Stream наследуется от Print

class Stream : public Print {
...
};

Если в программе имеются другие объекты, унаследованные от Stream, то с ними всё описанное будет работать.

Кстати, если быть уж совсем точным, то на самом деле всё это будет работать не только с потоками, а с любыми объектами унаследованными от Print, т.к. вся необходимая функциональность находится там.

Кстати, добавить такую функциональность в класс LCD нетрудно.

Maverik
Offline
Зарегистрирован: 12.09.2012

спасибо тебе, добрый человек!

очень интересен был бы этюд на тему - как в Ардуино IDE сделать библиотеку и загнать в нее свои функции как методы класса. 

потому что получается через раз. (((

Tomasina
Tomasina аватар
Offline
Зарегистрирован: 09.03.2013
inspiritus
Offline
Зарегистрирован: 17.12.2012

Очень интересно и красиво.

СПАСИБО ВАМ!

vosara
vosara аватар
Offline
Зарегистрирован: 08.02.2014

Спасибо Понравилось!!! Ждем продолжения.

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

Полезная тема! Cпасибo!

Модераторы, неплохо бы закрепить в начале ветки, чтобы не искать.

 

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Не понравилось.

Во-первых, автор, похоже, вообще не различает библиотеки и ООП, хотя в действительности это ортогональные понятия.

Ну и насчет предметной области.

Увы, многие из начинающих программистов уверены, что для того, чтобы написать хорошую программу, нужно уметь программировать. Это заблуждение. Для того, чтобы написать хорошую программу в первую очередь нужно хорошо разбираться в той предметной области, которой посвящена программа.

И автор статьи, похоже относится именно к упомянутому выше типу программистов. По крайней мере, его библиотека, названная Morse, не имеет ни какого отношения ни к азбуке морзе, ни к Сэмюэлю Морзе.

Tomasina
Tomasina аватар
Offline
Зарегистрирован: 09.03.2013

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

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Логичное замечание.

Как, на мой взгляд, следовало бы отредактировать статью следующим образом:

1. Переименовать из Morse в SOS.

2. Структурно разделить перевод из С в С++ и преобразование фрагмента скетча в библиотеку.

 

Ну и еще от себя.

На мой взгляд, почти вся документация по Ардуино (а также код в стандартных библиотеках) составлены людьми, не слишком хорошо разбирающимися в программировании. Ну, то есть, где-то на уровне студентов специализированного ВУЗа. И вообще-то, по-хорошему, там нужно переписывать все с начала. Причем, начинать с arduino.cc, т.к. материалы на arduino.ru в основном являются переводами, содержащими те же оштибки, что и оригинал.

Поэтому начинать с данной статьи, мне кажется вдвойне нерационально: во-первых, создание библиотеки не так уж сильно востребовано - большинство довольствуется написанем простых скетчей с использованием либо стандартных, либо где-нибудь скачанных библиотек; а во-вторых, думаю, большинство из тех, кто доходит до самостоятельного создания библиотек, способны сами разобраться, что принимать на веру из написанного, а что - нет.

Возвращаясь к "во-первых": меня, например, гораздо больше смущает код в статье blink without delay. И, судя по всему, не только меня. По крайней мере, мне попадались статьи, где содержится критика этого кода и говорится, как этот код улучшить. Вот только на "родном" arduino.ru от этого ничего не меняется и новичкам по-прежнему предлагается код, неоднократно подвергшийся критике.

Поэтому, Tomasina, хотя по существу Вы правы, подобные предложения без возможности внести изменения в документацию на профильном сайте arduino.ru, мне кажутся неконструктивными (оговорка: в данном контексте, в отличие от русскоязычной традиции трактовать частицу "не" как "анти", у меня именно "неконструктивными" без оттенка "антиконструктивными").

pav2000
Offline
Зарегистрирован: 15.12.2014

Шикарный  этюд -))

Для себя почерпнул кое что новое. Утянул в свою записную книжку.

Автору СПАСИБО.

Piskunov
Offline
Зарегистрирован: 13.02.2014

ЕвгенийП пишет:

... Если это кому-то нужно и интересно, отпишитесь, это будет для меня серьёзным стимулом продолжать такую практику.

...

Замечательный этюд, замечательная серия. Спасибо за труд по написанию этих статей.

Я - новичок. То, что мне в руки два года назад попала ардуина, сподвигло почитать книги по языкам, программированию и тп. Но этого явно мало.

За Вашей статьёй видна ШКОЛА, которую никакими RTFMами не постигнуть. Многим такая школа не доступна, по тем или иным причинам, а в Ваших статьях объясняются как раз те тонкости, которые в книгах не описаны.

Хотел бы подписаться на весь цикл "этюдов", но не знаю как это сделать ))

vvadim
Offline
Зарегистрирован: 23.05.2012

АВТОРУ РЕСПЕКТ И УВАЖУХА

мне как чайнику в программировании самое то

a споры кодеров это для других форумов

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

Отличное изложение материала, понятно даже беглым прочтением )))

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

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

bwn
Offline
Зарегистрирован: 25.08.2014

Пока раздела нет, я бы попросил Евгения, если нетрудно, запостить в песочницу ссылки на все его этюды, особенно утонувшие, как этот. Третьим, заключительным, постом в "ПЕСОЧНИЦЕ" я планирую сделать список полезных тем и для Евгения там отведено козырное место. И искать легче будет.
Пока что-то непишется, задора нет, но закончу обязательно.
 

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

bwn пишет:

Пока что-то непишется, задора нет, но закончу обязательно.
 

А что надо чтобы раззадорить )))

bwn
Offline
Зарегистрирован: 25.08.2014

ua6em пишет:

А что надо чтобы раззадорить )))

Да Вам моя графомания, уж точно, без надобности.)))

vvadim
Offline
Зарегистрирован: 23.05.2012

DetSimen пишет:

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

поддерживаю двумя руками....

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

Евгений, спасибо за прекрасный этюд!

Более всего мне в нем понравились рассуждение и логика действий после "затыка" (там где обычно в форуме "ничегонеполучается!!!"). Новичкам программирования зачитывать до дыр!

Densl
Offline
Зарегистрирован: 28.11.2018

Хорошо написано. Некоторые примеры для меня были лишними, но совсем новичкам может они и пригодятся. 

Как раз и хотелось нечто такое забабахать к себе в проект)))