Термостат на ThingJS (beta)


    Почти год назад я представил свой pet-проект — IoT платформу ThingJS. Честно сказать, я не достиг всех целей, которые ставил перед собой публикуя ту статью. Но работа окупилась. Удалось получить нечто иное — полезную критику.


    Я учел прошлый опыт. Теория без практики заходит плохо. В этот раз презентация будет построена на базе прикладного решения. Каждый сможет “потрогать” его и использовать в быту.


    Но сначала немного общей информации.



    Введение


    ThingJS отличает от других платформ архитектура, позволяющая создавать устройства от любительского до профессионального уровня.


    Буквально за несколько минут можно создать бытовое устройство для своих нужд. Для этого в арсенале любителя готовые шаблоны приложений, возможность переиспользовать чужой код и интеграция с большинством IoT сервисов через MQTT.


    Профессионал имеет возможность дорабатывать прошивку, делая надежные устройства. Развивать качественный UI, построенный на современных WEB технологиях. Выпускать готовые устройства.


    Но самое значимое в том, что оба этих очень разных “мира” могут сосуществовать в гармонии. Это возможно через уникальную особенность платформы — приложения.


    Если приводить простейшую аналогию, платформа напоминает ОС. Ее развитием занимаются майнтейнеры ядра (core) и разработчики драйверов (Resource Interfaces). Прикладные разработчики делают важные и нужные программы (приложения платформы), которые “оживляют” UI девелоперы. Все это, конечно, для любимого пользователя.


    Слой разработки покрыт профессиональным стеком. Нет специально выдуманных IDE. Только проф: СLion, CMake, webpack, npm и т.д. Лучшие практики полностью релевантны при разработке для ThingJS. TDD инструмент в комплекте.


    Платформа может “жить” как в автономном устройстве, так и интегрироваться с облачными сервисами. Она не “прибита гвоздями” к вендору. Не “стучит куда надо”. В нее заложены принципы открытости и безопасности для пользователя.


    Если вы хотите сразу попробовать платформу, достаточно воспользоваться пользовательским пакетом. Все инструкции по развертыванию и использованию там же.


    Архитектура


    В настоящий момент runtime платформы представляет из себя прошивку для контроллеров ESP32. Любой желающий может расширять ее функциональность и адаптировать под себя.


    На базе прошивки функционируют приложения платформы. Приложение это комплект backend и frontend скриптов поставляемых в специальном файле — бандле приложения. В него входят:


    • Манифест приложения (обязательно);
    • Frontend (опция);
    • Backend (опция);
    • Иконка приложения (обязательно);
    • Предзаполненные хранилища (опция);
    • Мультиязычный пакет (опция);
    • Прочие файлы (опция).

    Backend скрипты исполняются контроллером. Для этого используется облегченный JavaScript — mJS.


    Frontend почти классическое SPA WEB-приложение. Отличие заключается во встроенных интерфейсах взаимодействия с контроллером. Взаимодействуют части приложения исключительно через два интерфейса: UBUS и Storage.



    UBUS


    Универсальная шина обмена данными. Обмен производится в формате JSON-пакетов. В пакет входит идентификатор события и данные. Шина не имеет четких границ распространения информации. Любой подписант на события входящий в контур распространения может получить сообщение и обработать его.


    Контур распространения это некоторое подмножество обработчиков, которым разрешено получать сообщения шины. Они также могут посылать сообщения в шину. Обработчиком может быть frontend, backend, прошивка, облако, другие устройства.


    Сообщения могут распространяться любыми доступными средствами. На схеме контроллер и frontend обмениваются сообщениями по WEBSocket.


    Например, если frontend отсылает сообщение в шину, его получат все установленные на контроллере приложения. Но есть условие — они должны быть подписаны на эти сообщения. Это же справедливо для отсылки сообщения из backend.


    Важным свойством шины является негарантированная доставка сообщений.


    Таким образом, платформа предлагает асинхронное событийное взаимодействие компонентов системы. Предполагается, что любой компонент системы, в любой момент может перестать быть доступным.


    Storage


    Формализованное хранилище приложения. Описывается в манифесте. Доступ к нему имеет как frontend, так и backend.


    Хранилище не является СУБД. Необходимо рассматривать его как файл, в который могут вносить изменения несколько потоков. Когда в нем происходят изменения, платформа генерирует событие об изменении в хранилище. Событие генерируется тогда, когда данные сохранены.


    У хранилища нет четкой локализации размещения. Предполагается, что оно может располагаться как на контроллере, так и в облаке. Облачной реализации на текущий момент нет.


    Хранилище дает возможность накапливать данные. Например, вести лог показаний датчиков. Или хранить конфигурацию устройства.


    Поддержание консистентности данных реализуется самой платформой. Необходимо учитывать, что в любой момент хранилище может быть очищено. Это возможно по различным причинам. Например, по желанию пользователя освободить место.


    API


    Базовый набор функций платформы реализуется через API. Например, доступ к ресурсам через глобальный интерфейс $res или к шине через $bus.


    Перечень API функций минимизирован для упрощения и унификации. Все, что возможно, вынесено в понятие ресурсных интерфейсов (Resource interfaces).


    Resource interfaces


    На самом деле сама платформа, это больше свод правил с частной реализацией. Т.е. той, которую сделал я. Практические каждый элемент в ней может быть заменен. Например, можно полностью поменять launcher, выпилить vuetifyjs или переделать все под React.


    Ресурсные интерфейсы решают ту же задачу — кастомизация. Вы можете расширять функциональность платформы по своему усмотрению. Реализации существующих интерфейсов можно найти тут в исходных кодах.


    Интерфейс не обязан быть простым. Он может предоставлять доступ к бизнеслогике в прошивке. Например, можно на FreeRTOS реализовать термостат и предоставить скриптам интерфейс, который позволяет лишь указывать параметры работы. Наглядным примером такого подхода является интерфейс SmartLED.


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


    Разделение ресурсов


    Фундаментальным свойством платформы является разделение ресурсов между приложениями. На одном контроллере могут быть развернуты несколько приложений.


    Разделение ресурсов возможно двумя путями:


    • При установке приложение запрашивает ресурсы и пользователь по своему выбору делегирует их;
    • Приложение требует совершенно конкретные ресурсы.

    Первый случай более демократичен. Пользователь может сам конфигурировать приложение. Например, выдавая GPIO устройства. Таким образом, приложение будет адаптировано для ресурсного ландшафта конкретного устройства.


    Но такой подход требует определенных знаний у пользователя. Эта концепция хорошо подходит для любительской разработки.


    Второй вариант не подразумевает вмешательство пользователя в назначение ресурсов приложению. Он может либо поставить приложение, либо отказаться от установки.


    Это упрощает выбор пользователя, но приводит к тому, что конкурирующие за идентичные ресурсы приложения невозможно будет установить на один контроллер. Такой вариант наиболее приемлем для готовых устройств, где приложение является частью поставки.


    Ресурсы делятся на физические и виртуальные. К физическим ресурсам относятся, например, GPIO, внутренние таймеры, UART драйвера и т.п. Эти ресурсы лимитированы. К виртуальным ресурсам можно отнести софтовые таймеры, TCP сокеты и т.д. Их количество, условно, не ограничено.


    В целом, это все, что требуется знать для того, чтобы сделать первое приложение для платформы ThingJS.


    Термостат на ThingJS


    Готовая реализация приложения Thermostat в исходниках доступна тут.


    Прикладное назначение


    Распространенной задачей является управление климатом. Задача особенно актуальна в строениях нерегулярного пребывания, где требуется поддержание плюсовой температуры. Например, дачи, гаражи. Важно получать информацию о температуре в строении в период отсутствия.


    Аппаратное обеспечение


    Для проекта выбран чип ESP32 являющийся приемником известного ESP8266. Это, пожалуй, самый бюджетный и функциональный чип на рынке сегодня. Его тактовая частот 240МГц, два ядра, ОЗУ до 520Кб, Bluetooth, Wifi. Разнообразные GPIO. Немаловажным является наличие аппаратного криптографического ускорителя: ES, SHA-2, RSA, ECC, RNG.


    Оптимальным вариантом для энтузиаста станет ESP32-DevKitC.



    Развертывание среды разработки приложений


    Разработка приложений ведется в хорошо знакомом для JavaScript разработчиков окружении VUE CLI. Можно смело утверждать, что любой frontend разработчик быстро разберется со средой и получит массу удовольствия при работе с ней.


    Ключевыми преимуществами среды являются:


    • Внутрисистемная отладка — Вы можете отлаживать работу приложения непосредственно на контроллере.
    • Горячая перезагрузка скрипта на контроллер (hot reload) — Работает аналогично горячей перезагрузкой скриптов в браузере. Только загружает скрипты на контроллер. Это радикально упрощает разработку.
    • Локальный dev сервер — Разрабатывать приложения вы сможете в привычной среде локального dev-сервера на NodeJS. При этом, нужные запросы будут отсылаться на физический контроллер. В некоторых случаях разработка приложений вообще не требует железа.

    Развертывание несложное:


    git clone --branch beta https://github.com/rpiontik/ThingJS-front
    cd ThingJS-front
    npm install

    Далее, запускаем dev сервер:


    npm run dev

    Перейдите в браузере по ссылке http://0.0.0.0:8080/. Откроется вполне рабочая платформа в dev-режиме. Все приложения будут почти работать.



    Чтобы приложения работали на 100%, нужно подключить среду к железу. Воспользуйтесь пользовательским пакетом, чтобы подготовить устройство. Подключите его к локальной сети. Затем, в файле /config/dev.env.js укажите IP устройства.


    Для того, чтобы dev-среда и устройство “понимали” друг-друга, на устройстве должно быть установлено то приложение, которые вы собираетесь отлаживать. Т.е. если вы хотите “поиграться”, например, с термостатом, установите на контроллер приложение thermostat.smt.


    Теперь перезапустите dev сервер и запустите приложение thermostat с рабочего стола. Приложение должно полноценно функционировать.



    Для эксперимента, внесите изменения в файл src/applications/thermostat/scripts/thermostat.js. Например, поставьте вначале команду “debugger”. Сохраните файл.


    Скрипт будет загружен на контроллер, а dev-среда отобразит запрос на отладку:


    Кликнув по ссылке “Start debugger” вы попадете в отладчик.



    Код можно выполнять пошагово. Есть возможность мониторить значение переменных в watch панели справа. Снизу выводится лог. А слева навигатор по физическому контроллеру.


    Чтобы собрать бандл разработанного приложения, необходимо выполнить команду:


    npm run build

    Собранные бандлы приложений будут размещены в папке dist/apps/


    Подробнее со средой можно познакомиться в репе.


    Создание проекта приложения Thermostat


    Для создания нового приложения достаточно скопировать и доработать наиболее подходящий из уже существующих примеров. Исходные коды приложения находятся в папке /src/applications/. Для термостата основой станет “blink”. При копировании папки указывается новое название — thermostat.


    Далее следует сконфигурировать приложение. Делается это через manifest.json. Именно в этом файле содержится вся ключевая информация о развертывании приложения и его работе. Меняем его под нужды нового приложения и одновременно модифицируем код.


    Общее описание приложения


    "name": "Thermostat",
    "vendor": "rpiontik",
    "version": 1,
    "subversion": 0,
    "patch": 0,
    "description": {
        "ru": "Термостат",
        "en": "Thermostat"
    },

    Блок используется для контроля совместимости приложений при переустановке, а также для предоставления пользователю кратких сведений о назначении приложения.


    components


    Блок описывает frontend компоненты приложения.


    "components": {
     "thermostat-app": {
       "source": "thermostat.js",
       "intent_filter": [
         {
           "action": "thingjs.intent.action.MAIN",
           "category": "thingjs.intent.category.LAUNCH"
         }
       ]
     }
    },

    Приложение содержит один компонент — “thermostat-app”. Его код расположен в файле “thermostat.js”. Компонент будет вызван при возникновении намерения удовлетворяющего фильтру указанному в “intent_filter”. Описанный фильтр соответствует намерению запуска приложения.


    Переименовываем файл “blink.js” -> “thermostat.js” Код будет таким:


    import App from './Thermostat.vue';
    import Langs from './langs';
    
    $includeLang(Langs);
    $exportComponent('thermostat-app', App);

    В листинге подключается VUE компонент “Thermostat.vue”. Именно он будет реализовывать интерфейс пользователя. Необходимо связать идентификатор компонента с его реализацией. Это делается так:


    $exportComponent('thermostat-app', App);

    Почему так? Ведь в манифесте уже указана связь. На самом деле нет. Манифест сообщил системе, что результатом выполнения файла “thermostat.js” станет регистрация VUE компонента с идентификатором “thermostat-app”. Но в этом же файле могут происходить другие действия по инициализации компонента. В этом случае подключается мультиязычный пакет “langs.js”.


    Для завершения описания frontend осталось реализовать сам компонент “Thermostat.vue”. Шаблон я спрячу подкатом. Рассмотрим код.


    template
    <template>
      <v-flex fill-height style="max-width: 600px">
        <h1>{{ 'TITLE'|lang }}</h1>
        <v-container>
          <v-layout>
            <v-flex xs12 md12>
              {{ 'DESCRIPTION'|lang }}
            </v-flex>
          </v-layout>
        </v-container>
        <v-tabs
            centered
            icons-and-text
        >
          <v-tab href="#tab-1">
            {{ 'CONTROL'|lang }}
            <v-icon>dashboard</v-icon>
          </v-tab>
    
          <v-tab href="#tab-2">
            {{ 'CLOUD'|lang }}
            <v-icon>cloud</v-icon>
          </v-tab>
    
          <v-tab-item value="tab-1">
            <v-container>
              <v-layout>
                <v-flex class="current-temp" xs12 md4>
                    <span>
                      <template v-if="state.temp !== null">
                        {{ state.temp.toFixed(1) }}°
                      </template>
                      <template v-else>
                        --.--
                      </template>
                    </span>
                </v-flex>
                <v-flex xs12 md4 style="text-align: center; padding: 12px; ">
                  <template v-if="state.state === 1">
                    <v-icon
                        title="Power on"
                        class="indicator"
                    >power
                    </v-icon>
                  </template>
                  <template v-else-if="state.state === 0">
                    <v-icon
                        title="Power off"
                        class="indicator"
                    >power_off
                    </v-icon>
                  </template>
                </v-flex>
                <v-flex xs12 md4 style="text-align: center; padding: 12px;">
                  <template v-if="!!state.connected">
                    <v-icon
                        title="Connected"
                        class="indicator"
                    >cloud
                    </v-icon>
                  </template>
                  <template v-else>
                    <v-icon
                        title="Disconnected"
                        class="indicator"
                    >cloud_off
                    </v-icon>
                  </template>
                </v-flex>
              </v-layout>
            </v-container>
            <v-container grid-list-xl>
              <v-layout>
                <v-flex xs12 md3>
                  <v-select
                      label="Mode"
                      :items="modes"
                      v-model="state.mode"
                      @change="onChangeMode"
                  ></v-select>
                </v-flex>
                <v-flex xs12 md9>
                  <v-slider v-if="state.mode <= 1"
                            thumb-label="always"
                            v-model="state.target"
                            :disabled="!state.target"
                            @change="onChangeTarget"
                  ></v-slider>
                </v-flex>
              </v-layout>
            </v-container>
          </v-tab-item>
          <v-tab-item value="tab-2">
            <v-container>
              <p>
                Android applications:
                <ul>
                  <li><a href="https://play.google.com/store/apps/details?id=net.routix.mqttdash" target="_blank">MQTT Dash (RUS)</a></li>
                  <li><a href="https://play.google.com/store/apps/details?id=snr.lab.iotmqttpanel.prod" target="_blank">IoT MQTT Panel (EN)</a></li>
                </ul>
              </p>
              <p>
                Server params:
                <ul>
                  <li>Address: mqtt.eclipse.org</li>
                  <li>port: 1883</li>
                </ul>
              </p>
              <table class="topic-table">
                <tr>
                  <th>{{ 'TOPIC'|lang }}</th>
                  <th>{{ 'TOPIC_DESCRIPTION'|lang }}</th>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/temp</td>
                  <td>{{ 'TOPIC_TEMP_DESC'|lang }}</td>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/state</td>
                  <td>{{ 'TOPIC_STATE_DESC'|lang }}</td>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/target/out</td>
                  <td>{{ 'TOPIC_TARGET_OUT'|lang }}</td>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/target/in</td>
                  <td>{{ 'TOPIC_TARGET_IN'|lang }}</td>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/mode/out</td>
                  <td>{{ 'TOPIC_MODE_OUT'|lang }}</td>
                </tr>
                <tr>
                  <td>/thingjs/{{ state.chip_id }}/mode/in</td>
                  <td>{{ 'TOPIC_MODE_IN'|lang }}</td>
                </tr>
              </table>
            </v-container>
          </v-tab-item>
        </v-tabs>
      </v-flex>
    </template>

    data () {
       return {
           modes: [ // Доступные режимы срабатывания термостата.
               { text: 'Less then', value: 0 },
               { text: 'More then', value: 1 },
               { text: 'On', value: 2 },
               { text: 'Off', value: 3 }
           ],
           isHold: false, // Флаг ожидания. Если он установлен, приложение не обрабатывает сообщения из шины.
           state: { // Последнее актуальное состояние
               connected: null, //  Состояние соединения с MQTT брокером
               mode: null, // Текущий режим работы
               target: null, // Целевая температура
               temp: null, // Текущая температура
               state: null, // Состояние нагрузки (вкл/выкл)
               chip_id: null // Уникальный идентификатор для MQTT брокера.
           }
       };
    }

    Дам дополнительные пояснения:


    isHold — Флаг ожидания. Если он установлен, приложение не обрабатывает сообщения из шины. Состояние контроллера будет обновляться каждую секунду. Если отправить команду в шину, ее доставка и обработка займет время. В этот период могут приходить сообщения, которые уже неактуальны. Этот флаг позволяет избавиться от “дребезга” состояния.


    chip_id — Уникальный идентификатор чипа. Он необходим для создания уникальных топиков MQTT брокера.


    mounted () {
       this.$bus.$on($consts.EVENTS.UBUS_MESSAGE, (type, data) => {
           if (this.isHold) return;
    
           switch (type) {
           case 'thermostat-state':
               this.state = JSON.parse(data);
               break;
           }
       });
       this.refreshState();
    },

    При монтировании компонента происходит подписка на сообщения шины. Обрабатывается событие “thermostat-state”. В сообщении содержится структура состояния контроллера. Если установлен флаг “isHold” обработка событий не происходит.


    refreshState () {
       this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-refresh-state');
    },

    Метод отправляет сообщение для принудительного обновления статуса контроллера. В ответ контроллер отправляет свое состояние.


    flushData () {
       if (this.isHold) { clearTimeout(this.isHold); }
       this.isHold = setTimeout(() => {
           this.isHold = null;
           this.refreshState();
       }, 1000);
    },

    Метод приостанавливает на секунду обработку сообщений из шины. Затем, запрашивает текущее состояние.


    onChangeTarget (val) {
       this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-target', val);
       this.flushData();
    },
    onChangeMode (val) {
       this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-mode', val);
       this.flushData();
    }

    При изменении параметров они передаются через шину на контроллер.


    Вот в общем-то и все с frontend. Осталось “оживить” контроллер.


    requires


    Перед модифицированием скрипта, необходимо сообщить платформе какие ресурсы требуются для работы приложения. Делается это через блок “requires”.


    "requires": {
     "interfaces": {
       "mqtt": {
         "type": "mqttc",
         "required": true
       },
       "timers": {
         "type": "timers",
         "required": true,
         "description": {
           "ru": "Таймеры системы",
           "en": "System timers"
         }
       },
       "ds18x20": {
         "type": "DS18X20",
         "required": true
       },
       "relay": {
         "type": "bit_port",
         "required": true,
         "default": 2,
         "description": {
           "ru": "Реле",
           "en": "Relay"
         }
       },
       "sys_info": {
         "type": "sys_info",
         "required": true,
         "description": {
           "ru": "Информация о системе",
           "en": "System information"
         }
       }
     }
    }

    В листинге описывается необходимость в ресурсах:


    • mqttc — Интерфейс интеграции по MQTT протоколу. Потребуется приложению для дистанционного управления.
    • timers — Системные таймеры. Скрипт будет ежесекундно проверять температуру с датчиков и принимать решение о включении или отключении нагрузки.
    • DS18X20 — Интерфейс к шине OneWire с подключенными датчиками температуры.
    • bit_port — Интерфейс дискретного управления каналом. Именно он будет управлять реле нагрузки.
    • sys_info — Интерфейс общесистемной информации. Необходим для получения уникального идентификатора устройства.

    Скрипту предоставляются только те ресурсы, которые он затребовал. Требования могут быть обязательными и опциональными. Если требование опциональное, приложению может быть отказано в предоставлении этого ресурса. Обязательность ресурса определяется полем "required" в манифесте. В случае с термостатом все интерфейсы являются обязательными.


    Может быть выделено несколько ресурсов одного и того же типа. Например, можно указать несколько ресурсов типа “DS18X20”. В этом случае, приложение получит возможность работать с несколькими шинами OneWire.


    Возможно возникла некоторая путаница между ресурсами и интерфейсами. Интерфейс это контракт на предоставление ресурса. Интерфейс имеет документированные методы и позволяет через них работать с выделенным ресурсом. Таким образом, в манифесте указываются ресурсы как ключ структуры, например “relay”, а затем определяется интерфейс для ресурса через поле “type”. В случае с “relay” это “bit_port”.


    scripts


    Блок содержит информацию о скрипте исполняемом на стороне контроллера.


    "scripts": {
     "entry": "thermostat",
     "subscriptions": ["tmst-refresh-state", "tmst-set-target", "tmst-set-mode"],
     "modules": {
       "thermostat": {
         "hot_reload": true,
         "source": "scripts/thermostat.js",
         "optimize": false
       }
     }
    },

    • entry — Точка входа при выполнении скриптов. Содержит идентификатор модуля. Модулей скриптов может быть несколько. Поле необходимо для указания платформе с какого скрипта начинать выполнение.
    • subscriptions — Подписка на события в шине. Скрипт начнет выполняться только при наступлении какого-то из описанных событий. Пока событие не наступило, скрипт находится в “спячке”. Это позволяет оптимизировать использование ресурсов в платформе.
    • modules — Список моделей скриптов.
      • thermostat — идентификатор модуля.
      • hot_reload — Признак “горячей” загрузки скриптов на контроллер. Если он установлен в true, изменении скрипта в проекте будут тут же загружаться на контроллер. Скрипт будет перезапускаться. Для работы фичи требуется запущенный dev-сервер.
      • source — Путь к коду модуля.
      • optimize — Если true, скрипт будет оптимизироваться средствами webpack.

    Для начала необходимо переименовать файл “scripts/blink.js” в “scripts/thermostat.js”. Теперь в коде определить константы.


    let MQTT_SERVER = 'wss://mqtt.eclipse.org:443/mqtt';

    Определяет адрес MQTT брокера. Нежелательно использовать этот сервер при реальной эксплуатации. Он не использует авторизацию. Любой может повлиять на работу вашей системы. Есть масса MQTT брокеров с регистрацией, которые вы сможете сможете использовать без риска.


    let CHIP_ID = $res.sys_info.chip_id;

    Это первый случай использования подключенного ресурса в скрипте. Все выделенные ресурсы доступны через глобальную переменную $res. Ключом ресурса является ключ указанный в манифесте. В данном случае через ресурс sys_info константа инициализируется уникальным идентификатором устройства. Он потребуется для определения топиков MQTT брокера.


    let TOPIC_TEMP = '/thingjs/' + CHIP_ID + '/temp';
    let TOPIC_TARGET_OUT = '/thingjs/' + CHIP_ID + '/target/out';
    let TOPIC_TARGET_IN = '/thingjs/' + CHIP_ID + '/target/in';
    let TOPIC_MODE_OUT = '/thingjs/' + CHIP_ID + '/mode/out';
    let TOPIC_MODE_IN = '/thingjs/' + CHIP_ID + '/mode/in';
    let TOPIC_MODE_STATE = '/thingjs/' + CHIP_ID + '/state';

    Константы определяют топики MQTT брокера. Некоторый топики имеют постфиксы “out” и “in”. Они указывают направление данных относительно контроллера. out — из контроллера, in — в контроллер.


    Константы режимов работы:


    // Включение нагрузки если значение целевой температуры выше фактической. Актуально для нагревателей.
    let MODE_LESS = 0;
    // Включение нагрузки если значение целевой температуры ниже фактической. Актуально для охладителей.
    let MODE_MORE = 1;
    // Принудительное включение нагрузки.
    let MODE_ON = 2;
    // Принудительное отключение нагрузки.
    let MODE_OFF = 3;

    Переменные:


    // Флаг установленного соединения с MQTT брокером
    let isConnected = false;
    // Текущий режим работы
    let mode = MODE_LESS;
    // Целевая температура
    let target = 32;
    // Состояние нагрузки
    let state = 0;
    // Адрес сенсора температуры
    let sensor = null;
    // Текущая температура
    let temp = null;
    // Если сенсор не найден, приложение будет эмулировать его наличие через эту переменную. Используется для демонстратора. В “боевом” приложении этот функционал следует удалить.
    let fakeVector = 0.5;

    Начинается самое интересное. Вызывается функция поиска датчиков на шине OneWire. В переменную sensor записывается адрес первого найденного датчика.


    $res.ds18x20.search(function (addr) {
       if (sensor === null) {
           sensor = addr;
       }
    });
    
    function publishState () {
       $bus.emit('thermostat-state', JSON.stringify({
           connected: isConnected,
           mode: mode,
           target: target,
           temp: temp,
           state: state,
           chip_id: CHIP_ID
       }));
    
       if (isConnected) {
           $res.mqtt.publish(TOPIC_MODE_OUT, JSON.stringify(mode));
           $res.mqtt.publish(TOPIC_TARGET_OUT, JSON.stringify(target));
           $res.mqtt.publish(TOPIC_MODE_STATE, JSON.stringify(state));
           $res.mqtt.publish(TOPIC_TEMP, JSON.stringify(temp));
       }
    }

    publishState публикует состояние термостата сразу в два места:


    • В шину данных UBUS. Эта шина является встроенной в платформу. Нет необходимости указывать потребность в ней в манифесте. Публикуемые данные получат все, кто подписался на это события. Как скрипты на стороне контроллера, так и на стороне frontend. Именно так скрипт на контроллере общается с frontend.
    • В MQTT брокер. Эта публикация является опциональной. Она происходит только при активном соединении с брокером.

    Далее устанавливаются обработчики событий MQTT клиента:


    // При установке соединения происходит подписка на топики
    $res.mqtt.onconnected = function () {
       print('MQTT client is connected');
       isConnected = true;
       $res.mqtt.subscribe(TOPIC_TARGET_IN);
       $res.mqtt.subscribe(TOPIC_MODE_IN);
       publishState();
    };
    
    // Отслеживается разрыв соединения и меняется online статус
    $res.mqtt.disconnected = function () {
       print('MQTT client is disconnected');
       isConnected = false;
       publishState();
    };
    
    // Получение данных по подписке с MQTT брокера
    $res.mqtt.ondata = function (topic, data) {
       print('MQTT client received from topic [', topic, '] with data [', data, ']');
       if (topic === TOPIC_TARGET_IN) {
           target = JSON.parse(data);
       } else if (topic === TOPIC_MODE_IN) {
           mode = JSON.parse(data);
       }
    };

    Получение данных из шины UBUS. События будут поступать только те, на которых оформлена подписка в манифесте.


    $bus.on(function (event, data) {
       if (event === 'tmst-set-target') {
           target = JSON.parse(data);
       } else if (event === 'tmst-set-mode') {
           mode = JSON.parse(data);
       }
       publishState();
    }, null);

    Ежесекундно происходит оценка текущей ситуации и принимается решения в соответствии с установленным режимом.


    $res.timers.setInterval(function () {
       if (sensor !== null) {
           $res.ds18x20.convert_all();
           temp = $res.ds18x20.get_temp_c(sensor);
       } else { // Fake temperature
           if (temp > 99) {
               fakeVector = -0.5;
           } else if (temp < 1) {
               fakeVector = 0.5;
           }
    
           temp += fakeVector;
       }
       // Refresh sensor data
       if (mode === MODE_ON) {
           state = 1;
       } else if (mode === MODE_OFF) {
           state = 0;
       } else if (mode === MODE_LESS) {
           if (temp < target) {
               state = 1;
           } else {
               state = 0;
           }
       } else if (mode === MODE_MORE) {
           if (temp > target) {
               state = 1;
           } else {
               state = 0;
           }
       }
    
       publishState();
       // По результату принятого решения нагрузка включается или выключается
       $res.relay.set(!state);
    }, 1000);

    Последние штрихи:


    // Устанавливается начальное значение температуры для демонстратора. 
    temp = 34.5;
    // Конфигурируется выход GPIO
    $res.relay.direction($res.relay.DIR_MODE_OUTPUT);
    // Публикуется начальное состояние
    publishState();
    // Запускается процесс установки соединения с MQTT сервером.
    $res.mqtt.connect(MQTT_SERVER);

    Мультиязычность


    В платформе предусмотрена мультиязычность. Она реализуется через VUE фильтры. Использование этой фичи выглядит так:


    <h1>{{ 'TITLE'|lang }}</h1>

    Языковые константы содержатся в файле langs.js и подключаются при инициализации frontend компонента.


    favicon


    Для узнаваемости приложений используются ассоциативные иконки. Иконка размещается в файле favicon.svg Иконка будет демонстрироваться пользователю на рабочей столе и при установке приложения.


    Сборка приложения


    Сборка приложений происходит пакетно. Т.е. собираются все приложения которые есть в папке /src/applications/. Для начала сборки необходимо выполнить стандартную команду npm


    npm run build


    Результатом станет генерация smt файлов. Одним из которых будет thermostat.smt Это и есть собранное и готовое к установке приложение Thermostat. Найти сборки можно в папке /dist/apps/



    Развертывание приложения на контроллере


    Тут все просто. Необходимо зайти на контроллер по WEB. Перейти в раздел “Настройки” и на плитке “Приложения” кликнуть ссылку “Установить приложение”. В открывшемся окне выбрать файл thermostat.smt.



    Когда файл будет выбран, платформа предложит распределить запрашиваемые приложением ресурсы. Некоторые требования не используют аппаратные ресурсы. К таким относятся, например, таймеры или MQTT клиент. Другие используют. Например, для подключения шины OneWire требуется указать UART порт и GPIO на которых будет реализована шина. Также, требуется определить GPIO на котором будет реле управляющее нагрузкой.


    Для удобства пользователя все назначения можно указать в поле default требования. В этом случае, пользователю останется только нажать кнопку “Установить”. При этом у него остается возможность переконфигурировать приложение по своему усмотрению.


    Такой подход решает несколько задач:


    1. Разделение ресурсов и многозадачность. На контроллере можно устанавливать несколько приложений распределяя между ними ресурсы.
    2. Безопасность и прозрачность. Пользователь имеет полную информацию о доступных ресурсах для приложения.
    3. Кроссплатформенность. Приложение заявляет о необходимых ресурсах, а предоставляет их конкретная реализация платформы на контроллере.
      Контроль совместимости. Приложение не будет установлено на контроллеры, которые не предоставляют обязательные для приложения ресурсы.

    При необходимости пользователь может удалить приложение. Переконфигурирование приложений решается через перестановку.


    Использование приложения


    Приложение делится на две секции: управление термостатом и интеграция по MQTT. Плашка управления содержит индикаторы текущей температуры, статус нагрузки и интеграции через MQTT.



    Ниже находятся элементы конфигурирования. Слева выпадающий список доступных режимов. Справа целевая температура.


    Плашка интеграции содержит необходимую информацию по подключению консоли удаленного управления к устройству. Ее использование подробно будет описано ниже.


    Подключение дистанционной консоли



    На плашке интеграции есть перечень рекомендованных приложений для телефонов Android. В примере будет рассмотрено приложение MQTT Dash.


    Для подключения консоли убедитесь, что устройство находится в сети. Об этом говорит наличие IP выданного вашей сетью. Найти его можно слева внизу.


    Установите понравившееся вам приложение. Создайте новое соединение с MQTT брокером.



    Войдите в созданное подключение. Откроется пустой дашборд. Теперь необходимо добавить виджеты. Для начала добавим текущую температуру.



    Информация для заполнения полей топиков находится на плашке интеграции. Для температуры есть только один топик — /thingjs/TJS-030BE4/temp Тут данные только для чтения.


    Сохраните параметры виджета. Если все прошло удачно, вы тут же увидите результат — текущую температуру с устройства.


    Теперь добавим управление целевой температурой (порогом).


    Здесь потребуется указать топик для входящих и исходящих данных. Информацию об этих топиках также на плашке интеграции. Постфиксы out и in определяют направление данных относительно контроллера.


    После того как вы добавите оставшиеся виджеты, вы получите управление термостатом с телефона из любого места, где доступен Интернет.


    Приятного использования!


    Безопасность


    В какой-то степени проект затевался ради созания островка безопасности в IoT. Дело в том, что сейчас огромная масса различных устройств из поднебесной, в которых используются прошивки о безопасности которых просто невозможно судить.


    Не для кого не станет сенсацией, если в очередной железке обнаружится бэкдор.


    Краеугольным камнем безопасности платформы являются манифесты. Если приложению нужен какой-то ресурс, он должен быть затребован в манифесте. При установке пользователь будет извещен о всех ресурсах, которые предполагает использовать приложение. Такой подход позволяет утверждать, что приложение будет прозрачным для пользователя.


    В перспективе планируется ЭЦП приложений и прошивок.


    Что дальше?


    • Поиск партнеров;
    • Стабилизация кода и повышение надежности;
    • Создание собственных облачных сервисов, где смогут хостится приложения;
    • Установка приложений на мобильные устройства;
    • Разработка и выпуск ThingJS dev-kit для любителя;
    • Интеграция с популярными IoT экосистемами;
    • Внедрение голосового управления.

    Ссылки


    Ресурсы проекта ThingJS:



    Репозитории проекта ThingJS:



    Где уже используется платформа:



    Где будет использоваться:



    Используемые проекты:


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      В выходные (03.04.20-04.04.20) планируется zoom конференция по платформе.
      Планируется:
      1. Дать пояснения в режиме реального времени по старту;
      2. Ответить на вопросы;
      3. Собрать обратную связь.

      Если есть желание, пишите в личку.

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

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