Постановка задачи:
Создание сложной автоматизированной системы на основе контроллера для управления различной периферией (электронные замки, двигатели, светодиодные ленты и прочая электроника).

Создание данной системы потребовалась для квест комнаты, подобной этой, но в городе Хабаровск.
Наш квест в ином сеттинге, но в целом имеет примерно тот же набор исполнительных механизмов: реле, замки, ленты, герконы и т.д.

Основные требования к системе:
  • Надежность — при разработке сложных систем высока вероятность допустить трудно уловимые ошибки, чем больше код тем больше шанс пропустить ошибку и тем больше времени нужно на отладку, необходимо свести к минимуму вероятность некорректной работы.
  • Гибкость — возможность с минимальными временными затратами изменить логику работы
  • Функциональность — управление любым оборудованием и подключение любых сенсоров

image

В нашем квесте требовался достаточно сложный сценарий, в особенности:
  • Нелинейность сюжетной линии-игроки могут найти несколько различных путей решения головоломок,
  • Связанность — некоторые события должны происходить только после свершения ряда других игровых событий

image

Решение всех проблем было найдено во многом благодаря статье о «Парадигме ситуационно-ориентированного программирования», применив данный подход я получаю «из коробки»:
  • Гибкость — достаточно правильно построить архитектуру событий
  • Надежность — т.к. все события однотипны они могут быть реализованы схожим образом, используя простой код, надежность которого можно гарантировать
  • Функциональность — события вызывают действия, которые могут быть реализованы любым образом

Таким образом архитектуру системы можно построить на основе 3 видов базовых конструкций:
  • Событие,
  • Триггер,
  • Действие

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

Благодаря выделения действий в отдельный функциональный блок получаем возможность еще на этапе разработки провести тестирование программного кода каждого действия по отдельности, отладку такого кода произвести намного легче, чем при отладке всего проекта. На отлаженный код уже будут ссылаться триггеры.

image

Триггеры в качестве условия срабатывания могут использовать любое сочетание событий или состояний триггеров (и\или\не).

События могут быть любыми (состояния портов, сообщения в serial и тд), в текущей реализации достаточно было состояния портов, поэтому пока оставлен только этот вариант, расширения функционала не займет много времени.
При создании системы было решено использовать доступное на местном рынке оборудование, таковыми оказались только платы Arduino, учитывая проблемы с доставкой почтой из других регионов факт наличия на рынке запасных частей имеет значение.

Приложение настроено на генерации кода скетча для данной платформы, но при желании адаптация под любой другой язык программирования займет лишь пару минут.
Таким образом шаблоны для скетча Arduino будут выглядеть в простейшем виде например так:
Основной код программы
@init
void setup() {
@runonce
}

void loop() { 
@loopcode
}

@triggers
@sensors
@actions


Шаблон кода действия
//@description
void @name()
{	
bool debug=true;
//@id
@code
//@id
  if (debug) {
    Serial.println("DoAction @name");
  }
}


Шаблон кода триггера
//@description
bool @nameActivated=false;
bool @name(){
	if (@nameActivated){
		return true;
	}else
	{	
		if (@event){
			@nameActivated=true;
			Serial.println("@name Activated");
			@nameDoAction();
			return true;
		}
		return false;
	}	
	return false;
}

void @nameDoAction(){
@nameActivated=true;
//******************************************
@actions
//******************************************
}


Шаблон кода сенсора
//@description
bool @name()
{
	int @namePin=@pinNumber;
	pinMode(@namePin, INPUT_PULLUP);
	int sensorVal = digitalRead(@namePin);
	if (sensorVal == @trueval) {return true;}else{return false;}
}


Пример шаблона инициализации
В данном шаблоне можно подключить библиотеки и объявить глобальные для всего проекта переменные и функции.
#include <etherShield.h>
#include <ETHER_28J60.h>
#include <EEPROM.h>
static uint8_t mac[6] = {0x54, 0x55, 0x58, 0x10, 0x10, 0x24};  
static uint8_t ip[4] = {192, 168, 137, 15};    
static uint16_t port = 80;  
ETHER_28J60 ethernet;
bool started=false;


На основе данных шаблонов в окне приложения можно задать все параметры сценария.

image

После экспорта прошивки мы можем получить подобный код:
Очень много букв скетча


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

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

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

Тестовое приложение с типовыми настройками шаблонов можно найти на github.

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

Проголосовало 67 человек. Воздержалось 28 человек.

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

Комментарии (10)


  1. occam
    31.10.2015 21:20
    +2

    Что за сила ваш топик выкатила на главную?


    1. vpuhoff
      01.11.2015 03:19

      Думаю это зависит от того на какие хабы вы подписаны, не уверен точно как осуществляется отбор, но он явно от этого зависит.


  1. dtestyk
    31.10.2015 22:21
    +1

    Были ли у вас случаи, когда для срабатывания триггера нужно не просто множество событий, а некоторая их частичная упорядоченность по времени? Как боролись: введением вспомогательных триггеров?


    1. vpuhoff
      01.11.2015 03:13

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

      1. просто использовать delay или его аналог, подходит к случаям когда заранее известно, что ничего другого в это время не произойдет.
      2. вспомогательные триггеры, тут несколько вариантов, наиболее простой в реализации это добавление счетчиков, значение которых обновляется в соответствии с прошедшим временем (по millis к примеру), в таком случае условием для триггера будет значение этого счетчика. Похожее у нас используется для событий по времени.


      1. dtestyk
        01.11.2015 18:57
        +1

        при срабатывании триггера должно произойти несколько действий
        Не совсем это имел ввиду: «триггер» A должен сработать, когда сначала был открыт кран, а затем включен насос, а не просто когда кран открыт и мотор включен. Причем, насос может включаться вручную. Т.е сам по себе триггер на открытие крана не нужен. А ведь может быть еще и другой «триггер» В(аварийный), который срабатывает когда, сначала включается мотор, а потом открывается кран.
        история вопроса
        Предложил использовать событийный адрес, но не смог придумать убедительного примера. В комментарие к своей предыдущей статье автор Парадигма ситуационно-ориентированного программирования написал:
        Понятие «событийный адрес» не нужно.
        Мне кажется, случай с насосом как раз является таким примером. Хотя если и он не достаточно показателен(можно просто по событию включения проверять открытость крана), можно предствить цепочку из трех и более событий.
        P.S. Чтобы лишний раз не путаться в терминах, можно назвать «триггеры» А и В, например, «метатриггеры».


        1. vpuhoff
          02.11.2015 00:42

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


          1. dtestyk
            02.11.2015 03:28

            Спасибо, думал в этом направлении. Также нужны триггеры на обратные действия. Ведь мотор может включаться и выключаться несколько раз, также как и кран может открываться и закрываться. Возникает разделение триггеров на ключевые(активные), приводящие к действиям и вспомогательные(информативные), работающие в паре(открытие/закрытие, включение/выключение) и запоминающие время и последнее состояние.

            Другой вариант: для каждого событийного адреса на основании предыдущих состояний формировать ожидаемые(несколько, так как инициирующее(первое в цепочке) событие может произойти еще раз после начала отслеживания) состояния и сравнивать их с текущим. При совпадении по маске, продолжать отслеживать цепочку. При наступлении ключевого события сработает «метатриггер». Из плюсов этого варианта — большая скорость реагирования, поскольку не нужно смотреть историю — мы ожидали ключевое событие.

            На самом деле, возможна произвольная комбиная отслеживания и проверки истории. Событие, при котором происходит и проверка истории и запуск отслеживания можно назвать базовым. Но можно пойти еще дальше и допустить несколько базовых событий(дерево точек отсчета как вперед так и назад во времени), сброс ожидания событий по таймеру…

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


            1. vpuhoff
              02.11.2015 12:16

              Все так, сразу видно человека, который «в теме») По сути информативный и ключевой ничем не отличаются, просто информативный «ничего не делает». Второй вариант выглядит довольно красивым, но уже достаточно сильно зависит от языка реализации, в текущей реализации программа пригодна для любых языков с похожим на Си синтаксисом, если затачивать под конкретную платформу можно построить любой алгоритм выбора триггера. В текущей реализации можно поступить похожим образом, используя только более правильную «сортировку» триггеров. Метатриггер это насколько я понял просто триггер который зависит от других триггеров, такой в текущей реализации уже есть. По поводу сброса тоже думал, можно было реализовать на уровне приложения, но тогда это ставит жесткие рамки, по сути для сброса достаточно скинуть bool значение на false чтобы активировать триггер, это можно сделать из любого действия или вынести на уровень шаблона кода. Похожее я использовал для удаленного управления, все свободное от работы время контроллер слушает сеть на наличие команд.


  1. StarHunter
    05.11.2015 22:24

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


    1. vpuhoff
      06.11.2015 01:05

      Это очень хорошее решение для задач с идеальным ТЗ, в которых не может быть отклонений от изначально поставленной задачи. К примеру пример из практики, изначально было N устройств и определенная схема взаимодействия, к концу разработки было уже N+6 устройств и совсем другая схема взаимодействия. В случае с релейной логикой пришлось бы для каждого случая с нуля паять всю логику (которая была бы очень не простой). В случае с моим решением достаточно поменять пару триггеров в интерфейсе, чтобы получить полностью готовое решение с учетом изменений.