+7(952) 531-56-65

out4ru@gmail.com

Процессорные модули » Статьи » Контроллеры и OpenWRT

Контроллеры и OpenWRT


Описание


Методики работы, описанные в этой статье, могут показаться поверхностными. Они могут не подойти, например, тем, кто работает с enterprise-системами, с применением профессиональных методик программирования. Причина в том, что автору этой статьи долгое время пришлось работать не по специальности, и заниматься своими разработками в качестве хобби, в свободное от работы время. И методики, описанные здесь, родились именно в этот момент. Поэтому то, что описано здесь, проверено на практике, и успешно работает уже многие годы.

Мы рассмотрим построение системы управления удалёнными объектами с начала до конца: начиная от изготовления контроллеров для управления объектом, и заканчивая облачными приложениями и веб-интерфейсом оператора.

Вопрос, который наверняка возникнет одним из первых при разработке такой системы (а у нас он оказался и самым первым, и самым трудным) – это - какой контроллер выбрать. Всё многообразие контроллеров можно условно поделить на три типа:

1. Готовые контроллеры PLC, программируемые на языках типа Ladder Diagram

2. Контроллеры на основе однокристальных микроконтроллеров, таких, как stm32

3. Одноплатные микрокомпьютеры, работающие под управлением операционной системы (например, Linux)

При решении данного вопроса мы так и не пришли к единому мнению. Для нашей задачи мы решили использовать контроллеры либо типа 2, либо 3, но никак не могли решить, какого именно. Спор был настолько ожесточённым, что договориться мы так и не смогли; не договариваясь, мы начали разрабатывать каждый свой тип, а в проект системы внесли оба типа сразу. Я представлял сторону, отстаивающую тип 3. Забегая вперёд, скажу, что оба типа имеют право на существование, и у каждого есть свои преимущества и недостатки. В каждом конкретном случае какие-то из них приходится иметь ввиду. Давайте сравним их, и посмотрим, какие именно.



 

Контроллеры на основе однокристальных микроконтроллеров stm32

Одноплатные микрокомпьютеры Linux

Аппаратная часть

(+) Простота схемотехники. Минимум деталей.

(-) Сложность принципиальной схемы.

Стоимость

(+) Дешевизна

(-) Стоят в несколько раз больше

Надёжность

(+) Нечему ломаться, всего одна микросхема

(-) Деталей больше, надёжность может быть ниже

Затраты на программирование

(+) Простые программы пишутся быстро, и для их запуска не нужна операционная система

(+-) Простые программы тоже пишутся быстро, но вначале нужно скомпилировать, записать и запустить операционную систему, и уже в ней запускать программу

Затраты на отладку программ

(+-) Программы отлаживаются быстро, благодаря, в частности, встраиваемым функциям отладки

(+) Функции отладки могут не понадобиться вовсе: загружаем новые версии программ (напр. по FTP),  и запускаем их без перезагрузки операционной системы

Функции удалённого доступа

(-) Сложно сделать так, чтобы оператор мог получать удалённый доступ в командную оболочку контроллера, находящегося на объекте, и управлять объектом командами

(+) Для удалённого доступа уже всё почти готово: есть командный интерфейс самой операционной системы, часто есть удалённый графический экран, или даже веб интерфейс

Запуск нескольких функций управления одновременно

(-) Нужно подумать о разделении времени между задачами, подготовить задачи специальным образом, чтобы их можно было удалённо (ре)стартовать и т.д.

(+) Всё уже предусмотрено и сделано: разделение памяти, запуск многих процессов одновременно, есть встроенные средства для разделение общих ресурсов (портов, интерфейсов и пр.) Аварийное завершение одного процесса не приводит к зависанию системы

Многопользовательский удалённый доступ (управление одновременно из нескольких мест или командами нескольких управляющих процессов)

(-) Трудно организовать одновременно несколько потоков входящих команд и возможное разрешение конфликтов между противоречащими друг другу командами

(+) Большинство этих вопросов решено уже в самой оперативной системе. Возможно назначение большого числа операторских консолей. Есть встроенные средства блокировки ресурсов для проведения транзакций и для наблюдения за поведением управляемого объекта в реальном времени (если им одновременно управляет кто-то другой)

Поддержка задач реального времени

(+) Приложение, как правило, исполняется строго определённое число процессорных тактов

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


Поддержка задач ввода-вывода



GPIO, serial , SPI, i2C, W1, и прочее



(+) Дополнительно к этому – встроенная поддержка пакетных протоколов, огромного количества функций протокола IP



Как видно из таблицы выше, везде есть свои плюсы и минусы, и всё может зависеть от конкретной ситуации. А мы теперь так и используем оба типа контроллеров одновременно.


Операционная система 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 Generic Serial Driver

    <*>   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 Support  --->

    <*>   LED Class Support

    <*>   LED Support for GPIO connected LEDs

    <*>   PWM driven LED Support

  •   LED Trigger support  ---> 

    <*>   LED Timer Trigger

  •   LED Disk Trigger

    <*>   LED Heartbeat Trigger

    <*>   LED Default ON Trigger

    <*>   LED Netdev Trigger

  •   USB LED Triggers 

  • 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

  •   Wireless extensions

    <*>   cfg80211 - wireless configuration API

    <*>   Generic IEEE 802.11 Networking Stack (mac80211)

    <*>   USB Wireless Device Management support

  •   Realtek devices 

    <*>     RTL8723AU/RTL8188[CR]U/RTL819[12]CU (mac80211) support

  •       Include support for untested Realtek 8xxx USB devices (EXPERIMENTAL)

  • GPIO


  •   /sys/class/gpio/... (sysfs interface)

  • 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


  • Pulse-Width Modulation (PWM) Support  ---> 

    <*>   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

  • Miscellaneous filesystems  --->

    <*>   UBIFS file system support



  • Последовательность действий при прошивке контроллера будет следующей:

    1. Подключить к контроллеру консольный кабель и USB (Slave-порт для прошивки)

    2. Замкнуть пин программирования контроллера на “землю”, нажать reset.

    3. Запустить mfgtool и прошить U-boot через профайл openwrt (о том, как подготовить профайл, расскажем ниже)

    4. Подготовить 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(&lt);
    
      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 день показан ниже.



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

    Быстрая доставка

    Безопасная оплата

    Гарантия качества