Как стать автором
Обновить

Беспроводные коммуникации «умного дома». Часть вторая, практическая

Время на прочтение13 мин
Количество просмотров138K
После написания пары больших постов про «радиофицированный» умный дом, было достаточно много желающих получить код, который помог бы разобраться с этой темой более детально.
Свой исходный вариант кода по некоторым причинам выкладывать не хотелось — подготовил «облегченный» вариант, который позволит объяснить мои основные задумки.

Для того, чтобы пост получился наиболее зрелищным и полезным, сегодня реализуем домашнюю мини-систему (часы, календарь, погода, контроль уровня заряда батареек в датчиках и т.п.), состоящую из одного «главного» модуля (с большой светодиодной матрицей в качестве индикатора), двух автономных датчиков (будут измерять температуру) и модуля синхронизации времени через NTP.

Сначала кратко расскажу о составляющих проекта:

Shield MaTrix

Плата расширения для Arduino Mega (о процессе ее создания будет отдельный пост, но чуть позже). Это расширение позволяет управлять выводом информации на четыре биколорных светодиодных матрицы 8х8, образующих единое поле 32х8 (разрешение невелико, зато надписи получаются достаточно крупными).

Дополнительно на плате имеется:
  • Встроенный модуль часов (RTC) — на базе DS1307 с резервной батарейкой питания (CR1220 или CR1226)
  • Интерфейс для подключения RF-модуля nRF24L01+
  • Тактовая кнопка (например, для выключения звука будильника)
  • ИК-приемник на 38кГц (для удаленного управления)
  • Разводка для подключения RGB-светодиода с общим катодом или анодом (выбор осуществляется джампером)
  • Датчик освещенности (например, для автоматической регулировки яркости)
  • Пьезо-излучатель «пищалка»
  • I2C-интерфейс
  • xBee-интерфейс
  • Интерфейс для подключения Arduino-шилдов.


Модуль разрабатывался «из того, что было» и делался максимально простым для пользователей. Блок индикации построен на хорошо известных сдвиговых регистрах (74HD595D — управление светодиодами «в столбцах») и демультиплексоре (74HD138D — управление «строками»). Естественно, используется динамическая индикация.

Для того, чтобы было еще проще использовать — была написана библиотека с основными примитивами («вывести строку», «бегущая строка», регулировка яркости, есть несколько эффектов).

Сам шилд не содержит МК, к нему нужно подключать «Arduino Mega» или совместимый клон (я использовал такой).

Схема Shield MaTrix доступна по ссылке.

Sensor Node

Это ардуино-совместимый модуль, построенный на базе Атмега328, питающийся от одной литиевой батарейки CR2032, имеющий «на борту» датчик температуры (MCP9700), кнопку, светодиод и три разъема для подключения возможных «расширений» (датчиков и т.п.).

Из особенностей датчика:
  • Размер платы — 3х4 см.
  • Два варианта питания nRF24L01+: питание на модуль подается постоянно или берется с одного из цифровых пинов (выбор осуществляется джампером).
  • Программирование через ISP или программатор на базе FT232 (или подобном) из среды Arduino.
  • Имеется делитель напряжения (для измерения напряжения питания)
  • Два цифровых пина выведены на разъем «Digital» (в разъеме так же присутствует питание).
  • Два аналоговых пина — на разъем «Analog» (питание тоже есть).
  • Шина I2C — отдельный соответствующий разъем (тоже с питанием).
  • Тактирование МК возможно как с помощью кварцевого резонатора 16МГц, установленного на плате, так и с помощью встроенного осциллятора (это, конечно, не «фишка» конкретного модуля, а просто возможности atmega328).


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

В нашем случае, для экономии батарейки будем использовать МК с тактированием от внутреннего осциллятора на частоте 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


UPD: при параметрах выше модуль стартует с напряжением питания от 2.4В. Чтобы добиться старта от 1.8В нужно понизить рабочую частоту МК до 1МГц. Делается это следующим образом:

s328o1.name=Sensor328p (int1MHz, 1.8V)

s328o1.upload.protocol=arduino
s328o1.upload.maximum_size=30720
s328o1.upload.speed=19200

s328o1.bootloader.low_fuses=0x62
s328o1.bootloader.high_fuses=0xda
s328o1.bootloader.extended_fuses=0x06
s328o1.bootloader.path=atmega

s328o1.bootloader.file=ATmegaBOOT_168_atmega328_pro_8MHz.hex

s328o1.bootloader.unlock_bits=0x3F
s328o1.bootloader.lock_bits=0x0F

s328o1.build.mcu=atmega328p
s328o1.build.f_cpu=1000000L
s328o1.build.core=arduino
s328o1.build.variant=standard


Самое интересное в этом фрагменте — значения фьюзов, которые определяют режим работы МК. Помните, что фьюзы в среде Arduino прошиваются в момент записи загрузчика. Так же для установки фьюзов можно воспользоваться avrdude в командной строке (или avrdude GUI — кому как удобнее).

О батарейном питании датчика
К сожалению, я пока еще не успел провести полные натурные исследования на тему «сколько же модуль Sensor Node (SN) сможет работать от одной батарейки» — эксперимент еще продолжается.
Условия следующие:
  • Используется «энергосберегающий» код, который я привожу в данной статье
  • МК работает на частоте 8МГц от внутреннего осциллятора
  • SN «просыпается» 1 раз в 8 секунд, измеряет температуру и напряжение питания, отправляет данные в эфир (для индикации используется встроенный светодиод — просто чтобы видеть, что модуль еще «живой»)

На текущий момент (прошло уже более 20 дней) уровень заряда батареи упал до 2.83В.

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

Схема модуля доступна по ссылке.

iBoard

Эту плату я уже упоминал ранее. Отличительные особенности этой платы: атмега328, LAN на базе Wiznet W5100, интерфейс для nrf24l01+, SD, кучка аналоговых пинов и проч.
Поскольку эту плату я не разрабатывал (в отличие от двух предыдущих) — на ней останавливаться особо не буду.

nRF24l01+

Популярный RF-трансивер, работающий на 2.4ГГц. Каждый из наших модулей в этом проекте оснастим таким трансивером.
Для работы с этим модулем будем использовать библиотеку RF24 (и ее форк iBoardRF24).

Общая идея


Создадим систему, которая будет выполнять следующие функции:
  • Отображение текущего времени, дня недели, даты.
  • Вывод информации с двух беспроводных датчиков (температура «дома» и «на улице»).
  • Индикация состояния литиевых батареек в беспроводных модулях.
  • Контроль разряда батареи (с выдачей предупреждающего сообщения о необходимости замены элемента питания).
  • Получение времени от NTP-сервера и синхронизация даты/времени по нему.

Задача в целом достаточно понятная (особенно если разделить ее на маленькие простые подзадачки), но все-таки опишем, как мы это реализуем, делая упор именно на беспроводные коммуникации между нашими модулями.

Назначим каждому из наших модулей следующие функции:
  1. Shield MaTrix: прием данных от всех беспроводных датчиков (iBoard — тоже будет «датчиком даты/времени»), отображение данных, обработка информации о состоянии элементов питания, часы реального времени (с резервной батарейкой), авторегулировка яркости «дисплея» в зависимости от освещенности.
  2. Sensor Node: получение данных с датчика температуры, получение информации об уровне заряда элемента питания и передача этих данных. Дополнительно реализуем в скетче все, что необходимо для экономии батарейки (задействуем режим «сна» МК).
  3. iBoard: получение адреса с помощью DHCP, формирование запроса к NTP-серверу, получение и парсинг ответа от сервера, формирование значения даты/времени и отправка этих данных в эфир.


В общем и целом, думаю, все понятно и логично.
Ссылка на архив со всеми скетчами приведена в конце статьи.


Приступим


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

Особое внимание обратим на то, что «за раз» можно передать до 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. Алгоритм работы этих модулей следующий:
  1. В функции setup() — инициализация
  2. В основном цикле loop():
    • Проснуться по таймеру
    • Проверить, не настала ли «пора»
    • Если время пришло — получить данные о температуре и напряжении элемента питания
    • Включить RF-трансивер
    • В цикле отправить сообщения о значении всех полученных параметров
    • Выключить трансивер
    • Уснуть

Для экономии батарейки будем использовать пробуждение по таймеру (будем использовать интервал в 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).
Ничего придумывать сами не будем (все уже придумано до нас, к примеру, тут).
Универсальная функция для определения напряжения питания имеет следующий вид:
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).
Сам процесс получения даты/времени описывать не буду — есть соответствующие примеры.

Опишу только то, каким образом будем передавать эти данные.
Чтобы не нарушать «единообразие» нужно как-то втиснуть данные о дате/времени в переменную типа 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, первое значение выбрал опытным путем).

В нормальном режиме работы данные выводятся на «дисплей» в цикле в следующем порядке:
  1. Время (шрифт digit6x8future, зеленым цветом)
  2. День недели (шрифт font6x8, красным цветом)
  3. Дата (шрифт font6x8, оранжевым цветом (красный+зеленый)
  4. Температура дома (тот же шрифт, зеленый цвет). Дополнительно выводим значок с изображением батарейки и уровнем ее заряда.
  5. Температура на улице (тот же шрифт, оранжевым цветом).

Если же от какого-либо датчика пришли данные о том, что батарейка «на последнем издыхании» (соответствующий флаг) — добавляется еще один шаг — отображение предупреждающего сообщения (красным цветом, разумеется), типа: «Замените батарейку (CR2032) в уличном датчике!» с упоминанием того датчика, где эту батарейку следует заменить.

Результат


Собственно, на видео все видно:


Что дальше?


  1. Каждый из модулей при таком подходе может быть «датчиком» (к примеру, модуль MaTrix можно сделать «беспроводным датчиком освещенности»).
  2. К каждому датчику SN можно подключить дополнительные сенсоры (влажность, давление и т.п.).
  3. Из датчиков SN можно создать сеть (измерение всех «интересных» параметров и передача их значений в эфир).
  4. Каждый из модулей работает как по своей собственной «программе» и выполняет свои «изолированные» функции, так и быть «управляемым извне» по командам от остальных датчиков.
  5. В систему легко добавить логирование параметров (к примеру, на уже имеющемся в проекте iBoard: достаточно дописать кусок кода, чтобы он «слушал» эфир и записывал данные на SD-карту (напомню, на iBoard есть слот для microSD) или постил данные куда-нибудь в веб, например на проект "Народный мониторинг" или куда-нибудь на собственный сервер, где они бы складывались в БД для дальнейшего использования).
  6. Можно чуть расширить структуру и добавить адресные команды (например, от уличного датчика на MaTrix (300 -> 100) исполнить какое-нибудь действие при наступлении какого-нибудь условия).

В общем, проявите свою фантазию.

Полезные ссылки


Список покупок:


P.S. конечно, можно использовать то оборудование, что у вас есть с соответствующими корректировками кода (постарался дать максимум комментариев — разобраться должно быть не очень сложно).
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 64: ↑63 и ↓1+62
Комментарии145

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань