Начинаем работать с BACnet
Многие из нас, наверное, смотрели боевики и видели, как герои, сидя в темной комнате, удаленно управляют дверями в зданиях, открывая и закрывая их. Управляли освещением, лифтами, чтобы помочь кому-то куда-то пройти или, наоборот, помешать — ну и думали, что да, в теории такое возможно, и в каких-то зданиях, вероятно, есть какая-то автоматизация, но больше это кажется какой-то выдумкой. Так получилось, что в устройство, которое мы делаем, заказчик захотел добавить поддержку BACnet. Так я познакомился с тем, что, оказывается, существует специализированный протокол для автоматизации зданий, который развивается аж с 1987 года.
О протоколе
В этой статье я хочу немножко рассказать о самом протоколе в общем, и как можно самим что-то попробовать собрать из исходников, какие инструменты есть, возможно, кому-то придется добавлять поддержку протокола в свои устройства — и эта статья окажется полезной.
Протокол поддерживает разные среды передачи данных, из самого популярного, пожалуй — RS-485, Ethernet и ZigBee. Наше устройство поддерживает BACnet протокол, работающий поверх Ethernet (Annex J в спецификации BACnet).
Протокол интересен тем, что не нужно ничего особенно настраивать, в том смысле, что если взять, например, Modbus, то там будет просто набор входов, выходов и регистров, и как это все соотносится с реальным оборудованием, можно выяснить только при помощи документации, а потом вручную это нужно настроить, начиная с адреса устройства и заканчивая конкретными входами и выходами, или вот датчик температуры — он может быть представлен в виде целого значения в регистре, и кто-то его представляет в градусах Цельсия, кто-то в десятых долях градуса Цельсия, кто-то еще в чем. В BACnet же, новое подключенное оборудование находится средствами самого протокола, вручную нужно задать только уникальный идентификатор устройства Device Instance.
Идея в том, что вся структура устройства представлена в виде коллекции объектов, а каждый объект имеет набор свойств, которые можно читать, а некоторые и писать, т.е. устройство — это объект, который имеет как одно из свойств список других свойств, которые он содержит, а также как одно из свойств — список других объектов, которые находятся внутри него. Таким образом можно узнать все об устройстве, только зная то, что оно существует в сети. И если говорить про тот же датчик температуры, то это будет объект аналогового входа с текстовым описанием в description, что это температура в комнате, например, и что важно с единицами измерения, будь то градусы Цельсия или Фаренгейта.
Объекты, которые могут быть описаны протоколом, разнообразны, начиная от дискретных входов и выходов (прям как в Modbus, с тем исключением, что эти входы и выходы можно подписать нормальным текстом в самом объекте) и заканчивая календарями, освещением, лифтами и эскалаторами. Можно делать такие интересные вещи, как убавить яркость света в коридоре в нерабочее время, или, если нужно выключать освещение совсем, то предупредительно моргнуть светом, перед тем как оставить задержавшихся на рабочем месте людей в полной темноте, и все это средствами самого протокола, просто настраивая свойства объектов.
Да, про асинхронность протокола, тут все довольно гибко, можно вручную читать свойства объектов, когда они нужны, т.е. можно просто посмотреть значение температуры с датчика, а можно задать пределы допустимых значений для этого датчика, подписаться на события и получать сообщения только при выходе температуры за установленные пределы, или, как вариант, получать новые значения, когда текущее значение изменится на заданную величину от переданного до этого, или просто периодически.
Ну и в целом, протокол благодаря объектной модели очень гибкий, есть объекты и свойства, которые обязательны для поддержки устройством, но большинство свойств и объектов опциональны. А так как гибкость имеет и обратную сторону — взяв новое устройство в руки, может быть непонятно, а подойдет оно вообще сюда, то придумали несколько градаций соответствия — профилей, ну как пример: дисплей оператора (B-OD), умный сенсор (B-SS), или, что как раз мы реализовали в нашем проекте, — специальный контроллер B-ASC (BACnet Application Specific Controller). В спецификации даже имеется специальная форма: BACnet Protocol Implementation Conformance Statement, заполнив которую получится документ с описанием соответствия вашего устройства (прям как у Гермеса Конрада — бюрократа 36 уровня).
Проект
Наша железка представляет собой что-то вроде агрегатора разных протоколов от разных устройств, и внутри все представлено, как набор некоторых абстрактных сенсоров и объектов управления — контролов. И, как я упоминал ранее, появилась задача прикрутить BACnet так, чтобы наше устройство представляло все то, что к нему подключено (ну и некоторые свои внутренности) как устройство в BACnet сети (к слову не только BACnet, у нас есть еще и web-интерфейс, и командная строка, и SNMP, и AWS). Так как стандарт описан чуть менее чем на 1400 страницах и продолжает развиваться, то было понятно, что лучше за основу взять что-то готовое и желательно открытое.
Мы использовали библиотеку bacnet-stack — она написана на чистом C. Библиотека довольно компактная и может компилироваться под множество платформ: под Windows, под Linux и, даже для микроконтроллеров (PIC18, atmega8, stm32).
Так как наше устройство использует Linux и работает на ARM, то для сборки библиотеки мы пользовались кросс-компиляцией, надо отдать должное разработчикам библиотеки — все собралось нормально без необходимости какой-либо доработки.
Архитектурно поддержка протокола в нашем проекте выполнена в виде отдельного демона, взаимодействующего с основным приложением посредством API.
Кстати, примеры, которые идут вместе с библиотекой, вполне годные, и их можно взять как основу для своего проекта. Но если вы захотите что-то сделать на основе библиотечных примеров, есть такая особенность — объекты выделены статически, да-да, просто глобальные массивы объектов фиксированного размера. Для простых устройств на микроконтроллерах — это скорее плюс, в том смысле, что если структура объектов зафиксирована (а для простого устройства скорее всего так) то динамическое выделение памяти создаст только проблемы.
В нашем же случае кол-во устройств и объектов может меняться, а весь проект написан на C++ (и грех не воспользоваться контейнерами), поэтому работу с библиотекой мы, конечно, переделали.
Как собрать и запустить сервер и клиент
Если вам уже стало интересно, и захотелось попробовать BACnet в деле, то дальше описан небольшой туториал, как собрать библиотеку bacnet-stack со всем инструментарием, и запустить сервер и клиент у себя.
Проект bacnet-stack состоит из, собственно, библиотеки, и набора инструментария (можно сказать — примеров), который эту библиотеку использует.
Для запуска нам потребуются две Linux машины в одном сегменте сети — на одной будет запущен BACnet-сервер, а на второй мы будет запускать утилиты, которые будут выступать в роли клиента.
Процесс сборки я описал для Windows 10 с установленной в WSL Ubuntu 20.04, идея была такая, что у многих сейчас стоит десятка и поставить туда через Microsoft Store Ubuntu совсем не сложно, а у некоторых и так стоит Ubuntu, так что описание должно быть более-менее универсальным.
Нужно установить GCC и Git, если у вас уже установлены эти инструменты, то это можно пропустить.
sudo apt install build-essential
sudo apt-get install git
После этого склонировать репозиторий bacnet-stack к себе при помощи команды:
git clone https://github.com/bacnet-stack/bacnet-stack.git
Затем запустить сборку:
cd bacnet-stack
make clean all
Вот, собственно и все. После сборки в директории «bacnet-stack\bin\» появится набор исполняемых файлов, также там лежит текстовый документ «readme.txt » с описанием что есть что. Эти шаги можно повторить и на второй машине, а можно просто, как поступил я, скопировать каталог с уже собранными утилитами с первой на вторую.
Перед тем как запустить сервер, нужно создать переменную окружения с указанием интерфейса, на котором будет запущен сервер, без этого BACnet сервер запустится на первом попавшемся сетевом интерфейсе (если интерфейс всего один, то это может быть как раз то, чего вы хотите, а может быть и нет). Я проводил эксперимент на двух ноутбуках, подключенных к Wi-Fi роутеру, поэтому и там и там указывал Wi-Fi адаптер ноутбука (если что, то список всех адаптеров с IP адресами можно посмотреть командой «ip address»).
Ставим переменную окружения, с указанием интерфейса на первой машине:
export BACNET_IFACE=wifi0
После этого уже можно запустить BACnet сервер:
cd bacnet-stack/bin
./bacserv
BACnet Server Demo
BACnet Stack Version 1.0.0
BACnet Device ID: 260001
Max APDU: 1476
BACnet Device Name: SimpleServer
Сервер с этого момента запущен и устройство, которое он представляет, имеет Device ID = 260001, это значение выбрано по-умолчанию в утилите сервера, но вы можете указать в опциях при запуске сервера какое-то свое значение.
Теперь можно перейти ко второй машине и попробовать что-то прочитать с запущенного сервера.
Перед этим нужно, так же как и в случае с сервером, сконфигурировать интерфейс, путем инициализации переменной окружения BACNET_IFACE:
export BACNET_IFACE=wifi0
После настройки интерфейса можно запросить список видимых устройств при помощи BACnet службы Who Is:
./bacwi
;Device MAC (hex) SNET SADR (hex) APDU
;-------- -------------------- ----- -------------------- ----
260001 C0:A8:00:D5:BB:C3 0 00 1476
;
; Total Devices: 1
Тут мы видим в сети одно устройство с Device ID 260001 — это наш сервер, отлично!
Теперь можно прочитать свойства устройства, для этого нужно воспользоваться утилитой bacrp (Read Property), формат ее использования такой:
Usage: bacrp device-instance object-type object-instance property [index]
[--dnet][--dadr][--mac]
[--version][--help]
Т.е. для чтения списка объектов нужно указать device-instance — 260001, object-type — «device» или если указывать числом, то — 8, object-instance — в данном случае он равен device-instance 260001, и свойство, которое мы читаем — object-list (или если указывать через номер свойства, то это будет 76):
./bacrp 260001 8 260001 76
или, что то же самое:
./bacrp 260001 device 260001 object-list
{(device, 260001),(network-port, 1),(analog-input, 0),(analog-input, 1),
(analog-input, 2),(analog-input, 3),(analog-output, 0),(analog-output, 1),
(analog-output, 2),(analog-output, 3),(analog-value, 0),(analog-value, 1),
(analog-value, 2),(analog-value, 3),(binary-input, 0),(binary-input, 1),
(binary-input, 2),(binary-input, 3),(binary-input, 4),(binary-output, 0),
(binary-output, 1),(binary-output, 2),(binary-output, 3),(binary-value, 0),
(binary-value, 1),(binary-value, 2),(binary-value, 3),(binary-value, 4),
(binary-value, 5),(binary-value, 6),(binary-value, 7),(binary-value, 8),
(binary-value, 9),(characterstring-value, 0),(command, 0),(command, 1),
(command, 2),(command, 3),(integer-value, 1),(notification-class, 0),
(notification-class, 1),(life-safety-point, 0),(life-safety-point, 1),
(life-safety-point, 2),(life-safety-point, 3),(life-safety-point, 4),
(life-safety-point, 5),(life-safety-point, 6),(load-control, 0),
(load-control, 1),(load-control, 2),(load-control, 3),
(multi-state-input, 0),(multi-state-input, 1),(multi-state-input, 2),
(multi-state-input, 3),(multi-state-output, 0),(multi-state-output, 1),
(multi-state-output, 2),(multi-state-output, 3),(multi-state-value, 0),
(multi-state-value, 1),(multi-state-value, 2),(multi-state-value, 3),
(trend-log, 0),(trend-log, 1),(trend-log, 2),(trend-log, 3),(trend-log, 4),
(trend-log, 5),(trend-log, 6),(trend-log, 7),(lighting-output, 1),
(lighting-output, 2),(lighting-output, 3),(lighting-output, 4),
(lighting-output, 5),(lighting-output, 6),(lighting-output, 7),
(lighting-output, 8),(channel, 1),(file, 0),(file, 1),(file, 2),
(octetstring-value, 0),(octetstring-value, 1),(octetstring-value, 2),
(octetstring-value, 3),(positive-integer-value, 0),
(positive-integer-value, 1),(positive-integer-value, 2),
(positive-integer-value, 3),(schedule, 0),(schedule, 1),(schedule, 2),
(schedule, 3),(accumulator, 0),(accumulator, 1),(accumulator, 2),
(accumulator, 3),(accumulator, 4),(accumulator, 5),(accumulator, 6),
(accumulator, 7),(accumulator, 8),(accumulator, 9),(accumulator, 10),
(accumulator, 11),(accumulator, 12),(accumulator, 13),(accumulator, 14),
(accumulator, 15),(accumulator, 16),(accumulator, 17),(accumulator, 18),
(accumulator, 19),(accumulator, 20),(accumulator, 21),(accumulator, 22),
(accumulator, 23),(accumulator, 24),(accumulator, 25),(accumulator, 26),
(accumulator, 27),(accumulator, 28),(accumulator, 29),(accumulator, 30),
(accumulator, 31),(accumulator, 32),(accumulator, 33),(accumulator, 34),
(accumulator, 35),(accumulator, 36),(accumulator, 37),(accumulator, 38),
(accumulator, 39),(accumulator, 40),(accumulator, 41),(accumulator, 42),
(accumulator, 43),(accumulator, 44),(accumulator, 45),(accumulator, 46),
(accumulator, 47),(accumulator, 48),(accumulator, 49),(accumulator, 50),
(accumulator, 51),(accumulator, 52),(accumulator, 53),(accumulator, 54),
(accumulator, 55),(accumulator, 56),(accumulator, 57),(accumulator, 58),
(accumulator, 59),(accumulator, 60),(accumulator, 61),(accumulator, 62),
(accumulator, 63)}
Тут мы видим длинный список объектов, которые содержатся в объекте «устройство 260001»: первым идет, собственно, объект, описывающий само устройство, представленное сервером, потом объект сетевого подключения, аналоговые входы и выходы, дискретные входы и выходы, ну и множество других объектов.
Попробуем прочитать сначала список свойств нашего устройства:
./bacrp 260001 device 260001 property-list
{system-status,vendor-name,vendor-identifier,model-name,
firmware-revision,application-software-version,protocol-version,
protocol-revision,protocol-services-supported,
protocol-object-types-supported,object-list,max-apdu-length-accepted,
segmentation-supported,apdu-timeout,number-of-APDU-retries,
device-address-binding,database-revision,description,local-time,
utc-offset,local-date,daylight-savings-status,location,
active-cov-subscriptions,time-synchronization-recipients,
time-synchronization-interval,align-intervals,interval-offset}
А теперь и некоторые из этих свойств:
./bacrp 260001 device 260001 vendor-name
"BACnet Stack at SourceForge"
./bacrp 260001 device 260001 model-name
"GNU"
./bacrp 260001 device 260001 location
"USA"
./bacrp 260001 device 260001 description
"server"
./bacrp 260001 device 260001 object-name
"SimpleServer"
Ну, USA так USA… что-то про само устройство мы выяснили, теперь прочитаем описание, скажем, аналогового входа 0, и что там у него собственно на входе.
В принципе все точно так же, как и для объекта device, только object-instance уже не равен device-instance, читаем список всех свойств объекта:
./bacrp 260001 analog-input 0 property-list
{present-value,status-flags,event-state,out-of-service,units,description,
reliability,cov-increment,time-delay,notification-class,high-limit,low-limit,
deadband,limit-enable,event-enable,acked-transitions,notify-type,
event-time-stamps,9997,9998,9999}
А теперь сами значения основных свойств:
./bacrp 260001 analog-input 0 description
"ANALOG INPUT 0"
./bacrp 260001 analog-input 0 present-value
0.000000
./bacrp 260001 analog-input 0 units
percent
Видно, что единицы измерения тут проценты, а входное значение равно нулю.
Читать объекты, конечно интересно, но как на счет того чтобы чем-то поуправлять (виртуально, конечно, у нас же просто демо-сервер).
В списке объектов есть «lighting-output» — это, как раз, объект для управления освещением с диммированием (для светильников без диммирования в BACnet существует просто «binary-lighting-output»). Если почитать спецификацию, то можно увидеть, что управлять яркостью можно разными способами: или записью в свойство «present-value», или записью команды в «lighting-command», воспользуемся первым вариантом.
Читаем список свойств объекта «lighting-output»:
./bacrp 260001 lighting-output 1 property-list
{present-value,tracking-value,lighting-command,in-progress,status-flags,
out-of-service,blink-warn-enable,egress-time,egress-active,
default-fade-time,default-ramp-rate,default-step-increment,
priority-array,relinquish-default,lighting-command-default-priority}
Потом читаем текущую яркость:
./bacrp 260001 lighting-output 1 present-value
0.000000
Яркость 0%, это значит, что свет не горит, попробуем записать новое значение при помощи утилиты bacwp (Write Property):
./bacwp 260001 lighting-output 1 present-value 16 -1 4 100
WriteProperty Acknowledged!
А вот тут, видимо, нужно пояснить — если сначала все параметры идут точно такие же, как и у утилиты bacrp, то после указания записываемого свойства «present-value», уже может быть не все понятно: 16 — это приоритет записываемого значения, т.е. можно последовательно записать несколько значений с разным приоритетом, но действовать будет то, что с высшим (таким же образом, записанное значение с указанным приоритетом, можно как бы отменить, если указать тип данных 0, и действовать будет то, из оставшихся значений, что имеет более высокий приоритет); в данном случае 16 — это низший приоритет (ну, а 0 соответственно — высший), дальше: -1 — тут должен быть индекс в случае записи массива, но поскольку у нас просто одно значение, то »-1» значит то, что никакого индекса и нет, потом 4 — тип данных — «real », и, наконец, 100 — собственно само значение, которое мы записываем.
Проверяем, записалось ли значение:
./bacrp 260001 lighting-output 1 present-value
100.000000
Да, все как и ожидалось — записалось, теперь по идее свет должен гореть.
Ну, вот, как-то так — теперь вы тоже немного умеете управлять зданием!
Если вас заинтересовало, что за объект такой «accumulator», которых почему-то очень много в примере сервера — так в BACnet называется объект, представляющий счетчик, в данном случае это электросчетчики (но могут быть и счетчики воды или тепла), так что все это дело можно сразу интегрировать в систему АСКУЭ.
Инструменты
Что еще есть из инструментария в открытом доступе? Ну, я бы отметил программу с графическим интерфейсом Yabe (расшифровывается как Yet Another BACnet Explorer, видимо отсылка к YACC), программа написана на C#, и довольно таки функциональна, даже в сравнении с коммерческими аналогами. Так что, если вы собрали сервер и уже наигрались с клиентскими утилитами в командной строке, то можно запустить Yabe и посмотреть на свое BACnet устройство из графического интерфейса, только не надо запускать сразу и консольные клиентские утилиты и Yabe — они, скорее всего, не смогут поделить один физический интерфейс и нормально работать вместе не будут.
Хотелось бы рассказать про некоторые неочевидные (по крайней мере для меня) моменты использования Yabe. Сразу после запуска приложения, оно не начнет искать устройства, для этого нужно добавить интерфейс, через который это следует делать. Для этого нужно нажать на иконку «Add device», и там выбрать интерфейс, через который будет происходить общение, т.е. под device создатели программы почему-то подразумевают Ethernet или serial порт, а вовсе не BACnet устройство как могло бы показаться.
Если устройство находится в одной подсети (ну, например, на второй машине остался работать сервер из предыдущего примера), то оно уже появится в списке устройств. Если же сервер находится с вами в разных сегментах (например, у меня возникли сложности с подключением при работе через VPN), то можно задействовать возможность устройств к трансляции сообщений. Для этого сначала нужно добавить BACnet Device ID для самого клиента Yabe через Options/Settings и там найти параметр YabeDeviceId и поставить его в какое-нибудь значение, пусть это будет 3630000. IP адрес в меню Add Device при подключении должен быть указан тот, который, ваш внешний. После этого нужно нажать на иконку UDP соединения и через меню выбрать Functions/IP Services/Foreign Device Registration и там указать IP адрес и порт существующего устройства, которое будет использовано как мост для трансляции широковещательных BACnet пакетов.
Нажать Register и указанное устройство появится в списке устройств вместе с устройствами, которые находятся с ним в одном сегменте сети.
Примерно так выглядит окно Yabe, когда все сделано правильно.
В левой верхней части находится панель с устройствами, чуть ниже — список объектов в выбранном устройстве, справа — свойства выбранного объекта. На скриншоте выбран один из температурных сенсоров. Некоторые свойства могут быть отредактированы, если они поддерживают возможность записи значений, например можно добавить свое описание этого сенсора в поле Description.
Если вы решили делать какое-то BACnet устройство на базе bacnet-stack и у вас есть какие-то тесты (ручные или автоматические), основанные на этой же библиотеке, и все замечательно функционирует, то все равно неплохо бы запустить Yabe и посмотреть как это выглядит там, потому что всегда полезно попробовать как работают между собой реализации протокола не основанные на одной библиотеке (а Yabe как раз не основана на bacnet-stack).
Ну и напоследок — что делать, если что-то работает, а что-то никак не хочет, хотя все вроде сделано правильно? Где ошибка в реализации клиента или сервера? Оказывается, многим известный инструмент — Wireshark, вполне себе понимает протокол BACnet и позволяет посмотреть, что передается внутри пакетов, а потом уже можно свериться со спецификацией протокола и посмотреть что же там не так.
Заключение
Ну вот и все, надеюсь, что смог вкратце рассказать о BACnet. Конечно тяжело уместить все в одну статью, но то что есть, должно помочь начать работать с протоколом тому, кто строит свой «умный дом», занимается интеграцией, или создает устройства автоматики.