- PVSM.RU - https://www.pvsm.ru -
После написания пары [1] больших [2] постов про «радиофицированный» умный дом, было достаточно много желающих получить код, который помог бы разобраться с этой темой более детально.
Свой исходный вариант кода по некоторым причинам выкладывать не хотелось — подготовил «облегченный» вариант, который позволит объяснить мои основные задумки.
Для того, чтобы пост получился наиболее зрелищным и полезным, сегодня реализуем домашнюю мини-систему (часы, календарь, погода, контроль уровня заряда батареек в датчиках и т.п.), состоящую из одного «главного» модуля (с большой светодиодной матрицей в качестве индикатора), двух автономных датчиков (будут измерять температуру) и модуля синхронизации времени через NTP.
Сначала кратко расскажу о составляющих проекта:
Плата расширения для Arduino Mega (о процессе ее создания будет [3] отдельный пост, но чуть позже). Это расширение позволяет управлять выводом информации на четыре биколорных светодиодных матрицы 8х8, образующих единое поле 32х8 (разрешение невелико, зато надписи получаются достаточно крупными).
Дополнительно на плате имеется:
Модуль разрабатывался «из того, что было» и делался максимально простым для пользователей. Блок индикации построен на хорошо известных сдвиговых регистрах (74HD595D — управление светодиодами «в столбцах») и демультиплексоре (74HD138D — управление «строками»). Естественно, используется динамическая индикация.
Для того, чтобы было еще проще использовать — была написана библиотека [4] с основными примитивами («вывести строку», «бегущая строка», регулировка яркости, есть несколько эффектов).
Сам шилд не содержит МК, к нему нужно подключать «Arduino Mega» или совместимый клон (я использовал такой [5]).
Схема Shield MaTrix доступна по ссылке [6].
Это ардуино-совместимый модуль, построенный на базе Атмега328, питающийся от одной литиевой батарейки CR2032, имеющий «на борту» датчик температуры (MCP9700), кнопку, светодиод и три разъема для подключения возможных «расширений» (датчиков и т.п.).
Из особенностей датчика:
Про разработку этого модуля тоже напишу статью, если будет интерес у читателей.
В нашем случае, для экономии батарейки будем использовать МК с тактированием от внутреннего осциллятора на частоте 8МГц и «попросим» МК продолжать работать на пониженном напряжении (от 1.8В). Для этого надо в файле boards.txt добавить следующие строчки:
s328o8.name=Sensor328 (int8MHz, 1.8V)
s328o8.upload.protocol=arduino
s328o8.upload.maximum_size=30720
s328o8.upload.speed=19200
s328o8.bootloader.low_fuses=0xe2
s328o8.bootloader.high_fuses=0xda
s328o8.bootloader.extended_fuses=0x06
s328o8.bootloader.path=atmega
s328o8.bootloader.file=ATmegaBOOT_168_atmega328_pro_8MHz.hex
s328o8.bootloader.unlock_bits=0x3F
s328o8.bootloader.lock_bits=0x0F
s328o8.build.mcu=atmega328p
s328o8.build.f_cpu=8000000L
s328o8.build.core=arduino
s328o8.build.variant=standard
Самое интересное в этом фрагменте — значения фьюзов, которые определяют режим работы МК. Помните, что фьюзы в среде Arduino прошиваются в момент записи загрузчика. Так же для установки фьюзов можно воспользоваться avrdude в командной строке (или avrdude GUI — кому как удобнее).
На текущий момент (прошло уже более 20 дней) уровень заряда батареи упал до 2.83В.
Таким образом, можно предположить, что если опрашивать модуль реже, к примеру, один раз в 4 минуты и не зажигать светодиод, получим месяцы работы от одного элемента питания, что, согласитесь, очень неплохой результат.
Схема модуля доступна по ссылке [9].
Эту плату я уже упоминал ранее [1]. Отличительные особенности этой платы: атмега328, LAN на базе Wiznet W5100, интерфейс для nrf24l01+, SD, кучка аналоговых пинов и проч.
Поскольку эту плату я не разрабатывал (в отличие от двух предыдущих) — на ней останавливаться особо не буду.
Популярный RF-трансивер, работающий на 2.4ГГц. Каждый из наших модулей в этом проекте оснастим таким трансивером.
Для работы с этим модулем будем использовать библиотеку RF24 [10] (и ее форк iBoardRF24 [11]).
Создадим систему, которая будет выполнять следующие функции:
Задача в целом достаточно понятная (особенно если разделить ее на маленькие простые подзадачки), но все-таки опишем, как мы это реализуем, делая упор именно на беспроводные коммуникации между нашими модулями.
Назначим каждому из наших модулей следующие функции:
В общем и целом, думаю, все понятно и логично.
Ссылка на архив со всеми скетчами приведена в конце статьи.
Поскольку мы используем для передачи данных модуль nRF24l01+ — необходимо сразу ознакомиться с его возможностями, для чего лучше всего (хотя бы бегло) изучить его даташит [12].
Особое внимание обратим на то, что «за раз» можно передать до 32 байт данных.
Таким образом — мы должны придумать, как же удобно и «единообразно» передавать наши данные.
Предлагаю использовать следующую структуру:
// создаём структуру для передачи значений
typedef struct{
int SensorID; // идентификатор датчика
int ParamID; // идентификатор параметра
float ParamValue; // значение параметра
char Comment[16]; // комментарий
}
Message;
Очевидно, что в 32 байта эта структура замечательно умещается.
Всем модулям назначим свои идентификаторы (SensorID): Shield MaTrix — 100, Sensor Node 1 (SN1 — «домашний») — 200, SN2 («уличный») — 300, iBoard — 900.
Делаем это для того, чтобы когда устройство сообщало о своем состоянии — нам было бы понятно, о каком конкретно датчике идет речь.
В этом месте следует вспомнить о том, что у нас каждый «датчик» передает не один, а более параметров (к примеру, «домашний» датчик будет передавать как минимум 2 параметра — домашнюю температуру и напряжение на элементе питания (реально будем передавать три — дополнительно еще будем передавать флаг о необходимости замены батарейки)).
Чтобы это сделать, в скетче каждого «датчика» введем вот такую структуру:
/// создаем структуру для описания параметров
typedef struct{
float Value; // значение
char Note[16]; // комментарий
}
Parameter;
Все передаваемые значения (Value) будем передавать как float (для единообразия, хотя во многих случаях (к примеру, при передаче флага о необходимости замены батарейки) — это избыточно).
Параметр Note будем использовать для «человекопонятного комментария» (к примеру «TempIN, C» — для внутренней температуры).
Очевидно, что эта структура (Parameter) является «частью» структуры для передачи сообщений (Message).
Теперь перейдем к конкретике.
Модули SN1 м SN2. Алгоритм работы этих модулей следующий:
Для экономии батарейки будем использовать пробуждение по таймеру (будем использовать интервал в 8 секунд), но для изменения температуры (в домашних условиях) 8 секунд — это слишком часто, введем дополнительный счетчик, с помощью которого будем считать нужное количество таких 8-секундных интервалов. Предлагаю отправлять данные один раз в 4 минуты, таким образом, нужно, чтобы таймер сработал 30 раз до того момента, как данные будут измерены и отправлены.
Дополнительно для экономии — будем использовать режим энергосбережения и RF-трансивера (функция powerDown() соответствующего объекта).
Надеюсь, что пока еще не скучно и все понятно…
Итак, «домашний» датчик будет характеризоваться следующей структурой:
Parameter MySensors[NumSensors+1] = { // описание датчиков (и первичная инициализация)
NumSensors, "SN1 (in)", // в поле "комментарий" указываем пояснительную информацию о датчике и количество сенсоров
0, "TempIN, C", // температура со встроенного датчика
0, "VCC, V", // напряжение питания (по внутренним данным МК)
0, "BATT" // статус того, что батарейка в порядке (0 - батарейка "мертвая", 1 - "живая")
};
Message sensor;
А «уличный»:
Parameter MySensors[NumSensors+1] = { // описание датчиков (и первичная инициализация)
NumSensors, "SN1 (in&out)", // в поле "комментарий" указываем пояснительную информацию о датчике и количество сенсоров
0, "TempIN, C", // температура со встроенного датчика
0, "VCC, V", // напряжение питания (по внутренним данным МК)
0, "BATT", // статус того, что батарейка в порядке (0 - батарейка "мертвая", 1 - "живая")
0, "TempOUT, C" // температура со внешнего датчика
};
Message sensor;
«Уличный» датчик будет передавать как температуру с внутреннего датчика (TempIN), так и температуру с внешнего датчика (TempOUT), но использовать в данном проекте мы будем только последнюю.
Еще хотелось бы сказать немного об отличиях «домашнего» и «уличного» датчика. К «уличному» датчику дополнительно подключен (проводом) еще один сенсор на базе MCP9700 (чтобы его можно было «выбросить на улицу» и не морозить литиевую батарейку — сам модуль остается внутри помещения и не подвергается воздействию низких температур, пагубно влияющих на элементы питания.
Уличный сенсор — это маленькая платка, на которой размещен MCP9700 и блокировочный конденсатор по питанию. К плате подходит кабель (три проводка — общий, питание, сигнал). Саму платку я оградил от воздействия внешней среды с помощью нескольких слоев термоусадки. Кабель (со стороны беспроводного модуля) оснастил разъемчиком и подключил к гнезду «Analog»).
Теперь вернемся непосредственно к процессу измерения.
Для того, чтобы получить адекватное значение температуры с помощью сенсора MCP9700 необходимо знать напряжение питания, поэтому, именно с измерения этого параметра и начнем.
Чтобы реально оценить напряжения питания, воспользуемся возможностью МК (хотя мы можем так же воспользоваться имеющимся на SN делителем напряжения, подключенного к одному из аналологовых пинов атмега328).
Ничего придумывать сами не будем (все уже придумано до нас, к примеру, тут [13]).
Универсальная функция для определения напряжения питания имеет следующий вид:
long readVcc() {
// Read 1.1V reference against AVcc
// set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
ADMUX = _BV(MUX3) | _BV(MUX2);
#else
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif
delay(75); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Start conversion
while (bit_is_set(ADCSRA,ADSC)); // measuring
uint8_t low = ADCL; // must read ADCL first - it then locks ADCH
uint8_t high = ADCH; // unlocks both
long result = (high<<8) | low;
result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
return result; // Vcc in millivolts
}
Функция возвращает напряжение питания МК в миливольтах.
Теперь можно приступать к измерению всех остальных параметров, на примере датчика SN1 — 200:
// функция вычисления всех значений датчиков
void calculateValue(){
// код для получения данных
// напряжение питания
MySensors[2].Value = ((float) readVcc())/1000.0;
// температура встроенного датчика (подключен на А3)
MySensors[1].Value = (((float)analogRead(A3) * MySensors[2].Value / 1024.0) - 0.5)/0.01;
// если напряжение больше 2.4В - батарейка "живая" (1)
// если меньше - "скоро помрет" (0)
MySensors[3].Value = (MySensors[2].Value > 2.4) ? 1 : 0;
return;
}
По аналогии сделана функция и для датчика SN2 («уличный» — 300):
// функция вычисления всех значений датчиков
void calculateValue(){
// код для получения данных
// напряжение питания
MySensors[2].Value = ((float) readVcc())/1000.0;
// температура встроенного датчика (подлючен на А3)
MySensors[1].Value = (((float)analogRead(A3) * MySensors[2].Value / 1024.0) - 0.5)/0.01;
// если напряжение больше 2.4В - батарейка "живая" (1)
// если меньше - "скоро помрет" (0)
MySensors[3].Value = (MySensors[2].Value > 2.4) ? 1 : 0;
// температура внешнего датчика (подключен на А1 через разъем "Analog")
MySensors[4].Value = (((float)analogRead(A1) * MySensors[2].Value / 1024.0) - 0.5)/0.01;
return;
}
Видно, что отличие только в том, что добавилось измерение температуры «внешнего» датчика.
Чтобы как-то видеть, что наши датчики работают (и чтобы при первом включении не ждать 4 минуты до появления первых данных) сделал два режима работы датчика — «тестовый» (mode = 1) и «боевой» (mode = 2).
В «тестовом» режиме, данные отправляются один раз в 8 секунд и для индикации отправки используется встроенный на модуле светодиод.
В «боевом» режиме никакой индикации нет, данные отправляются один раз в 4 минуты (не расходуем батарейку понапрасну).
«Тестовый» режим включается сразу после установки элемента питания и работает 10 циклов (т.е. первые 10 отправок данных «визуально» идентифицируются — удобно при отладке). Можно было задействовать кнопку, имеющуюся на модуле для переключения режимов, но это я оставлю вам для самостоятельного изучения.
Собственно, с «погодными» датчиками закончили.
Теперь нужно что-то придумать с «датчиком даты/времени» (на базе iBoard).
Сам процесс получения даты/времени описывать не буду — есть соответствующие примеры [14].
Опишу только то, каким образом будем передавать эти данные.
Чтобы не нарушать «единообразие» нужно как-то втиснуть данные о дате/времени в переменную типа float.
Сделаем это следующим образом: дату будем передавать в виде «yymm.dd», а время — «hhmm.ss». При таком подходе сможем передать нужные нам значения и использовать float.
Собственно, это и отражено в структуре, описывающей наш «датчик даты/времени»:
Parameter MySensors[NumSensors+1] = { // описание датчиков (и первичная инициализация)
NumSensors, "iBoard NTP", // в поле "комментарий" указываем пояснительную информацию о датчике и количество сенсоров
0, "Date (yymm.dd)", // дата
0, "Time (hhmm.ss)" // время
};
Message sensor;
Получение и передачу значений о дате/времени вы можете сделать как угодно часто, я для теста сделал один раз в минуту (вообще, это, конечно, избыточно — имхо, достаточно раз в 10 минут это делать, чтобы иметь часы с хорошей точностью (как у Шелдона из ТВВТ)).
Чтобы код сделать достаточно универсальным — в начале скетча есть определение, указывающее на текущий часовой пояс (вы должны исправить его на правильное для вас значение):
#define TimeOffset 4 // часовой пояс - GMT +4 (МОСКВА)
Собственно, работа модуля iBoard практически идентична работе модулей SN, за единственным исключением — не используется режим энергосбережения МК и RF24 (iBoard питаем от внешнего БП и экономить «батарейку» нет смысла).
На этом, пожалуй, уже все — описана работа датчиков («проснулся — измерил — отправил — уснул»), дальше уже «рутина» — программирование модуля Shield MaTrix — получение соответствующих данных от датчиков, формирование необходимых строк и вывод их в нужном порядке.
Из «особенностей»: поскольку я программировал в основном в темное время суток, сразу добавил функцию авторегулировки яркости «дисплея» (чтобы ночью светодиоды не «выжигали» сетчатку глаза), для этого воспользовался данными, полученными с датчика освещенности, имеющегося на Shield MaTrix.
Алгоритм очень прост: измеряем текущую освещенность, если она больше, чем зафиксированный максимум — получили новый «максимум». Если же освещенность в пределах [0,«максимум»] — используем функцию map(...), чтобы задать значение яркости «дисплея» (от 20 до 255, первое значение выбрал опытным путем).
В нормальном режиме работы данные выводятся на «дисплей» в цикле в следующем порядке:
Если же от какого-либо датчика пришли данные о том, что батарейка «на последнем издыхании» (соответствующий флаг) — добавляется еще один шаг — отображение предупреждающего сообщения (красным цветом, разумеется), типа: «Замените батарейку (CR2032) в уличном датчике!» с упоминанием того датчика, где эту батарейку следует заменить.
Собственно, на видео все видно:
В общем, проявите свою фантазию.
Список покупок:
P.S. конечно, можно использовать то оборудование, что у вас есть с соответствующими корректировками кода (постарался дать максимум комментариев — разобраться должно быть не очень сложно).
Автор: avstepanov
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/umny-j-dom/48892
Ссылки в тексте:
[1] пары: http://habrahabr.ru/post/171613/
[2] больших: http://habrahabr.ru/post/195884/
[3] будет: http://toster.ru/q/48336
[4] библиотека: https://github.com/stepanovalex/MaTrix
[5] такой: http://devicter.ru/goods/Freaduino-Mega-ATMega-2560
[6] по ссылке: http://habrastorage.org/storage3/f40/57c/5fc/f4057c5fcd06168d494bbc2134a62065.jpg
[7] через ISP: http://devicter.ru/goods/USBtinyISP-Arduino-bootloader-programmer
[8] программатор на базе FT232: http://devicter.ru/goods/Foca-v2-1-FT232RL
[9] по ссылке: http://habrastorage.org/storage3/59e/d11/fd5/59ed11fd54a14dbc3f38611e8ecd9d98.jpg
[10] RF24: https://github.com/maniacbug/RF24/
[11] iBoardRF24: https://github.com/andykarpov/iBoardRF24
[12] даташит: https://www.pvsm.ruftp://imall.iteadstudio.com/Modules/IM120606002_nRF24L01_module/DS_nRF24L01.pdf
[13] тут: http://blog.unlimite.net/?p=224
[14] примеры: https://www.google.ru/search?q=arduino+ntp+wiznet
[15] Народный мониторинг: http://narodmon.ru/
[16] Архив со скетчами: http://medialounge.ru/files/sm-sn.rar
[17] Shield MaTrix: http://devicter.ru/goods/Shield-MaTrix
[18] Sensor Node: http://devicter.ru/goods/Sensor-Node
[19] iBoard: http://devicter.ru/goods/Iteaduino-Iboard
[20] nRF24l01+: http://devicter.ru/goods/modul-2-4g-nrf24l01
[21] Источник: http://habrahabr.ru/post/202898/
Нажмите здесь для печати.