Товары из Китая

Делаем секционные гаражные ворота более удобными


Делаем секционные гаражные ворота более удобными

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

Все описанное применимо ко многим типам приводов, в частности все работает на воротах фирмы Hormann.

Краткое описание исходной ситуации.

Имеется подземный гараж с двумя воротами. Одни ворота располагаются вверху (при въезде на спуск), вторые ворота располагаются внизу (непосредственно, при въезде в сам гараж). Ворота оснащены электрическими приводами, управляются с радио брелоков. Чтобы было понятнее, приведу иллюстрацию:

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

Недостатки исходного решения:

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

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

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

— брелоки работают на частоте 433 МГц без обратной связи, данные передают в нешифрованном виде, что может привести к копированию кода брелока и неприятным последствиям

— даже находясь в гараже требуется применять брелок чтобы выйти на улицу и зайти обратно, а брелоки имеют тенденцию к потерям/поломкам, просадке батарейки и тп…

— потеря брелока на улице требует провести процедуру стирания всех брелоков и прописывания только существующих и дополнительного

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

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

Например, некоторые производители продают свои решения для управления воротами через интернет, но цена у таких решений весьма высока (особенно учитывая что в данном случае 2 независимых механизма ворот), они требуют установки своих приложений, завязаны на серверную инфраструктуру производителя ворот (а в наше время это тоже риски), имеют ряд ограничений (например, интерфейс уже сделан и его нельзя изменить). По брелокам без переменного кода — тоже есть решения, заменить на модули с переменным кодом и/или обратной связью. В качестве кнопок, например, можно использовать беспроводные выключатели — но останется проблема безопасности радио эфира. По обратной связи — можно конечно поставить какие-то датчики, не связанные с приводом ворот — но они опять потребуют какую-то инфраструктуру. Кнопки можно подключить напрямую к приводам ворот, но как показали тесты они чувствительны к времени нажатия и дребезгу контактов.

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

Базовое описание решения

В качестве ядра системы решено использовать контроллер esp8266, который весьма бюджетен, имеет достаточную производительность, достаточное количество выводов, удобные среды разработки и встроенный интерфейс сетевого взаимодействия по WiFi. Большинство приводов ворот имеют встроенные кнопки для управления воротами, либо отдельные выводы для подключения устройств управления, изменение состояние происходит замыканием на короткий промежуток времени контактов, привод при этом меняет свое состояние (при закрытых воротах — начинается открывание, при открытых закрывание, при движении — происходит остановка привода. Для воздействия на эти контакты достаточно маломощного реле. Для отслеживания состояния ворот удобно использовать внешние герконы, которые весьма надежны, компактны и не требуют обслуживания. Наличие кнопок подключенных к контроллеру позволит на месте удобно управлять приводами. Наличие WiFi обеспечит связь нашего решения с внешним миром, интеграцию в инфраструктуру «умного дома» и поддержку нужного веб-интерфейса.

Устройство должно уметь работать с обоими приводами ворот, иметь малое энергопотребление.

Создание «железной» части устройства

В данном случае, нам потребуется контроллер (возьмем модуль esp-07), обвязка модуля согласно документации, два маломощных надежных реле (например HF49FD), подтягивающие резисторы датчиков и кнопок, питаться устройство будет от 5 Вольт постоянного тока, поэтому требуется понижающий преобразователь для питания контроллера от 3.3 Вольт, можно взять AMS1117. Для стабилизации питания установим электролитический и керамический конденсаторы. Для управления реле используем маломощные биполярные транзисторы 2N2222, управляющие выводы реле зашунтируем диодами M1. Принципиальная схема устройства выглядит таким образом:

Делаем секционные гаражные ворота более удобнымиR1, R2, R3 — подтягивающие резисторы, необходимы для корректной работы ESP-07

R4, R5, R9, R12 — подтягивают соответствующие выводы ESP-07 к питанию для корректной работы кнопок.

VR1(AMS1117) — линейный стабилизатор на 3.3 В

C1, C2 — электролитический и керамический конденсаторы для питания модуля

R6, HL3 — светодиодный сигнал наличия связи модуля по wifi

VT1, VT2 — биполярные транзисторы 2n2222, нужны для управления реле

R10, R11 — ограничивают ток управления транзисторами

R7, HL1 и R8, HL2 — для световой индикации состояния реле

S1, S2, S3, S4 — будущие кнопки: выключатели и герконы состояния ворот.

Разрабатываем нехитрую печатную плату, на которой предусмотрим все вышеописанное:

Делаем секционные гаражные ворота более удобнымиПлатка универсальная, поэтому предусмотрены:

— 6 выводов питания 5 Вольт

— 8 выводов земли (-)

— 6 выводов питания питания 3.3 В

— 2 вывода модуля ESP-07: 4 и 5 задействованы в управлении реле

— выводы 13, 12, 14, 2 и аналоговый вывод подключены к общему разъему с возможностью подтяжки к питанию

— 0 вывод расположен между подтяжками к земле и к питанию, для обеспечения заданных подтяжек, в зависимости от режима (программирование, обычная работа)

— также на общий разъем выведены TX и RX для взаимодействия с внешним миром

— питание устройства и выводы реле подключены к винтовым клемникам

— добавлена индикация включения реле в виде светодиодов с токоограничительными резисторами

— шелкография платы позволяет легко ориентироваться в расположении контактов

— круглые отверстия позволяют легко производить фиксацию платы в корпусе

Печатная плата в виде герберов была отправлена производителю плат (JLCPCB) и получены вот такие изделия:

Делаем секционные гаражные ворота более удобнымиДелаем секционные гаражные ворота более удобнымиПервым делом требуется произвести монтаж SMD-элементов, поэтому берем паяльную пасту в шприце и наносим с помощью пневматического дозатора (решение по кнопке дозатора описано здесь).

Делаем секционные гаражные ворота более удобнымиНажимая кнопку на шприце получаем капельки и колбаски такого вида:

Делаем секционные гаражные ворота более удобнымиДалее расставляем детальки на плату, мне удобно это делать с помощью такого пинцета:

Делаем секционные гаражные ворота более удобнымиПосле расстановки помещаем плату в «печку» и производим запекание согласно термопрофилю:

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

Результат запекания:

Делаем секционные гаражные ворота более удобнымиСам модуль контроллера и ряд даталек я предпочитаю пропаять еще паяльником. Для этого задействуем паяльную станцию:

Делаем секционные гаражные ворота более удобнымиРезультат получился вот такой:

Делаем секционные гаражные ворота более удобнымиДалее нужно припаять выводные элементы и контактные штыри. Я уже говорил об используемом реле, выглядит оно так:

Делаем секционные гаражные ворота более удобнымиИтог пайки выглядит так:

Делаем секционные гаражные ворота более удобнымиПереходим к водным процедурам, нужно смыть флюс, оптимальным для этого является ультразвуковая ванна, с последующим полосканием в дистиллированной воде. В качестве средства отмывки я использую Solins FA+ или Solins US:

Делаем секционные гаражные ворота более удобнымиИтог после мойки платы и сушки бытовым феном:

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

Пару слов про перемычки. Мне гораздо удобнее вот такие удлиненные перемычки, особенно на этапе отладки:

Делаем секционные гаражные ворота более удобными

Подготовка модуля

Первым делом подключим USB-to-Serial конвертер и переведем перемычку в положение заливки программы:

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

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

Делаем секционные гаражные ворота более удобнымиУдобный магнитик для подвижной части:

Делаем секционные гаражные ворота более удобными

Прежде чем что-то делать, заливаем пустой файл, чтобы убрать все программные ценности идущие от производителя:

Делаем секционные гаражные ворота более удобнымиПроцесс пошел:

Делаем секционные гаражные ворота более удобнымиИмеем готовый к новому коду модуль и приступаем к разработке логики.

Для разработки я буду использовать среду Visual Studio Code, вполне адекватная среда с большим количеством возможностей. Описывать подробно как настроить среду разработки для работы с PlatformIO я не буду. Скажу только что настроенная среда позволяет:

— компилировать программу под заданную архитектуру

— загружать как прошивку в модуль

— собирать образ файловой системы

— загружать образ в модуль

— вести отладку программы через встроенный терминал

Так выглядит панелька через которую все это делается:

Делаем секционные гаражные ворота более удобнымиМой файлик platformio.ini:

[env:esp07]

platform = espressif8266
board = esp07
framework = arduino
board_build.filesystem = littlefs
monitor_speed = 115200

Требования

Нам требуется:

— работа с сетью (WiFi как в режиме Station, так и в режиме AP)

— интерфейс должен быть адаптирован под мобильные устройства, легким и отображать все необходимое

— хочется связать web-интерфейс с модулем, чтобы оперативно понимать текущее состояние ворот (открыли кнопкой или на другом устройстве, а наш интерфейс оперативно отрисовал нужные изменения без перезагрузки страницы)

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

— управление нагрузками (2 реле и светодиод индикации состояния wifi-соединения)

— работа с конфигурационной информацией, которую требуется хранить в энергонезависимой памяти (сетевые настройки, настройка таймера реле)

— манипуляции с состоянием устройства во время его работы (safe-mode, закрыты или открыты ворота, доступна ли сеть)

— обеспечение API для того чтобы другие устройства могли общаться с нашим

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

Программная реализация

Структура проекта и рабочая область выглядят так:

Делаем секционные гаражные ворота более удобными

Чтобы удобнее было что-то рассказывать, приведу содержимое файлика main.cpp
#include "button_control.h"

#include "config.h"
#include "load_control.h"
#include "network.h"
#include "state.h"
ADC_MODE(ADC_VCC);
Config CFG;
State ST;
LoadsControl LC;
ButtonControl BC;
NetManager NM;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// init
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
// init state
ST.init();
// read config from EEPROM
CFG.get();
// init loads
LC.init();
// init buttons
BC.init();
// init network
NM.init();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// main loop
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
// update time
ST.updateClock();
// loads control
LC.control();
// reaction fo change buttons state
BC.logic();
// controlling change state buttons
BC.check();
// network control
NM.control();
}

Конструкция ADC_MODE(ADC_VCC); нужна для корректного программного измерения напряжения питания, что особенно актуально при питании от батарей.

Основные сущности нашей программы представлены объектами:

— CFG — хранит конфигурационную информацию и позволяет манипулировать ей

— ST — содержит текущее состояние системы и обеспечивает доступ к нему на запись/чтение из вне в разных формах

— LC — содержит информацию о нагрузках, позволяет менять их состояние и управлять поведением

— BC — манипулятор кнопок

— NM — менеджер сети (инициализация, контроль, обработка запросов)

В функции setup, которая выполняется один раз при старте программы, производится инициализация всех объектов.

В функции loop, которая выполняется в бесконечном цикле, реализовано поведение объектов в процессе жизни устройства.

Далее покажу некоторые принципиальные моменты по работе программы.

Объявление и реализация работы с конфигурационными файлами:

Заголовочный файл config.h
#ifndef CONFIG_H

#define CONFIG_H
#include <Arduino.h>
// Config
class Config {
public:
char host[32];
char user[16];
char pass[16];
bool wifimode;
char ssid[16];
char ssidpass[16];
bool ipmode;
char ip[16];
char gateway[16];
char subnet[16];
char primaryDNS[16];
char secondaryDNS[16];
uint32_t maxon;
// read config from EEPROM, if this is the first start, then write
void get();
// write config to EEPROM
void put();
};
extern Config CFG;
#endif // CONFIG_H

Видим что и как хранится в энергонезависимой памяти нашего модуля.

Файл реализации config.cpp
#include "config.h"

#include <EEPROM.h>
#define INIT_EEPROM_ADDR 0
#define INIT_EEPROM_KEY 1
#define SAVE_EEPROM_ADDR 1
#define EEPROM_MAX_SIZE 512
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// read config from EEPROM, if this is the first start, then write the value in
// EEPROM
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void Config::get() {
EEPROM.begin(EEPROM_MAX_SIZE);
if (EEPROM.read(INIT_EEPROM_ADDR) != INIT_EEPROM_KEY) { // first start
EEPROM.write(INIT_EEPROM_ADDR, INIT_EEPROM_KEY); // write key
// set default values
strcpy(host, "WIFI-RELAY");
strcpy(user, "user");
strcpy(pass, "pass");
wifimode = 1;
strcpy(ssid, "xxx");
strcpy(ssidpass, "xxxxxxx");
ipmode = 1;
strcpy(ip, "192.168.1.189");
strcpy(gateway, "192.168.1.1");
strcpy(subnet, "255.255.255.0");
strcpy(primaryDNS, "192.168.1.1");
strcpy(secondaryDNS, "8.8.8.8");
maxon = 2000;
// save
put();
}
EEPROM.get(SAVE_EEPROM_ADDR, CFG);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// write config to EEPROM
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void Config::put() {
EEPROM.put(SAVE_EEPROM_ADDR, CFG);
EEPROM.commit();
}

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

Заголовочный файл класса состояния устройства — state.h
#ifndef STATE_H

#define STATE_H
#include <Arduino.h>
#include "network.h"
#undef DEBUG
#define DEBUG // for debug: DPRINT & DPRINTLN
#ifdef DEBUG
#define DPRINT(...) Serial.print(__VA_ARGS__) // debug print
#define DPRINTLN(...) Serial.println(__VA_ARGS__) // debug print with new line
#define DPRINTF(...) Serial.printf(__VA_ARGS__) // debug printf
#else
#define DPRINT(...) // blank line
#define DPRINTLN(...) // blank line
#define DPRINTF(...) // blank line
#endif
// return amount object
template <typename T, size_t n>
inline size_t arraySize(const T (&arr)[n]) {
return n;
}
class State {
public:
// set start state
void init();
// get safe mode
inline bool isSafeMode() { return FLAGS.safeMode; }
// get safe mode text
inline String getSafeModeText() { return FLAGS.safeMode ? "ON" : "OFF"; }
// set open gate by num
inline void open(uint8_t num) {
extern NetManager NM;
if (num == 0)
FLAGS.firstState = true;
else if (num == 1)
FLAGS.secondState = true;
NM.sendEvents(getData(num), "change");
}
// set close gate by num
inline void close(uint8_t num) {
extern NetManager NM;
if (num == 0)
FLAGS.firstState = false;
else if (num == 1)
FLAGS.secondState = false;
NM.sendEvents(getData(num), "change");
}
// get state gate by num
inline char* getData(uint8_t num) {
static char _return[24];
sprintf(_return, "{"num":%d,"state":%d}", (num + 1),
(uint8_t)getState(num));
return _return;
}
// get state gate by num
inline bool getState(uint8_t num) {
if (num == 0)
return FLAGS.firstState;
else if (num == 1)
return FLAGS.secondState;
return false;
}

// get state gate Text by num
inline String getStateText(uint8_t num) {
bool flag = FLAGS.firstState;
if (num == 1) flag = FLAGS.secondState;
return flag ? "ОТКРЫТО" : "ЗАКРЫТО";
}
// get color gate by num
inline String getColor(uint8_t num) {
bool flag = FLAGS.firstState;
if (num == 1) flag = FLAGS.secondState;
return flag ? "#F08080" : "#98FB98";
}
// on flag wifi connect
inline void setConnect() { FLAGS.connectWIFI = true; }
// off flag wifi connect
inline void setDisconnect() { FLAGS.connectWIFI = false; }
// get flag wifi connect
inline bool isConnect() { return FLAGS.connectWIFI; }
// fill current time
void updateClock();
// get low part current time
inline uint32_t getTime() { return lowPartCurrentTime; }
// get full current time
inline uint64_t getFullTime() {
return (uint64_t)highPartCurrentTime << 32 | lowPartCurrentTime;
}
// get uptime string
char *getUptimeText();
private:
// low part uptime
uint32_t lowPartCurrentTime;
// high part uptime
uint32_t highPartCurrentTime;
// flags state
struct {
bool safeMode : 1; // run without password in AP mode
bool firstState : 1; // state near gate
bool secondState : 1; // state far gate
bool connectWIFI : 1; // is connect to WIFI
} FLAGS;
};
extern State ST;
#endif // STATE_H

Так как заголовочный файл класса состояний подключается во всем проекте, то я сюда вынес немногочисленные общие для всего проекта элементы. В частности:

— механизм включения и выключения отладочной информации

— универсальную функцию подсчета элементов в массиве любого типа

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

Реализация методов состояния устройства — state.cpp
#include "state.h"

#define SERIAL_SPEED 115200
#define SERIAL_DATA 45
#define SERIAL_TIME_SLEEP 1000
#define SERIAL_TIME_WAIT 10
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// first init state
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void State::init() {
Serial.begin(SERIAL_SPEED);
#ifdef DEBUG
// Serial.setDebugOutput(true);
delay(SERIAL_TIME_SLEEP);
#endif
DPRINTLN(F("start"));
Serial.write(SERIAL_DATA);
delay(SERIAL_TIME_WAIT);
uint8_t incomingByte = 0;
if (Serial.available() > 0) {
incomingByte = Serial.read();
if (SERIAL_DATA == incomingByte)
FLAGS.safeMode = true;
else
FLAGS.safeMode = false;
}
updateClock();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set current time
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void State::updateClock() {
uint32_t newCurrentTime = millis();
if (newCurrentTime < lowPartCurrentTime) highPartCurrentTime++;
lowPartCurrentTime = newCurrentTime;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// get string uptime
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
char* State::getUptimeText() {
uint64_t mills = getFullTime();
static char _return[32];
uint32_t secs = mills / 1000;
uint32_t mins = secs / 60;
uint16_t hours = mins / 60;
uint16_t days = hours / 24;
mills -= secs * 1000;
secs -= mins * 60;
mins -= hours * 60;
hours -= days * 24;
sprintf(_return, "%d days %2.2d:%2.2d:%2.2d %3.3d", (uint8_t)days,
(uint8_t)hours, (uint8_t)mins, (uint8_t)secs, (uint16_t)mills);
return _return;
}

Пара неочевидных моментов. Для определения находится ли наше устройство в защищенном режиме или нет используется следующий подход. Сам перевод устройства в защищенный режим осуществляется замыканием перемычкой выводов TX и RX. При старте мы посылаем в последовательный некое число и если выводы соединены, то ожидаем считать, то же самое число, если так происходит, значит ставим флаг работы в защищенном режиме (в этом режиме все доступно без пароля и модуль работает в режиме точки доступа без пароля). Второй момент заключается в счетчике времени. Захотелось знать время непрерывной работы устройства. Стандартный счетчик времени основанный на типе uint32_t переполняется и нам не подходит. Поэтому введем еще одну переменную типа uint32_t, при переполнении первой меняем на 1 вторую. Такого времени хватит на долго :). Также имеется функция которая возвращает время непрерывной работы (UPTIME) устройства в понятном текстовом виде.

Заголовочный файл класса сетевого менеджера — network.h
#ifndef NETWORK_H

#define NETWORK_H
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
class NetManager {
private:
static String processor(const String &var);
void connect();
void setAnsvers();
public:
void init();
void control();
void sendEvents(const char *data, const char *tag);
void postProcessing(AsyncWebServerRequest *request);
};
#endif // NETWORK_H

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

Реализация метода устанавливающего обработчики запросов NetManager::setAnsvers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// setting ansvers for network requests
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void NetManager::setAnsvers() {
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
request->send(LittleFS, "/index.html", String(), false, processor);
});
// Route for /set web page
server.on("/set", HTTP_ANY, [this](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
if (request->method() == HTTP_POST) {
postProcessing(request);
}
request->send(LittleFS, "/settings.html", String(), false, processor);
});
// Route for /reset web page
server.on("/reset", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
request->send(LittleFS, "/settings.html", String(), false, processor);
ESP.restart();
});
// Button 1 control
server.on("/button1", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
LC.on(L_NEAR);
request->send(200, "application/json", "{'ok':1}");
});
// Button 2 control
server.on("/button2", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
LC.on(L_FAR);
request->send(200, "application/json", "{'ok':1}");
});
// api
server.on("/sensors", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->authenticate(CFG.user, CFG.pass) && !ST.isSafeMode())
return request->requestAuthentication();
String answer = "hostname:" + String(CFG.host);
answer += ";GATE1:" + String(ST.getState(0));
answer += ";GATE2:" + String(ST.getState(1));
answer += ";";
request->send(200, "text/html", answer);
});
// route roe all static files
server.serveStatic("/", LittleFS, "/").setCacheControl("max-age:6000000");
// if page not found
server.onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "404: Not found");
});
}

Как видно, все запросы, которые выдают какие-то полезные данные, либо меняют состояние системы требуют авторизации. Авторизация требуется только в том случае, если не включен защищенный режим. Для хранения файлов я использовал файловую систему LittleFS.

По запросу ‘/set’ выдается страница с настройками, при этом, если запрос типа ‘POST’, то осуществляется обработка принятых от клиента данных. При запросе файлов index.html и settings.html они прогоняются через простенький шаблонизатор, где переменные заменяются своими значениями.

Заголовочный файл класса работы с нагрузками — load_control.h
#ifndef LOAD_CONTROL_H

#define LOAD_CONTROL_H
#include <Arduino.h>
class LoadsControl {
public:
// init load control
void init();
// set on
void on(uint8_t num);
// set off
void off(uint8_t num);
// check
void control();
// get state load
inline bool isOn(uint8_t num) { return L[num].flag.state; }
private:
// one load
struct Load {
uint8_t pin; // pin load
uint32_t start; // start time
// flags for load
struct LoadFlag_t {
bool state : 1;
bool autoOff : 1;
} flag; // load's flags
};
// array loads
static Load L[];
// amount loads
uint8_t countL = 0;
};
extern LoadsControl LC;
enum loadNum_t {
L_NEAR, // 0
L_FAR, // 1
L_LED, // 2
};
#endif // LOAD_CONTROL_H

Собственно, видны методы инициализации нагрузок, включения и выключения нагрузки по номеру. Каждая нагрузка имеет пару флагов: состояние и нуждается ли она в автоматическом отключении.

Метод обработки нагрузок, вызываемый в loop — LoadsControl::control
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// control loads
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void LoadsControl::control() {
// auto off relay
for (uint8_t i = 0; i < countL; i++) {
if (L[i].flag.state && L[i].flag.autoOff && CFG.maxon &&
((ST.getTime() - L[i].start) > CFG.maxon)) {
off(i);
}
}
// switching the connection led
if (ST.isConnect() && !L[L_LED].flag.state) {
on(L_LED);
} else if (!ST.isConnect() && L[L_LED].flag.state) {
off(L_LED);
}
}

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

Заголовочный файл класса работы с кнопками — button_control.h
#ifndef BUTTON_CONTROL_H

#define BUTTON_CONTROL_H
#include <Arduino.h>
struct Button {
uint8_t pin;
bool state; // current state
uint32_t startChange; // time begin change
uint32_t timeProtect; // bounce protect timer
bool change;
};
class ButtonControl {
private:
uint8_t countB;
static Button B[];
public:
void init();
void check();
void logic();

inline uint8_t getCount() { return countB; }
inline bool isChangeDown(uint8_t num) {
return B[num].change && !B[num].state;
}
inline bool isChangeUp(uint8_t num) {
return B[num].change && B[num].state;
}
};
enum buttonNum_t {
BT_FIRST, // 0
BT_SECOND, // 1
BT_FIRST_SENS, // 2
BT_SECOND_SENS, // 3
};
#endif // BUTTON_CONTROL_H

Метод check проверяет наличие изменений состояния кнопок, а метод logic — реализует логику в зависимости от этих изменений. Про работу с кнопками я как то писал тут.

Я кратко описал основные моменты работы программы на серверной стороне. Теперь про клиент.

Для того, чтобы интерфейс выглядел более-менее нормально и в то же время не занимал много места и работал быстро, я взял набор стилей PureCSS. Обработку логики на стороне клиента реализовал на чистом javascript, без использования сторонних библиотек.

Основной файлик интерфейса нашей системы — index.html
<!DOCTYPE html>

<html>
<head>
<meta charset="UTF-8">
<title>Управление воротами</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
</head>
<body>
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item pure-menu-selected">
<a href="/" class="pure-menu-link">Управление</a>
</li>
<li class="pure-menu-item">
<a href="/set" class="pure-menu-link">Настройки</a>
</li>
</ul>
</div>
<div class="pure-g">
<div class="pure-u-2-3">
<div>
<h1>СОСТОЯНИЕ ВОРОТ</h1>
<table class="pure-table pure-table-bordered">
<tbody>
<tr>
<td>УЛИЦА</td>
<td id="gate1" style="background-color:%BGCOLOR1%;">%GATE1%</td>
</tr>
<tr>
<td>ГАРАЖ</td>
<td id="gate2" style="background-color:%BGCOLOR2%;">%GATE2%</td>
</tr>
</tbody>
</table>
<h1>УПРАВЛЕНИЕ ВОРОТАМИ</h1>
<p>
<button name="button1" class="pure-button pure-button-primary button-xlarge">УЛИЦА</button>
</p>
<p>
<button name="button2" class="pure-button pure-button-primary button-xlarge">ГАРАЖ</button>
</p>
</div>
</div>
</div>
</body>
<script>
const buttons = document.querySelectorAll(".pure-button");
buttons.forEach(function (bt) {
bt.addEventListener('click', function (event) {
bt.classList.remove("pure-button-primary");
fetch('/' + bt.name).then(function (response) {
if (response.status == 200) {
setTimeout(() => {
bt.classList.add("pure-button-primary");
}, 800);
} else {
alert("Error HTTP: " + response.status);
}
})
.catch(function (err) {
alert("Error network: " + err);
});
})
})
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', (e) => {
console.log("Events Connected");
}, false);
source.addEventListener('error', (e) => {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('change', (e) => {
console.log(e.data);
const data = JSON.parse(e.data);
const { num, state } = data;
const id = 'gate' + num;
document.getElementById(id).innerHTML = state ? 'ОТКРЫТО' : 'ЗАКРЫТО';
document.getElementById(id).style.backgroundColor = state ? '#F08080' : '#98FB98';
}, false);
}
</script>
</html>

У нас имеется небольшое верхнее меню состоящие из 2-х пунктов: Управление и Настройки, выбранный пункт меню подсвечивается. Далее идет небольшая табличка с текущим состоянием ворот. Данный файл, как и файл настроек на стороне сервера обрабатывается простеньким шаблонизатором, где на место %VAR% подставляется значение VAR. В данном файле подставляется состояние ворот и цвет состояний, для удобного визуального контроля. Так сказазать «Server-Side Rendering». После этого расположены две кнопки изменения состояния ворот.

Клиентская часть кода, как уже писал ранее написана на javascript. В данном файле всем кнопкам с классом ".pure-button" ставится обработчик нажатия, по нажатию на сервер с помощью функции fetch уходит запрос с именем кнопки. Во время нажатия кнопка меняет свой цвет, чтобы визуально понять что произошло нажатие. При получении ответа с кодом 200 исходный цвет кнопки восстанавливается через 800мс (иначе трудно уловить, что что-то произошло.

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

Интерфейс настройки устройства — settings.html
<!DOCTYPE html>

<html>
<head>
<meta charset="UTF-8">
<title>Управление воротами</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
</head>
<body>
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a href="/" class="pure-menu-link">Управление</a>
</li>
<li class="pure-menu-item pure-menu-selected">
<a href="/set" class="pure-menu-link">Настройки</a>
</li>
</ul>
</div>
<div class="pure-g">
<div class="pure-u-2-3">
<div>
<h1>STATE</h1>
<table class="pure-table pure-table-bordered">
<tbody>
<tr>
<td>Safe mode</td>
<td>%SAFEMODE%</td>
</tr>
<tr>
<td>RSSI</td>
<td>%RSSI% dBm</td>
</tr>
<tr>
<td>Free memory</td>
<td>%MEM% B</td>
</tr>
<tr>
<td>Voltage</td>
<td>%VOLTAGE% mV</td>
</tr>
<tr>
<td>Uptime</td>
<td>%UPTIME%</td>
</tr>
</tbody>
</table>

<h1>SETTINGS</h1>
<form class="pure-form pure-form-aligned" method="post" action="/set" onsubmit="return validate()">
<fieldset>
<legend>Base</legend>
<div class="pure-control-group">
<label for="host">Host</label>
<input name="host" type="text" id="host" placeholder="Host name" value="%HOST%">
</div>
<legend>HTTP basic authentication</legend>
<div class="pure-control-group">
<label for="name">Username</label>
<input name="name" type="text" id="name" placeholder="Username" value="%USER%">
</div>
<div class="pure-control-group">
<label for="password">Password</label>
<input name="password" type="password" id="password" placeholder="Password" value="%PASSWORD%">
<button class="pure-button toggle-button" type="button">show</button>
</div>
<legend>WIFI settings</legend>
<div class="pure-control-group">
<label for="wifimode">WIFI mode</label>
<select name="wifimode" id="wifimode">
<option %APSELECTED% value="ap">AP</option>
<option %STSELECTED% value="station">STATION</option>
</select>
</div>
<div class="pure-control-group">
<label for="ssid">SSID</label>
<input name="ssid" type="text" id="ssid" placeholder="SSID" value="%SSID%">
</div>
<div class="pure-control-group">
<label for="ssidpass">WIFI-Password</label>
<input name="ssidpass" type="password" id="ssidpass" placeholder="WIFI password" value="%SSIDPASS%">
<button class="pure-button toggle-button" type="button">show</button>
</div>
<div class="pure-control-group">
<label for="ipmode">IP mode</label>
<select name="ipmode" id="ipmode">
<option %DHCPSELECTED% value="dhcp">DHCP</option>
<option %FIXSELECTED% value="static">STATIC</option>
</select>
</div>
<div class="pure-control-group">
<label for="ip">IP</label>
<input name="ip" type="text" id="ip" placeholder="xxx.xxx.xxx.xxx" value="%IP%" class="ipaddress">
</div>
<div class="pure-control-group">
<label for="gateway">Gateway</label>
<input name="gateway" type="text" id="gateway" placeholder="xxx.xxx.xxx.xxx" value="%GATEWAY%"
class="ipaddress">
</div>
<div class="pure-control-group">
<label for="subnet">Subnet</label>
<input name="subnet" type="text" id="subnet" placeholder="xxx.xxx.xxx.xxx" value="%SUBNET%"
class="ipaddress">
</div>
<div class="pure-control-group">
<label for="primaryDNS">Primary DNS</label>
<input name="primaryDNS" type="text" id="primaryDNS" placeholder="xxx.xxx.xxx.xxx" value="%PRIMARYDNS%"
class="ipaddress">
</div>
<div class="pure-control-group">
<label for="secondaryDNS">Secondary DNS</label>
<input name="secondaryDNS" type="text" id="secondaryDNS" placeholder="xxx.xxx.xxx.xxx"
value="%SECONDARYDNS%" class="ipaddress">
</div>
<legend>Device settings</legend>
<div class="pure-control-group">
<label for="maxon">On time</label>
<input name="maxon" type="text" id="maxon" placeholder="On time" value="%MAXON%">
</div>
<div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary">Save</button>
</div>
</fieldset>
</form>
</div>
<p>
<a href="/reset">reset</a>
</p>
</div>
</div>
<script>
const toggleButtons = document.querySelectorAll(".toggle-button");
toggleButtons.forEach(function (tb) {
tb.addEventListener('click', function () {
if (this.previousElementSibling.type === 'password') {
this.previousElementSibling.type = 'text';
this.textContent = 'hide';
} else if (tb.previousElementSibling.type === 'text') {
this.previousElementSibling.type = 'password';
this.textContent = 'show';
}
})
})
function validate() {
const ips = document.querySelectorAll(".ipaddress");
const ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
let retval = true;
ips.forEach(function (ip) {
if (!ip.value.match(ipformat)) {
ip.classList.add("invalid");
retval = false;
ip.onfocus = function () {
if (this.classList.contains('invalid')) {
this.classList.remove('invalid');
}
}
}
})
return retval;
}
</script>
</body>
</html>

Страница настроек содержит то же меню что и страница управления. За меню следуют текущие свойства устройства, такие как:

— включен ли защищенный режим

— уровень WIFI сигнала

— количество свободной памяти

— напряжение питания

— время непрерывной работы устройства.

Далее идет форма, позволяющая изменять настройки устройства:

— имя хоста

— имя пользователя для авторизации на странице

— пароль пользователя для авторизации на странице

— режим WIFI (либо точка доступа, либо подключение к существующей точке)

— имя точки доступа к которой будет подключение, либо которая будет создана, зависит от режима

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

— режим IP адреса (статический или динамический)

— сетевые настройки

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

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

Вид интерфейса

Так выглядит основная страница управления:

Делаем секционные гаражные ворота более удобнымиТак выглядит страница с настройками:

Делаем секционные гаражные ворота более удобнымиЗдесь можно посмотреть как интерфейс реагирует на изменение состояния ворот:

На видео виден механизм Server-Sent Events (SSE) в действии, когда браузер поддерживает открытым соединение с сервером и мгновенно реагирует на присылаемую сервером информацию.

Корпус

Многие из вас отлично понимают, что устройство без корпуса, это полуфабрикат.  Нужно сделать корпус максимально подходящий нашему устройству, с отверстиями в нужных местах. Я считаю, что вполне разумно прибегнуть к 3Д-печати. Рисуем модель нижней части корпуса. Предусмотрев отверстия для ввода проводов, крепежные отверстия и стойки для нашей платы. В одной части корпуса будет располагаться наше устройство, в другой уложенные провода. Результат вот такой:

Делаем секционные гаражные ворота более удобнымиЧтобы сделать крышечку, нужно отступить 0.2 мм, чтобы она плотно покрывала

наше устройство. В крышечке предусматриваем отверстие для светодиода индикации состояния сети:

Делаем секционные гаражные ворота более удобнымиРезультат печати:

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

Делаем секционные гаражные ворота более удобнымиЗаодно сделал примерный слепок ответной части, которую будем крепить к естественно торчащему болту конструкции ворот. Слепки выглядят так:

Делаем секционные гаражные ворота более удобнымиЧтобы иметь возможность регулировки расстояния от магнита до геркона предусмотрим крепежное отверстие в виде овала.

Далее по точкам, с помощью сплайна нетрудно воспроизвести криволинейную поверхность:

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

Запускаем печать:

Делаем секционные гаражные ворота более удобнымиРезультат:

Делаем секционные гаражные ворота более удобнымиДелаем секционные гаражные ворота более удобнымиСравниваем кривизну со слепком и убеждаемся, что все норм:

Делаем секционные гаражные ворота более удобными

Монтаж

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

Привод на картинке:

Делаем секционные гаражные ворота более удобнымиВот так выглядит привод уличных ворот без крышки:

Делаем секционные гаражные ворота более удобнымиВот так ближе интересующее нас место:

Делаем секционные гаражные ворота более удобнымиКолодка управления воротами:

Делаем секционные гаражные ворота более удобнымиСогласно документации, нам требуется к реле подключать выводы 2 и 7 колодки привода.

Монтируем выключатель данного привода, оставляя провод для геркона:

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

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

Делаем секционные гаражные ворота более удобнымиКак и было задумано, крышечка хорошо закрыла устройство вместе с проводами коммутации, по светодиоду видно что подключение к сети произошло и все норм:

Делаем секционные гаражные ворота более удобнымиНа состояние стен не обращайте внимание, в гараже предстоит ремонт.

Монтаж осуществлен (осталось только приклеить герконы в корпусах и ответные магнитики), ворота управляются, как и задумано. Далее можно через VPN вынести интерфейс наружу, либо реализовать в какой-то системе (типа OpenHub) управление, благо api есть, или дописать взаимодействие по mqtt. В крайнем случае можно просто пробросить порт из внутренней сети наружу. По моему, получилось вполне полезное устройство, которое облегчит взаимодействие с воротами своему владельцу.

На этом заканчиваю. Спасибо тем кто дочитал до конца, надеюсь кому-то информация окажется полезной!


СМОТРИ ТАКЖЕ

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *