Контроллеры и OpenWRT
Описание
Методики работы, описанные в этой статье, могут показаться поверхностными. Они могут не подойти, например, тем, кто работает с enterprise-системами, с применением профессиональных методик программирования. Причина в том, что автору этой статьи долгое время пришлось работать не по специальности, и заниматься своими разработками в качестве хобби, в свободное от работы время. И методики, описанные здесь, родились именно в этот момент. Поэтому то, что описано здесь, проверено на практике, и успешно работает уже многие годы.
Мы рассмотрим построение системы управления удалёнными объектами с начала до конца: начиная от изготовления контроллеров для управления объектом, и заканчивая облачными приложениями и веб-интерфейсом оператора.
Вопрос, который наверняка возникнет одним из первых при разработке такой системы (а у нас он оказался и самым первым, и самым трудным) – это - какой контроллер выбрать. Всё многообразие контроллеров можно условно поделить на три типа:
1. Готовые контроллеры PLC, программируемые на языках типа Ladder Diagram
2. Контроллеры на основе однокристальных микроконтроллеров, таких, как stm32
3. Одноплатные микрокомпьютеры, работающие под управлением операционной системы (например, Linux)
При решении данного вопроса мы так и не пришли к единому мнению. Для нашей задачи мы решили использовать контроллеры либо типа 2, либо 3, но никак не могли решить, какого именно. Спор был настолько ожесточённым, что договориться мы так и не смогли; не договариваясь, мы начали разрабатывать каждый свой тип, а в проект системы внесли оба типа сразу. Я представлял сторону, отстаивающую тип 3. Забегая вперёд, скажу, что оба типа имеют право на существование, и у каждого есть свои преимущества и недостатки. В каждом конкретном случае какие-то из них приходится иметь ввиду. Давайте сравним их, и посмотрим, какие именно.
| Контроллеры на основе однокристальных микроконтроллеров stm32 | Одноплатные микрокомпьютеры Linux | |||
Аппаратная часть | (+) Простота схемотехники. Минимум деталей. | (-) Сложность принципиальной схемы. | |||
Стоимость | (+) Дешевизна | (-) Стоят в несколько раз больше | |||
Надёжность | (+) Нечему ломаться, всего одна микросхема | (-) Деталей больше, надёжность может быть ниже | |||
Затраты на программирование | (+) Простые программы пишутся быстро, и для их запуска не нужна операционная система | (+-) Простые программы тоже пишутся быстро, но вначале нужно скомпилировать, записать и запустить операционную систему, и уже в ней запускать программу | |||
Затраты на отладку программ | (+-) Программы отлаживаются быстро, благодаря, в частности, встраиваемым функциям отладки | (+) Функции отладки могут не понадобиться вовсе: загружаем новые версии программ (напр. по FTP), и запускаем их без перезагрузки операционной системы | |||
Функции удалённого доступа | (-) Сложно сделать так, чтобы оператор мог получать удалённый доступ в командную оболочку контроллера, находящегося на объекте, и управлять объектом командами | (+) Для удалённого доступа уже всё почти готово: есть командный интерфейс самой операционной системы, часто есть удалённый графический экран, или даже веб интерфейс | |||
Запуск нескольких функций управления одновременно | (-) Нужно подумать о разделении времени между задачами, подготовить задачи специальным образом, чтобы их можно было удалённо (ре)стартовать и т.д. | (+) Всё уже предусмотрено и сделано: разделение памяти, запуск многих процессов одновременно, есть встроенные средства для разделение общих ресурсов (портов, интерфейсов и пр.) Аварийное завершение одного процесса не приводит к зависанию системы | |||
Многопользовательский удалённый доступ (управление одновременно из нескольких мест или командами нескольких управляющих процессов) | (-) Трудно организовать одновременно несколько потоков входящих команд и возможное разрешение конфликтов между противоречащими друг другу командами | (+) Большинство этих вопросов решено уже в самой оперативной системе. Возможно назначение большого числа операторских консолей. Есть встроенные средства блокировки ресурсов для проведения транзакций и для наблюдения за поведением управляемого объекта в реальном времени (если им одновременно управляет кто-то другой) | |||
Поддержка задач реального времени | (+) Приложение, как правило, исполняется строго определённое число процессорных тактов | (-) Невозможно предугадать за сколько тактов будет выполнена та или иная задача | |||
|
|
|
Как видно из таблицы выше, везде есть свои плюсы и минусы, и всё может зависеть от конкретной ситуации. А мы теперь так и используем оба типа контроллеров одновременно.
Операционная система OpenWRT
Ещё 15 лет назад, в 2005 году, рынок беспроводных точек доступа и маршрутизаторов выглядел совсем по-другому. Типичным представителем такого устройства того времени был, к примеру, D-Link 824VUP. Это устройство является комбинацией Wifi - точки доступа, LAN-свича на 4 порта, файрвола с NAT (с одним WAN-портом), USB-портом, в который можно было подключить только принтер, и LPT-портом. Оно могло функционировать только со своей прошивкой, в которой были свои раз и навсегда определенные разработчиком функции, которые нельзя было поменять. Хочешь чего-то большего (например, подключить в USB не принтер, а флеш-память) – покупай другое устройство. Доступ к управлению всеми этими функциями был возможен только через веб-интерфейс (причем, он работал корректно только на строго определённых браузерах). Уже одного этого достаточно, чтобы понять, насколько ущербными и неполноценными были устройства, подобные этому.
И после этого становится понятно, какую революцию совершили операционные системы, подобные OpenWRT, в этом классе устройств. Стало возможным превратить такой маршрутизатор в полноценный компьютер, который не только имел бы возможность гибкого управления, но и на который можно было бы добавлять новые функции, любые, какие только пожелаешь. Стало возможным устанавливать на него готовые пакеты, или писать свои приложения.
Но самой главной особенностью таких систем, как OpenWRT, которая отличает их от других современных линукс-систем, стало то, что, несмотря на то, что это Линукс, они имеет всё тот же самый веб интерфейс управления, который был присущ изначально тем самым старым негибким маршрутизаторам. Таким образом, все основные действия с устройством можно выполнять не прибегая к командной строке, пользуясь знакомым и универсальным веб интерфейсом. До сих пор он остаётся самым удобным средством управления в том случае, если устройств в сети много, или если вдруг возникла необходимость что-то поменять в настройках, а где, в каких файлах эти настройки находятся – уже и не вспомнишь. Только теперь веб интерфейс стал расширяемым и ещё более удобным.
Основные конфигурационные файлы OpenWRT:
/etc/config/wireless
Настройки беспроводного соединения маршрутизатора, у которого есть приёмопередатчик WiFi. Для быстрого поиска WiFi точек доступа в пределах досягаемости удобно пользоваться функцией обзора WiFi сети в веб интерфейсе
config wifi-device 'radio0'
option type 'mac80211'
option hwmode '11g'
option channel '11'
option txpower '23'
config wifi-iface
option device 'radio0'
option network 'lan'
option mode 'ap'
option ssid '<ssid>'
option encryption 'psk2'
option key ' encryption_key'
/etc/config/network
Описание сетевых интерфейсов. Задаются IP адреса и маски
Для удобства отладки и диагностики маршрутизатора можно оставить на одном из интерфейсов IP адрес по умолчанию (192.168.1.1), и пользоваться им как O&M интерфейсом. При отсутствии достаточного числа интерфейсов, можно прописать этот адрес как alias – дополнительный IP адрес
config interface 'loopback'
option ifname 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'lan'
option ifname 'eth0'
option type 'bridge'
option proto 'static'
option ipaddr '192.168.1.1'
option netmask '255.255.255.0'
config interface 'wan'
option ifname 'ppp0'
option proto '3g'
option device '/dev/ttyUSB2'
option service 'umts'
option apn 'internet'
option keepalive '30'
option defaultroute '1'
option pppd_options 'refuse-chap refuse-mschap refuse-mschap-v2 refuse-eap'
/etc/config/firewall
Задаёт зоны безопасности, и правила, исходя из которых системные скрипты формируют таблицы для iptables
Для нормальной работы сетевых протоколов нужно оставлять включённым ICMP для входящих пакетов
config zone
option name lan
list network 'lan'
option input ACCEPT
option output ACCEPT
option forward ACCEPT
config zone
option name 'wan'
list network 'wan'
option input REJECT
option output ACCEPT
option forward REJECT
option masq 1
option mtu_fix 1
config forwarding
option src lan
option dest wan
config rule
option name Allow-SSH
option src wan
option proto tcp
option dest_port 22
option target ACCEPT
option family ipv4
/etc/config/system
Задаётся таймзона, NTP сервер, имя хоста
config system
option hostname <hostname>
option timezone UTC-3
config timeserver ntp
# list server 0.openwrt.pool.ntp.org
# list server 1.openwrt.pool.ntp.org
# list server 2.openwrt.pool.ntp.org
# list server 3.openwrt.pool.ntp.org
list server 'your_ntp_server'
option enabled 1
option enable_server 0
/etc/opkg/distfeeds.conf
Задаётся путь к серверу, откуда маршрутизатор будет устанавливать свои пакеты
src/gz chaos_calmer_base
http://<your_server>/chaos_calmer/15.05.1/mxs/generic/packages/base
src/gz chaos_calmer_packages http://<your_server>/chaos_calmer/15.05.1/mxs/generic/packages/packages
src/gz chaos_calmer_routing http://<your_server>/chaos_calmer/15.05.1/mxs/generic/packages/routing
/etc/inittab
Задаётся имя устройства, через которое подключается консоль
(hint) Для удобства, должно совпадать с устройством консоли u-boot
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
::askconsole:/usr/libexec/login.sh
ttyAMA0::askfirst:/usr/libexec/login.sh
/usr/libexec/login.sh
Соответствующий скрипт для логина с консоли, в котором можно задавать дополнительные действия, наличие/отсутствие пароля при логине с консоли и прочее
#!/bin/sh
[ "$(uci -q get system.@system[0].ttylogin)" == 1 ] || exec /bin/ash --login
exec /bin/login
/etc/diag.sh
Задаётся имя светодиода (соответствующая директория должна быть в /sys/class/leds/), который будет показывать миганием процесс загрузки системы. Часто мигает- медленно мигает- загорается непрерывно. В Failsafe режиме мигает очень часто.
get_status_led() {
status_led="LED1_GREEN"
}
set_state() {
get_status_led
case "$1" in
preinit)
status_led_blink_preinit
;;
failsafe)
status_led_blink_failsafe
;;
preinit_regular)
status_led_blink_preinit_regular
;;
done)
status_led_on
;;
esac
}
/etc/crontabs/root, /etc/rc.local, /etc/hosts
Это, безусловно, наиболее часто используемые при настройках маршрутизатора файлы
/etc/dropbear/authorized_keys
Список ключей для входа по SSH без пароля
/home/applications/
Директория для размещения пользовательских файлов. Именно сюда мы будем помещать свои написанные приложения для управления оборудованием. Имя директории можно изменить на любое другое
/home/applications/tools
Вспомогательные скрипты для управления оборудованием
Компиляция OpenWRT
Для установки OpenWRT на контроллер требуется сформировать следующие файлы:
Бинарный файл device tree, например controller.dtb
Образ ядра, например uImage
Образ файловой системы, например, ubi.img
Эти файлы получаются путём компиляции OpenWRT из исходников, причём в файловую систему можно сразу добавить необходимые пакеты и приложения
Процесс компиляции может со временем видоизменяться, поэтому для получения самой свежей информации имеет смысл перечитать документацию на сайте openwrt.org
Компиляцию OpenWRT имеет смысл начать с развёртывания виртуальной машины, на которой будет происходит компиляция. Рекомендуемые параметры машины:
Архитектура x86-64
Объём оперативной памяти 2 гБайт
Объём диска 42 гБайт*
* Примечание: такой объём диска понадобится из расчёта на то, что на виртуальной машине будут храниться два файловых дерева OpenWRT. Первое дерево будет использоваться для компиляции новых образов OpenWRT, и отладки программ, а второе служит образцом для работы, и содержит ранее созданную и успешно работающую копию первого дерева
Например, на диске можно создать две директории:
mkdir /home/<имя пользователя>/openwrt.main
mkdir /home/<имя пользователя>/openwrt.aux
или mkdir ~/openwrt.main
mkdir ~/openwrt.aux
И сделать линк с именем openwrt:
ln –s ~/openwrt.main ~/openwrt
Для написания собственных программ можно создать дополнительную директорию, например
mkdir -p ~/Work/C/Controller_scripts/2020/March/
В дальнейшем, мы сделаем скрипт, копирующий получившиеся дополнительные собственные программы в получившуюся файловую систему openwrt.
При работе начнут образовываться различные трудно запоминаемые последовательности команд, которые имеет смысл сразу записывать в короткие текстовые файлы-подсказки с расширением .howto, и сохранять в специально созданную для них директорию.
mkdir ~/openwrt/HOWTO
Для выдачи и сохранения истории набранных команд удобно пользоваться командой
history
Однако, не всё из получающихся при работе документов удаётся сохранить в таком виде. Поэтому стоит завести ещё одну структуру вне виртуальной машины, для скриншотов, xls, pdf, jpeg и прочих файлов.
Скачаем, настроим и скомпилируем openwrt. Все эти действия выполняем из под обычного пользователя (не рута)
Клонируем репозитарий openwrt с зеркала:
git clone https://github.com/openwrt/openwrt.git
cd ~/openwrt
Выбираем, какой именно релиз openwrt мы будем использовать (например, возьмём один из стабильных последних релизов), и переходим именно к нему
git fetch --tags
git tag -l
git checkout v18.06.1
Далее мы воспользуемся конфигурационным меню (make menuconfig) для задания списка собираемых модулей, но, прежде чем его начать использовать, сделаем так, чтобы в этом меню показывались бы дополнительные пакеты «третьих производителей» из дистрибутива:
./scripts/feeds update -a
./scripts/feeds install -a
Опции для сборки хранятся в файле ~/openwrt/.config
Сохраним этот файл на всякий случай в первоначальном виде:
cp ~/openwrt/.config ~/openwrt/.config.initial
И, теперь можно запускать конфигурационное меню, и задавать список модулей для компиляции
make menuconfig
О том, какие модули нам будут необходимы, мы расскажем позже, а пока можно задать, например, архитектуру системы:
Target System (Freescale i.MX23/i.MX28) -->
(X) Freescale i.MX23/i.MX28
Далее, необходимо сохранить конфигурацию в файл .config и выйти.
Существует также второе конфигурационное меню «kernel_menuconfig», отдельно для ядра.
make kernel_menuconfig
Оно обладает тем свойством, что может видоизменяться «на лету», путём добавления в директории драйверов ядра файлов с именем «Kconfig». Новые пункты меню добавляются и убираются автоматически. Например, проверим, каким образом в меню «kernel_menuconfig» оказалась опция:
> Device Drivers > Character devices > Serial drivers >
<*> MXS AUART support
MXS AUART console support
cd ~/openwrt/build_dir/target-arm_arm926ej-s_uClibc-0.9.33.2_eabi/linux-mxs/linux-3.18.45/drivers
cd tty/serial/
more Kconfig
#
# Serial device configuration
#
if TTY
menu "Serial drivers"
...
config SERIAL_MXS_AUART
depends on ARCH_MXS
tristate "MXS AUART support"
select SERIAL_CORE
help
This driver supports the MXS Application UART (AUART) port.
config SERIAL_MXS_AUART_CONSOLE
bool "MXS AUART console support"
depends on SERIAL_MXS_AUART=y
select SERIAL_CORE_CONSOLE
help
Enable a MXS AUART port to be the system console.
После того, как заданы любые опции в menuconfig и kernel_menuconfig, можно приступать к компиляции, например:
make –j5
Примечание, по умолчанию OpenWRT не использует многопотоковую сборку. Поэтому, если вы обладатель многоядерного процессора - используйте параметр -j (число ядер*2 +1)
Процесс первой компиляции длится долго, поэтому неважно, что пока не все параметры заданы правильно, главное, его пройти один раз, и получить какой-то результат. В дальнейшем будут скомпилированы только изменения в концигурации и дополнительные опции.
При компиляции может выясниться, что не хватает каких-то пререквизитов для сборки. В этом случае их необходимо добавить в виртуальную машину. Список пререквизитов также на на сайте openwrt.org (https://openwrt.org/docs/guide-developer/build-system/install-buildsystem)
Например, для виртуальную машину на OC Fedora24 необходимо будет поставить:
dnf install binutils bzip2 gcc gcc-c++ gawk gettext git-core flex ncurses-devel ncurses-compat-libs zlib-devel zlib-static make patch unzip perl-ExtUtils-MakeMaker perl-Thread-Queue glibc glibc-devel glibc-static quilt sed sdcc intltool sharutils bison wget openssl-devel
В результате компиляции мы получаем нужные нам файл device tree, образ ядра uImage и образ файловой системы:
cd ~/openwrt
ls -l ./build_dir/target-arm_arm926ej-s_uClibc-0.9.33.2_eabi/root-mxs/boot/uImage
ls –l ./build_dir/target-arm_arm926ej-s_uClibc-0.9.33.2_eabi/root-mxs/boot/controller.dtb
ls ./build_dir/target-arm_arm926ej-s_musl-1.1.16_eabi/root-mxs
Файл device tree, образ ядра uImage и образ файловой системы нам нужно будет записать на контроллер – это будет сделано позже, а пока сделаем себе удобный скрипт для сохранения этих файлов «на видном месте» - в директории ~/openwrt/output
mkdir ~/openwrt/output
vi ./make_output.sh
#!/bin/bash
Target=”target-arm_arm926ej-s_uClibc-0.9.33.2_eabi”
tar cvzf ~/openwrt/output/rootfs.tar.gz ~/openwrt/build_dir/$Target/root-mxs
cp ./build_dir/$Target/root-mxs/boot/uImage ./output
cp ./build_dir/$Target/root-mxs/boot/controller.dtb ./output
chmod 755 ./make_output.sh
./make_output.sh
В дальнейшем, будем дописывать в этот файл полезные команды, и запускать его каждый раз после того, как выполнили компиляцию make
Периферия контроллера и дерево устройств device tree
Для работы контроллера необходимо, чтобы его периферийные устройства были бы описаны в дереве device tree, и это описание должно быть правильным.
Здесь приводится пример, как те или иные устройства описываются в дереве.
Вот наиболее часто используемые периферийные устройства контроллера, которые приходится заново описывать в дереве устройств:
GPIO
Светодиоды
Последовательные порты
Последовательные порты с управлением направлением передачи (например, RS485)
SPI
I2C
1-Wire
Для каждого из этих устройств необходимо зарезервировать один, или несколько выводов микропроцессора, для чего необходимо указать
Какой именно сигнал микропроцессора мультиплексируется на этот вывод
Какое установить ограничение максимального тока, который будет выдавать вывод
Если есть подтягивающий резистор, нужно его включать, или нет
Файл device tree находится в директории (пример):
cd ./build_dir/target-arm_arm926ej-s_uClibc-0.9.33.2_eabi/linux-mxs/linux-3.18.45/arch/arm/boot/dts/
touch controller.dts
vi controller.dts
При компиляции в этой же директории должен получиться файл controller.dtb, который также копируется в директорию boot
Если вы получили исходный файл controller.dts от производителя, и хотите его отредактировать, не забудьте перед этим сохранить исходную версию там же
cp controller.dts controller.dts.initial
Если вы создадите в этой директории новый файл, и захотите, чтобы он тоже начал компилироваться вместе с остальными, отредактируйте в этой же директории Makefile
cp Makefile Makefile.initial
vi Makefile
dtb-$(CONFIG_ARCH_MXC) += \
...
dtb-$(CONFIG_ARCH_MXS) += controller.dtb
GPIO
Посмотрим, как выглядит описание GPIO в файле controller.dts
pinctrl@80018000 {
pinctrl-names = "default";
/* pin 44 - GPIO0_23 - Recovery LED */
led_pin_gpio0_23: led_gpio0_23@0 {
reg = <0>;
fsl,pinmux-ids = <
0x0173 /* MX28_PAD_GPMI_RDY3__GPIO_0_23 */
>;
fsl,drive-strength = <0>;
fsl,voltage = <1>;
fsl,pull-up = <0>;
};
}
Вывод смультиплексирован на GPIO, работает без pull-up. Если ядро будет правильно сконфигурировано, то после загрузки на контроллер операционной системы мы сможем работать с GPIO23 через файловую систему:
cd /sys/class/gpio
echo "23" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio23/direction
Написание файла device tree значительно ускорится, если доступна принципиальная схема контроллера, и устройства, в котором он будет работать
Так, не имея принципиальной схемы, мы только экспериментальным путём сможем определить, в каком случае светодиод Recovery LED будет гореть, а в каком нет:
echo "1" > /sys/class/gpio/gpio23/value
echo "0" > /sys/class/gpio/gpio23/value
Светодиоды
Для работы со светодиодами будет в будущем удобно пользоваться отдельной директорией файловой системы cd /sys/class/leds/. Для этого светодиоды должны быть описаны в device tree, подобным образом:
leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&led_pin_gpio0_23>;
heartbeat {
label = "controller:green:status";
gpios = <&gpio0 23 0>;
linux,default-trigger = "heartbeat";
};
};
Строка default-trigger описывает поведение светодиода по умолчанию, после загрузки ядра. В данном случае, светодиод будет мигать двойными вспышками ("heartbeat")
Однако, чаще все-таки, поведение светодиода меняется в процессе работы прибора, которым управляет контроллер. В таком случае, default-trigger = "none", а светодиодом будем управлять мы сами, своим скриптом, через соответствующее gpio, как показано выше.
На самом деле, один из вариантов управления светодиодом из приложения - это когда им управляет сама OC openwrt, получая конфигурацию светодиода из своего конфигурационного файла /etc/config/system:
cat /etc/config/system
...
config led 'led_usb1'
option name 'USB1'
option sysfs 'controller:green:usb1'
option trigger 'usbdev'
option dev '1-1'
option interval '50'
Здесь светодиод 'USB1' зажигается при присоединённом к порту устройстве. Разумеется, предварительно этот светодиод необходимо аналогично описать в device tree.
Последовательные порты
Для описания последовательного порта необходимо:
смультиплексировать его линии приёма и передачи на нужные выводы процессора (для этого опять-таки желательно иметь в наличии схему)
Если используются выводы аппаратного квитирования, сделать то же самое с ними
Один из выводов аппаратного квитирования может использоваться для управления направлением передачи (TX/RX). В этом случае нужно передать соответствующий параметр драйверу последовательного порта в ядре, и проследить за тем, чтобы это драйвер корректно его обработал. Мы посмотрим подробнее, как это сделать, ниже
cat controller.dts
...
duart: serial@80074000 {
pinctrl-names = "default";
pinctrl-0 = <&duart_pins_a>;
status = "okay";
};
auart0: serial@8006a000 {
pinctrl-names = "default";
pinctrl-0 = <&auart0_3pins_a>;
linux,rs485-enabled-at-boot-time;
fsl,dte-mode;
status = "okay";
};
…
duart_pins_a: duart@0 {
reg = <0>;
fsl,pinmux-ids = <
MX28_PAD_PWM0__DUART_RX
MX28_PAD_PWM1__DUART_TX
>;
fsl,drive-strength = <MXS_DRIVE_4mA>;
fsl,voltage = <MXS_VOLTAGE_HIGH>;
fsl,pull-up = <MXS_PULL_DISABLE>;
};
auart0_3pins_a: auart0-3pins@0 {
reg = <0>;
fsl,pinmux-ids = <
MX28_PAD_AUART0_RX__AUART0_RX
MX28_PAD_AUART0_TX__AUART0_TX
MX28_PAD_AUART0_RTS__AUART0_RTS
>;
fsl,drive-strength = <MXS_DRIVE_4mA>;
fsl,voltage = <MXS_VOLTAGE_HIGH>;
fsl,pull-up = <MXS_PULL_DISABLE>;
};
Здесь даны два примера: для двухпроводного порта duart и для трёхпроводного порта auart0, в котором вывод RTS управляет направлением передачи данных
Примечание: некоторые части этого описания могут уже присутствовать вместо файла controller.dts в готовом файле с расширением .dtsi (например, imx28.dtsi). Их можно скопировать из dtsi в controller.dts и модифицировать там, или воспользоваться готовыми заготовками в .dtsi
В случае с трёхпроводным портом может понадобиться модифицировать файл драйвера последовательного порта (а в последних версиях ядра это уже сделано):
cd /build_dir/target-arm_arm926ej-s_uClibc-0.9.33.2_eabi/linux-mxs/linux-3.18.45/drivers/tty/serial
cp mxs-auart.c mxs-auart.c.initial
vi mxs-auart.c
static int serial_mxs_probe_dt(struct mxs_auart_port *s,
struct platform_device *pdev)
{
...
if (of_property_read_bool(np, "linux,rs485-enabled-at-boot-time"))
s->rs485.flags |= SER_RS485_ENABLED;
}
…
if (s->rs485.flags & SER_RS485_ENABLED) {
…
__raw_writel(AUART_CTRL2_RTS, s->port.membase + AUART_CTRL2_SET);
}
Подробнее обо всех необходимых изменениях в драйвере можно узнать, изучая исходники подобных драйверов. Смысл их в том, чтобы активировать вывод RTS на время, когда в FIFO порта находятся данные, плюс некоторое время, необходимое для передачи последнего байта данных.
Отлаживать данные изменения можно при помощи осциллографа, подключая его поочерёдно к выводам порта
Если всё сделано правильно, после загрузки операционной системы в директории /dev контроллера появляются новые устройства, /dev/ttyAPP0, /dev/ttyAPP1, и т.д. Порты можно настроить командой stty, и ею же просмотреть настройки:
stty -a -F /dev/ttyAPP0
speed 9600 baud; rows 0; columns 0; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 hupcl -cstopb cread clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
Для отправления и получения текстовых сообщений через последовательный порт удобно использовать соответственно echo и cat, или пользоваться терминальной программой, такой как minicom. Изменения в её настройках сохраняются в файлах /etc/minirc.<имя конфигурации minicom>
cat /etc/minirc.app1
# Machine-generated file - use "minicom -s" to change parameters.
pu port /dev/ttyAPP1
pu rtscts No
Другие периферийные устройства
Большинство остальных периферийных устройств описываются в device tree точно по такому же принципу:
указывается, на какие выводы процессора мультиплексируется то или иное устройство
указываются его дополнительные параметры и, если требуется, внешние устройства, присоединённые к нему.
Например:
&i2c2 {
clock_frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
status = "okay";
pcf8574: gpio@38 {
compatible = "nxp,pcf8574a";
reg = <0x38>; // PCF8574A Address -0-0-0
gpio-controller;
};
oled@3c {
compatible = "solomon,ssd1306fb-i2c";
reg = <0x3c>;
solomon,width = <128>;
solomon,height = <32>;
solomon,page-offset = <0>;
};
pinctrl_i2c2: i2c2grp {
fsl,pins = <
MX6UL_PAD_CSI_HSYNC__I2C2_SCL 0x4001b8b0 // check ok
MX6UL_PAD_CSI_VSYNC__I2C2_SDA 0x4001b8b0
>;
};
Здесь описана конфигурация порта i2c. Два вывода его мультиплексируются на определённые пины, и установлена скорость порта 100кбит/с
Показывается, что к этой шине i2c подключены два устройства: расширитель GPIO типа pcf8574a, на котором перемычками выставлен адрес на шине 0x38, и светодиодный дисплей ssd1306fb, имеющий адрес на шине 0x3F.
Если всё подключено правильно, и присутствуют драйвера в ядре, то устройства i2c будут работать. Их можно увидеть и продиагностировать, установив в системе i2ctools:
i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- UU -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
i2cdump -y 1 0x3c
No size specified (using byte-data access)
0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef
00: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
10: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
20: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
30: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
40: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
50: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
60: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
70: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
80: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
90: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
a0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
b0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
c0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
d0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
e0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
f0: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
Также, устройства i2c появятся в директории /sys/class/i2c-dev/i2c-X/device/
Так как в контроллер не попадает исходник device tree, спустя некоторое время, работая с контроллером удалённо, можно легко забыть, на какое GPIO, или на какой адрес какой шины что подключено. Поэтому, стоит создать в директории /home/applications/tools контроллера текстовый файл с их кратким описанием (а если нет времени это делать, то хотя бы положить туда сам исходный dts файл). Как эту директорию включить в состав прошивки, мы посмотрим ниже.
Пример такого файла:
/home/applications/tools# cat GPIOS.txt
BANK GPIO LINUX DESCR DIR
-----------------------------------------------------------
2 19 51 Radio PWR out
2 20 52 GSM PWR out
2 16 48 OpenWRT Green LED out
2 17 49 OpenWRT Red LED out
2 18 50 OpenWRT Blue LED out
&pcf8574 0 ? LED1 3G
&pcf8574 1 ? LED2 WIFI
&pcf8574 2 ? LED3 USB
&pcf8574 3 ? LED4 GPS
&pcf8574 4 ? LED5 SIM
&pcf8574 5 ? LED6 AUX
&pcf8574 6 ? LED7 ETH0
&pcf8574 7 ? LED8 ETH1
4 23 119 Dallas 1 Wire both
4 26 122 SPI CS out
4 18 114 WiFi PWR ON out
3 10 74 RADIO RESET out
1 25 25 SIM5300 RESET out
4 17 113 SIM5300 PowerKEY out
1 27 27 SIM Card select out
1 23 23 Reboot Pin for watchdog out
4 22 118 GYRO_INT in
4 21 117 GYRO_INT2 in
3 14 78 GYRO_INT1 in
3 13 77 TAMPER in
2 21 53 14 вывод - свободный
1 19 19 16 вывод - свободный
4 23 119 1wire i/o
1 22 22 up 1wire
i2c addresses:
38 pcf8574 8-led module ---> проверка i2cdetect -y 1
3c SSD1306 OLED Display SSD1306 Display I2C address 0x3C
68 mpu6050(0x68)
Здесь LINUX - это номер GPIO в директории /sys/class/gpio, вычисляемый по формуле LINUX=(BANK-1)*32+GPIO
Примечание - по данной формуле вычисляется номер GPIO для процессоров семейства iMX6ULL. Для семейства iMX28 формула будетLINUX = BANK*32 + GPIO. Для процессоров NUC980 PA0-PA15 это GPIO0 -GPIO15, PB0-PB16 это GPIO32-GPIO47 и т.д.
Сохранение полученного дерева device tree
Чтобы не вспоминать каждый раз, где находится dts файл, когда очередной раз захочется его отредактировать, внесём дополнительную строчку в ~/openwrt/make_output.sh:
echo ”cp ./build_dir/$Target/root-mxs/boot/controller.dts ./output” >> ~/openwrt/make_output.sh
Другой вариант - это сделать линк на этот файл, но тогда желательно всё-таки сохранять где-то копию файла, чтобы в случае неработоспособности быстро его восстановить.
cd /openwrt/output
ln -s ../build_dir/<путь к таргету>/root-mxs/boot/controller.dts
Подключение драйверов устройств и установка приложений в OpenWRT.
По мере необходимости, будем добавлять в конфигурационные меню menuconfig и kernel_menuconfig дополнительные компоненты. В таблицы ниже приведены основные пункты, которые необходмо отметить в обоих меню для подключения различных типов периферийных устройств
Тип периферийного устройства | menuconfig | kernel_menuconfig |
USB порт на плате контроллера | -*- kmod-usb-core <*> kmod-usb-ledtrig-usbport <*> kmod-usb-ohci <*> kmod-usb2 <*> usbutils | <*> Support for Host-side USB <*> Support for Freescale i.MX on-chip EHCI USB controller <*> OHCI HCD (USB 1.1) support |
USB-флеш | То же, плюс: -*- kmod-usb-storage <*> kmod-usb-storage-extras -*- kmod-scsi-core <*> kmod-fs-ext4 <*> kmod-fs-vfat <*> e2fsprogs <*> dosfstools <*> fdisk | <*> USB Mass Storage support |
MMC-флеш | То же, плюс: <*> kmod-mmc | <*> MMC/SD/SDIO card support <*> MMC block device driver <*> SDHCI platform and OF driver helper <*> SDHCI support for the Freescale eSDHC/uSDHC i.MX controller |
Последовательные порты | <*> coreutils-stty <*> minicom | |
USB-to-RS232 кабель | <*> kmod-usb-serial <*> kmod-usb-serial-pl2303 <*> kmod-usb-serial-ftdi | <*> USB Mass Storage support <*> USB Prolific 2303 Single Port Serial Driver <*> USB FTDI Single Port Serial Driver |
3G модем | <*> kmod-usb-acm <*> comgt -*- chat <*> ppp | <*> USB Modem (CDC ACM) support |
i2c порт | -*- kmod-i2c-core -*- kmod-i2c-gpio <*> kmod-i2c-gpio-custom <*> i2c-tools | |
Светодиоды | <*> kmod-leds-gpio <*> kmod-ledtrig-default-on <*> kmod-ledtrig-gpio <*> kmod-ledtrig-heartbeat <*> kmod-ledtrig-netdev <*> kmod-ledtrig-timer | <*> LED Class Support <*> LED Support for GPIO connected LEDs <*> PWM driven LED Support <*> LED Timer Trigger <*> LED Heartbeat Trigger <*> LED Default ON Trigger <*> LED Netdev Trigger |
1-Wire DS18B20 | <*> kmod-w1 <*> kmod-w1-gpio-custom -*- kmod-w1-master-gpio <*> kmod-w1-slave-therm | <*> Dallas's 1-wire support ---> <*> GPIO 1-wire busmaster <*> Thermal family implementation |
Wifi контроллер rtl8723bu | -*- kmod-cfg80211 <*> kmod-mac80211 <*> hostapd <*> wpa-supplicant -*- hostapd-common -*- iw <*> wireless-tools | <*> cfg80211 - wireless configuration API <*> Generic IEEE 802.11 Networking Stack (mac80211) <*> USB Wireless Device Management support <*> RTL8723AU/RTL8188[CR]U/RTL819[12]CU (mac80211) support |
GPIO | ||
I2c GPIO expander | <*> PCF857x, PCA{85,96}7x, and MAX732[89] I2C GPIO expanders | |
SPI порт | -*- kmod-spi-dev <*> spi-tools <*> spidev-test | -*- Utilities for Bitbanging SPI masters <*> Freescale i.MX SPI controllers |
MPU3050 гироскоп | <*> Industrial I/O support ---> <*> Invensense MPU3050 devices on I2C | |
PWM | <*> i.MX PWM support |
Во некоторых случаях, если изменения сделать только в menuconfig, то после выполнения компиляции дополнительных компонент, автоматически происходят недостающие изменения и в kernel_menuconfig
Также, стоит отметить наиболее полезные и востребованные приложения, сразу доступные для встраивания в прошивку OpenWRT:
Название приложения | Функция | menuconfig |
mc | Файловый менеджер | <*> mc Примечание: для корректного отображения рамок необходимо добавить в директорию /root контроллера файл .profile с содержимым “TERM=linux” |
xxd | Вывод hex-дампов на экран | <*> xxd |
tar | Архиватор | <*> tar |
bash | Оболочка | <*> bash |
openvpn | VPN приложение | <*> openvpn-openssl |
openssh client | Полнофункциональный клиент SSH (в отличие от встроенного в OpenWRT Busybox) | <*> openssh-client |
openssh-sftpclient | Полнофункциональный клиент SFTP | <*> openssh-sftp-client |
Установка пользовательских приложений. Дополнительные настройки системы
Для дальнейшей установки нам понадобится наложить “сверху” на файловую систему Openwrt собственные файлы. Для этого сформируем дополнительную директорию, которую в дальнейшем будет копировать в корень rootfs, и сделаем скрипт для этого копирования
На виртуальной машине:
mkdir ~/openwrt/additional_rootfs
cd ~/openwrt/additional_rootfs
mkdir etc
mkdir home
mkdir root
mkdir -p etc/config etc/crontabs etc/dropbear etc/modules.d
mkdir -p home/mnt home/applications home/applications/tools
Таким образом, у нас теперь есть дополнительное дерево каталогов, куда мы можем записывать собственные настройки системы и приложения
Для того, чтобы скопировать это дерево в rootfs, отредактируем наш make_output.sh, чтобы он выглядел следующим образом (изменения выделены жирным шрифтом):
vi ./make_output.sh
#!/bin/bash
Target=”target-arm_arm926ej-s_uClibc-0.9.33.2_eabi”
root_dir="~/openwrt/build_dir/$Target/root-mxs"
cp -r ~/openwrt/additional_rootfs/* $root_dir
tar cvzf ~/openwrt/output/rootfs.tar.gz ~/openwrt/build_dir/$Target/root-mxs
cp ./build_dir/$Target/root-mxs/boot/uImage ./output
cp ./build_dir/$Target/root-mxs/boot/controller.dtb ./output
cp ./build_dir/$Target/root-mxs/boot/controller.dts ./output
Теперь всё готово для копирования дополнительного дерева. Начнём наполнять его файлами.
Скопируем с контроллера его файл /etc/config/network в ~/openwrt/additional_rootfs/etc/config/network и отредактируем его, как это нужно.
Например, добавим туда описание второго Ethernet-интерфейса следующим образом:
config interface 'lan2'
option ifname 'eth1'
option proto 'static'
option ipaddr '192.168.10.1'
option netmask '255.255.255.0'
option ip6assign '60'
Так же поступим и с файлом с контроллера /etc/config/system, скопируем и отредактируем его, например, изменив time зону на UTC-3:
config system
option hostname OpenWRT-DBG
option timezone UTC-3
config timeserver ntp
list server 0.openwrt.pool.ntp.org
list server 1.openwrt.pool.ntp.org
list server 2.openwrt.pool.ntp.org
list server 3.openwrt.pool.ntp.org
option enabled 1
option enable_server 0
Теперь директория ~/openwrt/additional_rootfs/etc/config/ у нас содержит файлы network и system, которые после компиляции OpenWRT и копирования заменят собой дефолтные файлы на контроллере.
Таким же точно образом добавим файлы:
root в директорию etc/crontabs
authorized_keys в etc/dropbear
Файлы для запуска дополнительных модулей, например, 56-w1-gpio-custom в etc/modules.d
Файлы etc/hosts etc/inittab etc/minirc.acm etc/minirc.app1 etc/minirc.app2 /etc/rc.local
Примечание 1: некоторые файлы настроек на диске контроллера создаются автоматически системой при первой загрузке. В них записаны дефолтные настройки. При этом эти файлы ещё не существуют на тот момент, когда rootfs создан после компиляции Openwrt. Однако, если мы заранее создадим эти файлы в rootfs заранее, то контроллер не будет создавать дефолтные файлы, и в системе получатся именно такие настройки, которые нам нужны
Примечание 2: однако, всё равно, какие-то изменения в файловой системе контроллера, возможно, придётся делать после того, как он первый раз загрузится. Как это можно сделать?
Применение дополнительных настроек после первой загрузки
Переименуем файл /etc/rc.local в /etc/rc.local.main, и создадим /etc/rc.local, который будет применять дополнительные настройки после первой загрузки системы, и, затем, самоуничтожаться. Например:
mv rc.local rc.local.main
vi rc.local
chown root:root /etc/dropbear/authorized_keys
chmod 600 /etc/dropbear/authorized_keys
mv -f /etc/rc.local.main /etc/rc.local
chown root:root /etc/crontabs/root
echo -ne "export TERMINFO=/usr/share/terminfo\nexport TERM=xterm\nalias mc=\"mc -a\"\nexport LANG=ru_RU.UTF-8\n\n" >> /etc/profile && sync
reboot
exit 0
В результате исполнения такого файла, система после первой загрузки перезагрузится второй раз, и произойдут нужные нам изменения
Процесс прошивки контроллера
Настало время рассмотреть подробнее процесс прошивки контроллера.
В зависимости от наличия тех или иных интерфейсов ввода-вывода на плате контроллера процесс прошивки может выглядеть по-разному. Также, по-разному он выглядит для разных типов флеш памяти.
Рассмотрим вариант, когда на плате контроллера установлен разъём для внешней памяти MicroSD, и для типа флеш памяти NAND
Используем дополнительную флеш-карту MicroSD для переноса файлов с прошивкой с компьютера на контроллер. Объём скомпилированной OpenWRT небольшой (например, около 16 мегабайт), поэтому для этого нам подойдет почти любая MicroSD карта. Отформатируем её в FAT16, и запишем в её корень файлы прошивки (как их подготовить, написано ниже):
Controller.dtb (device tree)
Ubi.img (ядро)
uImage (образ файловой системы rootfs в UBIFS- формате)
Для того, чтобы всё это заработало, нам понадобится:
Загрузчик U-boot c поддержкой FAT16 и NAND
Ядро с поддержкой MTD, UBI и UBIFS
Для того, чтобы включить в ядре поддержку MTD, UBI и UBIFS, скомпилируем систему со следующими опциями:
Функциональность ядра | menuconfig | kernel_menuconfig |
MTD, UBI и UBIFS | <*> mtd <*> kmod-mtd-rw | <*> Memory Technology Device (MTD) support ---> <*> NAND Device Support ---> <*> GPMI NAND Flash Controller driver <*> Enable UBI - Unsorted block images ---> (4096) UBI wear-leveling threshold (20) Maximum expected bad eraseblock count per 1024 eraseblocks <*> UBIFS file system support |
Последовательность действий при прошивке контроллера будет следующей:
Подключить к контроллеру консольный кабель и USB (Slave-порт для прошивки)
Замкнуть пин программирования контроллера на “землю”, нажать reset.
Запустить mfgtool и прошить U-boot через профайл openwrt (о том, как подготовить профайл, расскажем ниже)
Подготовить micro-SD карту, на который записаны 3 файла:
controller.dtb
ubi.img
uImage
Вставить карту в micro-SD слот
5. Подать команды в консоли, перечисленные в таблице ниже
Команда | Комментарий |
setenv bootargs 'console=ttyAMA0,115200 ubi.mtd=6 root=ubi0:rootfs rootfstype=ubifs rw gpmi' | Настройка параметров, которые передаются ядру для загрузки. В частности, номер партиции (6), в которую мы запишем rootfs |
setenv mtdparts mtdparts=gpmi-nand:3m(bootloader)ro,512k(environment),512k(redundant-environment),4m(kernel),512k(fdt),8m(ramdisk),-(rootfs) | Присваиваем имена и размеры 6 партициям, которые мы собираемся использовать, в частности 1 - bootloader (3 МБайт) 4 - kernel (4 МБайт) 5 - device tree (512кБайт) 6 - rootfs (весь остальной диск) Примечание: другие варианты разметки диска, позволяющие организовать двойную загрузку системы, будут описаны ниже |
nand erase.part rootfs; nand erase.part fdt; nand erase.part kernel | Стираем партиции командами NAND |
fatload mmc 0 $loadaddr uimage | Загружаем в буфер обмена образ ядра Примечание: в переменной $filesize после этого получаем размер ядра |
setenv kernel_size 0x$filesize | Сохраняем размер ядра себе в переменную |
nand write $loadaddr kernel $filesize | Прошиваем ядро |
fatload mmc 0 $loadaddr controller.dtb; setenv dtb_size 0x$filesize; nand write $loadaddr fdt $filesize | Загружаем и прошиваем device tree. Сохраняем размер ядра в переменную |
fatload mmc 0 $loadaddr ubi.img; nand write.trimffs $loadaddr rootfs $filesize | Загружаем и прошиваем rootfs. Команда write.trimffs дополнительно проверяет, что если в конце образа rootfs находятся страницы, в которых находится только 0xff, их запись не производится |
setenv ethaddr F6:18:88:80:ff:ff | Пример того, каким образом прошивается уникальный MAC-адрес в каждое устройство. Мы будем пользоваться в дальнейшем его уникальным MAC-адресом для автоматической генерации Device ID при присоединении к сети IoT |
setenv bootcmd 'nand read 0x42000000 kernel $kernel_size; nand read 0x41000000 fdt $dtb_size; bootm 0x42000000 - 0x41000000'; saveenv | При будущей загрузке U-boot должен будет поместить ядро в RAM по адресу 0x42000000 и device tree по адресу 0x41000000 и передать управление ядру |
После этого необходимо нажать ресет и загрузиться. Система перезагрузится 2 раза (из-за применения изменений настроек в rc.local, которое мы предусмотрели выше)
В процессе загрузки удобно вести наблюдение за ней по поведению светодиода Recovery/Failsafe (см выше описание /etc/diag.sh)
В консоли появится приглашение Openwrt, и системой можно начинать пользоваться.
При первой загрузке рекомендуется сменить пароль командой passwd
Формирование файлов прошивки из готового образа Openwrt
Для формирования файлов прошивки можно воспользоваться следующими командами, которые сразу удобно внести в make_output.sh:
vi ./make_output.sh
#!/bin/bash
Target=”target-arm_arm926ej-s_uClibc-0.9.33.2_eabi”
root_dir="~/openwrt/build_dir/$Target/root-mxs"
cp -r ~/openwrt/additional_rootfs/* $root_dir
mkfs.ubifs -r $root_dir -m 2048 -e 126976 -c 1900 -o ubifs.img
ubinize -o ubi.img -m 2048 -p 128KiB -s 2048 ubinize.cfg
tar cvzf ~/openwrt/output/rootfs.tar.gz ~/openwrt/build_dir/$Target/root-mxs
cp ./build_dir/$Target/root-mxs/boot/uImage ./output
cp ./build_dir/$Target/root-mxs/boot/controller.dtb ./output
cp ./build_dir/$Target/root-mxs/boot/controller.dts ./output
vi ./ubinize.cfg
[rootfs]
mode=ubi
image=ubifs.img
vol_id=1
vol_size=80MiB
vol_type=dynamic
vol_name=rootfs
vol_flags=autoresize
Подключение модема
Работу контроллера с модемом рассмотрим на примере 3G USB модема Simcom SIM5320E/5360
Вначале рассмотрим случай, когда на плате контроллера расположен модем с одной сим картой, и нам нужно организовать выход контроллера в Интернет, для последующей связи с управляющим сервером.
Модем имеет USB порт, через который происходит обмен данными. После того, как в системе будут установлены основные драйверы usb и usb-acm (см. таблицу выше в разделе “подключение драйверов устройств”), и модем будет включен, он может определяться в системе как устройство со следующими идентификаторами:
lsusb
Bus 001 Device 045: ID 05c6:9000 Qualcomm, Inc. SIMCom SIM5218 modem
Подключенный модем виден в каталоге /dev как несколько USB устройств:
root@root:/etc/modules.d# ls /dev | grep USB
ttyUSB0
ttyUSB1
ttyUSB2
ttyUSB3
ttyUSB4
Нам понадобятся следующие порты:
/dev/ttyUSB0 - основной порт, через который происходит обмен данными по PPP.
/dev/ttyUSB3 - дополнительный порт. Через него мы сможем получать от модема данные о регистрации в сотовой сети, в частности LAC и Cell ID (это может помочь в установлении местоположения контроллера) и данные о местоположении со встроенного GPS приёмника модема
Для того, чтобы OpenWRT автоматически стартовала модемное PPP-соединение, требуется добавить в /etc/config/network следующее описание интерфейса:
config interface 'wan'
option ifname 'ppp0'
option proto '3g'
option device '/dev/ttyUSB2'
option service 'umts'
option apn 'internet'
option keepalive '30'
option defaultroute '1'
option pppd_options 'refuse-chap refuse-mschap refuse-mschap-v2 refuse-eap'
В этот WAN-интерфейс перенаправляется весь трафик (defaultroute '1'). Его также необходимо добавить в /etc/config/firewall:
config zone
option name wan
list network 'wan'
list network 'wan6'
option input REJECT
option output ACCEPT
option forward REJECT
option masq 1
option mtu_fix 1
config forwarding
option src lan
option dest wan
# We need to accept udp packets on port 68,
# see https://dev.openwrt.org/ticket/4108
config rule
option name Allow-DHCP-Renew
option src wan
option proto udp
option dest_port 68
option target ACCEPT
option family ipv4
# Allow IPv4 ping
config rule
option name Allow-Ping
option src wan
option proto icmp
option icmp_type echo-request
option family ipv4
option target ACCEPT
На модеме есть два выхода, Net и Status, к которым подключены светодиоды. По ним можно визуально оценить процесс соединения модема с Интернет и его текущее состояние.
Кроме того, у модема есть управляющие входы: PowerKey и Reset. Управляет ими процессор контроллера через GPIO. Управление по ним требуется в таких случаях, как первоначальное включение контроллера и обрыв/потеря связи с Интернет
Сделаем два скрипта для управления модемов. Первый - для первоначального включения модема:
vi ~/openwrt/additional_rootfs/home/applications/vkl_modem.sh
#/bin/bash
RESET = 106
POWERKEY = 98
#Reset modem
if [ ! -e /sys/class/gpio/gpio$RESET ]; then
echo "$RESET" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio$RESET/direction
fi
echo "1" > /sys/class/gpio/gpio$RESET/value
sleep 1
echo "0" > /sys/class/gpio/gpio$RESET/value
sleep 1
#Power on modem
if [ ! -e /sys/class/gpio/gpio$POWERKEY ]; then
echo "$POWERKEY" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio$POWERKEY/direction
fi
echo "1" > /sys/class/gpio/gpio$POWERKEY/value
sleep 1
echo "0" > /sys/class/gpio/gpio$POWERKEY/value
Добавим строчку “/home/applications/vkl_modem.sh” в ~/openwrt/additional_rootfs/etc/rc.local.main
Второй скрипт - для проверки и восстановления соединения в случае обрыва
vi ~/openwrt/additional_rootfs/home/applications/connection_check.sh
#!/bin/sh
POWERKEY = 98
BEACON_SERVER = "8.8.8.8"
MODEM_DEVICE = "/dev/ttyUSB0"
toggle_modem () {
echo "1" > /sys/class/gpio/gpio$POWERKEY/value
sleep 1
echo "0" > /sys/class/gpio/gpio$POWERKEY/value
}
# Configuring GPIO
if [ ! -e /sys/class/gpio/gpio$POWERKEY ]; then
echo "no GPIO found, configuring it"
echo "$POWERKEY" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio$POWERKEY/direction
fi
if [ -e $MODEM_DEVICE ]; then
echo "modem is present"
fi
if ping -c 4 $BEACON_SERVER &> /dev/null
then
echo "ping is ok"
else
echo "$BEACON_SERVER server is unavailable. Restarting modem!"
if [ -e $MODEM_DEVICE ]; then
echo "modem is present, shutting down it"
toggle_modem
echo "deregistration done, waiting"
sleep 30
fi
if [ ! -e $MODEM_DEVICE ]; then
echo "modem is switched off, powering it on"
toggle_modem
echo "powered, waiting for registration"
sleep 30
fi
if ping -c 4 $BEACON_SERVER &> /dev/null
then
echo "now ping is ok"
else
echo "!!! ping is still not ok, exiting"
fi
fi
Добавим второй скрипт в ~/openwrt/additional_rootfs/etc/crontabs/root
vi ~/openwrt/additional_rootfs/etc/crontabs/root
*/15 * * * * /home/verticali/connection_check.sh
Таким образом, при недоступности сервера (а его адрес мы прописали в BEACON_SERVER), модем будет принудительно выключен через GPIO, а затем опять включен. Задержки между операциями требуются для гарантированной дерегистрации и регистрации модема в сотовой сети
Рассмотрим теперь более сложный случай, когда модем на плате снабжён разъёмами для двух сим карт, в которые можно вставлять сим карты двух операторов.
Такие сим карты подключаются к модему с помощью мультиплексора, и номер активной сим карты (0 или 1) задаётся состоянием линии GPIO (simaddress)
Сделаем скрипт для переключения между сим операторами в случае длительного отсутствия связи
vi ~/openwrt/additional_rootfs/home/applications/connection_check.sh
#!/bin/bash
# Two-simcard modem connection check script copyright (С) EVODBG 2019
version="A2"
server_address="8.8.8.8"
modem_device="/dev/ttyUSB0"
reach_cntfile="/tmp/reach_counter"
sim_numberfile="/tmp/sim_number"
led_trigger_a="/sys/class/leds/LED5/trigger"
# [none] usb-gadget usb-host mmc0 timer default-on netdev gpio heartbeat usbport
led_brightness_a="/sys/class/leds/LED5/brightness"
max_attempts=3
modem_power_enable=52
modem_power_key=113
modem_reset=25
simaddress=27
set_gpio(){
if [ ! -e /sys/class/gpio/gpio$1 ]; then
echo "$1" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio$1/direction
fi
echo "$2" > /sys/class/gpio/gpio$1/value 2>&1
}
display_leds() {
# $1 is successful (1) or not (0), $2 is sim number
if [ $1 == 0 ]; then
echo none > "$led_trigger_a" 2>&1
fi
if [ $1 == 1 ]; then
echo 1 > "$led_brightness_a" 2>&1
if [ $2 == 0 ]; then
echo timer > "$led_trigger_a" 2>&1
else
echo heartbeat > "$led_trigger_a" 2>&1
fi
fi
}
restart_modem() {
if [ -e $modem_device ]; then
echo "modem is present, shutting down it"
# power off modem
set_gpio $modem_power_enable 0
sleep 1
# and activate reset
set_gpio $modem_reset 0
echo -ne "deregistration done, waiting..."
sleep 15
echo done
fi
if [ ! -e $modem_device ]; then
echo "modem is switched off, powering it on"
set_gpio $modem_power_enable 1
sleep 1
set_gpio $modem_reset 1
sleep 5
set_gpio $modem_power_key 0
sleep 1
set_gpio $modem_power_key 1
echo -ne "powered, waiting for registration..."
sleep 50
echo done
fi
}
# ********************************** Main program ***********************************
echo "*** Two-simcard modem connection check script ver. $version copyright (С) EVODBG 2019 ***"
# Reading sim number
if [ ! -f $sim_numberfile ]; then
sim_number="0"
echo $sim_number > $sim_numberfile
else
sim_number=$(cat $sim_numberfile)
fi
echo "using sim $sim_number"
# Reading unsuccessful reach counter
if [ ! -f $reach_cntfile ]; then
reach_counter="0"
echo $reach_counter > $reach_cntfile
else
reach_counter=$(cat $reach_cntfile)
fi
echo "unsuccessful reach counter = $reach_counter"
echo
if ping -c 4 $server_address &> /dev/null
then
echo "ping is ok"
display_leds 1 $sim_number
reach_counter="0"
else
# Restarting modem
echo "$server_address server is unavailable. Restarting modem!"
display_leds 0
restart_modem
if ping -c 4 $server_address &> /dev/null
then
echo "now ping is ok"
display_leds 1 $sim_number
reach_counter="0"
else
echo "!!! ping is still not ok, exiting"
display_leds 0
let "reach_counter = $reach_counter + 1"
fi
fi
# Changing sim
if [ $reach_counter == $max_attempts ]; then
echo "reach_counter $reach_counter is equal to $max_attempts, changing sim card"
if [ $sim_number == 0 ]; then
sim_number="1"
else
sim_number="0"
fi
echo $sim_number > $sim_numberfile
set_gpio $simaddress $sim_number
restart_modem
echo "SIM changed to $sim_number"
reach_counter="0"
if ping -c $server_address &> /dev/null
then
echo "now ping is ok"
display_leds 1 $sim_number
else
echo "!!! ping is still not ok, exiting"
display_leds 0
fi
fi
echo $reach_counter > $reach_cntfile
В этой версии скрипта, если произошло более трёх неудачных попыток установить связь с сервером, то на четвёртый раз он меняет одну сим карту на другую, и перезагружает модем. Для индикации номера сим карты используется светодиод /sys/class/leds/LED5/
Он вспыхивает однократными вспышками, если модем работает на первой сим карте, и двукратными - если на второй. Для хранения числа неудачных попыток используется файл /tmp/reach_counter - во избежании лишних перезаписываний диска, он расположен в оперативной памяти (/tmp)
Организация удалённого доступа к контроллеру через Интернет
Для удалённого доступа к контроллеру нам понадобится VPN сервер. Например, можно воспользоваться сервером openvpn.
Примечание: иногда есть возможность приобрести выделенный APN для своих сим карт для организации собственной сети на сети сотового оператора. Обслуживание таких сим карт стоит дороже, выше абонентская плата, но они обеспечивают дополнительную защиту контроллеру, так как в этом случае отсутствует соединение с Интернет
Предположим, мы хотим сделать такую схему удалённого доступа: сервер monitoring.evodbg.org доступен извне по SSH, и на нём работает служба openvpn, которая слушает по UDP порт 1194. Соответствующая конфигурация openvpn приведена ниже:
cat /etc/openvpn/openvpn-server.conf
mode server
port 1194
proto udp
dev tun0
tun-mtu 1300
ca keys/ca.crt
cert keys/issued/vpn-server.crt
key keys/private/vpn-server-nopass.key
dh keys/dh.pem
tls-auth keys/openvpn_key_ta.key 0
topology subnet
server 10.0.0.0 255.255.255.0
keepalive 60 240
cipher AES-256-CBC
comp-lzo
persist-key
persist-tun
tls-server
client-config-dir /etc/openvpn/client_config_dir
ifconfig-pool-persist ipp.txt
log /var/log/openvpn/openvpn.log
Соответственно, поставим openvpn на контроллер (см. установка приложений в OpenWRT) и сделаем у него следующую конфигурацию. Он будет работать в режиме клиента.
cat ~/openwrt/additional_rootfs/etc/config/openvpn
package openvpn
#################################################
# Sample to include a custom config file. #
#################################################
config openvpn custom_config
# Set to 1 to enable this instance:
option enabled 1
# Include OpenVPN configuration
option config /etc/openvpn/openvpn.conf
cat ~/openwrt/additional_rootfs/etc/openvpn/openvpn.conf
client
tls-client
dev tun0
proto udp
remote monitoring.out4.ru
cipher AES-256-CBC
tun-mtu 1300
persist-tun
persist-key
comp-lzo
ca /etc/openvpn/ca.crt
cert /etc/openvpn/controller001.crt
key /etc/openvpn/controller001.key
tls-auth /etc/openvpn/openvpn_key_ta.key 1
Для дальнейшей настройки нам понадобится сгенерировать и раздать ключи и сертификаты openvpn - серверу и контроллерам, и настроить выдачу того или иного статического IP адреса контроллеру в зависимости от используемого сертификата.
Генерация ключей и сертификатов может быть выполнена как на самом openvpn сервере, так и на другом компьютере, в зависимости от политики безопасности компании и предпочтений системного администратора.
Скачиваем easyrsa версии 3, разархивируем и переносим каталог easyrsa3 в /etc/openvpn/easy-rsa/
wget https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.6/EasyRSA-unix-v3.0.6.tgz
tar -xvf EasyRSA-unix-v3.0.6.tgz
cd EasyRSA-v3.0.6/
cp -p easyrsa /etc/openvpn/
cd /etc/openvpn/easy-rsa/
Инициируем структуру ключей и сертификатов:
./easyrsa init-pki
Создаём и проверяем корневой сертификат
./easyrsa build-ca
openssl x509 -in ./pki/ca.crt -text -noout
Создаём и проверяем файл отозванных ключей:
./easyrsa gen-crl
openssl crl -in ./pki/crl.pem -text -noout
Создаём и проверяем сертификат сервера
./easyrsa build-server-full vpn-server
openssl x509 -noout -text -in ./issued/vpn-server.crt
Создаём ключ клиента без пароля
openssl rsa -in vpn-server.key -out vpn-server-nopass.key
Создаём и проверяем сертификат клиента
./easyrsa build-client-full controller001
openssl x509 -noout -text -in ./pki/issued/controller001.crt
Создаём ключ клиента без пароля
openssl rsa -in controller001.key -out controller001-nopass.key
Создаем ключи Диффи-Хелмана
./easyrsa gen-dh
Создаём секретный ключ для первоначального обмена:
openvpn --genkey --secret /etc/openvpn/openvpn_key_ta.txt
Проверяем, всё ли у нас на месте:
cd /etc/openvpn/easyrsa3/pki
tree
.
├── ca.crt
├── certs_by_serial
│ ├── <serial_001>.pem
│ ├── <serial_001>.pem
│ ├── <serial_001>.pem
│ ├── <serial_001>.pem
│ ├── <serial_001>.pem
│ ├── <serial_001>.pem
│ └── <serial_vpn-server>.pem
├── crl.pem
├── dh.pem
├── extensions.temp
├── index.txt
├── index.txt.attr
├── index.txt.attr.old
├── index.txt.old
├── issued
│ ├── controller001.crt
│ ├── controller002.crt
│ ├── controller003.crt
│ ├── controller004.crt
│ ├── controller005.crt
│ ├── controller006.crt
│ └── vpn-server.crt
├── openssl-easyrsa.cnf
├── private
│ ├── ca.key
│ ├── controller001.key
│ ├── controller001-nopass.key
│ ├── controller002.key
│ ├── controller002-nopass.key
│ ├── controller003.key
│ ├── controller003-nopass.key
│ ├── controller004.key
│ ├── controller004-nopass.key
│ ├── controller005.key
│ ├── controller005-nopass.key
│ ├── controller006.key
│ ├── controller006-nopass.key
│ ├── vpn-server.key
│ └── vpn-server-nopass.key
├── renewed
│ ├── certs_by_serial
│ ├── private_by_serial
│ └── reqs_by_serial
├── reqs
│ ├── controller001.req
│ ├── controller002.req
│ ├── controller003.req
│ ├── controller004.req
│ ├── controller005.req
│ ├── controller006.req
│ └── vpn-server.req
├── revoked
│ ├── certs_by_serial
│ ├── private_by_serial
│ └── reqs_by_serial
├── safessl-easyrsa.cnf
├── serial
└── serial.old
На том компьютере, где генерируются ключи, создаётся корневой сертификат ca.crt, которым нужно подписывать остальные сертификаты. Если какой-то контроллер окажется скомпрометирован, корневой сертификат придётся заменять, и перегенерировать все остальные сертификаты. Чтобы этого не произошло, лучше использовать промежуточный сертификат, который нужно подписать корневым, а сертификаты какой-то части контроллеров уже подписывать этим промежуточным
Получившиеся ключи и сертификаты сервера копируем в директорию /etc/openvpn/keys сервера openvpn, чтобы получилась следующая структура:
cd /etc/openvpn/keys
tree
.
├── ca.crt
├── dh.pem
├── issued
│ └── vpn-server.crt
├── openvpn_key_ta.key
└── private
└── vpn-server-nopass.key
Создаём файлы привязки статических адресов контроллеров и сертификатов
mkdir /etc/openvpn/client_config_dir
cd /etc/openvpn/client_config_dir
tree
.
├── controller001
├── controller002
├── controller003
├── controller004
├── controller005
└── controller006
cat controller001
ifconfig-push 10.0.0.0 255.255.255.0
Ключи и сертификаты клиента передаются на контроллер (scp) после прошивки.
На контроллере получается следующий список файлов:
ls /etc/openvpn
ca.crt lbs.crt lbs.key openvpn.conf openvpn_key_ta.key
После запуска openvpn на контроллере, появляется новый интерфейс, через который мы сможем с ним связываться удалённо и заходить по SSH
ifconfig
tun0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
inet addr:10.80.0.1 P-t-P:10.80.0.1 Mask:255.255.240.0
inet6 addr: fe80::1ba:28d1:4a41:1cf0/64 Scope:Link
UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1300 Metric:1
RX packets:185532 errors:0 dropped:0 overruns:0 frame:0
TX packets:185397 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:100
RX bytes:5748333 (5.4 MiB) TX bytes:7662901 (7.3 MiB)
В заключение, настроим файрвол на vpn сервере и запишем его настройки в файл:
cat /etc/rc.local
...
/root/firewall.sh
...
exit 0
cat /root/firewall.sh
#!/bin/sh
# This script will be executed *after* all the other init scripts.
# Configuring firewall
iptables -F INPUT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Maximum 3 syns allowed from each unique IP address, counter decreases 2 times per minute
iptables -A INPUT -i eth0 -p tcp -m tcp --dport 22 -m state --state NEW -m hashlimit --hashlimit 2/m --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name SSH --hashlimit-htable-expire 36000 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --dport 22 --syn -j DROP
iptables -A INPUT -i eth0 -p icmp -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 1194 -j ACCEPT
# Drop all
iptables -A INPUT -i eth0 -j DROP
# Private networks
iptables -F OUTPUT
iptables -A OUTPUT -o eth0 -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -o eth0 -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -o eth0 -d 192.168.0.0/16 -j DROP
# Private networks
iptables -F OUTPUT
iptables -A OUTPUT -o eth0 -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -o eth0 -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -o eth0 -d 192.168.0.0/16 -j DROP
Защита контроллера от зависания
Зависания контроллера редко, но всё же происходят. Особенно уязвим контроллер к ним в те моменты, когда происходит загрузка системы после появления напряжения питания, и watchdog в ядре ещё не активирован.
Для того, чтобы исключить это явление, желательно иметь в составе контроллера внешний аппаратный watchdog, который будет выключать и, затем, включать питание контроллера, если на его входе какое-то время будет отсутствовать активность. Такой watchdog управляется с GPIO процессора.
Для того, чтобы гарантировать работу удалённого соединения с контроллером, для работы с watchdog будем контролировать всю цепочку процессов, которые это соединение обеспечивают. Иными словами, будем проверять наличие туннельного интерфейса vpn-сервера, и только при его наличии в системе, будем переключать состояние GPIO watchdog:
cd ~/openwrt/additional_rootfs
vi /etc/crontabs/root
...
0-59/2 * * * * /home/applications/watchdog 0
1-59/2 * * * * /home/applications/watchdog 1
...
Напишем скрипт для управления watchdog:
cat /home/applications/watchdog
# watchdog script copyright (С) EVODBG 2019
#/bin/bash
params=$@
self_progname="watchdog"
interface_name="tun0"
watchdog_gpio=82
# check total number of arguments
if [ $# -lt "1" ]; then
echo "Error 1: lack of arguments, run \"$self_progname -h\" for help"
exit 1
fi
for option in $params
do
case $option in
-h|--help)
echo " Usage: $self_progname <option> [option]"
echo " Available options:"
echo " (0)1 (re)set watchdog GPIO line"
echo " -h|--help show this help"
echo ""
exit 0
;;
0)
gpio_value=0
;;
1)
gpio_value=1
;;
*)
echo "Wrong option. Run with option -h to get help"
exit 1
;;
esac
done
ifconfig $interface_name > /dev/null
if [ $? -eq 0 ]; then
if [ ! -e /sys/class/gpio/gpio$watchdog_gpio ]; then
echo "$watchdog_gpio" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio$watchdog_gpio/direction
fi
if [ -e /sys/class/gpio/gpio$watchdog_gpio ]; then
echo $gpio_value > /sys/class/gpio/gpio$watchdog_gpio/value
echo "ok"
fi
fi
Этот подход можно будет использовать и тогда, когда для выхода в интернет будет использоваться другой интерфейс (ethernet, либо wifi)
Автоматическая загрузка новых приложений на контроллер
Мы можем запрограммировать контроллер, чтобы он обновлял приложения пользователя по расписанию. Напомним, приложения у нас будут находиться в директории /home/applications. Напишем скрипт для автообновления приложений
vi /etc/crontabs/root
…
45 01 * * * /home/applications/services.sh update
19 02 * * * /home/applications/maint.sh
Первый скрипт у нас будет загружать в директорию /home/applications новые приложения, которые мы в будущем напишем. Приложения мы разместим на сервере monitoring.evodbg.org таким образом, чтобы они были доступны по протоколу SFTP пользователю controller. Аутентификация этого пользователя у нас будет происходить по ключу, без пароля.
Таким образом, на каждый контроллер у нас будет загружаться набор новых приложений с сервера.
vi /home/applications/services.sh
#!/bin/sh
# Services update script (c) EVODBG 2017-2018
rootdir=$/home/applications
FILE="$rootdir/services/services"
server_name=”monitoring.evodbg.org”
case $1 in
stop)
while read LINE; do
echo "stopping $LINE"
killall $LINE
done < $FILE
;;
start)
while read LINE; do
echo "starting $LINE"
$rootdir/$LINE &
done < $FILE
;;
update)
# remove temporary files if exist
if [ -f /tmp/sysversion ]; then
rm /tmp/sysversion
fi
if [ -f /tmp/appversion ]; then
rm /tmp/appversion
fi
# download new sysversion file, chroot environment, and compare with local one
sftp -i ~/.ssh/id_rsa controller@$server_name:/version/sysversion /tmp
if [ -f /tmp/sysversion ]; then
newsysversion=`cat /tmp/sysversion`
oldsysversion=`cat /home/verticali/version/sysversion`
if [ "$newsysversion" != "$oldsysversion" ]; then
# copy applications
cd $rootdir && sftp -i ~/.ssh/id_rsa -b $rootdir/transfers/systransfers controller@$server_name
fi
# update local sysversion file
mv /tmp/sysversion $rootdir/version/
fi
# download new appversion file and compare with local one
sftp -i ~/.ssh/id_rsa controller@$server_name:/version/appversion /tmp
if [ -f /tmp/appversion ]; then
newappversion=`cat /tmp/appversion`
oldappversion=`cat $rootdir/version/appversion`
if [ "$newappversion" != "$oldappversion" ]; then
# copy applications
cd $rootdir && sftp -i ~/.ssh/id_rsa -b $rootdir/transfers/transfers controller@$server_name
fi
# update local appversion file
mv /tmp/appversion $rootdir/version/
fi
sync
sync
;;
*)
echo "Error. Available parameters are: stop, start, update"
;;
esac
Как написано выше, если запустить этот скрипт с ключом update, то он загрузит с сервера в /home/applications новые приложения.
Контроллер загрузит новые приложения только в том случае, если версии ПО на контроллере (/home/applications/version/sysversion или /home/applications/version/appversion) отличаются от указанных, соответственно, в файлах на сервере /version/sysversion и /version/appversion
Для удобства мы разделили все приложения на два типа: “системные” и “прикладные”. Например, мы можем производить контроллеры без предустановленных приложений, и при первом подключении он загрузит оба типа. А действующие контроллеры будут отличаться только более старой версией “прикладных” приложений, и загрузят при очередном подключении только их
Дополнительные наборы команд для SFTP-клиента занесём в файлы /home/applications/systranfers и /home/applications/transfers:
cat systransfers
cd system
get *
cat transfers
cd firmware
get *
Также, при запуске с ключом start, скрипт запустит на контроллере службы, согласно списку, помещённому в файл /home/applications/services/services
При запуске с ключом stop он остановит эти службы
Другой скрипт ищет в директории /home/applications/ исполняемый файл update.sh, и запускает его, если найдёт. Таким образом, мы можем массово выполнить на контроллерах одинаковые действия, например, отредактировать файл /etc/crontabs/root, создать новые каталоги, или скопировать нужные нам файлы из /home/applications в какие-то другие директории. В конце update.sh, как правило, помещается команда rm ./update.sh, чтобы после завершения работы он самоуничтожился
vi /home/applications/maint.sh
#!/bin/sh
if [ -f "/home/applications/update.sh" ]; then
echo "running temporary script"
/home/verticali/update.sh
fi
chmod 755 /home/applications/maint.sh
Примечание: прежде чем создавать подобные - исполняющие сами себя - файлы, необходимо взвесить все возможные последствия для контроллера с точки зрения атак на него и возможности запуска нежелательных программ. Если есть возможность, лучше проконсультироваться на этот счёт со специалистом по информационной безопасности в своей организации.
Службу SFTP Server на сервере желательно сконфигурировать так, чтобы данные для пользователя controller находились бы в chroot-окружении, и пользователь не смог бы таким образом прочитать ничего, кроме предназначенных для него программ и данных.
Командный интерфейс пользователя
В директории /home/applications мы будем размещать приложения, которые будут доступны пользователю контроллера (инженеру) для запуска в командной строке. В большинстве задач пользователю достаточно обращаться к контроллеру опосредованно через API. Oн при этом работает в веб интерфейсе центральной системы управления.
Командная строка и приложения на контроллере могут понадобиться только привилегированному пользователю, как низкоуровневое дополнение к центральной системе управления. С их помощью можно выполнять некоторые действия, которые могут привести к авариям и критическим последствиям (например, управление мощными реле прибора в ручном режиме). Поэтому, неквалифицированному пользователю не стоит давать доступ к командной строке.
Здесь мы приведём примеры нескольких приложений, с помощью которых можно управлять удалённо оборудованием, подключённым к контроллеру по шине Modbus RTU. Рассмотрим пример управления оборудованием светоограждения мачт (СОМ) через ведомый контроллер
Данные Modbus RTU передаются через порт RS485 в виде пакетов, которые содержат адрес ведомого устройства на шине, данные от ведущего к ведомому устройству и 16-битной контрольной суммы. Данные внутри пакета, в свою очередь, состоят из кода команды, адреса защёлки или регистра устройства, и опциональных параметров, например, количества регистров
Пример пакета Modbus RTU:
Адрес устройства на шине | Код команды “прочитать регистр” | Адрес регистра (MSB) | Количество 16-разрядных регистров (MSB) | Контрольная сумма |
14 | 0х04 | 0x00 0x03 | 0x00 0x01 | 2 байта |
С помощью пакета команды Modbus RTU, показанного выше, можно прочитать напряжение на батарее устройства СОМ
Допустим, формат пакета команды мы знаем, и хотим её передать. Но как это сделать? Обычной терминальной программой, такой как minicom, сделать это не удастся, она предназначена только для передачи текста через порт. Напишем короткое приложение, которое мы сможем запускать следующим образом:
ssh 10.20.14.230 -l root
cd /home/applications
./send /dev/ttyAPP0 14 04 00 03 00 01
Как мы видим, из команды убраны все лишние ненужные символы (0x), чтобы её быстрее можно было набирать. Контрольная сумма считается и добавляется автоматически.
Примечание 1: адрес ведомого устройства на шине всё же удобнее набирать в десятичном виде, тогда как остальные данные - в шестнадцатеричном. Так мы и сделаем в нашей программе.
Примечание 2: при неверном вводе или при отсутствии параметров необходимо выдать пользователю help
Вначале, сделаем функцию для работы с Modbus и подобными ему протоколами. На виртуальной машине:
cd ~/Work/C/Controller_scripts/2020/March/
mkdir send && cd send
Подготовим описание функции:
vi 485_ex.h
/* exchan485 function
Input:
string devname: device name such as "/dev/ttyAPP0"
char echo_flag: 1 - removing echo from the output, 0 - not removing
unsigned char devnum: modbus device address such as 99
unsigned char command: pointer to array of input bytes
int command_size: number of input bytes
Output:
unsigned char answer: pointer to array of output bytes
Returns (int):
positive value: number of bytes received from 485 bus
-2: error while opening device
-3: no data received from 485 bus
-4: wrong incoming CRC
-5: ioctl error
*/
int exchan485(char *devname, char echo_flag, unsigned char devnum, const unsigned char *command, const int command_size, unsigned char *answer);
Подготовим код функции:
vi 485_ex.c
#include <stdio.h>
#include <linux/serial.h>
#include <fcntl.h>
#include "485_ex.h"
unsigned char outbuf[50];
unsigned char inbuf[1];
// merkury CRC calculation
int update_crc(unsigned char c, unsigned int old_crc) {
unsigned char locrc, hicrc, i;
unsigned char hiarray[]= {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 };
unsigned char loarray[]= {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04,
0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8,
0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10,
0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0,
0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C,
0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 };
locrc = old_crc & 0xff;
hicrc = (old_crc >> 8) & 0xff;
i = hicrc ^ c;
hicrc = locrc ^ hiarray[i];
locrc = loarray[i];
old_crc = 256*hicrc+locrc;
return old_crc;
}
/* exchan485 function */
int exchan485(char *devname, char echo_flag, unsigned char devnum, const unsigned char *command, const int command_size, unsigned char *answer) {
unsigned char outbuf_len=0;
unsigned char data_received=0;
unsigned int data_counter=0;
unsigned int crc = 0xffff;
unsigned int rcv_crc = 0;
unsigned char right_crc = 0;
long receive_timer = 100000;
//#define VERBOSE
unsigned char dev_opening_error=0;
int fd1;
int read_result; // for read results from device
int i, temp;
long w; char recv;
fd1=open(devname, O_RDWR | O_NONBLOCK);
if (fd1==-1) {return(-2);} /*error while opening device*/
outbuf[0] = devnum; /*send counter bus id*/
crc=update_crc(outbuf[0],crc);
outbuf_len++;
// send command
for (i=0; i<command_size; i++) {
outbuf[i+1]= command[i];
crc=update_crc(outbuf[i+1],crc);
outbuf_len++;
}
// send CRC
outbuf[outbuf_len]=(crc >> 8) & 0xff;
outbuf_len++;
outbuf[outbuf_len]=crc & 0xff;
outbuf_len++;
write(fd1, outbuf, outbuf_len);
crc = 0xffff;
if (echo_flag==1) {
// receive (and waste) echo tx data
recv = 0;
#if defined VERBOSE
printf("\r\n%s",">");
#endif
for (w=0; w<receive_timer; w++) {
read_result=read(fd1, inbuf, 1);
if (read_result!=-1) {
#if defined VERBOSE
printf("%02x ", inbuf[0]);
printf("crc=%04x ", crc);
#endif
if (recv==1) {
data_received=1;
answer[data_counter]=inbuf[0];
// calculating and verifying checksum
crc=update_crc(inbuf[0],crc);
if (crc==0) {right_crc=1;}
}
data_counter++;
// when received more bytes than transferred, toggle recv flag
if (recv == 0 && data_counter >= outbuf_len) {
#if defined VERBOSE
printf("\r\n%s","<");
#endif
// reset crc for check on receiving
crc = 0xffff;
recv = 1;
data_counter=0;
}
}
}
}
else {
#if defined VERBOSE
printf("\r\n%s","<");
#endif
for (w=0; w<receive_timer; w++) {
read_result=read(fd1, inbuf, 1);
if (read_result!=-1) {
data_received=1;
answer[data_counter]=inbuf[0];
#if defined VERBOSE
printf("%02x ", inbuf[0]);
#endif
// calculating and verifying checksum
crc=update_crc(inbuf[0],crc);
#if defined VERBOSE
printf("crc=%04x ", crc);
#endif
if (crc==0) {right_crc=1;}
data_counter++;
}
}
}
close(fd1);
if (data_received!=0) {
if (right_crc==1) {return(data_counter);} /* all OK */
else {return(-4);} /* wrong incoming CRC */
}
else {return(-3);} /*no incoming data received*/
}
Код программы:
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include "485_ex.h"
char device_name[20];
unsigned char echo_flag=0;
unsigned char bin_answer[256];
char hex_answer[4096];
unsigned char devnum;
unsigned char c_out[20];
unsigned char outbuf_len=0;
unsigned char counter_address;
unsigned char echo_flag;
int exchan_result;
char ver[] = "A7";
char progname[] = "send";
void print_answer(int exchan_result, unsigned char *answer_bytes, char *hex_answer) {
int i;
char txt_buf[20];
for ( i=0; i<exchan_result; i++) {
sprintf(txt_buf, "%02x ", answer_bytes[i]);
strcat(hex_answer, txt_buf);
}
}
// Main
//
int main(int argc, char *argv[]) {
unsigned char dev_opening_error=0;
int i, temp;
printf("*** 485 bus gateway %s (c) EVODBG 2016 ***\r\n", ver);
if (argc<4) {
printf("Error 1: too few arguments. Please specify I/O device, device bus address and command bytes.\r\nExample: ./%s /dev/ttyAPP2 14 04 00 03 00 01 - battery voltage check with SOM device number 14.\r\n", progname);
printf("Return codes: 0- normal, 1- too few arguments, 2- error opening device, 3- no data received from 485 bus, 4- wrong incoming CRC\r\n");
return(1);
}
sscanf(argv[1],"%s",&device_name);
// device bus ID - get and send
sscanf(argv[2],"%d",&temp);
devnum = temp & 0xff;
for (i=3; i<argc; i++) {
sscanf(argv[i],"%x",&temp);
c_out[i-3]= temp & 0xff;
outbuf_len++;
}
printf(">\r\n< ");
// send command
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_out, outbuf_len, bin_answer)) > 0) {
print_answer(exchan_result, bin_answer, hex_answer);
printf("%s\r\nOK, Finished\r\n", hex_answer);
return(0);
}
else {
printf("\r\nError %d. Exiting\r\n", exchan_result);
return(4);
}
}
Компиляция программы:
vi Makefile
CC=arm-openwrt-linux-gcc
CFLAGS=-c
LDFLAGS=
SOURCES=
OBJECTS=$(SOURCES:.c.o)
EXECUTABLES=
PATH := ${PATH}:~/openwrt/staging_dir/toolchain-arm_arm926ej-s_gcc-4.8-linaro_uClibc-0.9.33.2_eabi/bin/
STAGING_DIR := ~/openwrt/staging_dir/toolchain-arm_arm926ej-s_gcc-4.8-linaro_uClibc-0.9.33.2_eabi/
export PATH STAGING_DIR
all: send
scp ./send root@192.168.1.1:/home/applications/
# Create executable file
send: send.o 485_ex.o
send.o: send.c
$(CC) $(CFLAGS) send.c -o send.o
# 485 interface file
485_ex.o: 485_ex.c 485_ex.h
$(CC) $(CFLAGS) 485_ex.c -o 485_ex.o
# Clean
clean:
rm -rf *.o send
make
После компиляции и передачи приложения на контроллер, подключаем к порту APP2 контроллера устройство COM, заходим на контроллер по SSH и проверяем работу программы:
ssh 192.168.1.1 -l root
cd /home/applications
./send /dev/ttyAPP2 14 04 00 03 00 01
*** 485 bus gateway A7 (c) EVODBG 2016 ***
>
< 0e 04 02 00 f6 ed 79
OK, Finished
На команду СОМ прислал ответ:
0e - номер устройства на шине (14 десят.)
04 - код команды верный, ошибки нет
03 - адрес регистра
00 f6 - 246 десятичное (напряжение на батарее = 24.6В)
ed 79 - контрольная сумма
Теперь, допустим, мы не знаем наизусть кодов всех команд для устройства СОМ. Напишем скрипт, запускаемый из командной строки, который прочитает и выведет всю информацию о СОМ на экран.
vi read_som_A4.sh
#!/bin/sh
# Settings
ver="A4"
prog_path="/home/applications/"
self_progname="read_som_$ver.sh"
gateway_progname="send"
device="/dev/ttyAPP2"
tx_timer=3500
verbose=0
welcome="*** SOM reader $ver (c) EVODBG 2018***"
# Send command and get answer
send_command_to () {
if [ -z "$2" ]; then
echo "send_command must have parameter"
fi
answer=$($prog_path$gateway_progname $device $1 $2)
error=$(echo $?)
sent=`echo "$answer" | grep '>'`
answer=`echo "$answer" | grep '<'`
if [ $verbose == 1 ]; then
echo $sent
echo $answer
fi
if [ $error != "0" ]; then
echo "Error $error when establishing link with device. See \"$gateway_progname\" program help. Exiting"
exit $error
fi
}
check_lamp_address () {
case $parameter in
1)
lamp_address="01"
;;
2)
lamp_address="02"
;;
*)
echo "Error 6: wrong lamp address"
exit 6
;;
esac
}
check_mode_id () {
case $parameter in
0|continuous)
mode_id="00"
;;
1|blinking)
mode_id="01"
;;
*)
echo "Error 8: wrong mode id"
exit 8
;;
esac
}
show_fourbytes_single_value () {
answer1=`echo $answer | awk '{print $4 $3 $6 $5}'`
answer1=`printf "%d" 0x$answer1`
echo "= $answer1"
}
two_hex_to_dec () {
t_high=`echo $answer | awk '{print $5}'`
t_high=`echo $((0x${t_high}))`
t_low=`echo $answer | awk '{print $6}'`
t_low=`echo $((0x${t_low}))`
temp=$(( t_high * 256 + t_low ))
}
print_two_hex_voltage () {
two_hex_to_dec
printf "\"$1\":\"%s.%s\"\n" $(( $temp / 10 )) $(( $temp % 10 ))
}
print_two_hex_current () {
two_hex_to_dec
printf "\"$1\":\"%s\"\n" $temp
}
# Start
echo "$welcome"
params=$@
# check total number of arguments
if [ $# -lt "1" ]; then
echo "Error 1: lack of arguments, run \"$self_progname -h\" for help"
return 1
fi
som_ps_address="15" # SOM power supply device address
som_lamp_address="14" # SOM light control device address
for option in $@
do
case $option in
--som1)
som_ps_address="$2"
shift
;;
--som2)
som_lamp_address="$2"
shift
;;
--on|--off|-m|--mode)
parameter="$2"
shift
;;
-v|--verbose)
verbose=1
echo "Verbose mode"
echo "TX timer=$tx_timer"
echo "Transmitted/Received dаta:"
;;
esac
shift
done
for option in $params
do
case $option in
-h|--help)
echo " Usage: $self_progname <option> [option]"
echo " Available options:"
echo " -s|--som poll current SOM status"
echo " --on (1,2) switch on SOM output line 1,2"
echo " --off (1,2) switch off SOM output line 1,2"
echo " -m|--mode (0,1) set continuous(blinking) light mode"
echo " --som1 specify non-standard som power supply address"
echo " --som2 specify non-standard som light control module address"
echo " -v|--verbose get raw data output"
echo " -h|--help show this help"
echo ""
exit 0
;;
-s|--som)
# Reading grid voltage
send_command_to $som_ps_address "04 00 01 00 01"
answer=`echo $answer | awk '{print $6}'` # 00, 01, 02
answer=`echo ${answer:1}`
if [ $(($answer & 3)) == "0" ]; then tmp="\"ok\""; fi
if [ $((($answer >> 1) & 1)) == 1 ]; then tmp="\"not present\""; fi
if [ $(($answer & 1)) == "1" ]; then tmp="\"ok, charging\""; fi
echo "\"input voltage\"":$tmp
tmp="\"off\""
# Reading heater state
if [ $((($answer >> 7) & 1)) == 1 ]; then tmp="\"on\""; fi
echo "\"heater\"":$tmp
# Reading battery voltage
send_command_to $som_ps_address "04 00 02 00 01"
print_two_hex_voltage "voltage measured by power supply, V"
send_command_to $som_ps_address "04 00 03 00 01"
print_two_hex_voltage "battery measured by power supply, V"
echo ""
# Reading status register
send_command_to $som_lamp_address "04 00 01 00 01"
answer=`echo $answer | awk '{print $6}'`
answer=`echo $((0x${answer}))`
if [ $((($answer >> 6) & 1)) == 1 ]; then tmp="\"yes\""; else tmp="\"no\""; fi
echo "\"on battery:\"":$tmp
if [ $(($answer & 1)) == 1 ]; then tmp="\"on\""; else tmp="\"off\""; fi
echo "\"out 1 state:\"":$tmp
if [ $((($answer >> 1) & 1)) == 1 ]; then tmp="\"on\""; else tmp="\"off\""; fi
echo "\"out 2 state:\"":$tmp
if [ $((($answer >> 2) & 1)) == 1 ]; then tmp="\"yes\""; else tmp="\"no\""; fi
echo "\"out 1 overcurr.:\"":$tmp
if [ $((($answer >> 3) & 1)) == 1 ]; then tmp="\"yes\""; else tmp="\"no\""; fi
echo "\"out 2 overcurr.:\"":$tmp
if [ $((($answer >> 4) & 1)) == 1 ]; then tmp="\"yes\""; else tmp="\"no\""; fi
echo "\"out 1 disconn.:\"":$tmp
if [ $((($answer >> 5) & 1)) == 1 ]; then tmp="\"yes\""; else tmp="\"no\""; fi
echo "\"out 2 disconn.:\"":$tmp
if [ $((($answer >> 7) & 1)) == 1 ]; then tmp="\"flashing\""; else tmp="\"continuous\""; fi
echo "\"light mode:\"":$tmp
# Reading input and battery voltage
send_command_to $som_lamp_address "04 00 02 00 01"
print_two_hex_voltage "input voltage, V"
send_command_to $som_lamp_address "04 00 03 00 01"
print_two_hex_voltage "battery voltage, V"
# Reading output current
send_command_to $som_lamp_address "04 00 06 00 01"
print_two_hex_current "out 1 current, mA"
send_command_to $som_lamp_address "04 00 07 00 01"
print_two_hex_current "out 2 current, mA"
# Reading output voltage
send_command_to $som_lamp_address "04 00 04 00 01"
print_two_hex_voltage "out 1 voltage, V"
send_command_to $som_lamp_address "04 00 05 00 01"
print_two_hex_voltage "out 2 voltage, V"
echo ""
echo OK
exit 0
;;
--on)
# on som output line
check_lamp_address
send_command_to $som_lamp_address "05 $lamp_address 01"
echo OK
exit 0
;;
--off)
# off som output line
check_lamp_address
send_command_to $som_lamp_address "05 $lamp_address 00"
echo OK
exit 0
;;
-m|-mode)
#som light mode
check_mode_id
send_command_to $som_lamp_address "05 03 $mode_id"
echo OK
exit 0
;;
*)
echo "Wrong option. Run with option -h to get help"
exit 1
;;
esac
done
Проверяем:
ssh 192.168.1.1 -l root
cd /home/applications
./read_som_A4.sh -s
*** SOM reader A4 (c) EVODBG 2018***
"input voltage":"ok"
"heater":"off"
"voltage measured by power supply, V":"28.9"
"battery measured by power supply, V":"24.7"
"on battery:":"no"
"out 1 state:":"on"
"out 2 state:":"on"
"out 1 overcurr.:":"no"
"out 2 overcurr.:":"no"
"out 1 disconn.:":"no"
"out 2 disconn.:":"yes"
"light mode:":"continuous"
"input voltage, V":"28.8"
"battery voltage, V":"24.5"
"out 1 current, mA":"602"
"out 2 current, mA":"29"
"out 1 voltage, V":"28.4"
"out 2 voltage, V":"28.3"
OK
Подготовка данных для передачи на сервер
Только что мы сделали функцию на си для чтения данных из устройства СОМ и скрипт для просмотра всех его параметров, и для изменения некоторых из них (подачи и снятия питания с ламп, и изменения режим работы СОМ проблесковый-постоянный)
Пользоваться им из командной строки удобно, потому что получившуюся распечатку легко читать, но получившийся формат данных не очень удобен для автоматической обработки на сервере. В крайнем случае, конечно, годится и такой формат: каждую строку можно разделить на пару ключ-значение и обработать, но лучше было бы упаковать данные в формат JSON и передать данные на сервер в таком виде.
Воспользуемся уже написанной функцией и сформируем JSON - пакет данных. Возможно, для упаковки в JSON было бы проще использовать стандартные библиотеки, но для наглядности сделаем это без них.
Описание новой библиотеки SOM:
Описание новой библиотеки SOM:
vi Som_libs.h
/* som_poll function
Input:
char action: pointer to string with action text. Example: "link link link"
unsigned char devnum: modbus device address such as 99
Output:
char hex_answer: pointer to hex answer string
char decoded_answer: pointer to decoded answer string
Returns (int):
-1: wrong action number
positive value: number of bytes received from 485 bus
other negative value: 485 error (see exchan485)
*/
int som_poll(char *action, char devnum, int devicetype, char somlocalid, int
somversion, char *hex_answer, char *decoded_answer);
Код библиотеки SOM:
vi Som_libs.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <ifaddrs.h>
#include <netpacket/packet.h>
#include <time.h>
#include "485_ex.h"
#include "Som_libs.h"
#define IFNAME "br-lan"
void show_twobytes_single_value (char *prefix, unsigned char *answer_bytes, char *decoded_answer) {
double result;
char temp[15];
result=answer_bytes[3]*256+answer_bytes[4];
result=result/10;
sprintf(temp, "\"%s\":\"%.1f\"", prefix, result);
strcat(decoded_answer, temp);
}
void show_twobytes_single_value_int (char *prefix, unsigned char *answer_bytes, char *decoded_answer) {
int result;
char temp[15];
result=answer_bytes[3]*256+answer_bytes[4];
sprintf(temp, "\"%s\":\"%d\"", prefix, result);
strcat(decoded_answer, temp);
}
void print_answer(int exchan_result, unsigned char *answer_bytes, char *hex_answer) {
int i;
char txt_buf[20];
for ( i=0; i<exchan_result; i++) {
sprintf(txt_buf, "%02x ", answer_bytes[i]);
strcat(hex_answer, txt_buf);
}
}
void get_mac_address(char *mac_addr) {
struct ifaddrs *ifaddr=NULL;
struct ifaddrs *ifa = NULL;
int i = 0;
char temp[10];
mac_addr[0]=0;
if (getifaddrs(&ifaddr) != -1) {
for ( ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if ( (ifa->ifa_addr) && (ifa->ifa_addr->sa_family == AF_PACKET) ) {
struct sockaddr_ll *s = (struct sockaddr_ll*)ifa->ifa_addr;
if (!strcmp(ifa->ifa_name, IFNAME)) {
for (i=0; i <s->sll_halen; i++) { sprintf(temp, "%02x%c", (s->sll_addr[i]), (i+1!=s->sll_halen)?':':'\0'); strcat(mac_addr, temp); }
}
}
}
freeifaddrs(ifaddr);
}
}
int som_poll(char *action_in, char devnum, int devicetype, char somlocalid, int somversion, char *hex_answer, char *decoded_answer) {
char device_name[]="/dev/ttyAPP2";
unsigned char echo_flag=0;
char *action_part;
int exchan_result;
int i,j,k,o;
char temp[150];
char action[100];
int ap_counter=0;
unsigned char temp1,temp2,temp3,temp4,temp5;
unsigned char avps_today;
char mac_addr[20], sysversion[20], appversion[20];
FILE *file;
const unsigned char c_link[2][5]={{4,1},{4,0,1,0,1}};
const unsigned char c_srpvintext[2][5]={{4,1},{4,0,1,0,1}};
const unsigned char c_srpvin[2][5]={{4,2},{4,0,2,0,1}};
const unsigned char c_srpvbat[2][5]={{4,3},{4,0,3,0,1}};
const unsigned char c_srpheater[2][5]={{1,1},{4,0,1,0,1}};;
const unsigned char c_srpstatus[2][5]={{4,1},{4,0,1,0,1}};
const unsigned char c_status[2][5]={{4,1},{4,0,1,0,1}};
const unsigned char c_vin[2][5]={{4,2},{4,0,2,0,1}};
const unsigned char c_vbat[2][5]={{4,3},{4,0,3,0,1}};
const unsigned char c_out1c[2][5]={{4,4},{4,0,6,0,1}};
const unsigned char c_out2c[2][5]={{4,5},{4,0,7,0,1}};
const unsigned char c_out1v[2][5]={{4,6},{4,0,4,0,1}};
const unsigned char c_out2v[2][5]={{4,7},{4,0,5,0,1}};
const unsigned char command_length[]={2,5};
unsigned char bin_answer[256];
char *prefix="ntd";
int read_address;
struct tm *ptr;
time_t lt;
lt = time(NULL);
ptr = localtime(<);
strcpy(action,action_in);
hex_answer[0]=0;
decoded_answer[0]=0;
read_address=4;
if (devicetype==0) {strcat(decoded_answer, "{\"srp\":{");} else {strcat(decoded_answer, "{\"som\":{");}
for ( action_part=strtok(action," "); action_part != NULL; action_part = strtok(NULL, " ")) {
exchan_result=-1;
if (ap_counter>0) { strcat(decoded_answer, ","); }
/* link */
if (!strcmp(action_part,"link")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_link[somversion], command_length[somversion], bin_answer)) > 0) {
strcat(decoded_answer, "\"link\":\"ok\""); // add the message to the end
}
}
/* srpvintext */
if (!strcmp(action_part,"srpvintext")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_srpvintext[somversion], command_length[somversion], bin_answer)) > 0) {
if (bin_answer[read_address]==0) {
strcat(decoded_answer, "\"srpvintext\":\"ok\""); // add the message to the end
}
if (bin_answer[read_address]==1) {
strcat(decoded_answer, "\"srpvintext\":\"nok\""); // add the message to the end
}
if (bin_answer[read_address]==2) {
strcat(decoded_answer, "\"srpvintext\":\"opast\""); // add the message to the end
}
}
}
/* srpvin */
if (!strcmp(action_part,"srpvin")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_srpvin[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("srpvin", bin_answer, decoded_answer);
}
}
/* srpvbat */
if (!strcmp(action_part,"srpvbat")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_srpvbat[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("srpvbat", bin_answer, decoded_answer);
}
}
/* srpheater */
if (!strcmp(action_part,"srpheater")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_srpheater[somversion], command_length[somversion], bin_answer)) > 0) {
if (somversion==0) {
sprintf(temp, "\"srpheatebin\":\"%d\"", (bin_answer[3] & 1));
}
if (somversion==1) {
sprintf(temp, "\"srpheatebin\":\"%d\"", ((bin_answer[4]>>7) & 1));
}
strcat(decoded_answer, temp);
}
}
/* srpstatus */
if (!strcmp(action_part,"srpstatus")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_srpstatus[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value_int("srpstatus", bin_answer, decoded_answer);
sprintf(temp, ",\"srpcharge\":\"%d\"", (bin_answer[4] & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"srpresprw\":\"%d\"", ((bin_answer[4]>>1) & 1));
strcat(decoded_answer, temp);
if (somversion==0) {
sprintf(temp, ",\"srpheater\":\"%d\"", ((bin_answer[4]>>6) & 1));
}
if (somversion==1) {
sprintf(temp, ",\"srpheater\":\"%d\"", ((bin_answer[4]>>7) & 1));
}
strcat(decoded_answer, temp);
}
}
/* status */
if (!strcmp(action_part,"status")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_status[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value_int("status", bin_answer, decoded_answer);
sprintf(temp, ",\"onbattery\":\"%d\"", ((bin_answer[read_address]>>6) & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out1state\":\"%d\"", (bin_answer[read_address] & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out2state\":\"%d\"", ((bin_answer[read_address]>>1) & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out1overc\":\"%d\"", ((bin_answer[read_address]>>2) & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out2overc\":\"%d\"", ((bin_answer[read_address]>>3) & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out1disc\":\"%d\"", ((bin_answer[read_address]>>4) & 1));
strcat(decoded_answer, temp);
sprintf(temp, ",\"out2disc\":\"%d\"", ((bin_answer[read_address]>>5) & 1));
strcat(decoded_answer, temp);
strcat(decoded_answer, ",\"lmode\":");
if ((bin_answer[read_address]>>7) & 1) {strcat(decoded_answer, "\"flash\"");} else {strcat(decoded_answer, "\"cont\"");}
}
}
/* vin */
if (!strcmp(action_part,"vin")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_vin[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("vin", bin_answer, decoded_answer);
}
}
/* vbat */
if (!strcmp(action_part,"vbat")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_vbat[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("vbat", bin_answer, decoded_answer);
}
}
/* out1c */
if (!strcmp(action_part,"out1c")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_out1c[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value_int("out1c", bin_answer, decoded_answer);
}
}
/* out2c */
if (!strcmp(action_part,"out2c")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_out2c[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value_int("out2c", bin_answer, decoded_answer);
}
}
/* out1v */
if (!strcmp(action_part,"out1v")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_out1v[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("out1v", bin_answer, decoded_answer);
}
}
/* out2v */
if (!strcmp(action_part,"out2v")) {
if ((exchan_result=exchan485(device_name, echo_flag, devnum, c_out2v[somversion], command_length[somversion], bin_answer)) > 0) {
show_twobytes_single_value("out2v", bin_answer, decoded_answer);
}
}
/* version */
if (!strcmp(action_part,"version")) {
file = fopen("/home/applications/version/sysversion", "r");
fscanf (file, "%s", sysversion);
fclose(file);
file = fopen("/home/applications/version/appversion", "r");
fscanf (file, "%s", appversion);
fclose(file);
sprintf(temp, "\"sysversion\":\"%s\",\"appversion\":\"%s\"", sysversion, appversion);
strcat(decoded_answer, temp); bin_answer[0]=0x80; exchan_result=1;
}
/* mac */
if (!strcmp(action_part,"mac")) {
get_mac_address(mac_addr);
sprintf(temp, "\"macaddress\":\"%s\"", mac_addr);
strcat(decoded_answer, temp); bin_answer[0]=0x81; exchan_result=1;
}
/* localid */
if (!strcmp(action_part,"localid")) {
sprintf(temp, "\"localid\":\"%d\"", somlocalid);
strcat(decoded_answer, temp); exchan_result=1;
}
/* date */
if (!strcmp(action_part,"date")) {
strftime(temp, 100, "\"date\":\"%d.%m.20%y\"", ptr);
strcat(decoded_answer, temp); exchan_result=1;
}
/* time */
if (!strcmp(action_part,"time")) {
strftime(temp, 100, "\"time\":\"%H:%M:%S\"", ptr);
strcat(decoded_answer, temp); exchan_result=1;
}
if (exchan_result>0) {
print_answer(exchan_result, bin_answer, hex_answer);
ap_counter++;
}
else {return (exchan_result);}
} //for
strcat(decoded_answer, "}}");
return 0;
}
Код приложения для формирования пакетов для сервера:
vi run-som.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
char a_filename[15];
char action[100];
char hex_answer[4096];
char decoded_answer[8192];
int devicetype; // 0 = SRP 1 = SOM
char devnum, somlocalid;
int exchan_result, exchan_result_2;
char temp[50];
char cn=0;
int i;
int somversion;
FILE *file;
int main(int argc, char *argv[]) {
printf("%s","*** EVODBG (c) iot sender for SOM v2 A 13.08.2019 ***\r\n");
printf("run-som <0=SRP 1=SOM> action\r\n");
printf("required files: modbusdevices/srp0, modbusdevices/som0, actions/srpaction, actions/somaction\r\n");
sscanf(argv[1],"%d",&devicetype);
sscanf(argv[2],"%s",&a_filename);
sprintf(temp, "/home/applications/actions/%s", a_filename);
if ((file = fopen(temp, "r"))==NULL) {
printf("Cannot open action string file.\n");
exit(1);
}
fgets(action, 100, file);
for ( i = 0; i < 100; i++ ) {
if ((action[i]==0x0d) || (action[i]==0x0a)) {action[i]=0;}
}
fclose(file);
//get devices version
somversion=0;
sprintf(temp, "/home/applications/modbusdevices/somversion");
if ((file = fopen(temp, "r"))!=NULL) {
fscanf (file, "%d", &somversion);
fclose(file);
}
//get device number
if (devicetype==0) {
sprintf(temp, "/home/applications/modbusdevices/srp%d", cn);
}
else {
sprintf(temp, "/home/applications/modbusdevices/som%d", cn);
}
while ((file = fopen(temp, "r"))!=NULL) {
fscanf (file, "%d", &devnum);
fclose(file);
//collect information from counter
printf("Action: %s\r\n", action);
printf("devnum: %d\r\n", devnum);
exchan_result=som_poll(action, devnum, devicetype, cn, somversion, hex_answer, decoded_answer);
if (exchan_result==0) {
printf("Hex answer: %s\r\n", hex_answer);
printf("Decoded answer: %s\r\n", decoded_answer);
//print to file
if ((file = fopen("/tmp/message", "w"))==NULL) {
printf("Cannot write action to message file.\n");
exit(1);
}
fwrite ( decoded_answer, sizeof(decoded_answer), 1, file );
fclose(file);
/* call iot client here */
}
else {
switch(exchan_result) {
case -1:
printf("\r\n***Error -1: wrong action code\r\n");
break;
case -2:
printf("\r\n***Error -2: error while opening device %s\r\n", argv[1]);
break;
case -4:
printf("\r\nError -4: wrong incoming CRC\r\n");
break;
case -3:
printf("\r\nError -3: no incoming data received\r\n");
break;
case -5:
printf("\r\nError -5: ioctl error\r\n");
break;
}
} //else
cn++;
if (devicetype==0) {
sprintf(temp, "/home/applications/modbusdevices/srp%d", cn);
}
else {
sprintf(temp, "/home/applications/modbusdevices/som%d", cn);
}
sleep(5);
} //while
return 0;
}
Файл для компиляции
vi Makefile
CC=arm-openwrt-linux-gcc
CFLAGS=-c
LDFLAGS=
SOURCES=
OBJECTS=$(SOURCES:.c.o)
EXECUTABLES=
PATH := ${PATH}:~/openwrt/staging_dir/toolchain-arm_arm926ej-s_gcc-4.8-linaro_uClibc-0.9.33.2_eabi/bin/
STAGING_DIR := ~/openwrt/staging_dir/toolchain-arm_arm926ej-s_gcc-4.8-linaro_uClibc-0.9.33.2_eabi/
export PATH STAGING_DIR
all: run-som
scp run-som root@192.168.1.1:/home/applications/
# Create executable file
run-som: run-som.o Som_libs.o 485_ex.o
# Shell file
run-som.o: run-som.c
$(CC) $(CFLAGS) run-som.c -o run-som.o
# Som_libs file
Som_libs.o: Som_libs.c Som_libs.h
$(CC) $(CFLAGS) Som_libs.c -o Som_libs.o
# 485 interface file
485_ex.o: 485_ex.c 485_ex.h
$(CC) $(CFLAGS) 485_ex.c -o 485_ex.o
# Clean
clean:
rm -rf *.o run-som
Перед проверкой создадим в /home/applications/ директорию modbusdevices, а в ней - файлы SRP0 и SOM0 с номерами ведомых устройств оборудования СОМ на шине modbus (всего там два ведомых устройства - они называются СОМ и СРП, и адреса на шине у них обычно 14 и 15 соответственно)
Примечание: в дальнейшем, при подключении любых других датчиков и устройств, на шину modbus, будем номер каждого из них на шине вносить в директорию /home/applications/modbusdevices
В этой же директории нам понадобится создать файл somversion, и записать в него 0 или 1. В зависимости от этого, библиотека Som_libs будет выдавать разные коды команд - так как разные ревизии оборудования СОМ требуют отличных друг от друга кодов.
Наконец, создадим каталог /home/applications/actions/, и запишем в него файлы srp_action и som_action с макрокомандами для отсылки на сервер тех или иных параметров.
Например,
vi /home/applications/actions/srp_action
mac date time localid srpvintext srpvin srpvbat srpstatus
vi /home/applications/actions/som_action
mac date time localid vin vbat out1c out2c out1v out2v status
В /etc/crontabs/root внесём такие строчки:
vi /etc/crintabs/root
15 */3 * * * /home/applications/run-som 0 srpaction
30 */3 * * * /home/applications/run-som 1 somaction
Каждые три часа на пятнадцатой минуте будут собраны параметры с ведомого устройства СРП, и отосланы на сервер. На тридцатой минуте будут собраны и отосланы на сервер параметры с ведомого устройства СОМ.
Отправка данных на сервер
Существует масса способов отправить данные с контроллера на сервер. Способы самые разные, начиная от простейших, таких, как положить данные в UDP пакет, и отправить через Интернет, и заканчивая сравнительно сложными, один из которых будет описан ниже. На практике логично было бы выбрать наиболее знакомый и проверенный способ передать данные, или подобрать способ, наиболее подходящий для конкретного типа данных. Но из-за того, что типов данных, собираемых с одного и того же контроллера, может быть несколько, иногда даже приходится комбинировать несколько способов на одном и том же контроллере
Итак, первый пример: передаём <данные> в UDP пакете. На контроллере:
echo <данные> | nc -u <hostname> <port>
На сервере можно запустить tcpdump, и увидеть присланный пакет, или написать небольшой скрипт - службу, которая будет слушать порт и обрабатывать посылки (ниже мы это сделаем)
tcpdump -vvnnA -i eth0 port <port>
Вот другой пример, чуть посложнее. Передадим данные по протоколу HTTP
curl -d "<данные>" -X POST http://<hostname>:<port>/listener.php
На сервере можно установить Apache и при получении POST запроса скрипт listener.php будет обрабатывать принятые данные
Примерно таким же образом можно передавать сообщения с устройств мониторинга в группу Telegram. Для этого нужно создать Telegram бот и получить ключ для его использования. Имея такой ключ, на каждом контроллере можно выполнить:
curl -s -X POST https://api.telegram.org/bot<ID бота>:<ключ бота>/sendMessage -d chat_id=<ID чата> -d text="<данные>"
Бот, получив сообщения, может помещать их в группу. Затем, мы можем просматривать их там, или забирать на сервер, например с помощью telegram cli. Получившийся таким образом frontend-шлюз api.telegram.org Телеграмм обладает существенным ограничением: через него нельзя передавать большой трафик, он будет ограничен 30-ю сообщениями в секунду. Однако он сравнительно удобен для просмотра и отладки сообщений контроллеров в реальном времени.
На данном примере хорошо видна основная функциональность Frontend-шлюза серверной части решений IoT. Шлюз должен аутентифицировать оконечное устройство, посылающее данные, и маршрутизировать данные в предназначенное для них приложение. Здесь как раз это и происходит: для аутентификации используется ключ бота, а для маршрутизации - ID чата.
В подавляющем большинстве случаев бывает достаточно аутентифицировать оконечное устройство по его ID и уникальному ключу, а маршрутизировать данные по заданному в пакете маршруту, который содержится в заголовке передаваемых данных. Мы посмотрим, как это делается, ниже
Третий пример: передача данных по протоколу MQТТ. Для многих MQТТ - это (причём, заслуженно) - самый любимый протокол. Пример:
mosquitto_pub -t 'application/<ID приложения>/controller/<ID контроллера>/uplink' -m '<данные>'
На сервер мы в этом случае должны поставить mosquitto mqtt broker, или использовать отдельный сервер для этого брокера. Можно использовать аутентификацию и шифрование по протоколу SSL
Передача данных через Microsoft Azure IoT Hub
Наконец, четвертый пример - использование для обработки данных в облаке шлюза интернета вещей Microsoft Azure IoT Hub. Данное решение позволяет достичь высокой надёжности сбора данных за счёт многократного резервирования облачной инфраструктуры, и за счёт применения двустороннего обмена с подтверждением на основе протокола AMQP. Также, здесь применяется и аутентификация по ключу устройства, и шифрование, что повышает безопасность корпоративной сети IoT.
В нашей компании для получения данных с устройств мы выбрали именно это решение, как основное, и важным аргументом для него была именно безопасность. В его пользу высказались и IT департамент, и специалисты по информационной безопасности.
Решение Microsoft Azure IoT Hub подробно и полностью описано на сайте Майкрософт. Здесь имеет смысл остановиться лишь на моментах, связанных с практической его реализацией на устройствах с OpenWRT.
Вначале на виртуальной машине создаём для компиляции каталог в домашней директории и клонируем туда Azure SDK:
mkdir ~/Azure
cd ~/Azure
git clone --recursive https://github.com/Azure/azure-iot-sdk-c.git
Затем компилируем исходники и примеры, прилагаемые к ним, для архитектуры
используемой виртуальной машины. Убеждаемся, что все зависимости разрешены успешно, и компиляция успешно выполнена.
Создаём в директории ~/Azure/azure-iot-sdk-c/build_all/linux/ файл toolchain-openwrt-CC_MXS.cmake
vi toolchain-openwrt-CC_MXS.cmake
INCLUDE(CMakeForceCompiler)
SET(CMAKE_SYSTEM_NAME Linux) # this one is important
SET(CMAKE_SYSTEM_VERSION 1) # this one not so much
# this is the location of the amd64 toolchain targeting the Raspberry Pi
SET(CMAKE_C_COMPILER $ENV{SDK_ROOT}/staging_dir/toolchain-arm_cortex-a9+neon_gcc-7.3.0_musl_eabi/bin/arm-openwrt-linux-gcc)
# this is the file system root of the target
#SET(CMAKE_FIND_ROOT_PATH $ENV{SDK_ROOT})
# search for programs in the build host directories
SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# for libraries and headers in the target directories
SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
SET(OPENSSL_ROOT_DIR /usr/lib64/)
SET(OPENSSL_INCLUDE_DIR /usr/lib64/)
Копируем имеющийся в этой же директории файл build.sh в новый файл build_for_openwrt.sh, и добавляем в новый файл следующие строчки:
toolchainfile="toolchain-openwrt-CC_MXS.cmake"
export SDK_ROOT=~/openwrt
export STAGING_DIR=~/openwrt/staging_dir
В качестве основы для редактирования и для написания своего приложения возьмём файл:
~/Azure/azure-iot-sdk-c/iothub_client/samples/iothub_client_sample_amqp_shared/iothub_client_sample_amqp_shared.c
cp ~/Azure/azure-iot-sdk-c/iothub_client/samples/iothub_client_sample_amqp_shared/iothub_client_sample_amqp_shared.c ~/Azure/azure-iot-sdk-c/iothub_client/samples/iothub_client_sample_amqp_shared/iothub_client_sample_amqp_shared.c.initial
Напишем в директории ~/Azure/azure-iot-sdk-c/ следующий скрипт:
vi make_all.sh
#!/bin/sh
cd ~/Azure/azure-iot-sdk-c/build_all/linux && ./build3.sh
cd ~/Azure/azure-iot-sdk-c/
cp ./cmake/iotsdk_linux/iothub_client/samples/iothub_client_sample_amqp_shared/iothub_client_sample_amqp_shared ./iothub_client_sample_amqp
cp ./iothub_client_sample_amqp ~/openwrt/additional_rootfs/home/applications
scp ./iothub_client_sample_amqp root@192.168.1.1:/home/applications
chmod 755 ./make_all.sh
И для компиляции amqp-клиента, и для его работы на контроллере нам понадобятся библиотеки libuuid and libcurl. Отметим их в menuconfig, скомпилируем с ними openwrt:
Название приложения | Функция | menuconfig |
libuuid | DCE compatible Universally Unique Identifier library | -*- libuuid |
libcurl | A client-side URL transfer library | <*> libcurl |
Добавляем линки на библиотеки в тулчейн:
target_dir='target-arm_cortex-a9+neon_musl_eabi'
cd ~/openwrt/staging_dir/toolchain-arm_cortex-a9+neon_gcc-7.3.0_musl_eabi/include
ln -s ~/openwrt/staging_dir/$target_dir/usr/include/uuid/ .
ln -s ~/openwrt/staging_dir/$target_dir/usr/include/openssl/ .
ln -s ~/openwrt/staging_dir/$target_dir/usr/include/curl/ .
cd ~/openwrt/staging_dir/toolchain-arm_cortex-a9+neon_gcc-7.3.0_musl_eabi/lib
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libcurl.so .
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libuuid.so .
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libssl.so .
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libmbedcrypto.so.1 .
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libmbedx509.so.0 .
ln -s ~/openwrt/staging_dir/$target_dir/usr/lib/libmbedtls.so.10 .
Можно компилировать клиент для контроллера:
cd ~/Azure/azure-iot-sdk-c
./make_all.sh
Дополнения к клиенту AMQP
Строка для соединения connectionString с сервером Microsoft Azure IoT Hub выглядит как
HostName=<имя или IP адрес сервера для соединения> DeviceId=<индивидуальное ID устройства, совпадающее с прописанным на сервере>;SharedAccessKey=<индивидуальных ключ устройства>
В исходный файл клиента мы добавили код, читающий на контроллере файл /home/applications/device/remote_hostname, и подставляющий значение в connectionString
Также, для удобства работы с устройством, его DeviceId формируется из MAC адреса одного из интерфейсов.
Ключ устройства читается из файла на контроллере /home/applications/device/primary_key
Само сообщение читается из файла /tmp/message
vi ~/Azure/azure-iot-sdk-c/iothub_client/samples/iothub_client_sample_amqp_shared/iothub_client_sample_amqp_shared.c
int main(void)
{
printf ("Verticali Azure IoT client v3.0 23.03.2018\r\n");
if ((file = fopen("/home/verticali/device/remote_hostname", "r"))==NULL) {
printf("Cannot open remote hostname file.\n");
exit(1);
}
fscanf (file, "%s", remote_hostname);
fclose(file);
get_mac_address(deviceId1);
//printf("deviceId1=%s\n", deviceId1);
if ((file = fopen("/home/verticali/device/primary_key", "r"))==NULL) {
printf("Cannot open primary key file.\n");
exit(1);
}
fscanf (file, "%s", deviceKey1);
fclose(file);
sprintf(connectionString, "HostName=%s;DeviceId=%s;SharedAccessKey=%s", remote_hostname, deviceId1, deviceKey1);
printf("%s\r\n", connectionString);
if ((file = fopen("/tmp/message", "r"))==NULL) {
printf("Cannot open message file.\n");
exit(1);
}
fscanf (file, "%s", input_message);
fclose(file);
iothub_client_sample_amqp_shared_hl_run();
return 0;
}
Также, в исходный код можем сразу добавить сертификаты .azure-devices.net, которые будут переданы в опции OPTION_TRUSTED_CERT:
if (IoTHubClient_SetOption(iotHubClientHandle1, OPTION_TRUSTED_CERT, certificates) != IOTHUB_CLIENT_OK)
{
printf("failure to set option \"TrustedCerts\"\r\n");
}
Компилируем получившуюся программу при помощи make_all.sh
Дополняем исходный файл run_som.c вызовом ioT клиента:
//print to file
if ((file = fopen("/tmp/message", "w"))==NULL) {
printf("Cannot write action to message file.\n");
exit(1);
}
fwrite ( decoded_answer, sizeof(decoded_answer), 1, file );
fclose(file);
// call iot client
system("/home/applications/iothub_client_sample_amqp");
Запускаем на контроллере /home/applications/run-som 0 srpaction и убеждаемся, что сообщение отправляется без ошибок и данные приходят в Microsoft Azure IoT Hub:
/home/applications/run-som 0 srpaction
*** *** EVODBG (c) iot sender for SOM v2 A 13.08.2019 ***
Action: mac date time localid srpvintext srpvin srpvbat srpstatus
devnum: 15
Hex answer: 81 81 81 81 0f 04 02 00 00 d0 f1 0f 04 02 01 2a 50 be 0f 04 02 00 fb 91 72 0f 04 02 00 00 d0 f1
Decoded answer: {"srp":{"macaddress":"<mac address>","date":"13.03.2020","time":"16:02:33","localid":"0","srpvintext":"ok","srpvin":"29.8","srpvbat":"25.1","srpstatus":"0","srpcharge":"0","srpresprw":"0","srpheater":"0"}}
Verticali Azure IoT client v2.3 02.02.2018
HostName=<IoT hub>.azure-devices.net;DeviceId=<mac address>;SharedAccessKey=<key>
OK: IoTHubClient_SetMessageCallback...successful.
OK: IoTHubClient_SendEventAsync has accepted data for transmission to IoT Hub
-> Header (AMQP 0.1.0.0)
<- Header (AMQP 0.1.0.0)
-> [OPEN]* {53832fb5-1c10-4159-87ce-4adcf6eb0057,<IoT hub>.azure-devices.net,4294967295,65535,240000}
<- [OPEN]* {DeviceGateway_e9c50cecc13842858be9cee37791c54d,10.0.15.58,65536,8191,240000,NULL,NULL,NULL,NULL,NULL}
-> [BEGIN]* {NULL,0,4294967295,100,4294967295}
<- [BEGIN]* {0,1,5000,4294967295,262143,NULL,NULL,NULL}
-> [ATTACH]* {$cbs-sender,0,false,0,0,* {$cbs},* {$cbs},NULL,NULL,0,0}
-> [ATTACH]* {$cbs-receiver,1,true,0,0,* {$cbs},* {$cbs},NULL,NULL,NULL,0}
<- [ATTACH]* {$cbs-sender,0,true,0,0,* {$cbs,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {$cbs,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,NULL,1048576,NULL,NULL,NULL}
<- [FLOW]* {0,5000,1,4294967295,0,0,100,0,NULL,false,NULL}
-> [TRANSFER]* {0,0,<01 00 00 00>,0,false,false}
<- [ATTACH]* {$cbs-receiver,1,false,0,0,* {$cbs,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {$cbs,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,0,1048576,NULL,NULL,NULL}
-> [FLOW]* {1,4294967295,1,99,1,0,10000}
<- [DISPOSITION]* {true,0,NULL,true,* {},NULL}
<- [TRANSFER]* {1,0,<01 00 00 00>,0,NULL,false,NULL,NULL,NULL,NULL,false}
-> [DISPOSITION]* {true,0,0,true,* {}}
-> [ATTACH]* {link-snd-<mac address>-3352c037-10eb-4711-aea6-f411a1022d06,2,false,0,0,* {link-snd-<mac address>-3352c037-10eb-4711-aea6-f411a1022d06-source},* {amqps://VerticaliLLC.azure-devices.net/devices/<mac address>/messages/events},NULL,NULL,0,18446744073709551615,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)]}}
-> [ATTACH]* {link-snd-<mac address>-ca707f1b-9eaa-4e55-bbfd-5c155d6efa3a,3,false,0,0,* {link-snd-<mac address>-ca707f1b-9eaa-4e55-bbfd-5c155d6efa3a-source},* {amqps://VerticaliLLC.azure-devices.net/devices/<mac address>/twin},NULL,NULL,0,18446744073709551615,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)],[com.microsoft:channel-correlation-id:twin:8e0ce09b-d44d-4d73-bf07-3a83be8b8003],[com.microsoft:api-version:2016-11-14]}}
<- [ATTACH]* {link-snd-<mac address>-3352c037-10eb-4711-aea6-f411a1022d06,2,true,0,NULL,* {link-snd-<mac address>-3352c037-10eb-4711-aea6-f411a1022d06-source,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {amqps://VerticaliLLC.azure-devices.net/devices/<mac address>/messages/events,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,NULL,1048576,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)]}}
<- [FLOW]* {1,5000,2,4294967295,2,0,1000,0,NULL,false,NULL}
<- [ATTACH]* {link-snd-<mac address>-ca707f1b-9eaa-4e55-bbfd-5c155d6efa3a,3,true,1,0,* {link-snd-<mac address>-ca707f1b-9eaa-4e55-bbfd-5c155d6efa3a-source,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {amqps://VerticaliLLC.azure-devices.net/devices/<mac address>/twin,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,NULL,1048576,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)],[com.microsoft:channel-correlation-id:twin:8e0ce09b-d44d-4d73-bf07-3a83be8b8003],[com.microsoft:api-version:2016-11-14]}}
<- [FLOW]* {1,5000,2,4294967295,3,0,1000,0,NULL,false,NULL}
-> [ATTACH]* {link-rcv-<mac address>-a8025520-f15d-4795-8c1d-c6ff26cf0741,4,true,0,0,* {amqps://<IoT hub>.azure-devices.net/devices/<mac address>/messages/devicebound},* {link-rcv-<mac address>-a8025520-f15d-4795-8c1d-c6ff26cf0741-target},NULL,NULL,NULL,65536,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)]}}
-> [ATTACH]* {link-rcv-<mac address>-72c24aad-fa39-45a5-a400-6a9845c38a77,5,true,0,0,* {amqps://<IoT hub>.azure-devices.net/devices/<mac address>/twin},* {link-rcv-<mac address>-72c24aad-fa39-45a5-a400-6a9845c38a77-target},NULL,NULL,NULL,18446744073709551615,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)],[com.microsoft:channel-correlation-id:twin:8e0ce09b-d44d-4d73-bf07-3a83be8b8003],[com.microsoft:api-version:2016-11-14]}}
<- [ATTACH]* {link-rcv-<mac address>-a8025520-f15d-4795-8c1d-c6ff26cf0741,4,false,NULL,1,* {amqps://<IoT hub>.azure-devices.net/devices/<mac address>/messages/devicebound,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {link-rcv-<mac address>-a8025520-f15d-4795-8c1d-c6ff26cf0741-target,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,0,65536,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)]}}
-> [FLOW]* {2,4294967294,1,99,4,0,10000}
<- [ATTACH]* {link-rcv-<mac address>-72c24aad-fa39-45a5-a400-6a9845c38a77,5,false,1,0,* {amqps://<IoT hub>.azure-devices.net/devices/<mac address>/twin,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},* {link-rcv-<mac address>-72c24aad-fa39-45a5-a400-6a9845c38a77-target,NULL,NULL,NULL,NULL,NULL,NULL},NULL,NULL,0,1048576,NULL,NULL,{[com.microsoft:client-version:iothubclient/1.1.30 (native; Linux; armv5tejl)],[com.microsoft:channel-correlation-id:twin:8e0ce09b-d44d-4d73-bf07-3a83be8b8003],[com.microsoft:api-version:2016-11-14]}}
-> [FLOW]* {2,4294967294,1,99,5,0,10000}
OK: client is successfully authenticated
-> [TRANSFER]* {2,1,<01 00 00 00>,2147563264,false,false}
<- [DISPOSITION]* {true,1,NULL,true,* {},NULL}
OK: confirmation[0] received for message tracking id = 0 with result = IOTHUB_CLIENT_CONFIRMATION_OK
QUIT: call DoWork 1 more time to complete final sending...
-> [DETACH]* {2,true}
-> [DETACH]* {4,true}
-> [DETACH]* {3,true}
-> [DETACH]* {5,true}
OK: client is unauthenticated
-> [DETACH]* {0,true}
-> [DETACH]* {1,true}
-> [END]* {}
-> [CLOSE]* {}
В веб интерфейсе portal.azure.com можем видеть графическую статистику по приходящим device-to-cloud сообщением (можно выбрать промежуток времени от получаса):
Для сохранения полученных сообщений в облаке создадим storage account, который будет содержать Tabular Data Storage CosmosDB. Создадим таблицу ControllerdataTable, состоящую из колонок PartitionKey, RowKey, Timestamp и Message. Для просмотра содержимого таблицы можно использовать Storage Explorer в веб интерфейсе portal.azure.com
Для добавления записей в таблицу ControllerdataTable напишем приложение FunctionApp, StoreControllerData,содержащее следующую функцию:
index.js
module.exports = function (context, myEventHubMessage) {
context.log(`jаvascript eventhub trigger function called for message: ${myEventHubMessage}`);
var message = {
"message": [myEventHubMessage]
}
message.partitionKey = formatDate(new Date());
message.rowKey = generateRowKey();
context.log(`result: ${JSON.stringify(message)}`);
context.bindings.tableOut = message;
context.bindings.outputTable = message;
context.done();
return message;
};
function generateRowKey(){
let maxDate = new Date(8340000000000000);
return (maxDate.getTime() - (new Date()).getTime()) * 10000;
}
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('');
}
Сконфигурируем вход и выход для этой функции через параметры integrate:
Trigger: Azure Event Hubs (myEventHubMessage)
Outputs: Azure Table Storage (outputTable)
Output Table (Name): ControllerdataTable
После запуска функции приложения, в окне monitor можно видеть результаты вызовов этой функции, а с помощью Storage Explorer, можно просмотреть новые появившиеся записи в таблице ControllerDataTable
Ниже мы расскажем, каким образом можно скопировать из облака, обработать данные, и загрузить в локальную базу данных для отображения в веб интерфейсе мониторинга
Простая программа для сервера: отслеживание активности контроллеров
Начнём программирование серверной части мониторинга с написания простейшей программы. Её задача - работать на сервере в режиме службы, и слушать UDP порт 4001. На этот порт каждый контроллер будет присылать UDP-датаграмму со своим mac-адресом, например, раз в минуту. Если контроллер не присылает пакеты какое-то время, скажем, 20 минут, мы будем считать, что с ним что-то произошло, и генерировать аварию. Для начала, будем записывать такие аварии в лог файл, а потом можно по факту каждой аварии запускать другое приложение, например, отсылать оператору электронную почту.
Программу будем для простоты писать на языке Python.
mkdir /opt/bin
cd /opt/bin
vi udp_processor_A30С.py
#!/usr/bin/env python
import sys
import subprocess
import os
import logging
logging.basicConfig()
from tgscheduler import start_scheduler
from tgscheduler import add_interval_task
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from time import strftime
ping_timer = {}
my_pid = os.getpid()
ver = 'A30C 02.04.2019 (c) EVODBG PID=%s' % my_pid
# in how many timer increments the alarm will be generated
alarm_delay=20
# how often (sec) the timer increments
timer_update_interval=60
verbose=0
cleaning=0
logfilename = '/opt/bin/controller.log'
dg_received=0
# ---- given arguments ----
for argument in sys.argv:
if argument=="-h":
print "Usage: -v turn on verbose mode"
print " -h show help"
quit()
if argument=="-v":
print "verbose mode is on"
verbose=1
# ---- logger ----
def log(data):
if verbose==1:
print data
logfile= open(logfilename, 'a')
logfile.write ("%s %s\n" % (strftime("%Y-%m-%d %H:%M:%S"), data))
logfile.close()
# ---- job for incrementing timers, run once a minute ----
def update_timers():
global dg_received
log("200 update_timers is running...")
if dg_received==1:
log("400 new datagrams received...")
dg_received = 0
for controller in ping_timer:
if ping_timer[controller] >= 0:
ping_timer[controller]+=1
if ping_timer[controller] > alarm_delay:
ping_timer[controller]=-1
log("01 alarm, no response from controller %s" % (controller))
# ---- udp message processor ----
class udpprocessor(DatagramProtocol):
def datagramReceived(self, data, addr):
global dg_received
ip_address=addr[0]
if data!=None:
dg_received = 1
ping_message=data.split(" ")
try:
controller=ping_message[1]
except IndexError:
log("09 invalid controller number received from ip %s" % (ip_address))
else:
if verbose==1:
print("50 controller %s with ip %s has responded" % (controller, ip_address))
if len(controller)==17:
if ping_timer.get(controller)==None:
if verbose==1:
log("03 controller %s with ip %s controller is added to monitoring" % (controller, ip_address))
if ping_timer.get(controller)==-1:
log("02 alarm ceasing for controller %s ip %s" % (controller, ip_address))
ping_timer[controller] = 0
# ---- main ----
log("00 %s udpprocessor is started" % ver)
add_interval_task(action=update_timers, interval=timer_update_interval, initialdelay=timer_update_interval)
start_scheduler()
reactor.listenUDP(4001, udpprocessor())
reactor.run()
В части main программы инициализируется реактор udpprocessor, экземпляр которого запускается каждый раз, когда UDP пакет приходит на порт 4001.
Содержимое UDP пакета разделяется на слова пробелами, в первом слове должен быть прислан mac-адрес контроллера.
В памяти сервера создаётся словарь ping_timer, ключами которого являются mac-адреса контроллеров. Кстати, если сеть построена таким образом, что все контроллеры в ней имеют различные уникальные IP адреса, что ключами можно сделать именно их.
В момент, когда приходит пакет, в словаре создаётся ключ со значением 0, или, если он уже был, в него записывается 0
Одновременно в программе работает задача update_timers, которая раз в минуту инкрементирует все записи в словаре. Если значение превысило 20, генерируется авария. Она записывается в лог файл, там же можно отмечать соответствующую запись для принятия последующих действий. Таким образом, доступность всех контроллеров постоянно находится под наблюдением.
База данных контроллеров
Работать только с лог файлами неудобно, потому что приходится перечитывать большие объёмы информации в поисках какого-либо события (командой grep или чем-то ещё). Для упрощения поиска результата, и для будущей интеграции с веб интерфейсом, сделаем базу данных контроллеров, и будем записывать всю информацию в неёБаза данных нам подойдёт пока любая, требования к ней невелики. Со временем данных накопится больше, и станет необходимо выбрать самую эффективную для конкретной задачи.
Для себя мы выбрали облачную базу MSSQL, для которой есть удобный графический инструмент для работы Microsoft SQL Server Management Studio. Единственный важный момент - это быстродействие этой базы, она должна успевать обрабатывать события, поступающие от контроллеров в реальном времени, поэтому нам не подойдут далеко расположенные облачные базы данных с большим временем открытия соединения и записи/чтения.
Структуру базы данных мы спроектировали таким образом, чтобы она соответствовала физическому расположению объектов и связям их между собой. Эта схема была наиболее наглядной и удобной для администратора и программиста.
На каждой мачте (Towers) стоит модем (Modems), через который мы связываемся с контроллером (Controller). На каждой мачте смонтированы одно или несколько девайсов (Devices), примером которого может быть модуль СОМ или СРП. Контроллеры присылают нам данные, которые имеют разный формат, в зависимости от типа девайса, с которого они получены. Чтобы не разделять похожие по назначению девайсы СОМ или СРП, мы объединили записи от этих двух видов девайсов в одну таблицу (SomRecords)
Наконец, состояние каждого контроллера (работает он, либо в аварии) мы будем сохранять в виде Integer числа, а более подробное описание каждого из них будем хранить в таблице Statuses
SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MS_Modems'
ModemID nchar // IP адрес модема
ModemLevel int // Если на мачте стоит более одного модема, распределим их по уровням, по мере удалённости от общего для всех источника питания. Так, самый ближний к источнику питания будет иметь уровень 0, далее уровень 1 и т.д
Например, если пропала связь с модемом уровня 1, а связь с модемом уровня 0 есть, то можно сделать вывод, что общее питание есть, а оборвана линия питания, идущая от 0 до 1 модема. Это часто помогает локализовать неисправность
ModemStatus int // См. Statuses, 1000 - работает нормально, 1001 - Critical Alarm, 1002 - Major Alarm, 1003 - Minor Alarm, 1004 - warning
ModemTower nchar // Номер (Идентификатор мачты)
ModemAlarmtimestamp datetime // Время последней аварии
ModemController varchar // mac адрес контроллера
ModemSerial varchar // серийный номер модема
ModemIMSI varchar // номер сим карты в модеме
ModemMsisdn varchar // номер телефона модема
ModemVersion varchar // версия модема у производителя
ModemVendor varchar // производитель
ModemChipset varchar // версия HW
ModemLatitude nchar // координаты места установки (приходят по GPS)
ModemLongitude nchar
ModemPower int // наличие и качество питающего напряжения
ModemFirmware varchar // версия SW
ModemLAC nchar // номер соты, откуда работает модем (передаётся контроллером)
ModemCID nchar
ModemUpdateflag int // Триггер для оповещений операторов системы об изменениях конфигурации модема
ModemIotHubChanged int // Триггер для переключения между несколькими IoT хабами
ModemWatchdog int // Аппаратная версия вочдог, поддерживаемая контроллером
ModemWeight int // Приоритет того или иного модема в формировании сигнала аварии объекта
SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MS_Towers'
TowerID nchar // Номер (Идентификатор мачты)
TowerAddress varchar // Адрес расположения мачты
TowerStatus int // См. Statuses, 0 - работает нормально, 1 - Critical Alarm, 2 - Major Alarm, 3 - Minor Alarm, 4 - warning
TowerUpdateflag int // Флаг для запланированного обновления статуса
Отслеживание активности контроллеров с записью статуса модема в базу данных
Добавим в скрипт для отслеживания активности код для подключения к базе данных и модификации статуса модемаvi udp_processor_A31С.py
#!/usr/bin/env python
import sys
import subprocess
import os
import _mssql
import json
import logging
logging.basicConfig()
from tgscheduler import start_scheduler
from tgscheduler import add_interval_task
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from time import strftime
ping_timer = {}
my_pid = os.getpid()
ver = 'A31C 04.04.2019 (c) EVODBG PID=%s' % my_pid
# in how many timer increments the alarm will be generated
alarm_delay=20
# how often (sec) the timer increments
timer_update_interval=60
verbose=0
cleaning=0
logfilename = '/opt/bin/controller.log'
dg_received=0
global conn
# --- reading credentials ---
credfilename = '/opt/bin/dbcredentials/dbcredentials.json'
try:
cred=open(credfilename, 'r')
except:
print("210 cannot open credentials file")
quit()
cred_txt=cred.read()
credentials = json.loads(cred_txt)
global database, password, user, server
database=credentials[u'MSdb'][u'database']
password=credentials[u'MSdb'][u'password']
user=credentials[u'MSdb'][u'user']
server=credentials[u'MSdb'][u'server']
# ---- given arguments ----
for argument in sys.argv:
if argument=="-h":
print "Usage: -v turn on verbose mode"
print " -h show help"
quit()
if argument=="-v":
print "verbose mode is on"
verbose=1
# ---- logger ----
def log(data):
if verbose==1:
print data
logfile= open(logfilename, 'a')
logfile.write ("%s %s\n" % (strftime("%Y-%m-%d %H:%M:%S"), data))
logfile.close()
# ---- sql update ----
def sqlupdate(ModemIDi, ModemController, alarm):
global conn
conn = _mssql.connect(server=server,user=user,password=password,database=database)
if alarm == 1:
NewModemStatus=1001
else:
NewModemStatus=1000
Controller_exists=0
conn.execute_query("SELECT ModemID, ModemTower FROM MS_Modems WHERE ModemController = %s", ModemController)
for row in conn:
try:
ModemID=row['ModemID']
except:
log("906 ModemID row is not found")
return
try:
TowerID=row['ModemTower']
except:
log("907 TowerID row is not found")
return
else:
Controller_exists=1
if Controller_exists==0:
log("060 Controller %s is not found in the database" % (ModemController))
else:
if ModemID.strip() != ModemIDi and alarm == 0:
log("070 received modem ip %s is different from the one in database %s" % (ModemController, ModemIDi, ModemID))
else:
conn.execute_query("UPDATE MS_Modems SET ModemStatus=%d, ModemAlarmtimestamp=GETDATE() WHERE ModemID=%s", (NewModemStatus, ModemID))
conn.execute_query("UPDATE MS_Towers SET TowerUpdateflag=1 WHERE TowerID=%s", (TowerID))
conn.close()
# ---- job for incrementing timers, run once a minute ----
def update_timers():
global dg_received
log("200 update_timers is running...")
if dg_received==1:
log("400 new datagrams received...")
dg_received = 0
for controller in ping_timer:
if ping_timer[controller] >= 0:
ping_timer[controller]+=1
if ping_timer[controller] > alarm_delay:
ping_timer[controller]=-1
log("01 alarm, no response from controller %s" % (controller))
sqlupdate(0, Controller_number,1)
# ---- udp message processor ----
class udpprocessor(DatagramProtocol):
def datagramReceived(self, data, addr):
global dg_received
ip_address=addr[0]
if data!=None:
dg_received = 1
ping_message=data.split(" ")
try:
controller=ping_message[1]
except IndexError:
log("090 invalid controller number received from ip %s" % (ip_address))
else:
if verbose==1:
print("500 controller %s with ip %s has responded" % (controller, ip_address))
if len(controller)==17:
if ping_timer.get(controller)==None:
if verbose==1:
log("03 controller %s with ip %s controller is added to monitoring" % (controller, ip_address))
sqlupdate(ip_address, Controller_number,2)
if ping_timer.get(controller)==-1:
log("02 alarm ceasing for controller %s ip %s" % (controller, ip_address))
sqlupdate(ip_address, Controller_number,0)
ping_timer[controller] = 0
# ---- main ----
log("00 %s udpprocessor is started" % ver)
try:
conn = _mssql.connect(server=server,user=user,password=password,database=database)
except:
print "impossible to connect"
log("900 no connnection to sql server during startup")
else:
log("100 connected to sql server")
try:
conn.execute_query("SELECT * FROM MS_Modems WHERE ModemStatus = 1001 and ModemLevel = 0")
except:
log("910 MSSQL SELECT failed")
else:
for row in conn:
controller_number="%s" % (row['ModemController'].strip())
ping_timer[controller_number]=-1
add_interval_task(action=update_timers, interval=timer_update_interval, initialdelay=timer_update_interval)
start_scheduler()
reactor.listenUDP(4001, udpprocessor())
reactor.run()
В новой версии скрипта мы добавили запись состояния модема в базу данных, которое изменяется при получении первого пакета от модема, и если модем не присылает пакеты какое-то время. Также, мы устанавливаем флаг TowerUpdateflag = 1. Это сделано для того, чтобы последующие скрипты могли обнаружить изменение состояния мачты, и де-дуплицировать аварию на ней, если она случится одновременно на нескольких модемах. Фрагмент простейшего скрипта приведён ниже
vi /opt/bin/alarm_joiner.py
conn = _mssql.connect(server=server,user=user,password=password,database=database)
conn2 = _mssql.connect(server=server,user=user,password=password,database=database)
query='''SELECT TowerID, TowerStatus, m.ModemID, m.ModemStatus
FROM MS_Towers p
LEFT JOIN MS_Modems m ON p.TowerID=m.ModemTower AND m.ModemLevel=0
WHERE TowerUpdateflag=1'''
conn.execute_query(query)
# ++++++++ Updating Towers ++++++++++++++++++++++++++++++++++++
for row in conn:
TowerID=row[0].strip()
ModemID=row[2]
ModemStatus=row[3]
insert_event=0
if not ModemID is None:
# ++++++++ modem exists +++++++++++++++++++++++++++++++++++++
if ModemStatus==1002: # initial status, no signal
TowerStatus=4 # warning
if ModemStatus==1001: # unavailable
TowerStatus=2 # major alarm
if ModemStatus==1000: # available
TowerStatus=0 # OK
print (TowerID, TowerStatus)
if not TowerID=='undefined':
# ++++++++ Updating Tower status, clearing Tower update flag
conn2.execute_query("UPDATE MS_Towers SET TowerStatus=%d, TowerUpdateflag=0 WHERE TowerID=%s", (TowerStatus, TowerID))
conn.close()
conn2.close()
Интеграция Microsoft Azure IoT Hub с базой данных
Для обработки сообщений от устройств и отображения в веб интерфейсе нам понадобятся ещё две таблицы. Создадим их. Таблица для размещения устройств:SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MS_Devices'
DeviceID nchar // Уникальный идентификатор утсройства, присоединённого к контроллеру. Например, может быть составлен из mac-адреса контроллера с добавлением номера шины, номера шкафа, и номера устройства в шкафу
DeviceType int // Для удобства работы с устройствами, разобъём их на типы (например, тип устройства СРП = 0, устройства СОМ = 1)
DeviceDecription varchar // Опциональное текстовое описание
DeviceTower nchar // Ссылка на идентификатор мачты, на которой работает устройство
Таблица для хранения информации, переданной от устройств СОМ:
SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MS_SomRecords'
RecordID varchar // Идентификатор записи, может состоять, например, из идентификатора устройства и временной метки
RecordDate date // Дата записи
RecordTime time // Время записи
RecordMac nchar // Mac адрес контроллера
DevLocalid decimal // Идентификатор устройства на контроллере
RecordType int // Тип записи: 0 -СРП, 1 - СОМ
SrpVintext nchar // Входное напряжение СРП (норм, авария)
SrpVin decimal // Входное напряжение СРП, В
SrpVbat decimal // Напряжение батареи, измеренное СРП
SrpStatus int // Статус блока СРП, код статуса
SrpHeater bit // Состояние реле обогревателя шкафа
SrpCharge bit // Флаг процесса зарядки батареи
SrpRespwr bit // Флаг резервного питание СРП
Status int // Статус блока СOM, код статуса
Onbattery bit // Флаг питания от батареи СОМ
Out1state bit // Флаг состояния огней первого яруса
Out2state bit // Флаг состояния огней второго яруса
Out1overc bit // Перегрузка на первом выходе
Out2overc bit // Перегрузка на втором выходе
Out1disc bit // Обрыв линии на первом выходе
Out2disc bit // Обрыв линии на втором выходе
Lmode nchar // Режим работы (постоянный, проблесковый)
VIn decimal // Входное напряжение на модуле СОМ
VBat decimal // Напряжение на батарее у модуля СОМ
Out1v decimal // Напряжение на первом выходе, B
Out2v decimal // Напряжение на втором выходе, B
Out2c decimal // Ток на первом выходе, А
Out1c decimal // Ток на втором выходе, А
Сделаем скрипт, который будет подсоединяться к облачному хранению Microsoft Azure и вычитывать данные, записывая их в локальную базу данных
cat /opt/bin/rec_som_A1.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# description is at https://github.com/Azure/azure-cosmosdb-python/tree/master/azure-cosmosdb-table
import sys
from azure.cosmosdb.table import TableService, Entity
import json
import _mssql
from datetime import datetime
table_service = TableService(account_name='<<name>>', account_key='<<key>>')
# --- reading credentials ---
credfilename = '/opt/bin/dbcredentials/dbcredentials.json'
try:
cred=open(credfilename, 'r')
except:
print("21 cannot open credentials file")
quit()
cred_txt=cred.read()
credentials = json.loads(cred_txt)
global database, password, user, server
database=credentials[u'MSdb'][u'database']
password=credentials[u'MSdb'][u'password']
user=credentials[u'MSdb'][u'user']
server=credentials[u'MSdb'][u'server']
conn = _mssql.connect(server=server,user=user,password=password,database=database)
logfilename='/opt/bin/collecteddata_som.log'
w=open(logfilename, 'w')
# Query a set of entities
now = datetime.now()
filter="PartitionKey eq '%s'" % now.strftime("%Y%m%d")
tasks = table_service.query_entities('devicetable', filter=filter)
print_flag=0
dbtable_flag=0
xml_flag=0
# Temporary onoff_print_flag
onoff_print_flag=0
av_print_flag=0
# --------------- Analysing given arguments -----------------
if len (sys.argv) == 1:
print "Lack of arguments"
print "Run program with -h to get help"
for argument in sys.argv:
if argument=="-h":
print "Help:"
print "-p print today's SOM data"
print "-d write to MS database today's SOM data"
print "-h show this help"
if argument=="-p":
print filter
print_flag=1
if argument=="-d":
print filter
w.write ("\n")
w.write (filter)
dbtable_flag=1
# ----------------- Reading data from json -----------------
for task in tasks:
message_string=task.message
#print(task.Timestamp)
#print message_string
controllers = json.loads(message_string)
# ----------------- Loop for all controllers ------------------
for controller in controllers:
object_is_srp=0
object_is_som=0
try:
controller[u'srp']
object_is_srp=1
except:
pass
try:
controller[u'som']
object_is_som=1
except:
pass
# ----------------- If print flag is given, print resulting data ----------------
if object_is_srp==1 and print_flag==1:
print ""
print "SRP:"
print "macaddress= %s" % controller[u'srp'][u'macaddress']
print "localid= %s" % controller[u'srp'][u'localid']
datepresent=0
timepresent=0
try:
messagedate=controller[u'srp'][u'date']
print "date=%s" % messagedate
datepresent=1
except:
pass
try:
messagetime=controller[u'srp'][u'time']
print "time=%s" % messagetime
timepresent=1
except:
pass
print "srpvintext= %s" % controller[u'srp'][u'srpvintext']
print "srpvin= %s" % controller[u'srp'][u'srpvin']
print "srpvbat= %s" % controller[u'srp'][u'srpvbat']
print "srpstatus= %s" % controller[u'srp'][u'srpstatus']
print "srpcharge= %s" % controller[u'srp'][u'srpcharge']
print "srpresprw= %s" % controller[u'srp'][u'srpresprw']
print "srpheater= %s" % controller[u'srp'][u'srpheater']
if object_is_som==1 and print_flag==1:
print ""
print "SOM:"
print "macaddress= %s" % controller[u'som'][u'macaddress']
print "localid= %s" % controller[u'som'][u'localid']
datepresent=0
timepresent=0
try:
messagedate=controller[u'som'][u'date']
print "date=%s" % messagedate
datepresent=1
except:
pass
try:
messagetime=controller[u'som'][u'time']
print "time=%s" % messagetime
timepresent=1
except:
pass
print "vin= %s" % controller[u'som'][u'vin']
print "vbat= %s" % controller[u'som'][u'vbat']
print "out1c= %s" % controller[u'som'][u'out1c']
print "out2c= %s" % controller[u'som'][u'out2c']
print "out1v= %s" % controller[u'som'][u'out1v']
print "out2v= %s" % controller[u'som'][u'out2v']
print "status= %s" % controller[u'som'][u'status']
print "onbattery= %s" % controller[u'som'][u'onbattery']
print "out1state= %s" % controller[u'som'][u'out1state']
print "out2state= %s" % controller[u'som'][u'out2state']
print "out1overc= %s" % controller[u'som'][u'out1overc']
print "out2overc= %s" % controller[u'som'][u'out2overc']
print "out1disc= %s" % controller[u'som'][u'out1disc']
print "out2disc= %s" % controller[u'som'][u'out2disc']
print "lmode= %s" % controller[u'som'][u'lmode']
# ------------------ If database write flag is given, write to database -------------------
# ---------- Write date if SRP
if object_is_srp==1 and dbtable_flag==1:
macaddress=controller[u'srp'][u'macaddress']
localid=controller[u'srp'][u'localid']
recordtype=0
datepresent=0
timepresent=0
try:
messagedate=controller[u'srp'][u'date']
#print "date=%s" % messagedate
datepresent=1
except:
pass
try:
messagetime=controller[u'srp'][u'time']
#print "time=%s" % messagetime
timepresent=1
except:
pass
if (datepresent==1 and timepresent==1):
DeviceCategory=1 # SOM equipment category
Record_ID="%s-%s-%s-%s-%s-%s" % (messagedate,messagetime,macaddress,DeviceCategory,localid,recordtype)
Device_ID="%s-%s-%s-%s" % (macaddress,DeviceCategory,localid,recordtype)
print "Record_ID= %s Device_ID= %s writing to database" % (Record_ID,Device_ID)
w.write ( "Record_ID= %s " % Record_ID )
try:
conn.execute_query("INSERT INTO MS_SomRecords (RecordID) VALUES (%s)", (Record_ID))
except:
print ("record %s cannot be inserted" % (Record_ID))
conn.execute_query("UPDATE MS_SomRecords SET RecordMac=%s, DevLocalid=%s, RecordDevice=%s, RecordType=0 WHERE RecordID=%s", (macaddress, localid, Device_ID, Record_ID))
conn.execute_query("UPDATE MS_SomRecords SET RecordDate=%s, RecordTime=%s WHERE RecordID=%s", (datetime.strptime(messagedate, '%d.%m.%Y'),datetime.strptime(messagetime, '%H:%M:%S'),Record_ID))
conn.execute_query("UPDATE MS_SomRecords SET SrpVintext=%s, SrpVin=%s, SrpVbat=%s, SrpStatus=%s, SrpCharge=%s, SrpResprw=%s, SrpHeater=%s WHERE RecordID=%s", (controller[u'srp'][u'srpvintext'], controller[u'srp'][u'srpvin'], controller[u'srp'][u'srpvbat'],controller[u'srp'][u'srpstatus'],controller[u'srp'][u'srpcharge'],controller[u'srp'][u'srpresprw'],controller[u'srp'][u'srpheater'], Record_ID))
# ---------- Write date if SOM
if object_is_som==1 and dbtable_flag==1:
macaddress=controller[u'som'][u'macaddress']
localid=controller[u'som'][u'localid']
recordtype=1
datepresent=0
timepresent=0
try:
messagedate=controller[u'som'][u'date']
#print "date=%s" % messagedate
datepresent=1
except:
pass
try:
messagetime=controller[u'som'][u'time']
#print "time=%s" % messagetime
timepresent=1
except:
pass
if (datepresent==1 and timepresent==1):
DeviceCategory=1 # SOM equipment category
Record_ID="%s-%s-%s-%s-%s-%s" % (messagedate,messagetime,macaddress,DeviceCategory,localid,recordtype)
Device_ID="%s-%s-%s-%s" % (macaddress,DeviceCategory,localid,recordtype)
print "Record_ID= %s Device_ID= %s writing to database" % (Record_ID,Device_ID)
w.write ( "Record_ID= %s " % Record_ID )
try:
conn.execute_query("INSERT INTO MS_SomRecords (RecordID) VALUES (%s)", (Record_ID))
except:
print ("record %s cannot be inserted" % (Record_ID))
conn.execute_query("UPDATE MS_SomRecords SET RecordMac=%s, DevLocalid=%s, RecordDevice=%s, RecordType=1 WHERE RecordID=%s", (macaddress, localid, Device_ID, Record_ID))
conn.execute_query("UPDATE MS_SomRecords SET RecordDate=%s, RecordTime=%s WHERE RecordID=%s", (datetime.strptime(messagedate, '%d.%m.%Y'),datetime.strptime(messagetime, '%H:%M:%S'),Record_ID))
try:
conn.execute_query("UPDATE MS_SomRecords SET VIn=%s, VBat=%s, Out1c=%s, Out2c=%s, Out1v=%s, Out2v=%s, Status=%s, Onbattery=%s, Out1state=%s, Out2state=%s, Out1overc=%s, Out2overc=%s, Out1disc=%s, Out2disc=%s, Lmode=%s WHERE RecordID=%s", (controller[u'som'][u'vin'],controller[u'som'][u'vbat'],controller[u'som'][u'out1c'],controller[u'som'][u'out2c'],controller[u'som'][u'out1v'],controller[u'som'][u'out2v'],controller[u'som'][u'status'],controller[u'som'][u'onbattery'],controller[u'som'][u'out1state'],controller[u'som'][u'out2state'],controller[u'som'][u'out1overc'],controller[u'som'][u'out2overc'],controller[u'som'][u'out1disc'],controller[u'som'][u'out2disc'],controller[u'som'][u'lmode'],Record_ID))
except:
print ("record %s cannot be updated" % (Record_ID))
conn.close()
Этот скрипт можно поставить на исполнение по расписанию, например, раз в полчаса:
5,35, * * * * somprocessor /opt/bin/rec_som_A1.py -d
В результате, наша база данных будет наполнена показаниями устройств за сегодня
Показания легко посмотреть в Microsoft SQL Server Managenent Studio. Для того, чтобы организовать рабочее место оператора, сделаем простой веб интерфейс к нашей базе данных
Веб интерфейс оператора
Пример простого веб интерфейса ниже сделан без применения какого-либо framework, на HTML, со вставками из PHP и jаvascript.Экран оператора разделяется на левую и основную части. Слева находится список пунктов меню для выбора того или иного типа устройств, а в центральной, основной части, в зависимости от выбранного пункта меню, выводится вся остальная информация. Для того, чтобы придать веб странице законченный внешний вид, сверху расположим шапку с логотипом компании и системы мониторинга, а в самом низу страницы сделаем “подвал” с обязательными ссылками на лицензируемые компоненты.
Основной код веб страницы поместим в три файла (PHP скрипта). В первом файле мы выдадим пользователю HTML-контент для всех областей экрана, кроме основной части. Основную часть мы отрисуем во втором скрипте, таких скриптов у нас потом может стать несколько. Третий файл - это файл стилей style.css, в нём мы можем оперативно менять свойства всех элементов
Ниже дан пример кода всех трёх файлов с подробными комментариями
Первый файл - index.php
vi /var/www/html/index.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Мониторинг СОМ</title>
<meta name="description" content="Web pages for Tower monitoring and other">
<meta name="author" content="EVODBG 2020">
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,200,300,600,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="assets/css/normalize.css">
Cсылка на дополнительный стиль библиотеки datatables, которую мы применим для отображения таблицы
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs/dt-1.10.16/datatables.min.css"/>
<link rel="stylesheet" href="assets/css/style.css">
ссылка на наш основной файл со стилями
<link rel="stylesheet" href="assets/css/style.css">
Скрипты для библиотеки datatables:
<script type="text/jаvascript" src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script>
<script type="text/jаvascript" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.flash.min.js"></script>
<script type="text/jаvascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script type="text/jаvascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/jаvascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/jаvascript" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.html5.min.js"></script>
<!-- <script type="text/jаvascript" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.print.min.js"></script> -->
<script type="text/jаvascript">
$(document).ready(function() {
var dtdefaults = {
"language": {"url": "//cdn.datatables.net/plug-ins/1.10.16/i18n/Russian.json"},
"stateSave": true,
dom: 'lfrtipB',
"lengthMenu": [ 50, 100, 150, 200 ],
buttons: [
'copy', 'csv', 'excel', 'pdf'
]
}
$('#towerdb').dataTable(dtdefaults);
} );
</script>
Добавляем скрипт для автоматического обновления экрана
<script>
function fresh() {
location.reload();
}
setInterval("fresh()",600000);
</script>
</head>
Печатаем заголовок веб страницы
<body>
<!--Заголовок-->
<header class="header-header">
<div class="monitoring-head">
<a href="/index.php">Система мониторинга СОМ</a>
</div>
<div class="monitoring-logo">
<a href="https://portal..ru/Pages/main.aspx"><img src="assets/img/logoV.png" alt="Логотип компании"></a>
</div>
</header>
Основное содержимое экрана
<!-- *********** Основное содержимое *********** -->
<!--Вертикальное меню слева-->
<main class="main-main">
<aside class="main-leftmenu">
<?php
$uri_tail=explode("?", $_SERVER['REQUEST_URI']);
switch ($uri_tail[1]) {
default:
break;
$class10="";
case 'som':
$class100="menu-active";
break;
}
?>
<h2>Управление</h2>
<ul>
<li><a class="<?php echo $class100;?>" href="index.php?som">СОМ</a></li>
</ul>
</aside>
<!--Основной контент в центре экрана-->
<section class="main-content">
<?php
switch ($uri_tail[1]) {
default:
break;
case 'som':
include 'read_som.php';
break;
}
?>
</section>
</main>
Печатаем “подвал”
<!--Футер-->
<footer class="footer-footer">
<p>Вертикаль 2016-2018</p>
<div class="copyright-reference">Icons made by
<a href="http://www.freepik.com" title="Freepik">Freepik</a>
from
<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
is licensed by
<a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a>
</div>
</footer>
</body>
</html>
Второй файл - read_som.php, для печати центральной части экрана
vi /var/www/html/read_som.php
<div class="main-content-header">
<h1>SRP и СОМ</h1>
</div>
<form action="index.php?som" method="post">
<div class="date-search">
<label for="date-search-field">Выберите дату (по умолчанию выбрана сегодняшняя дата):</label>
<?php
$MaxDate=htmlspecialchars($_POST['MaxDate']);
if (!$MaxDate) {
date_default_timezone_set("Europe/Moscow");
$timenow=time();
$MaxDate=date("Y-m-d", $timenow);
}
print "<input type='date' id='date-search-field' name='MaxDate' value=$MaxDate >";
?>
<button type="submit" class="fridge-search-button">Показать данные за эту дату</button>
</div>
</form>
<p>Выберите запись:</p>
<table id="electric" class="main-elec-table">
<thead>
<tr>
<th>Дата</th>
<th>Время</th>
<th>Мачта</th>
<th>Адрес</th>
<th>Mac address</th>
<th>№ СОМа</th>
<th>Dev type</th>
<th>SRP Vintext</th>
<th>SRP Vin</th>
<th>SRP Vbat</th>
<th>On batt</th>
<th>L1 state</th>
<th>L2 state</th>
<th>L1 overc</th>
<th>L2 overc</th>
<th>L1 disc</th>
<th>L2 disc</th>
<th>Lmode</th>
<th>VIn</th>
<th>VBat</th>
<th>L1 v</th>
<th>L2 v</th>
<th>L1 c</th>
<th>L2 c</th>
<th>SRP charge</th>
<th>Stat</th>
</tr>
</thead>
<tbody>
<?php
include '../connect.php';
$connection = mssql_connect ($server, $user, $password);
if (!$connection) {
print "Connection Failed";
}
else {
mssql_select_db($database, $connection);
$all_records = mssql_query("SELECT [RecordDate],[RecordTime],[RecordMac],[DevLocalid],[DeviceType],[SrpVintext],[SrpVin],[SrpVbat],[Onbattery],[Out1state],[Out2state],[Out1overc],[Out2overc],[Out1disc],[Out2disc],[Lmode],[VIn],[VBat],[Out1v],[Out2v],[Out1c],[Out2c],[SrpCharge],[Status],[DeviceTower],[TowerAddress] FROM MS.dbo.MS_SomRecords R LEFT JOIN MS_Devices D ON R.RecordDevice=D.DeviceID LEFT JOIN MS.dbo.MS_Towers P ON D.DeviceTower=P.TowerID WHERE RecordDate='$MaxDate' ORDER BY RecordTime DESC;");
while ($row = mssql_fetch_array($all_records)) {
$rows[]= $row;
$RecordDate=$row[0];
$RecordTime=substr($row[1],0,5);
$RecordMac=$row[2];
$DevLocalid=$row[3];
if ($row[4]==0) { $DeviceType="SRP"; }
if ($row[4]==1) { $DeviceType="SOM"; }
$SrpVintext=$row[5];
$SrpVin=$row[6];
$SrpVbat=$row[7];
$Onbattery=$row[8];
$Out1state=$row[9];
$Out2state=$row[10];
$Out1overc=$row[11];
$Out2overc=$row[12];
$Out1disc=$row[13];
$Out2disc=$row[14];
$Lmode=$row[15];
$VIn=$row[16];
$VBat=$row[17];
$Out1v=$row[18];
$Out2v=$row[19];
$Out1c=$row[20];
$Out2c=$row[21];
$SrpCharge=$row[22];
$Status=$row[23];
$DeviceTower=$row[24];
$TowerAddress=$row[25];
print "<tr>";
printf("<td>%s</td><td>%s</td><td><a class='TowerID' href='jаvascript:void(0);'>%s</a></td><td><div class=\"Tower-address-block\">%s</div></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td>", $RecordDate, $RecordTime, $DeviceTower, $TowerAddress,$RecordMac,$DevLocalid,$DeviceType,$SrpVintext, $SrpVin, $SrpVbat, $Onbattery,$Out1state,$Out2state,$Out1overc,$Out2overc,$Out1disc,$Out2disc,$Lmode,$VIn,$VBat,$Out1v,$Out2v,$Out1c,$Out2c,$SrpCharge,$Status);
print "</tr>";
}
mssql_close($connection);
}
?>
</tbody>
</table>
<script src="assets/js/read_db.js"></script>
<script src="assets/js/cufon/cufon-yui.js"></script>
<script src="assets/js/cufon/LED_500.font.js"></script>
<script type="text/jаvascript">
Cufon.replace(".timenow");
</script>
Третий файл- style.css. Стиль веб страницы выбирается, исходя из предпочтений разработчика, для конкретной компании. Здесь мы выбрали неяркий спокойный серый фон, на котором изображены светящиеся индикаторы состояния оборудования. Такое решение хорошо смотрится на большом экране, позволяя уменьшить общую яркость изображения и сосредоточиться на важных для пользователя его деталях. В данной конкретной веб странице из индикаторов применяется семисегментный индикатор текущего времени суток.
vi /var/www/html/assets/css/style.css
@font-face {
font-family: 'Proxima Nova';
font-style: normal;
font-weight: 400;
src: url('../fonts/Proxima Nova Regular.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova Bold';
font-style: normal;
font-weight: 700;
src: url('../fonts/Proxima Nova Bold.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova Semibold';
font-style: normal;
font-weight: 500;
src: url('../fonts/Proxima Nova Semibold.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova Extra Condensed';
font-style: normal;
font-weight: 500;
src: url('../fonts/Proxima Nova Extra Condensed Regular.woff') format('woff');
}
/* ========================================================================== General ========================================================================== */
body {
color: #737a84;
background-color: #1b2431;
/*font: 400 1.6rem/1.4 'source sans pro', 'helvetica neue', helvetica, arial; */
font-family: 'Proxima Nova', 'Arial', sans-serif;
padding: 0;
}
a {
color: #737a84;
}
a:hover {
color: #1b8aba;
}
p {
line-height: 1.3;
}
.main-content strong, .main-content b {
font-family: 'Proxima Nova Bold';
}
h1 {
font-family: 'Proxima Nova Semibold';
font-size: 36px;
color: #ccc;
font-weight: 400;
}
h2 {
font-family: 'Proxima Nova Semibold';
font-size: 24px;
color: #ccc;
font-weight: 400;
margin: 10px 10px;
}
h3 {
font-family: 'Proxima Nova Semibold';
font-size: 20px;
color: #ccc;
font-weight: 400;
margin-bottom: 10px;
}
h4 {
font-family: 'Proxima Nova Semibold';
font-size: 16px;
color: #ccc;
font-weight: 400;
margin-bottom: 10px;
}
.header-header {
display:flex;
justify-content: stretch;
height: 60px;
background-color: rgb(34, 44, 60);
border: 1px solid #313d4f; border-radius: 5px;
}
.vertical-head {
color: #ccc; background-color: rgb(34, 44, 60);
margin-left: 10px;
margin-top:10px;
margin-bottom:10px;
margin-right:auto;
font-size: 36px; line-height: 36px;
}
.vertical-head a {
/* font-family: 'Proxima Nova Extra Condensed'; */
text-decoration: none;
}
.vertical-head a:hover {
color: #fff; opacity: 1; -webkit-transition: opacity 0.5s ease;
}
.vertical-logo {
color: #fff; opacity: 0.4; background-color: rgb(34, 44, 60);
margin-top:14px;
margin-right:10px;
margin-bottom:10px;
}
.vertical-logo img {
vertical-align: bottom;
}
.vertical-logo:hover {
color: #fff; opacity: 1; -webkit-transition: opacity 0.5s ease;
}
/* .main { padding: 10px; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-flex-wrap: wrap; -ms-flex-wrap: wrap; flex-wrap: wrap; } */
/* .main { padding: 10px; display: flex; flex-wrap: wrap; } */
.main-main {
display: flex;
align-items:stretch;
}
.main-leftmenu {
display: flex;
flex-direction:column;
flex-shrink: 0;
color: #737a84;
background-color: rgb(34, 44, 60);
margin: 10px;
width: 250px;
line-height: 23px;
padding: 20px;
border: 1px solid #313d4f;
border-radius: 5px;
}
.main-leftmenu:hover {
border-color: #1b92ed;
-webkit-transition: border-color 0.5s ease;
}
.main-leftmenu a {
position: relative;
color: inherit;
text-decoration: none;
}
.main-leftmenu a:hover {
/*color: #1b92ed;*/
color: #ddd;
cursor: pointer;
text-shadow:none;
text-decoration: underline;
-webkit-transition: color 0.5s ease;
}
.main-leftmenu ul {
list-style: none;
padding-left: 30px;
}
.main-leftmenu li {
font-size: 16px;
}
.main-leftmenu li.active {
font-weight: 500;
color: #fff;
}
.main-leftmenu .menu-active {
/*color: #c37a84;*/
/*color: #a60000;*/
color:rgb(247, 152, 28);
text-shadow: 0 0 1px rgb(247, 152, 28);
}
.menu-monitoringdb::before, .menu-alarmtowers::before, .menu-maps::before, .menu-electric::before, .menu-skud::before, .menu-ts-single::before, .menu-ts-all::before, .menu-pies::before, .menu-timegraph::before {
position:absolute;
content: "";
background-size: contain;
opacity: 0.5;
width: 16px;
height: 16px;
left: -27px;
top: 1px;
}
.menu-monitoringdb:hover::before, .menu-alarmtowers:hover::before, .menu-maps:hover::before, .menu-electric:hover::before, .menu-skud:hover::before, .menu-ts-single:hover::before, .menu-ts-all:hover::before, .menu-pies:hover::before, .menu-timegraph:hover::before {
opacity: 1;
-webkit-transition: opacity 0.5s ease;
}
.menu-monitoringdb::before {
background-image: url("../img/home.svg");
}
.menu-alarmtowers::before {
background-image: url("../img/danger.svg");
}
.menu-maps::before {
background-image: url("../img/places-on-maps.svg");
}
.menu-electric::before {
background-image: url("../img/electric-plug.svg");
}
.menu-skud::before {
background-image: url("../img/key.svg");
}
.menu-ts-single::before {
}
.menu-ts-all::before {
background-image: url("../img/helmet.svg");
}
.menu-pies::before {
background-image: url("../img/line-chart.svg");
}
.menu-timegraph::before {
}
/* главный экран */
.main-content {
font-size: 16px;
padding: 15px;
margin-top: 10px;
margin-bottom:10px;
margin-right:10px;
background-color: rgb(34, 44, 60);
border: 1px solid #313d4f;
border-radius: 5px;
}
.main-content:hover { border-color: #1b92ed; -webkit-transition: border-color 0.5s ease; }
.main-content h2:first-child { margin-top: 0; }
.main-content p { line-height: 1.5; }
.main-content ul { padding-left: 30px; }
.main-content strong, .docs-content b { color: #222; font-weight: bold; }
.main-content li { margin-bottom: 5px; }
.main-content table {
border-collapse: separate;
color: #737a84; background-color: #273142;
font-size: 14px;
margin-bottom: 10px;
border: 1px solid #313d4f; border-radius: 3px;
}
.main-content tr.even { background-color: rgb(42, 55, 75); }
.main-content th {
color: #ddd;
border: solid 1px #313d4f;
padding: 10px;
text-align: left;
font-weight: normal;
}
.main-content th:hover {
color: #fff;
border-color: #1b92ed;
-webkit-transition: border-color 0.5s ease;
}
.main-content td {
padding:1px;
border: solid 1px #313d4f;
border-radius: 3px;
}
.main-content td:hover {
color: #ccc;
border-color: #1b92ed;
-webkit-transition: border-color 0.5s ease;
}
.tower-address-block, .event-address-block, .main-towerstore-table td:nth-child(10) {
display: flex;
width: 400px;
min-height: 45px;
text-align:left;
justify-content: left;
align-items: center;
}
.event-address-block {
min-height: 35px;
}
.tower-address-block a, .event-address-block a {
text-decoration: none;
}
.tower-address-block a:hover, .event-address-block a:hover {
color: #ddd;
text-decoration: underline;
}
#towerdb_filter, #alarms_filter, #onoff_filter, #electric_filter, #avpower_filter {
text-align: left;
}
/*.main-content table {
font-family: 'Proxima Nova Extra Condensed';
}*/
.main-tower-table td:not(:first-child):not(:nth-child(8)) {
text-align:center;
}
.events-table td:not(:nth-child(3)) {
text-align:center;
}
.main-elec-table td:not(:first-child):not(:nth-child(3)):not(:nth-child(4)) {
text-align:center;
}
.main-alarm-table td:not(:nth-child(2)):not(:nth-child(9)) {
text-align:center;
}
.tower-status {
display: flex;
align-items: center;
text-align: center;
justify-content: center;
border-radius: 3px;
margin-left: 20px;
padding-top: 5px;
padding-bottom: 5px;
font-family: 'Proxima Nova Bold';
font-size: 12px;
text-transform: uppercase;
color: #eee;
margin: 0 5px 0 5px;
}
.ts-normal, .htled-normal {
background-color: rgb(54, 175, 71);
box-shadow: 0 0 8px rgb(54, 175, 71);
}
.ts-alarm, .htled-alarm {
color: #eb6077;
background-color:#842333;
box-shadow: 0 0 8px #842333;
}
.ts-faulty, .htled-faulty {
background-color: rgba(246, 122, 0, 0.9);
box-shadow: 0 0 8px rgba(246, 122, 0, 0.9);
}
.ts-minor, .htled-minor {
color: #bdbdbd;
background-color: rgba(156, 113, 65, 0.6);
box-shadow: 0 0 8px rgba(156, 113, 65, 0.6)
}
.ts-warning, .htled-warming {
background-color: rgba(0, 96, 164, 0.87);
box-shadow: 0 0 8px rgba(0, 96, 164, 0.87);
color: #ccc;
}
.ts-blocked, .htled-blocked {
background-color: rgb(25, 145, 235);
box-shadow: 0 0 8px rgb(25, 145, 235);
}
.ts-dismantled, .htled-dismantled {
background-color: rgb(87, 91, 99);
box-shadow: 0 0 8px rgb(87, 91, 99);
}
.events-towerstatus-field {
display: flex;
width: 280px;
justify-content: space-between;
}
.events-towerstatus-indicator {
width:100px;
}
.status-comment {
width: 85px;
}
.footer-footer {
text-align:center;
margin-bottom: 30px;
}
.copyright-reference {
font-size: 12px;
}
/*********************************************************************************/
/* ************************** стили для dataTables ***************************** */
/*********************************************************************************/
.dataTables_length select, .dataTables_filter input, .fridge-search input, .date-search input, .counter-search input, .edit input, .gateway-output {
margin: 5px;
color: #ccc; background-color: #273142;
border: 1px solid rgb(49, 61, 79);
border-radius: 3px;
}
#counter-search-field {
-moz-appearance: textfield;
width: 120px;
}
.dataTables_filter label, .fridge-search label {
/*color:#ff6800;*/
color:rgb(247, 152, 28);
}
.dataTables_length select:hover, .dataTables_filter input:hover {
color: #fff;
border-color: #1b92ed;
cursor: pointer;
-webkit-transition: border-color 0.5s ease;
}
.paginate_button, .dt-button {
color:#737a84; background-color: #273142;
border: 1px solid #313d4f; border-radius: 5px;
margin: 2px;
padding: 5px;
cursor: pointer; -webkit-transition: color 0.5s ease;
}
.paginate_button:hover, .fridge-search-button:hover, .dt-button:hover {
color: #fff;
border-color: #1b92ed;
cursor: pointer;
-webkit-transition: border-color 0.5s ease;
}
.alarm-young td:nth-child(9) {
/*color: #A62F00;*/
color:rgb(0,204,0);
/*color: #ff7373;*/
/*opacity: 0.8;*/
text-align: center;
}
.alarm-older td:nth-child(9) {
color: rgb(247,152,28);
opacity: 0.8;
text-align: center;
}
.alarm-old td:nth-child(9){
opacity: 0.7;
color: #bf5930;
position:relative;
text-align: center;
animation:myfirst 2s;
animation-iteration-count: 30;
-moz-animation:myfirst 2s; /* Firefox */
-moz-animation-iteration-count: 120;
-webkit-animation:myfirst 2s; /* Safari и Chrome */
-webkit-animation-iteration-count: 120;
}
.alarm-old td:nth-child(9)::before {
display: inline-block;
transform: translateX(-6px) translateY(2px);
content: "";
background-image: url("../img/danger.svg");
background-size: contain;
opacity: 0.5;
width: 16px;
height: 16px;
right: 37px;
top: 15px;
}
@keyframes myfirst
{
0%, 50%, 100% {
opacity: 1;
}
25%, 75% {
opacity: 0;
}
}
@-moz-keyframes myfirst /* Firefox */
{
0%, 50%, 100% {
opacity: 1;
}
25%, 75% {
opacity: 0;
}
}
@-webkit-keyframes myfirst /* Safari и Chrome */
{
0%, 50%, 100% {
opacity: 1;
}
25%, 75% {
opacity: 0;
}
}
/*--------------- Storing tower ID into localstorage -----------*/
.towerID, .Tlink {
text-decoration: none;
cursor: pointer;
/*text-shadow: 0 0 0 #000;*/
}
.towerID:hover, .Tlink:hover {
text-decoration: underline;
color: #fff;
}
/*.towerID:focus {
text-decoration: none;
animation-name: towerID-storing;
animation-duration: 1s;
animation-fill-mode: forwards;
}
@keyframes towerID-storing {
from {
text-shadow: 0 0 0 #888;
}
75% {
text-shadow: -200px 100px 0 #aaa;
}
to {
text-shadow: -200px 100px 0 transparent;
}
}*/
.towerID:focus::before {
/*content: attr(data-copy);*/
content: "Опора скопирована в буфер";
color: transparent;
position: absolute;
display: block;
animation-name: towerID-move;
animation-duration: 1s;
animation-timing-function: ease;
animation-fill-mode: forwards;
}
@keyframes towerID-move {
75% {
transform: translate(0px, -100px) scale(1.7);
color: rgba(247, 152, 28, 1);
text-shadow: 0 0 10px #aaa;
}
to {
color: rgba(247, 152, 28, 0);
}
}
/*------------------------- Яндекс-карты ----------------------*/
#map {
width: 1200px;
height: 800px;
border: 8px solid #ccc;
border-radius: 10px;
animation-name: map-appear;
animation-duration: 2s;
animation-fill-mode: forwards;
}
@keyframes map-appear {
from {
opacity: 0;
border-color: transparent;
}
to {
opacity:0.9;
border-color: #ccc;
}
}
.balloon {
font-family: 'Proxima Nova', 'Arial', sans-serif;
}
.balloon h2, .balloon-address {
position:relative;
color: #a64b00;
}
.balloon h2 {
margin-left: 40px;
}
.balloon h2::after {
position:absolute;
content: "";
background-image: url("../img/signal-tower-symbol.svg");
background-size: contain;
opacity: 0.5;
width: 20px;
height: 20px;
left: -30px;
top: 1px;
z-index: -100;
}
/*------------------------- Google-карты ----------------------*/
.infowindow {
background-color: rgb(255, 254, 239);
border-radius: 10px;
}
.infowindow h3 {
color: black;
}
.infowindow p {
padding: 0; margin: 0;
}
.infowindow span {
color: rgb(192, 99, 84);
}
/* замена стандартных чекбоксов и радиокнопок на укрупнённые*/
/**************************************************************************/
.input-checkbox + label {
position: relative;
margin-left: 34px;
}
.input-checkbox + label::before {
content: "";
position: absolute;
top: 0px;
left: -33px;
width: 17px;
height: 17px;
border: 2px solid #737a84;
border-radius: 2px;
}
.input-checkbox + label:hover::before, .input-checkbox + label:focus::before {
border-color: #ccc;
}
.input-checkbox:checked + label::after {
content: "";
position: absolute;
width: 8px;
height: 15px;
top: -3px;
left: -24px;
background-color: transparent;
border-bottom: 3px solid #737a84;
border-right: 3px solid #737a84;
-webkit-box-shadow: inset -1px 0px 0 0 rgb(34,44,60), 1px 0px 0 0 rgb(34,44,60);
box-shadow: inset -1px 0px 0 0 rgb(34,44,60), 1px 0px 0 0 rgb(34,44,60);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.input-checkbox:checked + label:hover::after, .input-checkbox:checked + label:focus::after {
border-bottom-color: #ccc;
border-right-color: #ccc;
}
/* *************** disabled ****************/
.input-checkbox:disabled + .label, .input-radio:disabled + .label {
color: #555;
}
.input-checkbox:disabled + .label::before {
border-color: #555;
}
.input-checkbox:disabled + .label::after, .input-checkbox:disabled + .label:hover::after, .input-checkbox:disabled + .label:focus::after {
border-bottom-color: #555;
border-right-color: #555;
}
.input-checkbox {
display: none;
}
/*************************** то же, radio *********************************/
.filter-input-radio + label::before {
content: "";
position: absolute;
top: 7px;
left: -34px;
width: 19px;
height: 19px;
border: 2px solid #737a84;
border-radius: 50%;
cursor: pointer;
}
.filter-input-radio + label:hover::before, .filter-input-radio + label:focus::before {
border-color: #ccc;
}
.filter-input-radio:checked + label::after {
content: "";
position: absolute;
top: 14px;
left: -27px;
width: 9px;
height: 9px;
background-color: #737a84;
border-radius: 50%;
}
.filter-input-radio:checked + label:hover::after, .filter-input-radio:checked + label:focus::after {
background-color: #ccc;
}
/* *************** disabled ****************/
.filter-input-radio:disabled + .filter-label::before {
border-color: #ddd;
}
.filter-input-radio:disabled + .filter-label::after, .filter-input-radio:disabled + .filter-label:hover::after, .filter-input-radio:disabled + .filter-label:focus::after {
background-color: #ddd;
}
.filter-input-radio {
display: none;
}
/************************** для отчетов ****************************/
.report-body {
display: flex;
}
.report-pie {
}
.report-body div {
margin: -5px;
}
.timenow {
color: rgb(54,175,71);
/*color: rgb(247,152,28);*/
font-size: 24px;
width: 220px;
background-color: rgb(29, 62, 66);
border: 1px solid #6a6a6a;
text-align: center;
width: 195px;
border-radius: 3px;
box-shadow: inset 0 0 10px 0 rgb(29,62,66);
}
.timenow span {
animation:myfirst 2s;
animation-iteration-count: 12;
}
/*********************************************************************************/
Наша первая веб страница готова. На ней можно просматривать в виде таблице собранные контроллерами данные о напряжениях и токах на объектах. Для поиска неисправностей на объектах мы можем отсортировать таблицу по какой-либо колонке, и получить в начале списка объекты с минимальным или максимальным значением параметра. Например, мы знаем, что в исправном состоянии ярус огней светоограждения, состоящий из трёх светильников, потребляет ток 600 мА. Отсортируем таблицу по столбцу значений тока первого яруса по возрастанию. Если у нас в начале таблицы появятся значения тока 400 мА или ниже, это означает, что одна из ламп перегорела. По колонкам “Дата” и “Время” определяем, когда это произошло, а по “Мачта” и “Адрес” определяем, куда ехать выполнять ремонт
Отправка сообщений об аварии
Находясь в дороге, мы не всегда имеем возможность иметь перед собой открытый экран системы мониторинга. Для подобных случаев подходят разного рода оповещения об аварии, которые мы здесь сейчас настроим.Обычно, для определения аварийных ситуаций, делается скрипт, запускаемый регулярно по расписанию (например, раз в 5 минут), и ищущий базе данных параметр, который выходит за допустимые пределы. Например, в случае, описанном выше, такой скрипт ищет в последних присланных с контроллера данных значение тока первого яруса <= 400 мА, и инициирует отправку сообщения оператору, если такое значение найдено
Однако, надо учесть, что такое оповещение должно приходить оператору по каждому такому случаю однократно, поэтому в обработчике состояния объекта нужно предусмотреть флаг типа message_has_been_sent. Если наблюдаемый параметр вышел за допустимые пределы, сообщение отсылается, и флаг message_has_been_sent для данного объекта активируется. При активном флаге message_has_been_sent дальнейшие сообщения не отправляются, пока оператор не отреагирует на аварию, и по завершении своих действий не снимет этот флаг вручную
Другой способ добиться однократной отправки сообщения по каждому случаю аварии можно использовать, если до этого в системе был предусмотрен флаг перехода объекта в новое состояние. Такой флаг автоматически сбрасывается обрабатывающим это новое состояние скриптом. Именно такого типа скрипт мы сделали с вами раньше, когда писали скрипт для дедупликации аварий на мачте. Добавим в него отсылку аварийных сообщений. Добавленные строки выделены курсивом
vi /opt/bin/alarm_joiner.py
conn = _mssql.connect(server=server,user=user,password=password,database=database)
conn2 = _mssql.connect(server=server,user=user,password=password,database=database)
query='''SELECT TowerID, TowerStatus, m.ModemID, m.ModemStatus
FROM MS_Towers p
LEFT JOIN MS_Modems m ON p.TowerID=m.ModemTower AND m.ModemLevel=0
WHERE TowerUpdateflag=1'''
conn.execute_query(query)
# ++++++++ Updating Towers ++++++++++++++++++++++++++++++++++++
for row in conn:
TowerID=row[0].strip()
ModemID=row[2]
ModemStatus=row[3]
insert_event=0
if not ModemID is None:
# ++++++++ modem exists +++++++++++++++++++++++++++++++++++++
if ModemStatus==1002: # initial status, no signal
TowerStatus=4 # warning
if ModemStatus==1001: # unavailable
TowerStatus=2 # major alarm
if ModemStatus==1000: # available
TowerStatus=0 # OK
print (TowerID, TowerStatus)
if not TowerID=='undefined':
# ++++++++ Updating Tower status, clearing Tower update flag
conn2.execute_query("UPDATE MS_Towers SET TowerStatus=%d, TowerUpdateflag=0 WHERE TowerID=%s", (TowerStatus, TowerID))
if TowerStatus==1:
if TowerID=='10101' or TowerID=='10112' or TowerID=='10120':
p=subprocess.Popen("/opt/bin/mail_alarm.py %s %s" % (TowerID, TowerStatus), shell=True, stdout=subprocess.PIPE)
conn.close()
conn2.close()
Предположим, что мачты № 10101, 10112 и 10120 находятся в непосредственной близости от вертолётной площадки больницы, и поэтому имеют наивысший приоритет при устранении аварии. Тогда по любому факту потери связи с этими контроллерами будет отсылаться письмо, это делается с помощью скрипта /opt/bin/mail_alarm.py
vi /opt/bin/mail_alarm.py
cat /opt/bin/tower_alarm.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import smtplib
#From/to/subject/body:
fromaddr = 'Notification <notification@evodbg.com>'
toaddr = ['operation@evodbg.com', 'engineer@evodbg.com']
TowerID=sys.argv[1]
TowerStatus=sys.argv[2]
subj = "Авария на мачте %s" % (TowerID)
msg_txt = "Добрый день!\nCистема мониторинга обнаружила аварию на позиции с высоким приоритетом %s\n\nПожалуйста, выполните необходимые действия!\n\nС уважением\nМониторинг Evodbg\n" % (TowerID)
#Login/password
username = 'notification@evodbg.com'
password = '<<key>>'
#Create mail
msg = "From: %s\nTo: %s\nSubject: %s\n\n%s" % ( fromaddr, toaddr, subj, msg_txt)
print msg
#Smtp connection initiated
server = smtplib.SMTP('smtp.evodbg.com:587')
#Log for debug
#server.set_debuglevel(1);
server.starttls()
server.login(username,password)
server.sendmail(fromaddr, toaddr, msg)
server.quit()
Использовать электронную почту для рассылки сообщений обо всех авариях не рекомендуется, так как она обладает одним существенным недостатком - каждое письмо требуется открыть, прочитать и закрыть. Если эти действие со стороны оператора становятся постоянным и рутинными, человек устаёт, и, в результате, многие письма оказываются не прочитанными. Этого можно избежать, если вместо почты использовать мессенджер, и отсылать сообщения в группу реагирования.
Сделаем отсылку сообщения в группу Телеграмм известным нам ранее способом:
curl -s -X POST https://api.telegram.org/bot<ID бота>:<ключ бота>/sendMessage -d chat_id=<ID чата> -d text="<данные>"
Поиск причины аварии. Отображение данных в графическом виде
В нашей системе мониторинга данные можно условно разделить на два вида: исторические данные (например, значения напряжения и силы тока, хранящиеся в базе данных) и данные и события реального времени (например, факт пропадания связи и наступления аварии). Владение обоими типами данных даёт нам возможность проанализировать, что происходило непосредственно перед аварией, и, возможно, найти её причину.Например, пропала связь с контроллером. Просмотрев данные о напряжении батареи, мы видим, что источник питания перед этим несколько раз переходил на батарею и обратно, и видим высокие значения тока каждый раз при начале зарядки батареи. Можно предположить, что в какой-то момент ток вызвал защитное отключение автомата, что обесточило всю установку вместе с контроллером
Нахождение подобных сценариев поведение системы, предшествовавшего аварии, помогает упростить её обслуживание, поскольку становятся понятными типовые причины отказов. Можно попробовать даже предугадывать аварии в будущем, и научить систему обнаруживать развитие аварийной ситуации. Чем больше накоплено статистических данных, тем раньше и тем надёжнее можно обнаружить подобный сценарий.
Развитие поведения системы во времени удобно изучать в виде графиков, выведенных на экран. Пример такого графика показан на рисунке:

Сделаем скрипт для вывода графика на экран. В качестве библиотеки для отрисовки графиков будем использовать jqwidgets.
vi /var/www/html/read_temperature_graph.php
<div class="main-content-header">
<h1>Графики температуры и влажности</h1>
</div>
<form action="index.php?temperaturegraph" method="post">
<div class="date-search">
<label for="date-search-field">Выберите дату:</label>
<?php
$MaxDate=htmlspecialchars($_POST['MaxDate']);
$thermometer=htmlspecialchars($_POST['thermometer']);
if (!$MaxDate) {
date_default_timezone_set("Europe/Moscow");
$timenow=time()-24*3600;
$MaxDate=date("Y-m-d", $timenow);
}
print "<input type='date' id='date-search-field' name='MaxDate' value=$MaxDate >";
?>
<button type="submit" class="thermometer-search-button">Показать данные</button>
</div>
<div class="thermometer-search">
<label for="thermometer-search-field">Выберите термометр (напр. 4266268 ):</label>
<?php
print "<input type='text' id='thermometer-search-field' name='thermometer' value=$thermometer >";
?>
</div>
<p></p>
</form>
<?php
$connection = mssql_connect ($server, $user, $password);
if (!$connection) {
print "Connection Failed";
}
else {
mssql_select_db($database, $connection);
$all_records = mssql_query("SELECT TOP (1) ModemTower,TowerAddress FROM MS_ThermometerRecords R LEFT JOIN Msys_Modems M ON R.RecordMac=M.Modemthermometer LEFT JOIN Msys_Towers T ON M.ModemTower=T.TowerID WHERE RecordThermometer='$thermometer'");
while ($row = mssql_fetch_array($all_records)) {
$thermometerdescr=sprintf("Термометр %s, мачта %s, %s", $thermometer, $row[0], $row[1]);
$thermometerdescr2=sprintf("Датчик %s, мачта %s, %s", $thermometer, $row[0], $row[1]);
}
$all_records = mssql_query("SELECT TOP (30) [RecordDate],[RecordTime], [Temperature_0],[Temperature_1],[TemperatureBoard],[HumidityBoard],[RecordRowkey] FROM MS_ThermometerRecords WHERE RecordThermometer = '$thermometer' AND RecordDate < '$MaxDate' ORDER BY RecordDate DESC, RecordTime DESC;");
$chart_data='';
while ($row = mssql_fetch_array($all_records)) {
$RecordDateTime=substr($row[0] . ' ' .$row[1],0,19);
$datetime= date_create_from_format('Y-m-d H:i:s', $RecordDateTime);
$show_humidity=1;
if (is_null($row[5])) {$show_humidity=0;}
for ($i = 2; $i < 6; $i++) {
if (is_null($row[$i])) {$row[$i]=0;}
}
$date= date_format($datetime,'d M');
$time= date_format($datetime,'H:i');
$chart_data= sprintf("{Time:'%s<br>%s', T1:%s, T2:%s, T3:%s, H:%s},", $time, $date, $row[2], $row[3], $row[4], $row[5]) . $chart_data;
}
}
print "<div id='chartContainer' style=\"width:1600px; height: 400px\"></div>";
if ($show_humidity==1) {
print "<div id='chartContainer2' style=\"width:1600px; height: 400px\"></div>";
}
?>
<link rel="stylesheet" href="../jqwidgets/styles/jqx.base.css" type="text/css" />
<script type="text/jаvascript" src="../scripts/jquery-1.11.1.min.js"></script>
<script type="text/jаvascript" src="../jqwidgets/jqxcore.js"></script>
<script type="text/jаvascript" src="../jqwidgets/jqxchart.core.js"></script>
<script type="text/jаvascript" src="../jqwidgets/jqxdraw.js"></script>
<script type="text/jаvascript" src="../jqwidgets/jqxdata.js"></script>
<script type="text/jаvascript">
$(document).ready(function () {
// chart data
var chartData = [
<?php print $chart_data; ?>
];
// prepare jqxChart settings
var settings = {
title: "Температура, C, по всем датчикам",
description: "<?php print $thermometerdescr; ?>",
padding: { left: 5, top: 5, right: 45, bottom: 5 },
titlePadding: { left: 90, top: 0, right: 0, bottom: 10 },
source: chartData,
categoryAxis:
{
dataField: 'Time',
showGridLines: false
},
colorScheme: 'scheme01',
backgroundColor: "#969098",
seriesGroups:
[
{
type: 'line',
valueAxis:
{
description: 'Температура, C'
},
series: [
{ dataField: 'T1', displayText: 'Датчик 1', color: '#FFc500'},
{ dataField: 'T2', displayText: 'Датчик 2', color: '#006400'},
{ dataField: 'T3', displayText: 'Датчик 3', color: '#9b0000'}
]
}
]
};
var settings2 = {
title: "Влажность, %",
description: "<?php print $thermometerdescr2; ?>",
padding: { left: 5, top: 5, right: 45, bottom: 5 },
titlePadding: { left: 90, top: 0, right: 0, bottom: 10 },
source: chartData,
categoryAxis:
{
dataField: 'Time',
showGridLines: false
},
colorScheme: 'scheme01',
backgroundColor: "#969098",
seriesGroups:
[
{
type: 'line',
valueAxis:
{
description: 'Влажность, %'
},
series: [
{ dataField: 'H', displayText: 'Влажность'}
]
}
]
};
// select the chartContainer DIV element and render the chart.
$('#chartContainer').jqxChart(settings);
$('#chartContainer2').jqxChart(settings2);
});
</script>
С помощью скрипта, показанного выше, на веб странице показывается поле выбора даты показаний и идентификатора термометра. Для построения графика нужно нажать кнопку “показать данные”, и выполняется чтение выборки данных из базы данных за указанную дату.
Отображение картографической информации
Для анализа текущего статуса сети устройств мониторинга и обслуживаемых или систем, бывает полезно увидеть их расположение на карте. Если в зависимости от статуса устройства обозначить метку на карте тем или иным цветом, то становится возможно наблюдать преобладание меток одного и того же цвета в той или иной области, и сделать выводы о причинах этого явления. Пример карты с разноцветными метками показан на рисунке ниже.
Перед использованием карт того или иного поставщика, нужно обратить внимание на условия их использования, и внимательно прочитать лицензионное соглашение.
Ниже приведён пример скрипта для отрисовки меток на карте Google.
cat /var/www/html/all-map-google.php
<div class="main-content-header">
<h1>Карта модемов</h1>
</div>
<?php
$jsonfilename="data.json";
$connection = mssql_connect ($server, $user, $password);
if (!$connection) {
print "Connection Failed";
}
else {
date_default_timezone_set("Europe/Moscow");
$timenow=time();
printf("<p class=\"timenow\">%s</p>", date("d.m.Y H:i:s", $timenow));
$data_print = <<<_PRINT
<div id="map"></div>
<script>
function initMap() {
var mcenter = {lat: 55.753186, lng: 37.620393};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: mcenter
});
_PRINT;
mssql_select_db($database, $connection);
$all_records = mssql_query('SELECT TowerID, TowerLatitude, TowerLongitude, TowerAddress, ModemID, ModemLevel FROM MS_Towers t LEFT JOIN MS_Modems m ON m.ModemTower=t.TowerID WHERE ModemLevel=0 OR ModemLevel=2');
$counter=1;
while ($row = mssql_fetch_array($all_records)) {
$rows[]=$row;
$mapmetcol="red";
$Alarmtimestamp=strtotime($row[12]);
$AlarmDuration=($timenow-$Alarmtimestamp)/60;
if ($AlarmDuration >= 60) { $mapmetcol="darkOrange"; }
if ($AlarmDuration >= 100) { $mapmetcol="brown"; }
if ($row[5]==0) {
$data_print .= <<<_PRINT
var marker = new google.maps.Marker({
position: {
lat: $row[1],
lng: $row[2]
},
icon: {
url: "http://maps.google.com/mapfiles/ms/icons/blue-dot.png"
},
map: map
});
_PRINT;
}
if ($row[5]==2) {
$data_print .= <<<_PRINT
var marker = new google.maps.Marker({
position: {
lat: $row[1],
lng: $row[2]
},
icon: {
url: "http://maps.google.com/mapfiles/ms/icons/red-dot.png"
},
map: map
});
_PRINT;
}
$data_print .= <<<_PRINT
var infoWindow = new google.maps.InfoWindow()
google.maps.event.addListener(marker, "click", function() {
var contentString = '<div class="infowindow">' +
'<h3>Мачта: $row[0]</h3>' +
'</div>';
google.maps.event.addListener(map, "click", function() {
infoWindow.close();
});
infoWindow.setContent(contentString);
infoWindow.open(map, this);
});
_PRINT;
}
$data_print .= <<<_PRINT
}
</script>
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=<<key>>&callback=initMap">
</script>
_PRINT;
print $data_print;
mssql_close($connection);
}
?>
<script src="assets/js/cufon/cufon-yui.js"></script>
<script src="assets/js/cufon/LED_500.font.js"></script>
<script type="text/jаvascript">
Cufon.replace(".timenow");
</script>
Использование Round-robin Database для накопления и обработки данных
Обычную базу данных удобно использовать для накопления исторической информации и для обнаружения и анализа событий, предшествующих той или иной аварии. Но существует другой вид баз данных - Round-robin Database - информация в которых становится все более обобщённой и менее детальной по мере того, как проходит время с данного события.
Дело в том, что часто, по мере устаревания, информация становится бесполезной, и хранить её долгое время не имеет смысла. Именно для такого типа информации как нельзя лучше подходит RRD. Также, с её помощью можно мгновенно вывести на экран результаты обработки большого объёма данных за большой отрезок времени, так как вся обработка происходит не в момент отображения информации, а постепенно до этого, по мере её поступления.
С одной стороны, именно метод обработки информации в момент поступления, и, с другой стороны, предельно низкий объём получающихся данных, наделяют RRD-базу данных удивительным новым свойством - эти данные можно никуда не посылать, а обрабатывать и записывать их прямо на контроллере. В случае длительного отсутствия связи с IoT облаком, эти данные никуда не потеряются, и продолжат обрабатываться автономно.
Например, предположим, что мы присоединили к контроллеру датчик радиоактивности, который раз в 15 минут просыпается, и передаёт в последовательный порт целое число - измеренный уровень радиоактивного излучения в мкР/ч. При обычном природном фоне такое излучение составляет 6-10 мкР/ч. Если мы подсоединимся к контроллеру и прослушаем его последовательный порт, то увидим в нем соответствующие посылки:
cat /dev/ttyUSB6
7
6
10
8
9
6
...
и т.д
Для обработки будем использовать стандартную утилиту rrdtool:
cat /etc/rc.local
…
/home/applications/read_radiation.sh &
cat /home/applications/read_radiation.sh
#!/bin/bash
sensor_device='/dev/ttyUSB6'
rrd_file='/home/applications/radiation/radiation.rrd'
stty -F $sensor_device 9600
while read -r line; do
line=`echo $line | tr -cd "[:print:]"`
rrdtool update $rrd_file -t sensor_rad N:$line;
done < $sensor_device
Создадим базу данных RRD:
rrdtool create /home/applications/radiation/radiation.rrd --step=900 --start=now-15s DS:sensor_rad:GAUGE:1800:0:U RRA:AVERAGE:0.5:1:1000
Что она из себя представляет, понятно из её дампа, который приведён ниже.
Созданная таким образом база будет записана в бинарный файл /home/applications/radiation/radiation.rrd,
в этом файле ведётся обработка всего лишь одного значения измеряемой величины - sensor_rad типа GAUGE, то есть изменяющейся в любую сторону величины
Величина sensor_rad с нулевым нижним и с неопределённым верхним пределом, и показания не могут поступать реже, чем раз в 1800 секунд (30 минут) (DS:sensor_rad:GAUGE:1800:0:U)
Шаг инкремента времени в базе составляет 15 минут (--step=900). В базе хранится 1000 усреднённых значений, при этом устреднение производится при получении каждого первого из них (1) и данные считаются не потерянными, даже если половина (0.5) входных данных не пришло (RRA:AVERAGE:0.5:1:1000)
Вот как выглядит дамп созданного таким образом файла
rrdtool dump /home/applications/radiation/radiation.rrd
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE rrd SYSTEM "http://oss.oetiker.ch/rrdtool/rrdtool.dtd">
<!-- Round Robin Database Dump -->
<rrd>
<version>0003</version>
<step>900</step> <!-- Seconds -->
<lastupdate>1586513175</lastupdate> <!-- 2020-04-10 13:06:15 GMT-3 -->
<ds>
<name> sensor_rad </name>
<type> GAUGE </type>
<minimal_heartbeat>1800</minimal_heartbeat>
<min>NaN</min>
<max>NaN</max>
<!-- PDP Status -->
<last_ds>8</last_ds>
<value>3.0026964800e+03</value>
<unknown_sec> 0 </unknown_sec>
</ds>
<!-- Round Robin Archives -->
<rra>
<cf>AVERAGE</cf>
<pdp_per_row>1</pdp_per_row> <!-- 900 seconds -->
<params>
<xff>5.0000000000e-01</xff>
</params>
<cdp_prep>
<ds>
<primary_value>8.8341441156e+00</primary_value>
<secondary_value>0.0000000000e+00</secondary_value>
<value>NaN</value>
<unknown_datapoints>0</unknown_datapoints>
</ds>
</cdp_prep>
<database>
<!-- 2020-04-05 08:15:00 GMT-3 / 1586063700 --> <row><v>NaN</v></row>
<!-- 2020-04-05 08:30:00 GMT-3 / 1586064600 --> <row><v>NaN</v></row>
<!-- 2020-04-05 08:45:00 GMT-3 / 1586065500 --> <row><v>NaN</v></row>
<!-- 2020-04-05 09:00:00 GMT-3 / 1586066400 --> <row><v>NaN</v></row>
<!-- 2020-04-05 09:15:00 GMT-3 / 1586067300 --> <row><v>NaN</v></row>
...
<!-- 2020-04-09 20:15:00 GMT-3 / 1586452500 --> <row><v>NaN</v></row>
<!-- 2020-04-09 20:30:00 GMT-3 / 1586453400 --> <row><v>NaN</v></row>
<!-- 2020-04-09 20:45:00 GMT-3 / 1586454300 --> <row><v>NaN</v></row>
<!-- 2020-04-09 21:00:00 GMT-3 / 1586455200 --> <row><v>8.0000000000e+00</v></row>
<!-- 2020-04-09 21:15:00 GMT-3 / 1586456100 --> <row><v>8.5809538289e+00</v></row>
<!-- 2020-04-09 21:30:00 GMT-3 / 1586457000 --> <row><v>8.4190203467e+00</v></row>
<!-- 2020-04-09 21:45:00 GMT-3 / 1586457900 --> <row><v>6.8379795844e+00</v></row>
...
<!-- 2020-04-10 10:30:00 GMT-3 / 1586503800 --> <row><v>7.5826037067e+00</v></row>
<!-- 2020-04-10 10:45:00 GMT-3 / 1586504700 --> <row><v>8.0000000000e+00</v></row>
<!-- 2020-04-10 11:00:00 GMT-3 / 1586505600 --> <row><v>8.5826711722e+00</v></row>
<!-- 2020-04-10 11:15:00 GMT-3 / 1586506500 --> <row><v>9.5826922522e+00</v></row>
<!-- 2020-04-10 11:30:00 GMT-3 / 1586507400 --> <row><v>8.2517952600e+00</v></row>
<!-- 2020-04-10 11:45:00 GMT-3 / 1586508300 --> <row><v>7.0000000000e+00</v></row>
<!-- 2020-04-10 12:00:00 GMT-3 / 1586509200 --> <row><v>8.1655909822e+00</v></row>
<!-- 2020-04-10 12:15:00 GMT-3 / 1586510100 --> <row><v>7.2515234067e+00</v></row>
<!-- 2020-04-10 12:30:00 GMT-3 / 1586511000 --> <row><v>7.1657278222e+00</v></row>
<!-- 2020-04-10 12:45:00 GMT-3 / 1586511900 --> <row><v>9.1657749022e+00</v></row>
<!-- 2020-04-10 13:00:00 GMT-3 / 1586512800 --> <row><v>8.8341441156e+00</v></row>
</database>
</rra>
</rrd>
Обратим внимание на размер файла:
ls -l radiation.rrd
-rw-r--r-- 1 root root 4548 Apr 10 13:06 radiation.rrd
Такой файл можно теперь по запросу, или в автоматическом режиме копировать на сервер мониторинга и показывать на веб странице в виде таблицы или графика.
Сделаем скрипт для формирования PNG файла с графиком радиации на сервере:
cat rrd_graph.sh
#!/bin/sh
rrd_file="/opt/bin/radiation/radiation.rrd"
DAYS="6h 1d 7d 30d"
DATE="`date '+%d-%m-%Y %H\:%M\:%S'`"
output_path="/var/www/html/rad"
for day in $DAYS; do
rrdtool graph "$output_path${day}.png" \
--start end-${day} \
--title "Статистика" \
--width 500 --height 200 \
--slope-mode \
--lower-limit 0 \
--imgformat PNG \
DEF:rad_av=$rrd_file:sensor_rad:AVERAGE:step=1 \
TEXTALIGN:left \
COMMENT:" " \
COMMENT:"Средний\l" \
LINE1:rad_av#FF6666:"уровень радиации за период\:" \
GPRINT:rad_av:AVERAGE:"%13.2lf\l" \
AREA:rad_av#FF6666 \
COMMENT:"\s" \
COMMENT:"\s" \
COMMENT:"Последнее обновление\: $DATE\r"
done
Такой скрипт сформирует несколько графиков с разным временным разрешением. Например, график за 1 день показан ниже.

Продолжение следует