Контроль дверей: делаем «умный датчик»
Как ни странно, но до сих пор у меня не было ни одного датчика из тех, которые принято относить к «охранным сигнализациям».
Но вот понадобилось поставить сигнализацию на некоторую Железную Дверь, которая должна быть всегда закрыта, если без присмотра. Весь процесс — далее.
Задача простая: сообщать, когда дверь открывается, когда закрывается, и если осталась открытой — напоминать об этом.
Но дверь железная — буквально, в том смысле что она из железных профтруб, в железной раме, а еще — она на улице, т.е. всякие варианты с микровыключателями тут не подходят, всё это будет съедено коррозией.
Поэтому за основу взял индуктивный датчик: такие обычно используются во всяких промышленных системах. Работают просто: подносишь железку — включаются, убираешь — выключаются. Продаются на маркетплейсах, довольно распространенная штука.

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

Питание 12 вольт уже есть, датчик работает от 10 до 30. Остается подключить к этому всему ESP, завязать в PainlessMesh, ловить события «дверь открыта».
Также, для контроля работоспособности, отправлять периодически состояние статус: если очередное сообщение не приходит больше трех минут — значит что-то вышло из строя.
Подключить напрямую датчик нельзя: там будет 12в (может быть и можно, ограничив напряжение стабилитроном, но нет такой необходимости), поэтому использую обычный оптрон.

Также, потребуется понижение напряжения питания с 12 до 3.3 вольт, для этого использую готовый модуль DC-DC.
Ну, и чтобы все это было как-то аккуратнее — соберу на печатной плате.
Размеры печатной платы под коробку 60×35 мм. Разводку платы делаю как обычно во Fritzing. Знаю, что «профессионалы» ее терпеть не могут за «ардуинистость», но она как раз подходит для подобных случаев.
Проблема в том, что для создания печатной платы нужны «отпечатки» деталей, и если с оптронами, резисторами, и даже ESP12 проблемы нет — то найти готовый «отпечаток» под безымянный модуль с Али — задача непростая.
А во Fritzing его можно просто нарисовать!
Для этого нужно сначала сфотографировать модуль. У него несколько контактных отверстий, размещать его я буду на штырьках на лицевой стороне платы, соответственно, фотографирую «сверху», а контактные площадки будут на плате «снизу».

Тот самый DC-DC
Фотографию модуля нужно развернуть так, чтобы он был по возможности горизонтально и без искажений — с этим прекрасно справится Gimp.
Теперь нужен SVG-редактор. Как ни странно, наиболее известный Inkscape здесь не подходит, создаваемые им SVG не подходят без ручного ковыряния текста SVG, а это как бы неудобно.
Поэтому я использую тут boxy-svg.
Принцип следующий: импортируем картинку модуля, под ее размеры подгоняем viewport, и указываем реальные размеры в миллиметрах.
Теперь всё что будет нарисовано «по фотографии» — уже будет соответствовать реальным размерам.


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

Контактные площадки группируются в группу, называемую copper1 — это значит они будут отображаться на нижней стороне платы. (copper0 — лицевая, верхняя сторона).Нюанс в том, что у группы есть название, и есть ID — вот тут важно чтобы ID был copper1.


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

Дальше во Fritzing находим что-нибудь похожее, чтобы не рисовать еще и схему — тоже 4 контакта, вход-выход, и редактируем, сохраняя как новую деталь.
Отмечаем контакты как штырьки — это заставит программу размещать деталь на лицевой стороне.
В разделе PCB загружаем новый SVG, и отмечаем какой контакт к какой площадке относится.

В разделе Schematic уже есть схема «блока питания», и контакты к выводам привязаны — можно не менять.
В разделе Breadboard снова загружаем SVG — должна появится условная картинка с деталями — тут тоже нужно привязать контакты к площадкам.
В разделе Icon можно просто повторно использовать Breadboard, это только для картинки в списке деталей.
Всё это сохраняется как новый элемент — теперь можно добавлять его в схему:

И конечно, развести плату:

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

PDF затягиваем в Gimp с разрешением 1200 dpi, инвертируем цвета.
Так же рядом затягиваем маску с тем же разрешением, ничего не инвертируем.

Печатается это все на листе тонкой бумаги, около 80 г/м2, или на кальке

На кусок стектотекстолита накатывается ламинатором пленка фоторезиста (Китай, маркетплейс)

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

4 секунды под светодиодной УФ-лампой, сдираем верхнюю защитную пленочку — готово к проявке

3 минуты в растворе строительного «жидкого стекла» с водой, примерно 1:2 — незасвеченное сползает лохмотьями

Травление: аптечная 3% перекись + чайная ложка соли + чайная ложка лимонной кислоты — примерно 10 минут

Смывка фоторезиста — «Крот» + вода, соотношение примерно 1:3, ок. 3 минут — остатки фоторезиста обесцвечиваются и сползают.

Ну, дальше нанесение маски, засветка, смывка — это всегда плохо получалось, поэтому углубляться не стоит. Сверление отверстий, обрезка краев шлифмашинкой.
В итоге получается плата, готовая к распайке:

Теперь программная часть.
Уже практически типовой скетч:
/*
* check PIN state, set event
*
*/
#include "painlessMesh.h"
#include
#include
#define VERSION "Guard v1.0"
#define OTA_NAME "guard"
#define MESH_PREFIX "ХХХХХ"
#define MESH_PASSWORD "ХХХХХ"
#define MESH_PORT 5678
painlessMesh mesh;
size_t gw_id = 0;
size_t me_id = 0;
int state;
#define PIN_CTL 13
#define IND 2
// =========================================================
// Prototypes
void receivedCallback( uint32_t from, String &msg );
void newConnectionCallback(uint32_t nodeId);
// =========================================================
void MeshSetup(){
gw_id = 0;
mesh.setDebugMsgTypes( ERROR | STARTUP | DEBUG | CONNECTION );
mesh.init( MESH_PREFIX, MESH_PASSWORD, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.initOTAReceive(OTA_NAME);
mesh.setContainsRoot(true);
}
// =========================================================
#define TTL 180000
unsigned long ttl_timer;
int ttl_check;
void TtlSetup(){
ttl_timer = millis();
ttl_check = 0;
}
void TtlLoop(){
if(abs((long)(millis() - ttl_timer)) > TTL){
ttl_timer = millis();
if (ttl_check == 0){
ESP.reset();
}else{
ttl_check = 0;
mesh.sendBroadcast("ping");
}
}
}
// time ===================================================
#include
#include
void TimeSetup(){
sntp_stop();
}
// =========================================================
unsigned long state_timer;
#define STATE_PERIOD 60000
#define STATE_RND 5000
void StateSetup(){
state_timer = 0;
gw_id = 0;
}
void StateLoop(){
DynamicJsonDocument doc(300); // messages
if(abs((long)(millis() - state_timer)) > STATE_PERIOD){
state_timer = millis();
state_timer += random(STATE_RND);
doc["ident"] = OTA_NAME;
time_t now = time(nullptr);
doc["dtm"] = now;
doc["block"] = "state";
long rssi = WiFi.RSSI();
doc["rssi"] = rssi;
doc["state"] = state;
if(rssi < -60){
digitalWrite(IND,HIGH);
}else{
digitalWrite(IND,LOW);
}
String message;
serializeJson(doc, message);
if(gw_id > 0){
mesh.sendSingle(gw_id, message);
}
else{
mesh.sendBroadcast(message);
}
}
}
// =========================================================
void setup() {
LittleFS.begin();
pinMode(IND,OUTPUT);
digitalWrite(IND,HIGH);
MeshSetup();
TimeSetup();
pinMode(PIN_CTL, INPUT_PULLUP);
state = digitalRead(PIN_CTL);
TtlSetup();
StateSetup();
}
void loop() {
mesh.update();
TtlLoop();
int x = digitalRead(PIN_CTL);
if(state != x){
String str = "{\"alarm\":1,\"state\":";
str += x;
str += "}";
state = x;
if(gw_id > 0){
mesh.sendSingle(gw_id, str);
}
else{
mesh.sendBroadcast(str);
}
}
StateLoop();
delay(100);
}
void newConnectionCallback(uint32_t nodeId) {
char buf[80];
time_t now = time(nullptr);
sprintf(buf,"settime %u",now);
mesh.sendSingle(nodeId, buf);
}
void receivedCallback( uint32_t from, String &msg ) {
ttl_check = 1;
// ===============================
if (msg == "state") {
gw_id = from;
state_timer = 0;
} else
// ===============================
if (msg == "version") {
mesh.sendSingle(from, VERSION);
} else
// ===============================
if (msg == "restart") {
mesh.sendSingle(from, "OK");
ESP.restart();
} else
// ===============================
if (msg.startsWith("settime ")) {
String num = msg.substring(8);
time_t tim = num.toInt();
time_t now = time(nullptr);
if(tim != 0 && tim > now){
timeval tv = { tim, 0 };
settimeofday(&tv, nullptr);
}
}
}
Суть программы простая: подключаемся в mesh-сеть, ловим сообщения, отправляем свои.
При подключении к какому-то из модулей он отправляет команду settime — это подерживает единое время во всей сети.
Периодически отправка пингов, периодически отправка сообщений о своем состоянии. Чтобы не накладывались сообщения — рандомизация.
Если нет новых сообщений более чем 180 сек — перезагрузка модуля.
Специфическое для данного модуля — за каждую итерацию проверка пина на подтягивание к «земле», с изменением статуса и отправкой сообщения.
Итого, пока дверь закрыта — от датчика идет ток на оптрон, который опускает пин к «земле». Как только дверь открылась — ток выключается, оптрон закрывается, на входе 1, отправляется сообщение. Потом наоборот.
Периодически передается текущее состояние.
Сообщения, отправляемые датчиком, выглядят примерно так:
mesh/from/2874622439 {"ident":"guard","dtm":1738189126,"block":"state","rssi":-91,"state":0}
Теперь их нужно применить в общую систему. Для этого делается скрипт-драйвер:
#!/usr/bin/perl -w
$|=1;
use Net::MQTT::Simple;
use JSON;
use Data::Dumper;
$SIG{CHLD} = "IGNORE";
########################################
my $mqtt = Net::MQTT::Simple->new("127.0.0.1");
#------------------------------------------
# send MQTT message
sub pub {
my ($topic, $message, $retain) = @_;
my $pid = fork();
if(defined $pid && $pid == 0){
if($retain){
$mqtt->retain($topic => $message);
}else{
$mqtt->publish($topic => $message);
}
sleep(2);
exit(0);
}
}
#------------------------------------------
my $door1 = '2874622439';
# processing
$mqtt->run(
# receive Info requests
"mesh/from/$door1" => sub {
my ($topic, $message) = @_;
print ".";
my $data = undef;
if($message =~ /^{.*}$/){
$data = from_json($message);
}
if(defined $data && defined $data->{ state }){
my $t = $data->{state};
if($t == 1){
pub("alarm/text","Дверь открыта!");
}
elsif($data->{ alarm }){
pub("alarm/text","Дверь закрыта");
}
}
},
);
exit;
Вот теперь всё: дверь открывается — на телефон приходит сообщение, дверь закрывается — снова приходит сообщение.
Избыточно сложно? Может быть, но в пару строк в драйвере можно добавить отправку команды на включение чего-нибудь (красной лампочки, прожектора, сирены), вести лог когда что открывалось.
А еще получился по сути универсальный модуль, который можно подключить к любому «шлейфу», будь это герконные датчики на окнах, PIR-сенсоры, или какие-нибудь лазерные барьеры — просто сообщения будут приходить от разных ID.
Но такой задачи нет. Пока нет.
