вторник, 24 декабря 2013 г.

Shield MaTrix и Sensor Node. Продолжение...

Начало было описано тут.

Напомню, на тот момент было реализовано:
  • C помощью Shield MaTrix (SM) выводим следующую информацию:
    • текущее время,
    • день недели,
    • дата,
    • температура дома,
    • температура "за бортом",
    • сообщение о необходимости заменить батарейку (когда это действительно требуется).

  • С помощью Sensor Node (SN) получаем и передаем информацию:
    • о домашней температуре,
    • о температуре "за бортом",
    • уровне заряда батарейки модуля.


Устройства между собой коммутируются с помощью беспроводных модулей nRF24l01+.


После нескольких полноценных дней эксплуатации выяснилось, что есть вещи, которые не слишком удобны:
  • При разрядке батареи питания одного из сенсоров (попалась "севшая" батарейка среди новых):
    • слишком много времени всего цикла отображения занимает назойливая надпись с требованием замены батарейки,
    • отображается неверная информация о текущей температуре (SM все время показывает последние полученные данные от датчика).
  • Поскольку модуль хотелось использовать как "продвинутые" часы - необходимо увеличить интервал отображения собственно текущего времени.
  • Предусмотреть краткое представление даты и дня недели (опять же для более оперативного получения информации).
Опять же (помимо исправлений "недочетов"), всегда "хочется большего", поэтому добавим:
  • Индикатор "уровня сигнала" от беспроводных датчиков (показатель скорее качественный и будет анализировать период получения данных от SN).
  • Добавим датчик давления и отобразим полученную информацию.
  • Построим систему поздравлений (или напоминаний о памятных событиях).
  • Более точно будем отображать информацию об уровне заряда элемента питания беспроводного датчика.
  • Задействуем кнопку на SM для переключения формата дня недели и даты между "полным" (Понедельник 23 декабря 2013 года) и "кратким" (Пн 23.12.13) представлениями.
Итак, "Поехали!" (с).

Чтобы адекватно отслеживать информацию по активности датчика введем новую структуру:
typedef struct {
  int SensorID;
  uint32_t Last;    // последнее время коннекта unixtime
  int Period;       // периодичность отправки данных датчиком (штатная) в секундах
  int Batt;         // сколько раз сообщили о разряженной батарейке
  int Signal;       // сколько раз пропустили сообщение
}
dSensorStruct;
Собственно, комментарии описывают, для чего нужен тот или иной параметр.

Сразу же эту структуру заполним актуальными данными:
dSensorStruct dSensor[2] = {
  200, 0, 250, 0, 0,
  300, 0, 250, 0, 0,
};
Обратите внимание, что для датчиков мы указываем периодичность отправки данных в 250 секунд (хотя на самом деле отправляем данные один раз в 240 секунд (раз в 4 минуты)) - сделано это специально, чтобы гарантированно отслеживать период отправки.

Теперь необходимо немного модифицировать функцию listenRF24() таким образом, чтобы в ней заполнялись данные вышеприведенной структуры:
void listenRF24() {
  waitRF24 = false;
  if ( radio.available() )
  {
    bool done = false;
    while (!done)
    {
      done = radio.read( &sensor, sizeof(sensor) );
   
          // коррекция времени (только если уже получили данные о дате и времени)
          if(sensor.SensorID==900 && sensor.ParamID==2 && dataReady) {
            syncH=int(sensor.ParamValue/100);
            syncM=int(sensor.ParamValue-syncH*100);
            syncS=int((sensor.ParamValue-syncH*100-syncM+0.002)*100);
            RTC.adjust(DateTime(syncYear, syncMonth, syncDay, syncH, syncM, syncS));
            //tone(45, 2000, 200);
            dataReady = false;
          }
          if(sensor.SensorID==900 && sensor.ParamID==1) {
            syncYear=int(sensor.ParamValue/100);
            syncMonth=int(sensor.ParamValue-syncYear*100);
            syncDay=int((sensor.ParamValue-syncYear*100-syncMonth+0.002)*100);
            dataReady = true;
          }
       
          now = RTC.now();
          uTime=now.unixtime();

          // датчик 1 (домашний)
          if(sensor.SensorID==200) {
            dSensor[0].Last = uTime; // обновим время коннекта
            dSensor[0].Signal = 0;   // поскольку "услышали" датчик, сбросим счетчик "пропущенных пакетов"
            if(sensor.ParamID==1) t1in=sensor.ParamValue;
            if(sensor.ParamID==2) vcc1=sensor.ParamValue;
            if(sensor.ParamID==3) b1=sensor.ParamValue;
            if(sensor.ParamID==4) bar=sensor.ParamValue;
            if(sensor.ParamID==5) t1bar=sensor.ParamValue;
          }
          // датчик 2 (домашний & уличный)
          if(sensor.SensorID==300) {
            dSensor[1].Last = uTime; // обновим время коннекта
            dSensor[1].Signal = 0;   // поскольку "услышали" датчик, сбросим счетчик "пропущенных пакетов"
            if(sensor.ParamID==1) t2in=sensor.ParamValue;
            if(sensor.ParamID==2) vcc2=sensor.ParamValue;
            if(sensor.ParamID==3) b2=sensor.ParamValue;
            if(sensor.ParamID==4) t2out=sensor.ParamValue;
          }
      }
  }
}
Правки в функции выделены полужирным шрифтом.

Таким образом, после этих подготовительных шагов можно приступать к "визуальным эффектам".

Уровень сигнала

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

Будем оценивать информацию о времени между полученными посылками данных от беспроводных датчиков.

При этом, если интервал получения посылок меньше, чем указано в структуре (параметр Period), то будем считать, что уровень приема - 100%, если же получаем данные чаще, чем 2*Perod - 75%, 4*Period - 50%, 8*Period - 25%. Если же данные получаем и того реже - отображаем 0% и увеличиваем счетчик "пропущенных пакетов". 

Реализуем это с помощью функции:
// возвращает символ уровня приема в зависимости от идентификатора датчика
char signalSymbol(int sensor_id) {
  char Sym;
  now = RTC.now();
  uTime=now.unixtime();

  uint32_t tDelay = uTime-dSensor[sensor_id].Last;

  if(tDelay < dSensor[sensor_id].Period) {
    Sym='\xB5'; // 100%
  }
  else if (tDelay < 2*dSensor[sensor_id].Period) {
    Sym='\xB6';  // 75%
  }
  else if (tDelay < 4*dSensor[sensor_id].Period) {
    Sym='\xB7';  // 50%
  }
  else if (tDelay < 8*dSensor[sensor_id].Period) {
    Sym='\xB8';  // 25%
  }
  else {
    Sym='\xB9';  // 0%
    // увеличим счетчик "пропущенных пакетов"
    dSensor[sensor_id].Signal++;
  }
  return Sym;
}

Уровень заряда элемента питания

В ходе стресс-тестов SN удалось получить полную кривую разряда батареи, поэтому можно подправить функцию, которая возвращает символ уровня заряда батареи в зависимости от напряжения питания для более корректного отображения статуса батарейки:
// возвращает символ батарейки (уровень заряда) в зависимости от напряжения питания
char battSymbol(float vcc) {
  char Sym;
  if(vcc > 2.85) {
    Sym='\xB0'; // 100%
  }
  else if (vcc > 2.80) {
    Sym='\xB1';  // 75%
  }
  else if (vcc > 2.75) {
    Sym='\xB2';  // 50%
  }
  else if (vcc > 2.70) {
    Sym='\xB3';  // 25%
  }
  else {
    Sym='\xB4';  // 0%
  }
  return Sym;
}
В общем-то, функция осталась без изменений, поменялись только конкретные пороговые значения, с которыми идет сравнение.

Переключение форматов даты

Задействуем встроенную кнопку на SM (на фото сверху). 



Кнопка уже имеет внешний подтягивающий резистор, поэтому просто включим соответствующий цифровой пин в режим "вход":
  // кнопка
  pinMode(BUTTON, INPUT);
Для определения режимов, введем бинарную переменную mode (1 - короткие даты, 0 - длинные).

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

Чтобы отслеживать состояние кнопки, добавим соответствующий блок в функцию code():
void code(){
  // автоматическая регулировка яркости в зависимости от освещенности
  brightLcur = analogRead(LightSENS);
  if(brightLcur > brightLmax) {
    brightLmax = brightLcur;
  }

  brightL = map(brightLcur, 0, brightLmax, 20, 255);

  mymatrix.brightness(brightL);
  // обработка ИК-команд
  if (irrecv.decode(&results)) {
    storeCode(&results);
    irrecv.resume();
  }

  // радио
  if (waitRF24) {
    listenRF24();
  }

  // кнопка
  if ((millis()>lastButton) && (digitalRead(BUTTON) == LOW)) {
    mode = !mode;
    if (mode) {
      tone(45, 2000, 100);
// короткий сигнал
    }
    else {
      tone(45, 2000, 250); // длинный сигнал
    }
    lastButton = millis()+500;
  }
}
В данном случае никак не отрабатывается "дребезг" кнопки, но введена задержка на опрос кнопки не чаще, чем один раз в 0.5 сек.

Поздравления

Чтобы подготовить наш скетч к выдаче поздравительных сообщений необходимо создать структуру:
// структура для поздравлений
typedef struct {
  int Day;      // день
  int Month;    // месяц
  char Message[256];       // поздравление
}
GreatingsStruct;
и наполнить ее данными:
#define NumMessages 8
GreatingsStruct gMessage[NumMessages] = {
 1, 1, "C Ho\x97\xAB\xA1"" \x81""o\x99""o\xA1""!",  // C Новым годом!
 7, 1, "C Po\x9B\x99""ec\xA4\x97""o\xA1""!",  // С Рождеством!
 23, 2, "C 23 \xA5""e\x97""pa\xA0\xAF""!",  // С 23 февраля!
 8, 3, "C 8 \xA1""ap\xA4""a""!",  // С 8 марта!
 1, 4, "C \x99\xA2""e\xA1"" \x82""ypa\x9F""a!",  // С днем Дурака!
 1, 5, "C \x99\xA2""e\xA1"" \x97""ec\xA2\xAB"" \x9D"" \xA4""py\x99""a!", // С днем весны и труда!
 9, 5, "C \x99\xA2""e\xA1"" \x89""o\x96""e\x99\xAB""!", // С днем Победы!
 31, 12, "C Hac\xA4""y\xA3""a\xAE\xA9\x9D\xA1""!", // С Наступающим!
};
Собственно, тут все достаточно очевидно. Есть день (первый параметр в структуре), есть месяц (второй параметр в структуре) и собственно сообщение (третий параметр).

Вы самостоятельно можете дополнить эту структуру собственными памятными датами (дни рождения, годовщины и т.п.)

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

Атмосферное давление

У меня не было готового датчика давления, но был электронный компонент BMP085, поэтому я просто развел маленькую платку и сделал датчик для подключения к SN через разъем i2c:


Вы можете поступить так же (изготовить датчик самостоятельно) или приобрести уже готовый датчик

При выборе датчика обратите внимание на следующую вещь: SN работает от элемента питания с напряжением 3В, а большая часть готовых датчиков рассчитана на работу от 5В (хотя сам сенсор BMP085 работает как раз от 3В и для его питания на плату датчика ставится линейный стабилизатор питания, который понижает "входные" 5В до необходимых 3В). 

Наличие стабилизатора не является чем-то особенным, но как правило его эффективнсть крайне мала (т.е. будет неэффективно расходоваться батарейка модуля SN и ее придется чаще менять).

Если же датчик без линейного стабилизатора найти не удастся, то такой датчик можно подключить непосредственно к Shield MaTrix, который тоже имеет i2c-разъем и подключен к источнику питания (а не батарейке, как SN). Естественно, функции чтения данных с датчика нужно будет перенести в скетч SM.

Но вернемся к нашему случаю (датчик подключен к Sensor Node):


Для этого датчика пришлось чуть-чуть поправить код (добавил пару функций для чтения данных с датчика давления) и расширил структуру с передаваемыми SN данными:
Parameter MySensors[NumSensors+1] = {    // описание датчиков (и первичная инициализация)
  NumSensors, "SN1 (in)",                // в поле "комментарий" указываем пояснительную информацию о датчике и количество сенсоров
  0, "TempIN, C",                        // температура со встроенного датчика
  0, "VCC, V",                           // напряжение питания (по внутренним данным МК)
  0, "BATT",                             // статус того, что батарейка в порядке (0 - батарейка "мертвая", 1 - "живая")
  0, "Pressure",                         // давление (мм.рт.ст.)
  0, "TempBAR, C"                        // температура с барометра
};
Message sensor; 

Отображение данных

Теперь все подготовительные шаги сделаны, данные готовы - можно править ту часть скетча, где организован вывод соответствующих параметров.

Это реализовано в основном цикле скетча (функция loop()) - через конструкцию switch - case.

Поскольку эта функция достаточно объемная, в данной записи мы ее приводить и подробно разбирать не будем - смотрите полный код скетча, внутри него достаточно много комментариев, которые помогут разобраться с соответствующими "ветками" переключателя.


Заключение

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

И очевидно, что не стоит останавливаться на достигнутом - далеко не все возможности Shield MaTrix и Sensor Node исчерпаны. 

Дерзайте!


Примечание: для корректного отображения "уровня приема" необходимо использовать актуальную версию библиотеки MaTrix.

Архив со всеми необходимыми скетчами находится по ссылке.

1 комментарий:

  1. Ранее не пользовался USBTinyISP. очевидно поэтому испытываю проблемы с программированием SensorNode. Все установил, в Arduino IDE настроил- выбрал SensorNode, программатор, но при попытке записи скетча avrdude: stk500_getsync(): not in sync: resp=0x00
    что посоветуете?

    ОтветитьУдалить