Статей о создании метеостанции на базе Arduino не счесть. Можно сказать, если статья про метеостанцию, то это про микроконтроллеры Arduino, ESP32 или STM32. Но только не в этот раз. Будем запускать метеостанцию на Banana Pi BPI-M64 под Linux, без использования Arduino-подобных оберток в виде WiringPi, на C# .NET5. Пример метеостанции является демонстрацией встраиваемого решения работы с GPIO, датчиками и вывода пользовательского интерфейса напрямую на LCD. В решении используется: Linux (Armbian) — основная ОС, .NET и C# — платформа для создания прикладного ПО, AvaloniaUI — графической интерфейс с интерактивными графиками и анимацией, Docker — инструмент для развертывания, управления, доставки приложений, RabbitMQ — брокер сообщений для передачи сообщений между контейнерами. Благодаря использованию универсального подхода и технологии Docker, приложение можно запустить не только на Banana Pi BPI-M64, но и на других Banana/Orange/Rock/Nano Pi одноплатных компьютерах, включая Raspberry Pi.
Оглавление
- О графическом интерфейсе
- Постановка задачи
- Принцип работы
- Архитектура
- Аппаратное обеспечение
- Схема подключения
- Docker контейнер с библиотекой Libgpiod
- Приложение dotnet-gpioset на C#
- Подключение датчика BME280 к шине I2C
- Подключение датчика DS18B20
- Установка и настройка брокера сообщений RabbitMQ
- Приложение WeatherStation.Sensors
- Приложение WeatherStation.Panel.AvaloniaX11
- Docker Compose. Особенности запуска контейнеров
- Утилизация ресурсов
- Вывод
- Что дальше?
- Ресурсы
- Битва графических интерфейсов: Avalonia и Uno Platform VS. HTML (Node.js, Electron)
- Литература
О графическом интерфейсе
Особенностью предложенного решения является использование кроссплатформенного фреймворка AvaloniaUI с передачей Docker-контейнеру виртуального устройства Linux framebuffer. Совершенно другой подход к построению UI был рассмотрен на Хабре в статье «Интерфейсы для встраиваемых устройств на современных Web-технологиях», проще говоря, интерфейс на HTML в браузере. Какие особенности присутствуют в обоих предложенных вариантах, а также их достоинства и недостатки рассмотрим в конце данного поста.
Постановка задачи
Нам предстоит считывать показания датчиков, затем отправлять полученные данные брокеру сообщений RabbitMQ. Где второе приложение заберет полученные данные из очереди RabbitMQ и выведет их на LCD-экран. RabbitMQ позволяет организовать слабосвязанную архитектуру. Если в дальнейшем потребуется отправлять данные в облако и/или сохранять на диске, то, не меняя существующие приложения, потребуется лишь добавить еще одно приложение, которое реализует новую функциональность.
Принцип работы
После старта ОС Armbian на Banana Pi BPI-M64, запускается Docker-демон, который в свою очередь запускает контейнеры. Графический интерфейс выводится на LCD ILI9341. Доступно два состояния экрана. На первом экране отображается текущая температура, влажность, давление. На втором экране отображается график изменения температуры с автоматическим обновлением. Пользователь может взаимодействовать с системой посредством кнопки. Нажатие на кнопку приводит к включению светодиода для индикации текущего действия. При отпускании кнопки светодиод выключается и отправляется команда смены экрана. На экране отображения датчиков присутствует иконка индикации состояния подключения к серверу RabbitMQ. Когда установлено соединение с сервером RabbitMQ, иконка — зеленого цвета; соединение отсутствует — иконка красного цвета.
Отображение физических величин на LCD ILI9341
Отображение графика изменения температуры
Запуск метеостанции на Banana Pi M64 (сокращенный вариант):
Архитектура
Решение работает на одноплатном компьютере Banana Pi BPI-M64, ОС Armbian 21.02.1 (Ubuntu 18.04.5 LTS Bionic Beaver, ядро Linux 5.10.12). Приложения развертываются в виде Docker-контейнеров. Поэтому в случае развертывания приложений метеостанции на других одноплатных компьютерах, потребуется поддержка Docker. Технология Docker позволяет существенно упростить развертывание и доставку приложений на устройство. Одно из самых главных качеств Docker является его атомарность. Контейнер целиком либо развернется, либо нет. В случае сбоя запуска нового контейнера можно применить систему A/B, при которой контейнер предыдущей версии не удаляется, а происходит откат при невозможности запуска нового контейнера. Подобная манипуляция выполняется быстро без задержек, что снижает время простоя устройства. При традиционном подходе пошагового обновления, в случае сбоя в процессе обновления, откат изменений становится весьма сложной задачей. Европейская компания Toradex в своих встраиваемых решениях для автомобильных грузоперевозок для доставки и развертывания приложений использует технологию Docker. Компанией был разработан модуль Verdin на базе NXP i.MX8 M Mini SoC, который взаимодействует с автомобилем через CAN-шину для получения телеметрии от автомобиля и отправляет эти данные в облако. Работа с CAN-шиной и отправка данных в облако выполняется в Docker-контейнере.
Подробнее почитать в публикации Reading Vehicle OBD-II data through CAN within a containerized application in Embedded Linux — CNX SOFTWARE. Установить Docker на ARM и 64-bit ARM можно по руководству Установка Docker для ARM и 64-bit ARM (Armbian, Linux) или Install Docker Engine on Ubuntu.
Данные между контейнерами передаются через брокер сообщений RabbitMQ по протоколу AMQP. Протокол AMQP (Advanced Message Queuing Protocol) — позволяет гибко связывать отдельные подсистемы (или независимые приложения) между собой. AMQP-брокер осуществляет маршрутизацию, возможно, гарантирует доставку, распределение потоков данных, подписку на нужные типы сообщений. Например, на устройстве работает два приложения, первое собирает телеметрию, а второе отправляет данные в облако. Если второе приложение по каким-либо причинам аварийно завершится, то при использовании брокера сообщений данные телеметрии не потеряются, а будут накапливаться в очереди брокера сообщений. После успешного запуска второго приложения данные из очереди будут прочитаны и отправлены в облако. В случае разработки системы без использования брокера сообщений разработчикам потребовалось бы отдельно решать задачу накопления и хранения данных в случае невозможности их отправки в облако. Поэтому брокер сообщений является универсальным решением подобной задачи и избавляет разработчиков от лишней заботы по сохранению данных. Универсальность протокола AMQP позволяет серверу RabbitMQ взаимодействовать с самыми различными устройствами, включая микроконтроллеры и различные типы клиентских приложений.
Взаимодействие с сервером RabbitMQ
Для замера физических величин температуры, влажности и давления используется датчик BME280, подключается к шине I2C. В связи с торговой войной между США и Китаем некоторые позиции датчиков резко взлетели в цене, поэтому в качестве альтернативы можно использовать датчик DS18B20, работающий по протоколу OneWire. Таким образом на борту вашего одноплатного компьютера должны быть контакты GPIO и/или шина I2C в случае подключения BME280. Изображение выводится через виртуальное устройство Linux Framebuffer. Содержимое framebuffer обычно напрямую отображается на доступном экране. В качестве экрана может использоваться HDMI-монитор или LCD на SPI интерфейсе. В данном случае используется дисплей SPI LCD ILI9341, под его разрешение сделана разметка.
Большая схема, как все это работает:
Архитектура метеостанции на Banana Pi M64
Далее будет рассмотрено, как все это адаптировать под свой одноплатный компьютер. Некоторые технические моменты в статье пропущены, т.к. статья основывается на публикации Управляем контактами GPIO из C# .NET 5 в Linux на одноплатном компьютере Banana Pi M64 (ARM64) и Cubietruck (ARM32). Поэтому желательно в указанной публикации ознакомиться со следующими понятиями: что такое GPIO, библиотека Libgpiod, как вычисляются номера контактов GPIOXX, библиотеки .NET IoT, принцип работы со светодиодом и обработки событий от кнопки. Раздел установки библиотеки Libgpiod и .NET можно пропустить, т.к. Docker-контейнер уже будет содержать библиотеку и среду исполнения.
Аппаратное обеспечение
На плате Banana Pi BPI-M64 размещен SoC Allwinner A64, в него входит 4 ядра Cortex-A53 с частотой 1.2 ГГц, с 2 ГБ DDR3. Для хранения данных используется карта microSD Class 10 объемом 16 Гб. На уровне ОС требуется поддержка Docker CE. На плате должны быть контакты GPIO для подключения датчиков, кнопки и светодиода. Минимальное требование — это возможность подключения температурного датчика DS18B20 по OneWire-протоколу. К 40-контактному совместимому с Raspberry Pi разъему подключаются:
- BME280 — датчик температуры, влажности и давления;
- DS18B20 — альтернативный датчик температуры, в случае отсутствия BME280;
- Кнопка — для переключения экранов, наличие необязательно;
- Светодиод — для индикации нажатия на кнопку, наличие необязательно;
- LCD ILI9341 — экран, подключаемый к SPI-шине, можно использовать любой другой LCD на HDMI или VGA интерфейсе.
Схема подключения
Устройства для подключения:
- Датчик BME280 будет подключен к шине I2C, как в публикации Работа с GPIO в Linux на примере Banana Pi BPI-M64. Часть 5. Device Tree overlays. Шина I2C, подключение датчиков Bosh BMx;
- Экран ILI9341 будет подключен к шине SPI, как в публикации Работа с GPIO в Linux на примере Banana Pi BPI-M64. Часть 4. Device Tree overlays. Подключение дисплея SPI LCD ILI9341;
- Кнопка и светодиоды будут подключены как в публикации Управляем контактами GPIO из C# .NET 5;
- Датчик DS18B20 будет подключен на №31 контакт разъема, название контакта «PB5», номер линии — 37.
Итоговая схема будет выглядеть следующим образом:
Принципиальная схема подключения устройств к Banana Pi BPI-M64
Для включения интерфейсов I2C, OneWire, SPI необходимо на уровне ОС Linux включить соответствующие устройства. Это делается путем формирования файла наложения дерева устройств DTO. Как формируются файлы наложения устройств (DTS), можно почитать в публикации Работа с GPIO на примере Banana Pi BPI-M64. Часть 2. Device Tree overlays.
Файлы DTS для включения устройств:
- Датчик BME280 — sun50i-a64-i2c1-bme280.dts или интерфейс I2C — sun50i-a64-i2c1-on.dts;
- Экран ILI9341 — sun50i-a64-spi-ili9341-backlight-on-off.dts;
- Датчик DS18B20 — sun50i-a64-w1-gpio-pb5.dts.
Подключенные устройства к Banana Pi BPI-M64, вид сверху
Docker-контейнер с библиотекой Libgpiod
Библиотека Libgpiod позволяет работать с контактами GPIO одноплатного компьютера из .NET среды исполнения, она может быть установлена на Linux (Armbian). Библиотека не является аппаратно-зависимой, что позволяет ее использовать на различных одноплатных компьютерах архитектуры ARM32, ARM64, и x86.
Для работы с контактами GPIO в публикации Управляем контактами GPIO из C# .NET 5 в Linux на одноплатном компьютере Banana Pi M64 (ARM64) и Cubietruck (ARM32) требовалась установка искомой библиотеки с репозитория. Сейчас создан Docker-контейнер с библиотекой, и можно запускать из контейнера утилиты: gpiodetect, gpioinfo, gpioset, gpioget, gpiomon. Образ Docker devdotnetorg/libgpiod, GitHub devdotnetorg/docker-libgpiod.
Доступ к контактам GPIO осуществляется через устройства /dev/gpiochipX . Для просмотра доступных устройств на одноплатном компьютере необходимо выполнить команду:
$ ls /dev/gpiochip*
Результат выполнения команды:
root@bananapim64:~# ls /dev/gpiochip* /dev/gpiochip0 /dev/gpiochip1 /dev/gpiochip2
Соответственно, доступ к данным устройствам необходимо передать контейнеру devdotnetorg/libgpiod. Предоставим доступ ко всем доступным устройствам /dev/gpiochipX и выполним команду gpiodetect. Утилита gpiodetect выведет список всех чипов GPIO, их метки и количество линий. Команда будет выглядеть следующим образом:
$ docker run --rm --name test-libgpiod --device /dev/gpiochip0 --device /dev/gpiochip1 --device /dev/gpiochip2 devdotnetorg/libgpiod gpiodetect
где «—device /dev/gpiochip0» — доступ к устройству, gpiodetect — название утилиты, вызываемой из контейнера.
Результат выполнения команды:
root@bananapim64:~# docker run --rm --name test-libgpiod --device /dev/gpiochip0 --device /dev/gpiochip1 --device /dev/gpiochip2 devdotnetorg/libgpiod gpiodetect gpiochip0 [1f02c00.pinctrl] (32 lines) gpiochip1 [1c20800.pinctrl] (256 lines) gpiochip2 [axp20x-gpio] (2 lines)
В публикации Управляем контактами GPIO из C# .NET 5, кнопка и светодиод располагались в устройстве /dev/gpiochip1 , поэтому достаточно предоставить доступ только к данному устройству. Повторим выполнение команды gpiodetect, но только предоставляя доступ к /dev/gpiochip1 , команда:
$ docker run --rm --name test-libgpiod --device /dev/gpiochip1 devdotnetorg/libgpiod gpiodetect
Результат выполнения команды:
root@bananapim64:~# docker run --rm --name test-libgpiod --device /dev/gpiochip1 devdotnetorg/libgpiod gpiodetect gpiochip1 [1c20800.pinctrl] (256 lines)
Команда выполнена успешно.
Теперь, как в предыдущей публикации, включим светодиод на 36-м контакте, выставим значение «1», команда:
$ docker run --rm --name test-libgpiod --device /dev/gpiochip1 devdotnetorg/libgpiod gpioset 1 36=1
В результате светодиод включится.
Приложение dotnet-gpioset на C#
В пространство имен System.Device.Gpio.Drivers входит драйвер — LibGpiodDriver. Драйвер LibGpiodDriver использует библиотеку Libgpiod для получения доступа к портам GPIO, заменяет драйвер SysFsDriver. Приложение dotnet-gpioset на .NET5, используя драйвер LibGpiodDriver, выполняло ту же самую функцию, что и утилита gpioset из состава библиотеки Libgpiod. Данное приложение доступно в виде Docker-контейнера devdotnetorg/dotnet-gpioset GitHub dotnet-libgpiod-gpioset.
Включим светодиод, но уже из .NET кода, используя библиотеку Libgpiod, команда:
$ docker run --rm --name test-dotnet-gpioset --device /dev/gpiochip1 devdotnetorg/dotnet-gpioset 1 36=1
Результат выполнения команды:
root@bananapim64:~# docker run --rm --name test-dotnet-gpioset --device /dev/gpiochip1 devdotnetorg/dotnet-gpioset 1 36=1 Args gpiochip=1, pin=36, value=High OK
В результате светодиод включится. Dockerfile данного контейнера можно использовать в качестве шаблона для других приложений, использующих драйвер LibGpiodDriver для доступа к контактам GPIO одноплатного компьютера.
Подключение датчика BME280 к шине I2C
Датчик BME280 подключается к шине I2C. I2C (InterIC, или IIC) — двунаправленная шина передачи данных, разработанная еще в 1980 году компанией Philips для осуществления связи между разными схемами и устройствами. Очень часто на схемах указывают I2C/TWI(Two Wire Interface) — это одно и то же. Дело в том, что компания Philips шину I2C запатентовала. В результате для использования шины I2C другие разработчики обязаны были заплатить роялти, что конечно же не очень-то хотелось. Так появился клон шины I2C — шина TWI, свободная от лицензионных отчислений. Все, что применимо к шине I2C, применимо и к шине TWI, и наоборот.
К шине I2C можно подключить до 127 устройств. Передача данных осуществляется по двум проводам:
- SDA (Serial Data) — эта линия отвечает непосредственно за передачу данных;
- SCL (Serial Clock) — эта линия отвечает за синхронизацию соединения.
Распайка шины I2C с подтягивающими резисторами на 4,7 кОм
Адресация в шине I2C
Каждое устройство, подключённое к шине, может быть программно адресовано по уникальному адресу. В обычном режиме используется 7-битная адресация.
Шина I2C/TWI на процессоре Allwinner A64
Рассмотрим спецификацию SoC Allwinner A64:
- Два контроллера интерфейса TWI;
- Поддержка стандартного режима (до 100 Кбит/с) и быстрого режима (до 400 Кбит/с) передачи данных;
- Роль ведущего(master)/ведомого(slaves) настраивается;
- Доступны транзакции с 10-битной адресацией;
- Возможность работы в широком диапазоне входных тактовых частот.
На самой плате разведено два интерфейса TWI:
- TWI0 — располагается на разъеме MIPI DSI display;
- TWI1 — располагается на 40-контактном разъеме (типа Raspberry Pi 3 GPIO), к нему будем подключать датчики.
Для считывания значений с датчика BME280 из .NET кода необходимо знать номер линии I2C. В данном случае будет использоваться интерфейс TWI1 с индексом «1». Поэтому номер линии I2C для .NET кода будет — «1».
Датчик BME280
Модуль BME280 фирмы Bosch Sensortec предназначен для измерения атмосферного давления, температуры и влажности. По сравнению с первыми датчиками серии (BMP085 и BMP180) он имеет лучшие характеристики и меньшие размеры. Отличие от датчика BMP280 – наличие гигрометра, что позволяет измерять относительную влажность воздуха и создавать на его основе маленькую метеостанцию.
Технические характеристики модуля BME280:
- Интерфейс: SPI, I2C;
- Напряжение питания: от 3,3 до 5 В;
- Диапазон измерений давления: 300-1100 hPa;
- Диапазон измерений температуры: -40 — +85 °C;
- Диапазон измерений влажности: 0 — 100 %;
- Энергопотребление: режим измерений — 3.6 мкА, спящий режим: — 0.1 мкА;
- Точность измерений: давление — 0.01 hPa ( < 10 cm), температура — 0.01° C, влажность – 3%.
Назначение контактов:
- VCC — питание модуля 3.3 В или 5 В;
- GND — Ground;
- SCL — линия тактирования;
- SDA — линия данных.
В данном проекте модуль работает по двухпроводному интерфейсу I2C, адрес по умолчанию 0x76.
Новое поколение датчиков BOSCH обладает низким энергопотреблением. Например, для сбора показаний влажности и температуры с частотой раз в секунду потребуется всего 1,8 мкА. Если нужно анализировать еще и давление, суммарный ток составит 3,6 мкА. В режиме сна датчик потребляет и вовсе 0,1 мкА.
Такая энергоэффективность позволяет использовать датчик в мобильных устройствах умного дома, которые годами питаются от одного литиевого элемента питания, например, CR2450.
Данный датчик емкостного типа, что заведомо делает его более точным, чем резистивные датчики типа DHT11.
Datasheet на датчик BME280 можно загрузить по ссылке BST-BME280_DS001-10 [PDF 1,84 МБ].
Схема подключения датчика BME280 к шине I2C
Исходя из схемы Распиновка GPIO для Banana Pi BPI-M64, шина I2C располагается на контактах № 3 и 5. Контакт №3 — TWI1-SDA — передача данных. Контакт №5 — TWI1-SCL — синхронизация, такты. Линии TWI1-SDA и TWI1-SCL подтянем к питанию VCC, установив подтягивающие резисторы сопротивлением 4,7 кОм.
Схема подключения датчика BME280 к шине I2C
Файл наложения устройств DTS для включения шины I2C
По умолчанию в основном дереве устройств sun50i-a64-bananapi-m64.dts есть узел для шины I2C, и он включен по умолчанию.
Узел i2c1-pins (из файла sun50i-a64-bananapi-m64.dts) по адресу «/soc/pinctrl@1c20800/i2c1-pins» содержит указания используемых контактов:
i2c1-pins { pins = "PH2", "PH3"; function = "i2c1"; bias-pull-up; phandle = <0x3b>; };
Узел i2c@1c2b000 (из файла sun50i-a64-bananapi-m64.dts) по адресу «/soc/i2c@1c2b000» содержит само устройство I2C:
i2c@1c2b000 { compatible = "allwinner,sun6i-a31-i2c"; reg = <0x1c2b000 0x400>; interrupts = <0x0 0x7 0x4>; clocks = <0x2 0x40>; resets = <0x2 0x2b>; pinctrl-names = "default"; pinctrl-0 = <0x3b>; status = "okay"; #address-cells = <0x1>; #size-cells = <0x0>; phandle = <0x88>; };
Как видно из примера, статус устройства status = «okay» означает, что шина I2C включена и можно подключать датчики. Но если будет выключена, то потребуется создать файл dts для включения шины I2C. На этот случай приведен пример такого файла.
Создадим файл DTS с названием: sun50i-a64-i2c1-on.dts:
/dts-v1/; /plugin/; / { compatible = "allwinner,sun50i-a64"; fragment@0 { target-path = "/aliases"; __overlay__ { i2c1 = "/soc/i2c@1c2b000"; }; }; fragment@1 { target = <&i2c1>; __overlay__ { pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; status = "okay"; }; }; };
Рассмотрим параметры:
- pinctrl-0 = <&i2c1_pins> — ссылка на используемые контакты узла по адресу «/soc/pinctrl@1c20800/i2c1-pins»;
- status = «okay» — задействует шину I2C на плате для подключения устройств.
Разместим файл по пути /boot/dtb/allwinner/overlay . Затем компилируем файл .dts в .dtbo:
$ dtc -O dtb -o sun50i-a64-i2c1-on.dtbo sun50i-a64-i2c1-on.dts
Запустим утилиту конфигурирования платы: armbian-config . Перейдем по меню: System > Hardware, включим слой (overlay): sun50i-a64-i2c1-on. После перезагрузки платы шина I2C будет включена.
Поиск устройств на шине I2C
осле включения шины I2C и подключения датчика BME280 необходимо убедиться, что все работает и датчик отзывается. Для этого необходимо установить утилиту i2c-tools , которая производит поиск всех устройств, подключенных к шине I2C.
Установка утилиты i2c-tools :
sudo apt-get update sudo apt-get install -y python-smbus sudo apt-get install -y i2c-tools
Формат команды поиска устройств на шине I2C: sudo i2cdetect -y 0 , где 0 — номер шины I2C (1,2,3,..). На 40-контактном разъеме (типа Raspberry Pi 3 GPIO) располагается TWI1, к которому подключили датчик BME280. Поэтому вызываемая команда будет выглядеть так: sudo i2cdetect -y 1 . Выполним поиск устройств на шине I2C порт «1»:
root@bananapim64:~# sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- 76 -- root@bananapim64:~#
Шина I2C работает, и датчик BME280 по адресу 0x76 найден. В ОС Linux считать показания с датчика возможно через виртуальную файловую систему sysfs. Подробнее про шину I2С в публикации Работа с GPIO в Linux на примере Banana Pi BPI-M64. Часть 5. Device Tree overlays. Шина I2C, подключение датчиков Bosh BMx.
Если по каким-либо причинам вызов утилиты завершается ошибкой, проверьте наличие включенных устройств I2C. В файловой системе должны быть файл-устройства в зависимости от номера линии I2C: /dev/i2c-0 , /dev/i2c-1 и т.д. Если данного устройства нет, значит, необходимо проверить файл DTS для включения шины I2C.
Подключение датчика DS18B20
Датчик DS18B20
Этот раздел можно пропустить, если подключение датчика BME280 прошло успешно. Подключать датчик DS18B20 можно на любой доступный контакт GPIO одноплатного компьютера. Для возможности получения значений датчика из .NET кода необходимо на уровне ОС включить интерфейс 1-Wire. Как это сделать, можно узнать в разделе «Создание своего DTBO для протокола 1-Wire» в публикации Работа с GPIO на примере Banana Pi BPI-M64. Часть 2. Device Tree overlays.
Установка и настройка брокера сообщений RabbitMQ
RabbitMQ ‒ это брокер сообщений. Его основная цель ‒ принимать и отдавать сообщения. Ознакомится с RabbitMQ можно в небольшом цикле публикаций RabbitMQ. Часть 1. Introduction. Erlang, AMQP.
В этом разделе будут настройки, которые необходимо выполнить на сервере RabbitMQ. Настройка клиентов сервера RabbitMQ будет в следующих разделах.
Протокол AMQP вводит три понятия:
- exchange (обменник или точка обмена) — принимает сообщения от поставщика. Обменник распределяет сообщение в одну или несколько очередей. Он маршрутизирует сообщения в очередь на основе созданных связей (binding) между ним и очередью;
- queue (очередь) — структура данных, состоящая из сообщений, существует внутри RabbitMQ. Хотя сообщения проходят через RabbitMQ и приложения, хранятся они только в очередях. Очередь не имеет ограничений на количество сообщений, она может принять сколь угодно большое их количество ‒ можно считать ее бесконечным буфером. Любое количество поставщиков может отправлять сообщения в одну очередь, также любое количество подписчиков может получать сообщения из одной очереди;
- binding (привязка) — правило, которое сообщает точке обмена, в какую из очередей эти сообщения должны попадать. Обменник и очередь могут быть связаны несколькими привязками.
Пользователи делятся на:
- Publishers (поставщики) — клиентское приложение, отправляет сообщения;
- Consumers (подписчики) — клиентское приложение, принимает сообщения из очереди. Обычно подписчик находится в состоянии ожидания сообщений.
При организации доступа к данным в RabbitMQ будем придерживаться принципа минимальных привилегий. В RabbitMQ будет два пользователя:
- user-sensors: отправка сообщений на сервер, для приложения WeatherStation.Sensors;
- user-lcd: чтение сообщений, для приложения WeatherStation.Panel.
Соответственно, пользователь user-sensors будет Поставщиком (Publisher), а user-lcd — Подписчиком (Consumer).
Сервер RabbitMQ позволяет заранее создавать очереди с необходимыми параметрами. В результате для Поставщика можно запретить создавать другие очереди. С точки зрения безопасности данный подход самый лучший, т.к. Поставщик будет работать только с той очередью, которую для него создали. Но для упрощения процедуры настройки учетной записи user-sensors будут выданы права на создание очереди.
Шаг 1 — Создание Docker-сети
До создания Docker контейнеров создадим Docker-сеть «mynetwork», IP-подсеть 172.21.0.0/24:
$ docker network create --driver bridge --subnet 172.21.0.0/24 --ip-range=172.21.0.0/25 --gateway 172.21.0.127 mynetwork
Шаг 2 — Создание Docker-контейнера с сервером RabbitMQ
Выполним развертывание сервера RabbitMQ на основе официального Docker-образа rabbitmq: RabbitMQ is an open source multi-protocol messaging broker. Создадим контейнер с RabbitMQ командой:
$ docker run -d --hostname rabbitmq-iot --name rabbit-iot -v rabbit-iot-config:/etc/rabbitmq -v rabbit-iot-lib:/var/lib/rabbitmq --net mynetwork --ip 172.21.0.5 rabbitmq:alpine
Сервер RabbitMQ использует два TCP-порта: 5672 — для доступа клиентов, 15672 — для управления через web-интерфейс.
По умолчанию веб-интерфейс не работает, но рекомендуется включить для мониторинга состояния очереди. Дополнительно, для отладки рекомендуется опубликовать доступ к порту 5672/TCP, т.к. приложение на API AvaloniaUI неудобно каждый раз запускать на Banana Pi BPI-M64 в режиме отладки. Для тестирования команда по созданию Docker-контейнера будет следующей:
$ docker run -d --hostname rabbitmq-iot --name rabbit-iot -p 5672:5672 -p 15672:15672 -v rabbit-iot-config:/etc/rabbitmq -v rabbit-iot-lib:/var/lib/rabbitmq --net mynetwork --ip 172.21.0.5 rabbitmq:alpine
После успешного запуска контейнера необходимо перейти в консоль Linux для настройки сервера RabbitMQ. Для включения web-интерфейса необходимо выполнить команду:
$ docker exec -ti rabbit-iot rabbitmq-plugins enable rabbitmq_management
Затем перейти по IP-адресу одноплатного компьютера, например: http://192.168.43.208:15672. Логин/пароль по умолчанию: guest/guest.
Шаг 3 — Создание виртуального хоста
Создадим виртуальный хост /host-iot:
$ docker exec -ti rabbitmq-iot rabbitmqctl add_vhost /host-iot
Шаг 4 — Создание пользователей
Для создания пользователей необходимо выполнить команды:
$ docker exec -ti rabbitmq-iot rabbitmqctl add_user user-sensors Password1 $ docker exec -ti rabbitmq-iot rabbitmqctl add_user user-lcd Password2
где Password1, Password2 — пароли учетных записей.
Шаг 5 — Выдача привилегий
Как создавать пользователей и разграничивать доступ рассказано подробно в документации Authorisation: How Permissions Work. Существует три операции: configure (конфигурирование), write (запись), и read (чтение). Операции назначаются на сущности: exchange (обменник), queue (очередь).
В результате:
- пользователь user-sensors будет обладать правами: configure exchange, configure queue, write exchange, write queue, read exchange, read queue;
- пользователь user-lcd будет обладать правами только read queue.
Права выдаются путем задания регулярного выражения. Для выдачи прав необходимо выполнить команды:
$ docker exec -ti rabbitmq-iot rabbitmqctl set_permissions -p "/host-iot" "user-sensors" ".*" ".*" ".*" $ docker exec -ti rabbitmq-iot rabbitmqctl set_permissions -p "/host-iot" "user-lcd" "" "" "queue"
Три параметра после имени пользователя, например «user-lcd», выдают привилегии на операции: configure, write, и read. В первых двух ничего не записано, значит, никаких прав configure и write у пользователя «user-lcd» не будет. Третий параметр выдает права на операцию read, в значении параметра указано «queue», значит, пользователю «user-lcd» выдаются права на чтение данных из очереди.
На этом настройка сервера RabbitMQ закончена.
Приложение WeatherStation.Sensors
Считывает показания с датчиков и отправляет данные на сервер RabbitMQ. Опрос датчиков производится с определенным временным интервалом, который задается в настройках. Если датчик BME280 не удается инициализировать, то будет произведен поиск любого доступного датчика по протоколу OneWire. Проект на GitHub — WeatherStation.Sensors. Подключенные Nuget-пакеты:
- Iot.Device.Bindings;
- System.Device.Gpio;
- Newtonsoft.Json;
- RabbitMQ.Client;
- Microsoft.Extensions.Hosting.
Файл конфигурации располагается по пути config/appsettings.json . Содержимое файла appsettings.json:
{ "AppSettings": { "Sensors": { "ReadEvery": 50, "I2CBusId": 1, "BME280Address": 118, "GPIOCHIP": 1, "pinLED": 36, "pinLED_active_low": true, "pinBUTTON": 38 }, "RabbitMQ": { "UserName": "user-sensors", "Password": "PASSWORD1", "VirtualHost": "/host-iot", "HostName": "192.168.43.208", "ClientProvidedName": "app:sensors component:event-consumer", "x_message_ttl": 300000, "x_max_length": 5, "exchangeName": "exchange-sensors", "queueName": "queue-sensors", "routingKey": "rKey1" } } }
Рассмотрим параметры:
- Sensors\ReadEvery — интервал времени считывания показаний датчиков в секундах;
- Sensors\I2CBusId — ID шины I2C, может быть: 0,1,2, и т.д. Если нет, то задать null;
- Sensors\BME280Address — адрес датчика BME280 на шине I2C, константа в формате byte. Если нет, то задать null;
- Sensors\GPIOCHIP — ID чипа GPIO. Если нет кнопки и светодиода, то задать null;
- Sensors\pinLED — номер контакта (GPIOXX, ножки процессора) светодиода. Если нет светодиода, то задать null;
- Sensors\pinLED_active_low — инвертирование значения вывода для светодиода. Если задать «1», то на логическую «1» светодиод будет выключен, и наоборот;
- Sensors\pinBUTTON — номер контакта (GPIOXX, ножки процессора) кнопки. Если нет кнопки, то задать null;
- RabbitMQ\UserName — имя пользователя для подключения к серверу RabbitMQ;
- RabbitMQ\Password — пароль пользователя UserName;
- RabbitMQ\VirtualHost — виртуальный хост;
- RabbitMQ\HostName — IP-адрес или DNS-имя сервера RabbitMQ;
- RabbitMQ\x_message_ttl — время жизни сообщения в очереди в миллисекундах (мс);
- RabbitMQ\x_max_length — максимальное количество сообщений в очереди. При превышение данного значения, из очереди автоматически удаляются самые старые сообщения;
- RabbitMQ\exchangeName — название обменника;
- RabbitMQ\queueName — название очереди, в которую будут отправляться сообщения;
- RabbitMQ\routingKey — ключ маршрутизации сообщений.
Минимальная конфигурация датчиков — это наличие BME280 или DS18B20. Если не будет светодиода, но будет кнопка, то кнопка будет работать. Если не будет кнопки, то просто второй экран с графиком не удастся отобразить.
Работа с шиной I2C и считывание значений с BME280 на C#
Весь программный код работы с датчиками располагается в файле Services/ReadSensorsServices.cs. Для работы с шиной I2C предназначено пространство имен: System.Device.I2c. Для датчика BME280 пространство имен: Iot.Device.Bmxx80. Примеры работы с датчиками серии Bmxx80 доступны по ссылке GitHub Bmxx80/samples.
Следующий фрагмент кода инициализирует датчик BME280 и считывает показания:
I2cConnectionSettings i2cSettings = new(_appSettings.Sensors.I2CBusId.Value, _appSettings.Sensors.BME280Address.Value); I2cDevice i2cDevice = I2cDevice.Create(i2cSettings); Bme280 bme280 = new Bme280(i2cDevice) { // set higher sampling TemperatureSampling = Sampling.UltraHighResolution, PressureSampling = Sampling.UltraHighResolution, HumiditySampling = Sampling.UltraHighResolution, FilterMode = Bmx280FilteringMode.X2 }; //Test Read bme280.ReadAsync().Wait();
Класс I2cConnectionSettings предназначен для задания настроек I2C устройства, первый аргумент — ID шины I2C, второй аргумент — адрес датчика I2C. В классе Bmx280Base определены константы адреса BME280:
// Default I2C bus address. public const byte DefaultI2cAddress = 119; // Secondary I2C bus address. public const byte SecondaryI2cAddress = 118;
Таким образом, параметр в настройках Sensors\BME280Address=118 соответствует значению SecondaryI2cAddress. Значение Sampling.UltraHighResolution задает максимальную точность замера физических величин.
Если использовать файл DTS sun50i-a64-i2c1-bme280.dts, который включает шину I2C и задействует драйвер получения данных с BME280 через файловую систему sysfs, и одновременно получать данные из .NET кода, то все это работает без ошибок.
Работа с DS18B20 на C#
Программный код работы с датчиком DS18B20 самый простой. Классы из пространства Iot.Device.OneWire по сути являются обертками над драйвером OneWire в Sysfs. Примеры работы с температурными датчиками по протоколу 1-wire доступны по ссылке GitHub OneWire/samples.
Следующий программный код находит первый доступный датчик, работающий по протоколу OneWire, и считывает его значение:
OneWireThermometerDevice devOneWire = OneWireThermometerDevice.EnumerateDevices().FirstOrDefault(); devOneWire.ReadTemperatureAsync().Wait();
Согласно перечислению enum в библиотеке Iot.Device.OneWire, файл DeviceFamily.cs, помимо DS18B20, поддерживается подключение следующих датчиков, работающих по OneWire-протоколу: DS18S20, MAX31820, DS1825, MAX31826, MAX31850, DS28EA00. Все указанные датчики тоже должны работать, но это неточно. Тестирование было проведено только с датчиком DS18B20.
Работа с кнопкой и светодиодом
Подробно рассмотрено в публикации Управляем контактами GPIO из C# .NET 5, в разделе Создание приложения обработки прерывания от кнопки. Номера контактов те же самые.
Отправка данных на сервер RabbitMQ
Алгоритм отправки данных на сервер RabbitMQ написан с учетом отсутствия доступности и потери соединения с ним. Если на момент запуска приложения сервер RabbitMQ будет недоступен, то приложение через определенный интервал времени будет пытаться соединиться с сервером. Если во время работы приложения будет разрыв соединения, то будут предприняты попытки восстановления соединения.
В очередь отправляются данные в формате JSON. Пример сообщения:
{ "HomeTemperature": 30.19, "HomePressure": 100254.0, "HomeHumidity": 39.0, "DateTimeNow": 637622330482396513 }
Структура данных следующая:
- Home* — значения физических величин;
- DateTimeNow — дата и время считывания показаний датчиков в тиках по времени UTC: DateTime.UtcNow.Ticks.
Когда срабатывает событие отпускания кнопки, сообщение отправляется незамедлительно. Пример сообщения:
{ "DateTimeNow": 637622332649242921, "Command": "PressButton1" }
Структура данных следующая:
- DateTimeNow — дата и время считывания показаний датчиков в тиках по времени UTC: DateTime.UtcNow.Ticks;
- Command — команда, нажата кнопка №1.
Запуск Docker-контейнера
Контейнер для запуска приложения WeatherStation.Sensors — devdotnetorg/dotnet-ws-sensors, доступна сборка только под архитектуру ARM64. Команда запуска контейнера:
$ docker run -d --name test-dotnet-ws-sensors --restart always --net mynetwork --ip 172.21.0.6 --device /dev/i2c-1 --device /dev/gpiochip1 -v /sys/bus/w1/devices:/sys/bus/w1/devices -v /sys/devices/w1_bus_master1:/sys/devices/w1_bus_master1 -v test-dotnet-ws-sensors-config:/app/config -e RabbitMQUserName=user-sensors -e RabbitMQPassword=PASSWORD1 devdotnetorg/dotnet-ws-sensors
Контейнеру необходимо передать доступ к устройствам: /dev/i2c-1 и /dev/gpiochip1 . Доступ к OneWire-датчикам осуществляется через sysfs, поэтому необходимо передать пути OneWire-шины: /sys/devices/w1_bus_master1 . Если используется BME280, то пробрасывать volume /sys/devices/w1_bus_master1 нет необходимости. Переменные окружения RabbitMQUserName, RabbitMQPassword переопределяют значения в config/appsettings.json . Поэтому в целях безопасности логин/пароль доступа к серверу RabbitMQ можно не хранить в конфигурационном файле.
Приложение WeatherStation.Panel.AvaloniaX11
Отображает графический интерфейс на LCD экране. Проект на GitHub — WeatherStation.Panel.AvaloniaX11. Подключенные Nuget-пакеты:
- Avalonia;
- System.Device.Gpio;
- Avalonia.Desktop;
- Avalonia.Diagnostics;
- Microsoft.Extensions.Configuration;
- Microsoft.Extensions.Hosting;
- Newtonsoft.Json;
- RabbitMQ.Client.
Является классическим desktop-приложением, построенным на фреймворке AvaloniaUI (кроссплатформенный фреймворк на основе XAML, WPF/UWP). Для первой реализации не использовался подход MVVM для построения интерфейса. Первоначальная задача заключалась в простом запуске на Linux в Docker-контейнере. Разметка элементов рассчитана на разрешение 320×240 точек. Работа с сервером RabbitMQ такая же, как и в приложении WeatherStation.Sensors. При потере соединения с сервером приложение будет пытаться повторно установить соединение, и пиктограмма соединения с зеленого цвета изменится на красный.
Графический интерфейс в Windows 7
В верхней части экрана отображается текущая дата и время, затем текущая температура, влажность и давление. Последняя строка служит для информирования времени замера показаний датчиков. Если данные были получены менее 10 секунд назад, то отображается надпись — «Данные получены недавно».
Внешний вид в Linux немного отличается:
Графический интерфейс в Linux Alpine
Пока не удалось решить проблему с настройкой culture в .NET, название месяца отображается по-английски.
График изменения температуры отображается интерактивным элементом с автоматическим обновлением, поддерживается анимация.
Для построения диаграмм использовалась библиотека LiveCharts2 от Alberto Rodriguez, которая работает на WPF, WinForms, Xamarin и Avalonia. В проект добавлена в виде программного кода /Libs/ .
Примеры диаграмм библиотеки LiveCharts2
На основе этой библиотеки можно сделать довольно красивый графический интерфейс, но нужно помнить, что это съест некоторые ресурсы.
Отображение графика изменения температуры:
Файл конфигурации располагается по пути config/appsettings.json . Содержимое файла appsettings.json:
{ "AppSettings": { "RabbitMQ": { "UserName": "user-lcd", "Password": "PASSWORD2", "VirtualHost": "/host-iot", "HostName": "192.168.43.208", "ClientProvidedName": "app:sensors component:event-consumer", "queueName": "queue-sensors" } } }
Параметры подключения к серверу RabbitMQ такие же, как и в приложении WeatherStation.Sensors.
Важное замечание. Разрабатывая графические приложения для Linux в IDE Windows, обращайте внимание на страницу кодировки текстовых строк, ресурсов и файлов исходного кода. Все ресурсы необходимо перекодировать в UTF-8. А то можете увидеть подобное:
Интерфейс в Linux с кодировкой Windows-1251
Как работает Docker контейнер
Для запуска приложения необходима графическая подсистема Xorg и оболочка xfce4. Запускается подсистема Xorg, затем xfce4, и срабатывает автозагрузка приложения dotnet /app/WeatherStation.Panel.AvaloniaX11.dll . Пока без xfce4 приложение аварийно закрывается. В следующей версии эта проблема будет решена.
Контейнер напрямую задействует Linux Framebuffer по адресу /dev/fb0 . Адрес пока захардкорен, в следующей версии будет задаваться через переменную. Поэтому вывод изображения возможен на любой LCD-дисплей, SPI LCD, HDMI и VGA.
Запуск Docker-контейнера
Контейнер для запуска приложения WeatherStation.Panel.AvaloniaX11 — devdotnetorg/dotnet-ws-sensors, доступна сборка под архитектуры: ARM64 и x86. Команда запуска контейнера:
$ docker run -d --name test-dotnet-ws-panel --restart always --net mynetwork --ip 172.21.0.7 --privileged --device=/dev/fb0 -v test-dotnet-ws-panel-config:/app/config -e RabbitMQUserName=user-lcd -e RabbitMQPassword=PASSWORD2 -e TZ=Europe/Moscow devdotnetorg/dotnet-ws-panel:avaloniax11
Контейнеру необходимо передать доступ к устройству Linux framebuffer: /dev/fb0 . Без выдачи привилегий не запускается Xorg. Переменные окружения RabbitMQUserName, RabbitMQPassword переопределяют значения в config/appsettings.json . Поэтому в целях безопасности логин/пароль доступа к серверу RabbitMQ можно не хранить в конфигурационном файле. Для настройки часового пояса текущего времени необходимо задать переменную «TZ». Её значения соответствую международному стандарту задания временных зон — List of tz database time zones.
Docker Compose. Особенности запуска контейнеров
Итоговый файл docker-compose.ws.AvaloniaX11.yml для Docker Compose будет следующим:
version: '2.2' services: # RabbitMQ rabbitmq-iot: container_name: rabbitmq-iot image: rabbitmq:alpine # restart: always hostname: rabbitmq-iot ports: - 5672:5672/tcp - 15672:15672/tcp volumes: - rabbit-iot-config:/etc/rabbitmq - rabbit-iot-lib:/var/lib/rabbitmq networks: mynetwork: ipv4_address: 172.21.0.5 healthcheck: test: rabbitmq-diagnostics -q ping interval: 10 s timeout: 10 s retries: 10 cpus: 0.6 #WeatherStation.Sensors test-dotnet-ws-sensors: image: devdotnetorg/dotnet-ws-sensors container_name: test-dotnet-ws-sensors restart: always devices: - /dev/i2c-1 - /dev/gpiochip1 environment: - RabbitMQUserName=user-sensors - RabbitMQPassword=PASSWORD1 volumes: - /sys/bus/w1/devices:/sys/bus/w1/devices - /sys/devices/w1_bus_master1:/sys/devices/w1_bus_master1 - test-dotnet-ws-sensors-config:/app/config networks: mynetwork: ipv4_address: 172.21.0.6 depends_on: rabbitmq-iot: condition: service_healthy links: - rabbitmq-iot cpus: 0.2 #WeatherStation.Panel test-dotnet-ws-panel: image: devdotnetorg/dotnet-ws-panel:avaloniax11 container_name: test-dotnet-ws-panel restart: always privileged: true devices: - /dev/fb0 networks: mynetwork: ipv4_address: 172.21.0.7 environment: - RabbitMQUserName=user-lcd - RabbitMQPassword=PASSWORD2 - TZ=Europe/Moscow volumes: - test-dotnet-ws-panel-config:/app/config # - /etc/timezone:/etc/timezone:ro depends_on: rabbitmq-iot: condition: service_healthy links: - rabbitmq-iot cpus: 0.8 volumes: rabbit-iot-config: name: rabbit-iot-config rabbit-iot-lib: name: rabbit-iot-lib test-dotnet-ws-sensors-config: name: test-dotnet-ws-sensors-config test-dotnet-ws-panel-config: name: test-dotnet-ws-panel-config networks: mynetwork: external: true
Для установки Docker Compose необходимо выполнить команду:
$ sudo apt-get update $ sudo apt-get install docker-compose
Для развертывания контейнеров необходимо скопировать на одноплатный компьютер файл docker-compose.ws.AvaloniaX11.yml, и в каталоге с этим файлом выполнить команду
$ docker-compose -p "DotnetServiceWeatherStation" --file docker-compose.ws.AvaloniaX11.yml up -d
Запускать все контейнеры одновременно не рекомендуется. При одновременном запуске контейнеров с сервером RabbitMQ и приложением WeatherStation.Panel.AvaloniaX11, накопитель данных просто умирает, и система уходит в ступор на 20 минут. При первоначальном запуске сервер RabbitMQ очень сильно загружает операциями I/O накопитель данных. Поэтому в Docker Compose автоматически стартует только два контейнера: WeatherStation.Sensors и WeatherStation.Panel. Запуск сервера RabbitMQ решен с помощью cron задачи. Эмпирическим путем было установлено, что двух минут достаточно для полной загрузки контейнеров WeatherStation.Sensors и WeatherStation.Panel. Выполним проверку работы cron демона, командой:
$ systemctl status cron
Результат выполнения команды:
root@bananapim64:~# systemctl status cron ● cron.service - Regular background program processing daemon Loaded: loaded (/lib/systemd/system/cron.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2021-07-16 01:45:28 MSK; 2 days ago Docs: man:cron(8) Main PID: 707 (cron) Tasks: 1 (limit: 2219) CGroup: /system.slice/cron.service └─707 /usr/sbin/cron -f
Cron демон успешно работает.
Для запуска сервера RabbitMQ создадим cron задачу, пополним команду:
crontab -e
В конец файла добавим строку:
@reboot sleep 120 && docker start rabbitmq-iot
Команда sleep 120, ожидание двух минут. Сервер RabbitMQ запускается примерно 2 минуты, таким образом, на полный запуск всех приложений уходит около 4 минут.
Утилизация ресурсов
Теперь посмотрим, сколько ресурсов потребляют контейнеры. В ОС не запущено никаких дополнительных фоновых процессов и лишних контейнеров, кроме portainer/portainer. Будем оценивать только потребление оперативной памяти и процессора. Загрузка четырех ядер Cortex-A53 на 100% будет взята за 100% загрузки CPU. Эквивалентом одноплатного компьютера Banana Pi BPI-M64 по производительности примерно является Raspberry Pi 3 и китайский вариант Orange Pi Zero 2.
Контейнер devdotnetorg/dotnet-ws-sensors
При первоначальном запуске контейнера потребление RAM составляет около 20 Мб, затем через некоторое время (около 30 минут) падает до ~7 Мб. Загрузка CPU на длительном периоде времени колеблется в диапазоне 1.5-3%, каких-либо скачков загрузки CPU во время старта не зафиксировано.
Потребление ресурсов контейнером devdotnetorg/dotnet-ws-sensors (источник графика — portainer/portainer)
Контейнер devdotnetorg/dotnet-ws-panel
Потребление RAM составляет около 75 Мб. Загрузка CPU на длительном периоде времени колеблется в диапазоне 12-18%. Первый пик (переключение на отображение графика температуры) вызвало увеличение загрузки CPU на 80%. Затем экран был переключен обратно в режим отображения температуры. Второй пик, повторное переключение на график, загрузка CPU 50%, повторная прорисовка графика требует уже меньше ресурсов. Третий пик, в третий раз переключили на график, загрузка CPU 50%. Если с режима отображения температуры переключаться на график и обратно с небольшим интервалом, около 10 секунд, то это не приводит к увеличению загрузки CPU.
Интерпретация полученных данных следующая. Первая инициализация графика приводит к загрузке CPU на 80%. После этого, если были добавлены новые точки, которые требуют перерисовки графика, при отображении графика загрузка CPU составляет 50%. Если новые точки не были добавлены, и переключиться на график, то это не приводит к увеличению загрузки CPU. Из этого следует, что отображение графика расходует довольно много ресурсов CPU. Потребление RAM при всех действиях не изменяется, т.к. график температуры находится в памяти в независимости от режима отображения.
Потребление ресурсов контейнером devdotnetorg/dotnet-ws-panel (источник графика — portainer/portainer)
Запуск метеостанции на Banana Pi M64 (полный вариант):
Вывод
Данная реализация проекта была проверкой работоспособности первоначальной идеи работы с GPIO из Docker-контейнеров и вывода полноценного UI на LCD без использования популярного фреймворка Qt for Embedded Linux. Результаты оказались более чем положительными. Производительности SoC Allwinner A64 (был выпущен в 2015 г.) оказалось достаточно для выполнения подобных задач. Загрузка контейнера devdotnetorg/dotnet-ws-sensors всего на 3% дает возможность запустить с десяток подобных контейнеров. Единственный недостаток — обнаружилось узкое место в низкой скорости обмена данными с microSD. Если использовать более быструю память eMMC, то, скорее всего, ситуации с долгой загрузкой можно избежать. Использование сервера RabbitMQ все же тяжеловато для подобного устройства. Желательно использовать более легкий брокер сообщений. У кого есть предложения, как говорится, welcome в комментарии.
Что дальше?
Нет предела совершенству, проект будет переписан, переработан и переделан, а именно:
- Изменить архитектурный шаблон в приложении WeatherStation.Sensors. При реализации была допущена ошибка в построении архитектуры приложения, которая приводит к невозможности нормального завершения работы. Ошибка была обнаружена когда приложение уже было готово.
- Решить проблему с выставлением культуры. На данный момент, не смотря на принудительную смену культуры на «ru-RU», интерфейс все равно остается на «en-US». В Docker-контейнере на основе buster смена культуры работает.
- Заменить сервер RabbitM другим брокером сообщений. Сервер RabbitMQ сильно загружает операциями I/O накопитель данных, что потенциально может привести к длительной блокировке устройства.
- Повысить безопасность. При формировании Docker-контейнеров применить лучшие практики, и воспользоваться руководством Безопасность встраиваемых систем Linux.
- Исключить использование xfce4. В приложении WeatherStation.Panel.AvaloniaX11 решить проблему с исключением при запуске поверх Xorg. В результате снизится нагрузка на CPU и уменьшится объем занимаемой оперативной памяти. РЕШЕНО.
- Собрать контейнер для ARMv7. Разобраться в проблеме сборки контейнера для архитектуры ARM32. РЕШЕНО.
- Сделать адаптивный интерфейс. В приложении WeatherStation.Panel.AvaloniaX11 сделать разметку для экранов с различным разрешением.
- Добавить темную тему. Для приложения WeatherStation.Panel.AvaloniaX11 включить возможность использования темной темы.
- Добавить управление яркостью экрана. Добавить еще один Docker-контейнер с приложением, который будет получать данные по уровню освещения от датчика TSL2561 Light Sensor, и генерировать аналоговый сигнал с помощью MCP4725 I2C DAC для управления яркостью LCD ILI9341. Дополнительно, будет доступно управление яркостью через драйвер gpio-backlight в случае данной поддержки у дисплея. Заодно практически будет проверена работа с шиной I2C в режиме совместного доступа из различных контейнеров.
- Сделать реализацию графического интерфейса на фреймворке Uno Platform. Фреймворк Uno Platform, так же как и Avalonia, позволяет запускать графические приложения поверх Xorg. Сравнить реализации по используемым ресурсам компьютера.
- Сделать реализацию графического интерфейса на фреймворке Uno Platform с использованием только Linux Framebuffer. Uno Platform позволяет нативно выводить приложение на виртуальное устройство для вывода графики Linux Framebuffer без использования какой-либо графической подсистемы в виде Xorg, и это точно. Было проведено успешное тестирование на Banana Pi BPI-M64. С точки зрения минимизации потребления ресурсов — идеальный вариант.
Ресурсы
Проект на GitHub — dotnet-service-weather-station.
Docker-контейнеры: devdotnetorg/dotnet-ws-sensors, devdotnetorg/dotnet-ws-panel.
Битва графических интерфейсов: Avalonia и Uno Platform VS. HTML (Node.js, Electron)
Теперь рассмотрим собственно суть подхода. Автор предлагает запускать web-браузер на самом устройстве для отображения HTML-страницы (интерфейс управления). HTML-страница загружается с http-сервера, в данном случае с Node.js. Таким образом, система отображения состоит из трех компонентов:
- Http-server и web-приложение — сервер/приложение отдачи контента;
- Веб-браузер — среда исполнения скриптового кода;
- Контент — скриптовый код, исполняемый в web-браузере.
Первое с точки зрения эксплуатации системы — браузер является лишним звеном, которое необходимо дополнительно обслуживать. Второе, когда в браузере вы отображаете «Hello word!», все работает нормально. Но ситуация меняется, когда вам необходимо отобразить графический интерфейс со сложной структурой на устройстве с малой производительностью. Дополнительно браузер съедает достаточно много оперативной памяти. Запускать все в Electron? На моем компьютере с процессором Intel Core i7-3520M запуск «насыщенных» приложений Electron приводит к тому, что вентиляторы начинают работать так, как будто я запускаю Watch Dogs 2. Это хорошо, если ваше устройство работает от электросети, а если используется автономный источник питания? Так что Electron — просто безумно тормозная и очень требовательная к ресурсам штука. Native-app must have!
Третье, другая серьезная проблема — это отслеживание ошибок в JS-коде. Сервер отдает только JS-код и контент в формате JSON. Отслеживание ошибок в JS-коде и отправки стека исключения на сервер, еще та затея. Основная моя претензия к построению интерфейса на HTML заключается в низкой производительности и существенно худшем контроле со стороны устройства, что отображается на экране у пользователя. Можно добавить обработчики исключений в JS-код. Но вам придется контролировать исключения в Node.js, JS-коде на стороне браузера и контролировать работу браузера.
Программный код Avalonia и Uno Platform выполняется нативно в системе и будет всегда быстрее JS-кода исполняемого в контексте браузера. А в случае вывода графики напрямую на Linux Framebuffer, не расходуются ресурсы на Xorg и браузер.
Теперь поговорим о команде разработчиков. Встраиваемое решение можно разделить на два приложения. Первое приложение — работа с GPIO и другими устройствами. Второе приложение — собственно сам графический интерфейс. Если графический интерфейс разрабатывать на Node.js, то потребуется разработчик с квалификацией программирования на JS и/или TypeScript. Первое приложение будете разрабатывать на C или C#. Таким образом, в команде должны быть разработчики с разным набором квалификации.
Если графический интерфейс разрабатывается на AvaloniaUI, и работу с датчиками реализовать на C# коде, то вам достаточно будет разработчиков с квалификацией по .NET (C#). В результате у вас будет однородная команда, что существенно упрощает кадровый вопрос.
Теперь рассмотрим аргументы за интерфейс на HTML, и действительно ли интерфейс, сделанный с использованием других технологий, создает такие проблемы. Рассмотрим тезисы:
«Программное обеспечение для них, как правило, зависимо от операционной системы, и попытка апгрейда любого компонента устройства (например, замена дисплея на более совершенную модель) часто оборачивается серьезной проблемой.» Нет, не оборачивается. История развития аппаратного и программного обеспечения — это история развития стандартизации и унификации. Если вы смените на Raspberry Pi один монитор с HDMI интерфейсом на другой, тоже с поддержкой HDMI, то вам не придется переписывать никакое ПО. Возможно, потребуется лишь поменять некоторые настройки для решения проблем с мерцанием изображения. Но это не переписывание основного прикладного ПО.
Если в моем примере решите заменить LCD ILI9341 на SPI-интерфейсе, на другой дисплей, например на MIPI DSI(Display Serial Interface, наиболее часто используется в смартфонах и планшетах) интерфейсе, то вам, безусловно, под новый дисплей потребуется новый драйвер, который может предоставить разработчик дисплея. Но переписывать приложение, например, такое как WeatherStation.Panel.AvaloniaX11 не потребуется, и это точно. Приложение WeatherStation.Panel.AvaloniaX11 использует абстрактное виртуальное устройство Linux Framebuffer, которое является платформонезависимым. API работы c данным устройством не зависит ни от модели конечных графический панелей, ни от аппаратной архитектуры: x86, ARM, RISC-V.
Вы готовы потратить ресурсы на разработку этого приложения и драйверов? Будете ли вы создавать их для каждой из популярных операционных систем: Windows, OSX, Linux? Не забудьте еще и о мобильных девайсах. Нет, и в этом нет проблемы. Если вы разрабатываете встраиваемое решение, то вас, скорее, будет волновать совместимость в пределах одной из платформ. Если это Linux, то решение должно работать на Ubuntu, Debian и т.д. Но если вы используете Docker-технологию для развертывания прикладного ПО, то эта проблема практически решается. А использовать железо с OS X — дорогое удовольствие. Windows — это отдельная тема, ее популярность в секторе встраиваемых устройств уже не такая как в 2000-х годах. Сейчас все большую роль играют Linux-решения и решения ,основанные на Linux-ядре (Android). Но, несмотря на это, приложение WeatherStation.Panel.AvaloniaX11 прекрасно работает на Windows 7, 10 (x86) и Linux (x86, ARM64), причем без какого-либо переписывания исходного кода. А если графический интерфейс построить на Uno Platform, то тот же программный код будет работать в Windows, iOS, macOS, Android, Linux и напрямую выводить изображение через Linux Framebuffer.
Почему же мы до сих пор не наблюдаем таких устройств? На самом деле, они есть: такие интерфейсы есть почти у всех роутеров и точек доступа, и они давным-давно отлично работают. Да, на роутере есть Web-интерфейс, но рендером HTML-странички занимается CPU и GPU моего компьютера. Роутер лишь отдает статику (файлы html, js) и обслуживает GET и POST запросы с JSON и/или XML данными. Я без особых проблем могу сделать приложение на ASP.NET Web API, прикрутить для статики nginx и все это запустить в Docker на Banana Pi BPI-M64. Затем нарисовать крутой Web-интерфейс и показывать его на своем компьютере. Только вот рендером крутой графики будет занимать мой компьютер, а не Banana Pi BPI-M64. И в этом заключается большая разница.
Как насчет задач, которые необходимо решать в реальном времени? И это не проблема. Помимо самого очевидного пути с установкой дополнительных микроконтроллеров, для действий в реальном времени можно использовать DMA. Вот вам работающий пример — PyCNC, который безо всяких дополнительных микронтроллеров управляет драйверами шаговых электродвигателей. В принципе да, но неплохо было бы еще рассказать и о недостатках DMA и заодно расшифровать, что это значит. DMA (Direct Memory Access) — прямое обращение к памяти в Linux по пути /dev/mem . Управлять GPIO путем прямой записи в регистры памяти дает большой выигрыш в скорости, но приводит к двум серьезным проблемам:
- Низкая надежность. Манипулировать регистрами памяти необходимо с ювелирной точностью и четко следить за их состоянием. Запись в соседний регистр в случае переполнения буфера может привести к краху операционной системы. А если залезете на регистры управления электропитанием, то можете из своего устройства сделать запеканку;
- Зависимость от SoC на одноплатном компьютере. Адресация регистров памяти для каждого чипа является индивидуальной, а значит, для каждого процессора придется переписывать драйвер обращения к памяти.
Для работы с контактами GPIO в посте Управляем контактами GPIO из C# были приведены результаты работы товарища ZhangGaoxing. Он для платы Orange Pi Zero написал драйвер поддержки процессоров Allwinner H2+/H3 под .NET платформу. В самом начале, я тоже хотел сделать подобный драйвер для Allwinner A64. Поняв, какие есть нюансы с безопасностью, а также что по факту можно запускать лишь один Docker-контейнер для работы с GPIO, взял на вооружение библиотеку Libgpiod. Данная библиотека гарантирует надежность и безопасность работы с GPIO и является платформонезависимой. Использование подхода DMA подойдет далеко не для всех проектов.
Но веб-приложение работает медленно! Конечно, это ненативное приложение. Но такая ли низкая скорость у интерфейса вашего роутера? У Slack, Skype или редактора Atom? Три последних примера — такие же приложения на базе Electron, и медленными их не назовешь. Без подтверждения быстрой работы этих приложений на Raspberry Pi сложно в это поверить. А Skype даже на моем компьютере временами лагает. Может быть и быстро работает, но хотелось бы видеть конкретные примеры.
Наконец, ваш исходный код веб-приложения можно спокойно перенести, если в будущем вы решите сменить железо на более современное. Приложение почти всегда продолжает работать и после этого. Сейчас этим никого не удивишь! Приложение WeatherStation.Panel.AvaloniaX11 на C# ( AvaloniaUI) работает просто с компиляцией для x86 архитектуры, ничего фантастического.
Для примера сделаем гипотетическую модернизацию до Intel Core i5 и запустим Docker-контейнер devdotnetorg/dotnet-ws-panel в Ubuntu 20.04.2.0 LTS (Focal Fossa). Среда исполнения: виртуальная машина x86 в VMWare Workstation:
Подведем общий итог
Каких-либо особых преимуществ использования графического интерфейса HTML на самих устройствах не проглядывается. Согласен, в каком-то смысле удобно. Но платишь за это усложнением решения, появляются лишние компоненты и проблемы, и это более требовательно к ресурсам устройства.
Как верно заметил @Stanejkee в комментариях, предложенный вариант является ничем иным как переключением работы браузера в режим Kiosk. Во многих торговых центрах, холлах гостиниц, выставках размещают информационные стенды. Внутри них обычный компьютер x86 с материнской платой в форм-факторе Mini-ITX со встроенным процессором, например, ASROCK J4105-ITX. С лицевой стороны обычный монитор с прикрученной touch-панелью, которая выполняет функции мыши. Внешний вид такой системы не походит на привычный офисный ПК, но по факту то же самое. Для контента просто берете любой сайт и заталкиваете его в браузер, и все готово! Если вам необходимо уже готовый сайт показать в публичном месте, то, безусловно, этот вариант прекрасен. Но если необходимо разработать интерфейс для технологического оборудования, домашней бытовой техники и т.д., подход к построению интерфейса будет совершенно другой.
Небольшой пример. Тоже «встраиваемое устройство» на Windows 10, показывающее фотографии сцен, спектаклей, и подобного материала в Театре юных зрителей им. А.А. Брянцева, г. Санкт-Петербург (дата съемки май 2021 г.):
Запуск приложения WeatherStation.Panel.AvaloniaX11 без использования Xfce4
Удалось запустить приложение с графической панелью на Xorg без установки Xfce. Проблема заключалась в отсутствии пакета менеджера шрифтов и самих шрифтов от MS. После добавления в файл Dockerfile.WS.Panel.AvaloniaX11.alpine, пакетов msttcorefonts-installer и fontconfig, приложение заработало. Проблема описана в Avalonia Issues: System.InvalidOperationException: Default font family name can’t be null or empty #4427 и Avalonia on Raspbian. Контейнер с запуском только на базе Xorg — dotnet-ws-panel:avalonia-xorg. Потребление RAM снизилось на 25 Мб, по сравнению с образом Xfce4, и составило 50 Мб. Нагрузка на CPU составила 8-12% по сравнению с 12-18%.
Потребление ресурсов контейнером devdotnetorg/dotnet-ws-panel:avalonia-xorg (источник графика — portainer/portainer)
Соответственно, загрузка интерфейса на старте системы ускорилась. Но перестал работать полноэкранный режим, и он в Xorg не работает.
Графический интерфейс без использования Xfce4 на HDMI-панели 7-Inch-1024×600 Display Kit, одноплатный компьютер Cubietruck (ARM32).
Проблема была решены путем запуска утилиты xrandr. Данная утилита возвращает ширину и высоту экрана в точках, соответственно получаем значения и выставляем их окну.
Теперь, в не зависимости от окружения, работает полноэкранный режим.
Графический интерфейс в окружение Xfce4 на HDMI-панели 7-Inch-1024×600 Display Kit, одноплатный компьютер Cubietruck (ARM32).
Контейнеры для архитектуры ARMv7 (ARM32)
Контейнеры собираются для различных платформ, таких как x86, ARM, RISC-V, с помощью инструмента Buildx. При сборке проекта под ARM32, шаг «dotnet restore …» завершался с ошибкой. В конечном итоге, перебрав все возможные варианты, остановился на варианте запуска сборки на одноплатном компьютере Cubietruck для платформы ARM32. Образы для данной платформы в названии тега содержат *-armhf. Из-за давней проблемы с вызовом функций, связанной с временем в Docker и Alpine, образ построен на основе Debian 10 (buster). И внезапно решилась проблема с «культурой», теперь месяц и день недели отображается на русском языке, как и задумывалось:
Контейнер с графическим интерфейсом на основе образа debian:buster, одноплатный компьютер Cubietruck (ARM32).
Теперь контейнеры можно запускать на Raspberry Pi, начиная со второй версии, соответственно, кроме моделей Zero и Pico.
Обсуждение на Habr.com
Литература
- AMQP (Advanced Message Queuing Protocol) — Wikipedia
- RabbitMQ tutorial: Hello World! — RabbitMQ
- RabbitMQ tutorial — Publish Subscribe — RabbitMQ
- Authentication, Authorisation, Access Control — RabbitMQ
- How to work with RabbitMQ in C# — Joydip Kanjilal. InfoWorld
- AMQP 0-9-1 Model Explained — RabbitMQ
- RabbitMQ. Часть 1. Introduction. Erlang, AMQP — Habr
- RabbitMQ REST API — Funprojects.blog
- Part 4. RabbitMQ Exchanges, routing keys and bindings — LOVISA JOHANSSON. CloudAMQP
- dt-overlays for Banana Pi BPI M64
- GitHub — dotnet/iot
- .NET IoT Libraries — Docs Microsoft
- GitHub — LiveCharts2. Powerful charts, maps and gauges for .Net, WPF, WinForms, Xamarin, Avalonia
- List of tz database time zones — Wikipedia