[Из песочницы] Бюджетный мониторинг температуры: Arduino + Zabbix

logo.png

У нас в организации развёрнут сервер Zabbix для мониторинга работоспособности серверов и АРМов. Из-за особенностей техпроцесса оборудование «размазано» по нескольким помещениям и разнесено по территории предприятия. Естественно, вместе с основными параметрами компьютеров (работает/не работает) хочется контролировать и микроклимат в серверных. При этом, как обычно, возможности весьма ограничены, и «выбить» значительные средства на сложные системы мониторинга температуры (к ним я отношу и платы управления с термодатчиками для стоечных ИБП APC) — это отдельный квест.

В основной серверной всё просто — установлена одна такая плата (закуплена давным-давно предшественником вместе с основным оборудованием), воткнут APC-шный термодатчик, заведён агент в Заббиксе, всё работает по SNMP. Скучно :) На мониторинг удалённой аппаратной оборудования нет, средств тоже — см. выше. Поэтому было решено проявить смекалку, сэкономить бюджет и заодно прокачать новый навык путём конструирования простого и дешёвого «наколенного» решения, вписывающегося, тем не менее, в существующую инфраструктуру мониторинга Zabbix.
Необходимые компоненты:


Общая стоимость компонентов — $10 с доставкой.

Сборка устройства не составляет труда. Сетевой модуль надевается на основную плату «бутербродом», термодатчик припаивается к его пинам. Подключение датчика: красный +5 В, чёрный — земля, жёлтый — данные; между +5V и Data припаиваем подтягивающий резистор 4,7 кОм.

Пин для данных выбирается с учётом пинов, используемых сетевым модулем (D10 — SS; D11 — MOSI; D12 — MISO; D13 — SCK; D2 — IRQ).

Грабли: в прототипе устройства столкнулся с конфликтом — данные о температуре выдавались случайным образом, «через два на третий». Причиной оказалось то, что я прицепил термодатчик на пин 2, который, как потом нашёл на просторах интернета, используется сетевым модулем для генерации прерывания при поступлении пакета. Переставил на 4-й — заработало как часы.

После сборки аппаратной части переходим к программной.

Устройство будет работать в сети и притворяться заббикс-агентом, для этого ему нужен MAC и IP-адрес. Решаем, как удобнее — жёстко зашить при программировании, генерировать MAC из адреса температурного датчика и получать IP по DHCP, и т.д. Я пошёл по простейшему пути и захардкодил оба параметра.

Протокол обмена с заббикс-сервером описан в документации. Наше устройство будет откликаться на две команды — agent.ping и env.temp (здесь оставлен простор для дальнейшего творчества, можно привязать любой из модулей расширения, доступных для ардуино — хоть датчик влажности, хоть освещённости — да что душе угодно). На все остальные команды оно будет ругаться отвечать стандартным ответом, понятным заббикс-серверу.

Для тех, кто начинает с нуля (как я) — программирование Arduino выполняется с помощью Arduino IDE, установка и настройка которой элементарны. Для работы компонентов необходимы библиотеки UIPEthernet и OneWire, которые устанавливаются и подключаются к проекту через меню Скетч — Подключить библиотеку — Управлять библиотеками…
Если у вас будут другие компоненты (например, сетевой модуль не на enc28j60, а на другом чипе) — понадобятся и другие библиотеки!

Код работы с сетевым модулем и с датчиком температуры — типовой, из интернета, с некоторыми допущениями и упрощениями.

После заливки кода в контроллер и подключения ethernet-кабеля проверяем из консоли:

$ zabbix_get -s 192.168.4.5 -k agent.ping
1
$ zabbix_get -s 192.168.4.5 -k env.temp
23.12
$ zabbix_get -s 192.168.4.5 -k bla-blah
ZBX_NOTSUPPORTED


Грабли: выложенная на zabbix.com скомпилированная версия zabbix_get для Windows устарела и использует другой протокол (с заголовком ZBXD\x01 в запросе сервера). Линуксовая версия актуальна и протокол соответствует приведенному коду.

Всё работает, как и задумано. В админке заббикса создаём новый хост с выбранным IP, в нём — два ключа, Numeric (unsigned) agent.ping и Numeric (float) env.temp, наслаждаемся работой. Графики, триггеры — всё как обычно.

Питание устройства — через родной USB. Корпус — по желанию: подходящая пластиковая коробочка, термоусадка, синяя изолента.

Разрешение датчика — примерно 0.06 (точнее, 1/16) °С, точность — при погружении в таящий снег показал 0.19 °С (может, опустился бы и ниже, но снега было мало и он весь быстро растаял). Считаю, для устройства стоимостью 10 долларов и описанных целей — более чем достаточно.

Скетч
#include 
#include 

byte mac[] = { 0xDE, 0x05, 0xB6, 0x27, 0x39, 0x19 }; // random MAC
byte ip[] = { 192, 168, 4, 5 }; // IP address in local network
String readString = String(20); 
byte addr[8];

OneWire ds(4); // DS18B20 at pin 4
EthernetServer server(10050); // Zabbix port

void setup() {
  Ethernet.begin(mac, ip);
  server.begin();
  ds.search(addr);
}

void loop() {
  byte data[2];
  float celsius;

  readString = "";
  if (EthernetClient client = server.available())
  {
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (c == '\n') // end of query from zabbix server 
        {
          client.print("ZBXD\x01"); // response header
          if (readString == "agent.ping") {
            byte responseBytes [] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '1'}; 
            client.write(responseBytes, 9);
          } else 
          if (readString == "env.temp") {
            ds.reset();
            ds.select(addr);
            ds.write(0x44);  // start conversion with regular (non-parasite!) power
            delay(1000);  
            ds.reset();
            ds.select(addr);
            ds.write(0xBE);  // read Scratchpad

            data[0] = ds.read();
            data[1] = ds.read();

            int16_t raw = (data[1] << 8) | data[0];
            celsius = (float)raw / 16.0;

            byte responseBytes [] = {(byte) String(celsius).length(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
            client.write(responseBytes, 8);
            client.print(celsius);
          }
          else {
            byte responseBytes [] = {0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
            client.write(responseBytes, 8);
            client.print("ZBX_NOTSUPPORTED");
          }
          break;
        }
        else if (readString.length() < 20) {
          readString = readString + c;
        }
      }
    }
    delay(10);
    client.stop();
  }
}

© Geektimes