This commit is contained in:
Nikolay Vasilchuk
2018-09-27 18:17:10 +03:00
parent f6acdc7d82
commit b9ff70f506
20 changed files with 830 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json

24
data/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Domofon</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4QkUEBQ2PpUhggAAAM5JREFUOMut0rFKQzEUANBDXwfpWnDoB9i5v1C/wJ9wcLCCu6sfIAodWwehLh3qIrjW1T8QB10FEV2kdcmDy4O25tlAICHJyc3N5Z+tCOMO5mjhsQ52iWXqp7mHh9jDLCBHOcAnngOywGEO8JZuLZGD3CfchtAf6iRwPwAvaNZBbgJyjUYusBtyscRVBWngAsfrkB4+AjLGTlob/LVO+hXkHU/oVurkZFMkr2Fz+cVd3KX5ZFNO2hitQM7KpxVrgG9McZ8qs0jIF87xYxvtFxFQQMNN792iAAAAAElFTkSuQmCC">
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div class="content">
<h2>Domofon</h2>
<textarea id="terminal" readonly></textarea>
<div class="controls">
<input id="clear" type="button" value="Clear"/>
<input id="restart" type="button" value="Restart"/>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

63
data/script.js Normal file
View File

@@ -0,0 +1,63 @@
(function() {
var terminal = document.getElementById('terminal'),
clear = document.getElementById('clear'),
restart = document.getElementById('restart'),
ws = null,
reconnectTimeout = null,
prefix = '';
function _connect() {
if (!ws || ws.readyState === WebSocket.CLOSED) {
try {
ws = new WebSocket('ws://' + window.location.hostname + '/ws');
ws.onopen = _onOpen;
ws.onmessage = _onMessage;
ws.onclose = _onClose;
} catch (e) {
_onClose();
}
}
}
function _onOpen() {
clearTimeout(reconnectTimeout);
}
function _onClose(e) {
var code = e && e.code || 1012;
ws = null;
if (code > 1000) {
reconnectTimeout = setTimeout(_connect, 1000);
}
}
function _onMessage(message) {
var data = message && message.data;
if (data) {
data = prefix + data;
if (data.endsWith("\n")) {
prefix = "\n";
data = data.substr(0, data.length - 1);
} else {
prefix = '';
}
terminal.value += data;
terminal.scrollTop = terminal.scrollHeight;
}
}
clear.addEventListener('click', function(e) {
terminal.value = '';
prefix = '';
});
restart.addEventListener('click', function(e) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/restart', true);
xhr.send(null);
});
_connect();
})();

80
data/style.css Normal file
View File

@@ -0,0 +1,80 @@
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: sans-serif;
}
.content {
line-height: 1.6em;
margin: 0 auto;
padding: 30px 0 50px;
max-width: 50pc;
padding: 0 2em;
overflow: hidden;
}
h2 {
font-size: 3em;
font-weight: 300;
margin: .5em 0 .3em;
text-align: center;
line-height: 1em;
}
input, textarea {
border-radius: 4px;
padding: .5em .6em;
box-sizing: border-box;
outline: none;
}
input {
height: 36px;
width: 20%;
min-width: 140px;
font-family: sans-serif;
vertical-align: middle;
line-height: normal;
display: inline-block;
white-space: nowrap;
font-size: 100%;
cursor: pointer;
user-select: none;
color: #fff;
text-shadow: 0 1px 1px rgba(0,0,0,.2);
text-align: center;
}
input:active {
box-shadow: 0 0 0 1px rgba(0,0,0,.15) inset, 0 0 6px rgba(0,0,0,.2) inset;
}
.controls {
margin: .5em 0 1em;
}
#clear {
background: #009a3e;
}
#restart {
float: right;
background: #c01200;
}
#terminal {
display: block;
width: 100%;
background: #222;
height: 25pc;
max-height: 60vh;
color: #c9ea7b;
font-family: Courier New, monospace;
font-size: 80%;
line-height: 110%;
resize: none;
box-shadow: inset 0 1px 3px #ddd;
border: 1px solid #ccc;
}

41
lib/readme.txt Normal file
View File

@@ -0,0 +1,41 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link them to executable files.
The source code of each library should be placed in separate directories, like
"lib/private_lib/[here are source files]".
For example, see the structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- readme.txt --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Then in `src/main.c` you should use:
#include <Foo.h>
#include <Bar.h>
// rest H/C/CPP code
PlatformIO will find your libraries automatically, configure preprocessor's
include paths and build them.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

33
platformio.ini Normal file
View File

@@ -0,0 +1,33 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
env_default = nodemcuv2
src_dir = src
[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
framework = arduino
lib_deps =
PubSubClient
Bounce2
ESP Async WebServer
; targets = upload, uploadfs
upload_port =
upload_flags = --auth=domofon
; upload_port = /dev/cu.wchusbserial1430
; upload_speed = 115200
; monitor_port = /dev/cu.wchusbserial1430
; monitor_speed = 115200

17
src/config/hardware.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef HW_H_
#define HW_H_
// Hardware configuration
#define PIN_RELAY_ANSWER 16 //D0
#define PIN_RELAY_DOOR_OPEN 5 //D1
#define PIN_LED_RED 4 //D2
#define PIN_LED_GREEN 0 //D3
#define PIN_LED_BLUE 2 //D4
#define PIN_CALL_DETECT 14 //D5
#define PIN_BUTTON_GREEN 12 //D6
#define PIN_BUTTON_RED 13 //D7
#endif // HW_H_

19
src/config/mqtt.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef MQTT_H_
#define MQTT_H_
// High level protocol messages
#define MSG_STATUS_READY "R" // ready; sent after successfull boot-up or after receiving of 'P' message
#define MSG_STATUS_LAST_WILL "L" // last will message; send when device goes offline
#define MSG_OUT_CALL "C" // call; sent after detecting of incoming intercom call
#define MSG_OUT_HANGUP "H" // hangup; sent after detected incoming call finished
#define MSG_OUT_OPENED_BY_BUTTON "B" // button; sent when "door open" has been performed by green hw button press
#define MSG_OUT_REJECTED_BY_BUTTON "J" // reJected; sent when incoming call has been rejected by red hw button press
#define MSG_OUT_SUCCESS "S" // success; sent in response to 'O' or 'N' command
#define MSG_OUT_FAIL "F" // fail; sent in response to 'O' or 'N' command (this means that 'O' or 'N' command has been received but no incoming call detected)
#define MSG_IN_OPEN 'O' // door open command
#define MSG_IN_REJECT 'N' // call reject command (door will not open)
#define MSG_IN_PING 'P' // ping command (answers with 'R')
#endif // MQTT_H_

24
src/config/software.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef SW_H_
#define SW_H_
// Software configuration
#define HOST_NAME "domofon"
#define HOST_PASSWORD "domofon"
#define OTA_PORT 8266
#define WIFI_SSID ""
#define WIFI_PASSWORD ""
#define MQTT_SERVER_ADDR ""
#define MQTT_SERVER_PORT 1883
#define MQTT_USER_NAME ""
#define MQTT_USER_PASSWORD ""
#define MQTT_CLIENT_ID "domofon"
#define MQTT_TOPIC_IN "domofon/in"
#define MQTT_TOPIC_OUT "domofon/out"
#define MQTT_TOPIC_STATUS "domofon/status"
#define CALL_HANGUP_DETECT_DELAY 3000
#define RELAY_ANSWER_ON_TIME 1500
#define RELAY_OPEN_ON_TIME 600
#endif // SW_H_

12
src/debug.ino Normal file
View File

@@ -0,0 +1,12 @@
#include "inc/include.h"
void DEBUG_LN(const char *text) {
Serial.println(text);
webSocket().printfAll("%s\n", text);
}
template<typename... Args>
void DEBUG_F(const char *format, Args... args) {
Serial.printf(format, args...);
webSocket().printfAll(format, args...);
}

145
src/domofon.ino Normal file
View File

@@ -0,0 +1,145 @@
#include "inc/include.h"
EState state = IDLE;
EAction action = NO_ACTION;
Bounce debouncerBtnGreen = Bounce();
Bounce debouncerBtnRed = Bounce();
unsigned long lastCallDetectedTime = 0;
void callAnswer() {
DEBUG_LN("[HW] Call answer...");
digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF);
digitalWrite(PIN_RELAY_ANSWER, RELAY_ON);
DEBUG_LN("[HW] Done");
}
void callHangUp() {
DEBUG_LN("[HW] Hang up...");
digitalWrite(PIN_RELAY_ANSWER, RELAY_OFF);
digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF);
DEBUG_LN("[HW] Done");
}
void doorOpen() {
DEBUG_LN("[HW] Door open...");
digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_ON);
delay(RELAY_OPEN_ON_TIME);
digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF);
DEBUG_LN("[HW] Done");
}
void answerAndOpen() {
callAnswer();
delay(RELAY_ANSWER_ON_TIME);
doorOpen();
callHangUp();
}
void answerAndReject() {
callAnswer();
delay(RELAY_ANSWER_ON_TIME);
callHangUp();
}
void handleIdle(EState oldState) {
if (oldState != IDLE) {
mqttSendCommand(MSG_OUT_HANGUP);
ledOff();
DEBUG_LN("[HW] Current state: IDLE");
}
if (action != NO_ACTION) {
mqttSendCommand(MSG_OUT_FAIL);
action = NO_ACTION;
}
if (debouncerBtnGreen.fell()) {
DEBUG_LN("[HW] Button click");
ledBlink(PIN_LED_GREEN, 2);
}
}
void handleCall(EState oldState) {
if (oldState != CALL) {
action = NO_ACTION;
mqttSendCommand(MSG_OUT_CALL);
ledOn(PIN_LED_RED);
DEBUG_LN("[HW] Current state: CALL");
}
if (action == NO_ACTION) {
if (debouncerBtnRed.fell()) {
action = REJECT_BY_BUTTON;
} else if (debouncerBtnGreen.fell()) {
action = OPEN_BY_BUTTON;
}
}
switch (action) {
case OPEN_BY_BUTTON:
answerAndOpen();
mqttSendCommand(MSG_OUT_OPENED_BY_BUTTON);
break;
case REJECT_BY_BUTTON:
answerAndReject();
mqttSendCommand(MSG_OUT_REJECTED_BY_BUTTON);
break;
case OPEN:
answerAndOpen();
mqttSendCommand(MSG_OUT_SUCCESS);
break;
case REJECT:
answerAndReject();
mqttSendCommand(MSG_OUT_SUCCESS);
break;
default:
break;
}
action = NO_ACTION;
}
void setStateIdle() {
state = IDLE;
}
void setStateCall() {
state = CALL;
}
void domofonSetup() {
debouncerBtnGreen.attach(PIN_BUTTON_GREEN);
debouncerBtnGreen.interval(25);
debouncerBtnRed.attach(PIN_BUTTON_RED);
debouncerBtnRed.interval(25);
}
void domofonLoop() {
debouncerBtnGreen.update();
debouncerBtnRed.update();
EState oldState = state;
if (digitalRead(PIN_CALL_DETECT) == LOW) {
setStateCall();
lastCallDetectedTime = millis();
} else if (millis() - lastCallDetectedTime > CALL_HANGUP_DETECT_DELAY) {
setStateIdle();
}
switch (state) {
case IDLE:
handleIdle(oldState);
break;
case CALL:
handleCall(oldState);
break;
}
}

28
src/hardware.ino Normal file
View File

@@ -0,0 +1,28 @@
#include "inc/include.h"
Ticker _defer_restart;
void hardwareSetup() {
pinMode(PIN_BUTTON_GREEN, INPUT_PULLUP);
pinMode(PIN_BUTTON_RED, INPUT_PULLUP);
pinMode(PIN_CALL_DETECT, INPUT_PULLUP);
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_LED_BLUE, OUTPUT);
pinMode(PIN_RELAY_ANSWER, OUTPUT);
pinMode(PIN_RELAY_DOOR_OPEN, OUTPUT);
digitalWrite(PIN_LED_RED, LED_OFF);
digitalWrite(PIN_LED_GREEN, LED_OFF);
digitalWrite(PIN_LED_BLUE, LED_OFF);
digitalWrite(PIN_RELAY_ANSWER, RELAY_OFF);
digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF);
}
void restart() {
ESP.reset();
}
void deferredRestart(unsigned long delay) {
_defer_restart.once_ms(delay, restart);
}

14
src/inc/include.h Normal file
View File

@@ -0,0 +1,14 @@
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>
#include <FS.h>
#include <RemoteDebug.h>
#include <PubSubClient.h>
#include <Bounce2.h>
#include <ESPAsyncWebServer.h>
#include <Ticker.h>
#include "../config/hardware.h"
#include "../config/software.h"
#include "../config/mqtt.h"
#include "types.h"

22
src/inc/types.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef TYPES_H_
#define TYPES_H_
#define LED_ON HIGH
#define LED_OFF LOW
#define RELAY_ON LOW
#define RELAY_OFF HIGH
typedef enum {
IDLE,
CALL
} EState;
typedef enum {
NO_ACTION,
OPEN,
OPEN_BY_BUTTON,
REJECT,
REJECT_BY_BUTTON
} EAction;
#endif // TYPES_H_

21
src/led.ino Normal file
View File

@@ -0,0 +1,21 @@
#include "inc/include.h"
void ledBlink(int pin, int count) {
for (int i = 0; i < count; i++) {
digitalWrite(pin, LED_ON);
delay(125);
digitalWrite(pin, LED_OFF);
delay(125);
}
}
void ledOff() {
digitalWrite(PIN_LED_GREEN, LED_OFF);
digitalWrite(PIN_LED_RED, LED_OFF);
digitalWrite(PIN_LED_BLUE, LED_OFF);
}
void ledOn(int pin) {
ledOff();
digitalWrite(pin, LED_ON);
}

20
src/main.ino Normal file
View File

@@ -0,0 +1,20 @@
#include "inc/include.h"
// Код частично заимствован и переделан
// @see https://github.com/Metori/mqtt_domofon
void setup() {
hardwareSetup();
wifiSetup();
otaSetup();
mqttSetup();
webServerSetup();
domofonSetup();
}
void loop() {
wifiLoop();
otaLoop();
mqttLoop();
domofonLoop();
}

89
src/mqtt.ino Normal file
View File

@@ -0,0 +1,89 @@
#include "inc/include.h"
WiFiClient espClient;
PubSubClient mqttClient(espClient);
void mqttSendCommand(const char *msg) {
mqttClient.publish(MQTT_TOPIC_OUT, msg, 1);
DEBUG_F("[MQTT] Message sent: %s\n", msg);
}
void mqttSendStatus(const char *msg) {
mqttClient.publish(MQTT_TOPIC_STATUS, msg, 1);
DEBUG_F("[MQTT] Status sent: %s\n", msg);
}
void onMqttMsgReceived(char* topic, byte* payload, unsigned int len) {
if (len != 1) {
char* command = (char*)malloc(len + 2);
memcpy(command, payload, len);
command[len] = '\0';
DEBUG_F("[MQTT] Message received [%u]: %s\n", len, command);
return;
}
char cmd = (char)payload[0];
DEBUG_F("[MQTT] Command received: %c\n", cmd);
switch (cmd) {
case MSG_IN_OPEN:
action = OPEN;
break;
case MSG_IN_REJECT:
action = REJECT;
break;
case MSG_IN_PING:
mqttSendStatus(MSG_STATUS_READY);
break;
default:
DEBUG_LN("[MQTT] Unknown command");
break;
}
}
void mqttConnect() {
DEBUG_F("[MQTT] (Re)connecting to server on %s...\n", MQTT_SERVER_ADDR);
for (int i = 0; !mqttClient.connected(); i++) {
// Если не получилось за 5 попыток - перезагружаемся
if (i >= 5) {
ESP.restart();
return;
}
if (!mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER_NAME, MQTT_USER_PASSWORD, MQTT_TOPIC_STATUS, 0, 0, MSG_STATUS_LAST_WILL)) {
// Ждем 2 секунды
DEBUG_F(".");
ledBlink(PIN_LED_GREEN, 8);
}
}
DEBUG_LN("\n[MQTT] Done");
mqttSendStatus(MSG_STATUS_READY);
mqttClient.subscribe(MQTT_TOPIC_IN);
setStateIdle();
DEBUG_LN("[MQTT] Current state: IDLE");
}
void mqttStop() {
mqttClient.disconnect();
ledOff();
}
void mqttSetup() {
mqttClient.setServer(MQTT_SERVER_ADDR, MQTT_SERVER_PORT);
mqttClient.setCallback(onMqttMsgReceived);
mqttConnect();
}
void mqttLoop() {
if (!mqttClient.connected()) {
mqttConnect();
}
mqttClient.loop();
}

58
src/ota.ino Normal file
View File

@@ -0,0 +1,58 @@
#include "inc/include.h"
void otaSetup() {
ArduinoOTA.setHostname(HOST_NAME);
ArduinoOTA.setPort(OTA_PORT);
ArduinoOTA.setPassword(HOST_PASSWORD);
ArduinoOTA.onStart([]() {
DEBUG_LN("[OTA] Start update");
webServerStop();
mqttStop();
});
ArduinoOTA.onEnd([]() {
DEBUG_LN("[OTA] End update");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
DEBUG_F("[OTA] Progress: %u%%\n", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
String type;
switch (error) {
// "Ошибка при аутентификации"
case OTA_AUTH_ERROR:
type = "Auth Failed";
break;
// "Ошибка при начале OTA-апдейта"
case OTA_BEGIN_ERROR:
type = "Begin Failed";
break;
// "Ошибка при подключении"
case OTA_CONNECT_ERROR:
type = "Connect Failed";
break;
// "Ошибка при получении данных"
case OTA_RECEIVE_ERROR:
type = "Receive Failed";
break;
// "Ошибка при завершении OTA-апдейта"
case OTA_END_ERROR:
type = "End Failed";
break;
default:
type = "Unknown";
}
DEBUG_F("[OTA] Error[%u]: %s\n", error, type.c_str());
});
ArduinoOTA.begin();
}
void otaLoop() {
ArduinoOTA.handle();
}

72
src/server.ino Normal file
View File

@@ -0,0 +1,72 @@
#include "inc/include.h"
AsyncWebServer _server(80);
AsyncWebSocket _ws("/ws");
AsyncWebSocket webSocket() {
return _ws;
}
void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
uint32_t id = client->id();
switch (type) {
case WS_EVT_CONNECT:
DEBUG_F("[WS] Client connected - ID: %u\n", id);
client->ping();
break;
case WS_EVT_DISCONNECT:
DEBUG_F("[WS] Client disconnected - ID: %u\n", id);
break;
case WS_EVT_ERROR:
DEBUG_F("[WS] Error - ID: %u, code: %u, error: %s\n", id, *((uint16_t*)arg), (char*)data);
break;
case WS_EVT_PONG:
DEBUG_F("[WS] Pong - ID: %u\n", id);
break;
case WS_EVT_DATA:
AwsFrameInfo * info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len) {
if (info->opcode == WS_TEXT) {
DEBUG_F("[WS] Message received - ID: %u, message: %s\n", id, (char*)data);
} else {
DEBUG_F("[WS] Bynary messages not supported - ID: %u\n", id);
}
} else if (info->final && (info->index + len) == info->len) {
DEBUG_F("[WS] Message too long - ID: %u\n", id);
}
break;
}
}
void webServerStop() {
DEBUG_LN("[WS] Server stopped");
SPIFFS.end();
_ws.enable(false);
_ws.closeAll(1012);
}
void webServerSetup() {
SPIFFS.begin();
_server.rewrite("/", "/index.html");
_server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "404: Not Found");
});
_server.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200);
DEBUG_LN("Restarting...");
webServerStop();
mqttStop();
deferredRestart(200);
});
_server.serveStatic("/", SPIFFS, "/");
_server.begin();
_ws.onEvent(_wsEvent);
_server.addHandler(&_ws);
}

44
src/wifi.ino Normal file
View File

@@ -0,0 +1,44 @@
#include "inc/include.h"
void wifiDisconnect() {
WiFi.disconnect();
ledOff();
}
void wifiConnect() {
DEBUG_F("[WIFI] (Re)connecting to \"%s\"\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.hostname(HOST_NAME);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; WiFi.status() != WL_CONNECTED; i++) {
// Если не получилось за 5 попыток - перезагружаемся
if (i >= 5) {
ESP.restart();
return;
}
// Ждем 2 секунды
DEBUG_F(".");
ledBlink(PIN_LED_BLUE, 8);
}
DEBUG_LN("\n[WIFI] Done");
DEBUG_F("[WIFI] IP address: %s\n", WiFi.localIP().toString().c_str());
}
void wifiReconnect() {
wifiDisconnect();
wifiConnect();
}
void wifiSetup() {
wifiConnect();
}
void wifiLoop() {
if (WiFi.status() != WL_CONNECTED) {
wifiReconnect();
}
}