Сигнализация для холодильника. Не жизнь, а «малина» c RaspberryPi 3

    Запылилась за месяц у меня на полке Raspberry Pi 3 со встроенным Wi-Fi. Ресурсов процессора и объема памяти уже достаточно для запуска ресурсоемких программ. Как же быстро разработать и запустить на ней свою программу состоящую всего из одного небольшого файла с отправкой фото на почту и веб сервером мониторинга?



    Соберем простую систему для охраны холодильника от незаконного проникновения с фото регистрацией и интеграцией в интернет через smtp. Устроим у себя настоящий интернет вещей на кухне!

    Эта же функция будет полезна худеющим. Система будет фотографировать нарушителя кухонного спокойствия и момент возможного правонарушения. Открытие холодильника будем обнаруживать с помощью герконового реле, видеосъемка с помощью обычной веб камеры в свете открытого холодильника. Чтобы нарушитель точно не ушел от ответа, серию его фото будем отправлять на почтовый ящик.

    Краткое содержание статьи


    • Аппаратная часть
    • Программная часть
    • На Groovy
    • На Java
    • Как жить дальше с этими знаниями?

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


    Геркон — любой для охранной системы.
    Веб-камера — у меня Logitech C310, подойдет любая поддерживаемая подсистемой Video4Linux
    Raspberry Pi 3 Model B — есть встроенный WiFi и не нужен USB hub.

    Геркон нужно подключить между контактом №17 и №15 — т.е. между GPIO3 и +3.3V по схеме.

    Программная часть


    В open source фреймворке Apache Camel и его компонентах Rhiot для JVM сделали многое, чтобы найти применение пылящемуся в шкафу одноплатному компьютеру. Достаточно, используя его язык для описания конфигурации, скомпоновать из готовых компонент «маршрут» сигналов/данных в системе и Camel превратит его в приложение.

    В прошлой своей статье про разработку для интернета вещей в JVM я обещал пример для Camel и сегодня сдерживаю обещание! Идея этого проекта навеяна примером «Intruder detection with Raspberry-Pi». Только геркон доступнее и программно работать с ним так же как и с обычной кнопкой — не нужно никакого протокола I2C.

    С помощью RouteBuilder создаем маршрут. Источники и приемники данных в camel описываются в виде URL и для каждого компонента/протокола описание формата любого компонента сможете прочитать на странице.

    • controlbus — это компонент для управления маршрутами. В нашем случае с помощью него запускаем и останавливаем фотосъемку.
    • pi4j-gpio — использует библиотеку pi4j для получения сигналов с GPIO «малины».
    • webcam — получает кадр с веб камеры через интервалы времени, определенные компонентом-таймером.
    • smtps — передача сообщения электронной почты.

    camelContext.start() инициализирует компоненты и запускает маршрут. Реагировать на размыкание контакта геркона очень просто:



    Визуализация реакции на геркон в hawt.io


    Маршрут же фото регистрации с отправкой снимка на почту в hawt.io


    addEventNotifier() позволяет нам перехватывать события маршрута. Мы будем реагировать на запуск и остановку маршрута и отправлять сообщение о статусе сигнализации на почтовый ящик.

    Если ваша почта не на сервере mail.ru, то найдите smtp хост, порт для вашей почты и внесите их вместо «smtps://smtp.mail.ru:465».

    Пробовал также искать лицо на фото в том же маршруте Camel, но даже Raspberry PI 3 model B подтормаживает на этой задаче.

    Фрагмент кода запускает веб консоль hawt.io для мониторинга и управления приложением:

    MavenClassLoader.usingCentralRepo()
            .forMavenCoordinates('io.hawt:hawtio-app:2.0.0').loadClass('io.hawt.app.App')
    Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader())
    hawtIoConsole.main('--port','10090')
    

    Если же функциональности почти двухсот компонентов вам окажется мало, то разработать свой новый компонент для Apache Camel достаточно легко. Недавно делал это в проекте camel-gcode для отправки команд станку ЧПУ под управлением LinuxCNC из программы в JVM.

    На Groovy


    AlarmSystem.groovy
    @Grab('org.apache.camel:camel-groovy:2.18.0')
    @Grab('org.apache.camel:camel-core:2.18.0')
    @Grab('org.apache.camel:camel-mail:2.18.0')
    @Grab('io.rhiot:camel-webcam:0.1.4')
    @Grab('io.rhiot:camel-pi4j:0.1.4')
    @Grab('org.slf4j:slf4j-simple:1.6.6')
    import org.apache.camel.builder.RouteBuilder
    import org.apache.camel.impl.DefaultAttachment
    import org.apache.camel.impl.DefaultCamelContext
    import org.apache.camel.management.event.CamelContextStartedEvent
    import org.apache.camel.management.event.CamelContextStoppedEvent
    import org.apache.camel.support.EventNotifierSupport
    
    import javax.mail.util.ByteArrayDataSource
    import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader
    
    def login = System.properties['login']
    def password = System.properties['password']
    
    def camelContext = new DefaultCamelContext()
    camelContext.setName('Alarm system')
    def mailEndpoint = camelContext.getEndpoint("smtps://smtp.mail.ru:465?username=${login}&password=${password}&contentType=text/html&debugMode=true")
    camelContext.addRoutes(new RouteBuilder() {
        def void configure() {
            from('pi4j-gpio://3?mode=DIGITAL_INPUT&pullResistance=PULL_DOWN').routeId('GPIO read')
                    .choice()
                    .when(header('CamelPi4jPinState').isEqualTo("LOW"))
                        .to("controlbus:route?routeId=RaspberryPI Alarm&action=resume")
                    .otherwise()
                        .to("controlbus:route?routeId=RaspberryPI Alarm&action=suspend");
    
            from("timer://capture_image?delay=200&period=5000")
                    .routeId('RaspberryPI Alarm')
                    .to("webcam:spycam?resolution=HD720")
                    .setHeader('to').constant(login)
                    .setHeader('from').constant(login)
                    .setHeader('subject').constant('alarm image')
            .process{
                def attachment = new DefaultAttachment(new ByteArrayDataSource(it.in.body, 'image/jpeg'));
                it.in.setBody("<html><head></head><body><img src=\"cid:alarm-image.jpeg\" /> ${new Date()}</body></html>");
                attachment.addHeader("Content-ID", '<alarm-image.jpeg>');
                it.in.addAttachmentObject("alarm-image.jpeg", attachment);
                //set CL to avoid javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed
                Thread.currentThread().setContextClassLoader( getClass().getClassLoader() );
            }
            .to(mailEndpoint)
        }
    })
    registerLifecycleActions(camelContext, mailEndpoint, login)
    camelContext.start()
    
    def hawtIoConsole = MavenClassLoader.usingCentralRepo()
            .forMavenCoordinates('io.hawt:hawtio-app:2.0.0').loadClass('io.hawt.app.App')
    Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader())
    hawtIoConsole.main('--port','10090')
    
    
    void registerLifecycleActions(camelContext, mailEndpoint, login) {
    
        camelContext.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
            boolean isEnabled(EventObject event) {
                return event instanceof CamelContextStartedEvent | event instanceof CamelContextStoppedEvent
            }
    
            void notify(EventObject event) throws Exception {
                def status = event instanceof CamelContextStartedEvent ? 'up' : 'down'
                if ('up' == status){
                    def suspendEndpoint = camelContext.getEndpoint("controlbus:route?routeId=RaspberryPI Alarm&action=suspend")
                    suspendEndpoint.createProducer().process(suspendEndpoint.createExchange())
                }
                def message = mailEndpoint.createExchange();
                message.in.setHeader('to', login)
                message.in.setHeader('from', login)
                message.in.setHeader('subject', "Alarm system is ${status}")
                message.in.setBody("System is ${status}: ${new Date()}");
                mailEndpoint.createProducer().process(message)
            }
        })
        addShutdownHook { camelContext.stop() }
    }
    

    На Java


    Чтобы сделать то же самое на java понадобилось больше букв, файлов и конечно Reflection API.

    Класс com.github.igorsuhorukov.alarmsys.AlarmSystem:
    AlarmSystem.java

    
    package com.github.igorsuhorukov.alarmsys;
    
    //dependency:mvn:/com.github.igor-suhorukov:mvn-classloader:1.8
    //dependency:mvn:/org.apache.camel:camel-core:2.18.0
    //dependency:mvn:/org.apache.camel:camel-mail:2.18.0
    //dependency:mvn:/io.rhiot:camel-webcam:0.1.4
    //dependency:mvn:/io.rhiot:camel-pi4j:0.1.4
    //dependency:mvn:/org.slf4j:slf4j-simple:1.6.6
    import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;
    import org.apache.camel.Endpoint;
    import org.apache.camel.Exchange;
    import org.apache.camel.Processor;
    import org.apache.camel.builder.RouteBuilder;
    import org.apache.camel.impl.DefaultAttachment;
    import org.apache.camel.impl.DefaultCamelContext;
    import org.apache.camel.management.event.CamelContextStartedEvent;
    import org.apache.camel.management.event.CamelContextStoppedEvent;
    import org.apache.camel.support.EventNotifierSupport;
    
    import javax.mail.util.ByteArrayDataSource;
    import java.lang.reflect.Method;
    import java.util.Date;
    import java.util.EventObject;
    
    class AlarmSystem {
        public static void main(String[] args) throws Exception{
    
            String login = System.getProperty("login");
            String password = System.getProperty("password");
    
            DefaultCamelContext camelContext = new DefaultCamelContext();
            camelContext.setName("Alarm system");
            Endpoint mailEndpoint = camelContext.getEndpoint(String.format("smtps://smtp.mail.ru:465?username=%s&password=%s&contentType=text/html&debugMode=true", login, password));
            camelContext.addRoutes(new RouteBuilder() {
                @Override
                public void configure() throws Exception {
                    from("pi4j-gpio://3?mode=DIGITAL_INPUT&pullResistance=PULL_DOWN").routeId("GPIO read")
                            .choice()
                            .when(header("CamelPi4jPinState").isEqualTo("LOW"))
                            .to("controlbus:route?routeId=RaspberryPI Alarm&action=resume")
                            .otherwise()
                            .to("controlbus:route?routeId=RaspberryPI Alarm&action=suspend");
    
                    from("timer://capture_image?delay=200&period=5000")
                            .routeId("RaspberryPI Alarm")
                            .to("webcam:spycam?resolution=HD720")
                            .setHeader("to").constant(login)
                            .setHeader("from").constant(login)
                            .setHeader("subject").constant("alarm image")
                            .process(new Processor() {
                                @Override
                                public void process(Exchange it) throws Exception {
                                    DefaultAttachment attachment = new DefaultAttachment(new ByteArrayDataSource(it.getIn().getBody(byte[].class), "image/jpeg"));
                                    it.getIn().setBody(String.format("<html><head></head><body><img src=\"cid:alarm-image.jpeg\" /> %s</body></html>", new Date()));
                                    attachment.addHeader("Content-ID", "<alarm-image.jpeg>");
                                    it.getIn().addAttachmentObject("alarm-image.jpeg", attachment);
                                    //set CL to avoid javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed
                                    Thread.currentThread().setContextClassLoader( getClass().getClassLoader() );
    
                                }
                            }).to(mailEndpoint);
                }
            });
    
            registerLifecycleActions(camelContext, mailEndpoint, login);
            camelContext.start();
    
            Class<?> hawtIoConsole = MavenClassLoader.usingCentralRepo()
                    .forMavenCoordinates("io.hawt:hawtio-app:2.0.0").loadClass("io.hawt.app.App");
            Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader());
            Method main = hawtIoConsole.getMethod("main", String[].class);
            main.setAccessible(true);
            main.invoke(null, (Object) new String[]{"--port","10090"});
        }
    
        private static void registerLifecycleActions(final DefaultCamelContext camelContext, final Endpoint mailEndpoint, final String login) {
            camelContext.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
    
                public boolean isEnabled(EventObject event) {
                    return event instanceof CamelContextStartedEvent | event instanceof CamelContextStoppedEvent;
                }
    
                public void notify(EventObject event) throws Exception {
                    String status = event instanceof CamelContextStartedEvent ? "up" : "down";
                    if ("up".equals(status)){
                        Endpoint suspendEndpoint = camelContext.getEndpoint("controlbus:route?routeId=RaspberryPI Alarm&action=suspend");
                        suspendEndpoint.createProducer().process(suspendEndpoint.createExchange());
                    }
                    Exchange message = mailEndpoint.createExchange();
                    message.getIn().setHeader("to", login);
                    message.getIn().setHeader("from", login);
                    message.getIn().setHeader("subject", "Alarm system is "+status);
                    message.getIn().setBody("System is "+status+": "+new Date());
                    mailEndpoint.createProducer().process(message);
                }
            });
            Runtime.getRuntime().addShutdownHook(new Thread(){
                @Override
                public void run(){
                    try {
                        camelContext.stop();
                    } catch (Exception e) {
                        System.exit(-1);
                    }
                }
            });
        }
    }
    


    Для сборки нужен:

    pom.xml c зависимостями проекта
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.github.igor-suhorukov</groupId>
        <artifactId>alarm-system</artifactId>
        <packaging>jar</packaging>
        <version>1.0-SNAPSHOT</version>
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
        <dependencies>
            <dependency>
                <groupId>com.github.igor-suhorukov</groupId>
                <artifactId>mvn-classloader</artifactId>
                <version>1.8</version>
            </dependency>
            <dependency>
                <groupId>org.apache.camel</groupId>
                <artifactId>camel-core</artifactId>
                <version>2.18.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.camel</groupId>
                <artifactId>camel-mail</artifactId>
                <version>2.18.0</version>
            </dependency>
            <dependency>
                <groupId>io.rhiot</groupId>
                <artifactId>camel-webcam</artifactId>
                <version>0.1.4</version>
            </dependency>
            <dependency>
                <groupId>io.rhiot</groupId>
                <artifactId>camel-pi4j</artifactId>
                <version>0.1.4</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
                <version>1.6.6</version>
            </dependency>
        </dependencies>
    </project>
    


    Запускаем результат на Raspberry Pi 3 Model B


    Сборка linux Raspbian на SD карте уже чудесным образом содержит Java 8 от Oracle. Настройте подключение к интернет по WiFi или подключите патчкордом и сконфигурируйте доступ к интернет по ethernet сети через RJ-45 разьем на плате.

    Так что вся установка будет состоять из простых команд:

    wget https://repo1.maven.org/maven2/com/github/igor-suhorukov/groovy-grape-aether/2.4.5.4/groovy-grape-aether-2.4.5.4.jar
    wget https://raw.githubusercontent.com/igor-suhorukov/alarm-system/master/AlarmSystem.groovy
    

    И запуска программы:

    java -Dlogin=...ВАША_ПОЧТА...@mail.ru -Dpassword=******* -jar groovy-grape-aether-2.4.5.4.jar AlarmSystem.groovy
    

    Или вы можете просто внести свои логин и пароль в скрипт, чтобы не светить их в истории команд:

    def login = ...
    def password = ...
    

    Сразу после запуска скрипта маршрут «GPIO read» ждет сигнала с геркона и запущен, а второй маршрут «RaspberryPI Alarm» с вебкамерой — на паузе.

    На это можно посмотреть в веб консоли...


    Еще можно отложить jconsole в сторону. Ведь потоки и метрики jvm можно смотреть в hawt.io



    Эта консоль мониторинга доступна по адресу http:// АДРЕС_МАЛИНЫ :10090/hawtio/

    Java версию нужно собрать с помощью maven. Или же можно пойти на хитрость и запустить Java программу как скрипт с динамическим разрешением зависимостей следующим образом:

    java -Dlogin=...YOUR_EMAIL...@mail.ru -Dpassword=******* -DscriptPath=https://raw.githubusercontent.com/igor-suhorukov/alarm-system/master/src/main/java/com/github/igorsuhorukov/alarmsys/AlarmSystem.java -jar java-as-script-1.0.jar

    Про то как работает java-as-script-1.0.jar и что еще можно делать с его помощью будет отдельная статья.

    Как жить дальше с этими знаниями?



    Apache Camel оказался отличным инструментом для быстрого прототипирования, так как есть много готовых компонент для различной периферии, интернет-сервисов. Хоть его обычно и используют в enterprise приложениях для интеграции, но даже на современных одноплатных компьютерах и в решениях для «интернета вещей» он даст фору другим подходам для разработки систем. Просто «распробуйте» его и он вам понравится, особенно вместе с Groovy!

    Проект доступен в github репозитарии alarm-system и засветился на официальном сайте Apache Camel в разделе «Camel and the IoT (Internet of Things)».
    Поделиться публикацией

    Комментарии 15

      +1
      Как жить дальше с этими знаниями?

      Как жить дальше с тем что вы знаете кто, когда и за чем лазил в холодильник?
      — Стройте рядом сторожевую башню и пехоту!
        0
        Пехоту из Огромных боевых человекоподобных роботов?
        +1

        Представил вашу систему на древнем-предревнем холодильнике в студенческой общаге… Её развитием будет система контроля численности тараканов (с лазером, с лазером, с лазером):)?

          0
          О, да! С акустическими датчиками, чтобы по топоту их засекать!)
          0
          а будет статья как обойти защиту
            0
            Не планировал. Я лучше про динамический компилятор java-as-script расскажу!
            +1
            А Telegram можно прикрутить?
            0
            Как все сложно… Использовать пакет motion с детектором движения, не?
            Чтобы не только проникновение жены в холодильник ночью фиксировать, но и ее тяжкие душевные терзания рядом.
            И зачем фотки в почту слать? Там на третий день будет тихий ужас. По WebDAV выгружать фотки на Яндекс.Диск и все. А потом жене показывать все разом, чтоб уж точно не отвертелась :)
              0
              Там на третий день будет тихий ужас.

              Так фото идут только при открытом холодильнике. Не стоит держать его три дня открытым.

              По WebDAV выгружать фотки на Яндекс.Диск и все.

              Есть компонент camel-dav. Но я им не пользовался.
                0
                А сколько писем в день приходит? Мне кажется, что сильно больше 10.
                WebDAV подключается довольно просто:
                https://habrahabr.ru/post/208058/
                Но это на любителя, как говорится. Если лично Вам проще настроить почту и открывать письмо каждый раз — то, может, и правда так лучше.
                  0
                  Спасибо за ссылку! С интересом почитал.
                  Просто электронная почта первое что приходит на ум и есть сейчас у всех. В Apache Camel можно и по webdav и в hadoop HDFS записывать данные, смотря какой компонент подключить и сконфигурировать.
              +2
              Бородатый анекдот:
              Надо чтобы в каждом холодильнике стояла вебкамера, постящая фото прямо на фейсбук при открытии дверцы.
              А в коврик перед дверцей встроить датчик веса. И все глобально начнут худеть, потому что иначе ваши друзья зайдут в фейсбук,
              а вы там такая испуганная в труселях и с кастрюлей. И подпись «Нина, 63 кг, жрет борщ в час ночи».
                0
                Лучше авторегулировка огня на плите. Автопомешивание…
                  0
                  Полшага до газовой мультиварки

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое