Связь с ESP8266 через Node.js

Связь с ESP8266 через Node.js

Один из способов подружить телефон и модуль ESP8266 через интернет с помощью сервера на nodejs.

Готовое устройство в этой статье.

Иногда забываю включить камеру наблюдения уходя из дома. Решил сделать управление питанием через WiFi  c помощью модуля ESP-01.

Но вот вопрос, как получить доступ к модулю WiFi через интернет не имея статик IP у модуля или же модуль за NAT.

Будучи любителем node.js я решил сделать прослойку и поместить ее на одном из моих VPS серверов. Подойдет любой хостинг с поддержкой node.js или самая дешевая vps (у меня за 100 руб. в месяц).

Я покажу простой пример с express и socket.io для web странички. В данном примере я покажу как получить температуру и с датчика DHT-11 и переключать какие то данные.

Можно пойти несколькими путями, например node.js будет просто proxy сервером между соединением по web-сокетам от браузера и tcp-сокетом от eps8266.

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

Протокол общения ESP8266 и Node.js

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

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

Но благодаря тому, что библиотека под Arduino написана больше для передачи текста, я решил остановится на текстовом формате данных.

Пакеты разбиты переводом строки, при чем библиотека ESP8266 шлет \r\n для завершения строки, а сервером я шлю только \n.

Пакет может состоять из частей, раз дело имеем с web я остановился на амперсанде (&) в качестве разделителя.
Ключ - значение  я делю двоеточием.

Пакет для запроса статуса будет такой "status\n"
ответ: "status&l:0&t:25.10&h:56.50"

Пакет включения/выключения нагрузки будет: 'load&on' и 'load&off'

Создание проекта Node.js для соединения ESP8266 и Web-сокетов

Создаем каталог, например esp

mkdir esp
cd esp

инициализируем npm, данные можете заполнить а можете оставить все поля пустыми

npm init

Устанавливаем необходимые библиотеки

npm install socket.io --save
npm install express --save

создаем файл index.js или какой вы указали при настройки проекта после команды init.

touch index.js

Вставьте в него следующий код:

let net = require('net');
let app = require('express')();
let http = require('http').createServer(app);
let io = require('socket.io')(http);

// фейковое хранилище всех подключенных ESP
// в реальной жизни вам понадобится различать их
// вы можете при подключении передавать информацию, например уникальный номер
let esps = [];

// буфер входящих данных
let packet = '';

// создаем TCP сервер для приема соединений от наших ESP
var server = net.createServer(function(sock) {
	console.log('ESP connected');
	// кладем в наше хранилище новый коннект
	esps.push(sock);

	// подписываем на событие прихода данных по сокету
	sock.on('data', (data) => {
		// складываем в буфер
		// в реальной жизни нужно учитывать если платок больше 1ой
		// на каждую нужен отдельный буфер
		packet += data;
		// ищем символы переноса строки
		const idx = packet.indexOf('\r\n');
		// если найдены то мы получили полный пакет
		if (idx != -1) {
			// берем из буфера часть, которая содержит 1 целый пакет
			// и вызываем обработчик для этого пакета
			handler(packet.substr(0, idx));
			// удаляем обработанный пакет из буфера
			packet = packet.substr(idx + 2);
		}
  })

	// в реальной жизни, в случае ошибки на сокете,
	// обрабатываем ее, например отключаем сокет и удаляем из хранилища
	sock.on('error', (err) => {
		console.log(err);
	});

	// если сокет отключился удаляем его из нашего хранилища
	sock.on('end', () => {
    console.log('ESP disconnected');
		const pos = esps.indexOf(sock);
		if (pos != -1) {
			esps.splice(pos, 1);
		}
  });
});

// указываем какой порт и ip адрес нужно начать слушать нашему TCP серверу
server.listen(3030, '192.168.0.102');

// вспомогательная функция для отправки пакета
function send_packet(data) {
	// если есть подключенные esp
	if (esps.length) {
		//перебираем их и шлем по очереди наш пакет
		for (const esp of esps) {
			try {
				esp.write(data + '\n');
			} catch (err) {
				console.log(err);
			}
		}
	} else {
		console.log('Error: ESP not found');
	}
}

// вспомогательная функция для обработки пакетов
function handler(packet) {
	console.log('Packet:', packet);
	// формат пакета описан в статье
	// разбиваем на части
	const parts = packet.split('&');
	// смотрим что за пакет
	switch (parts[0]) {
		case 'status':
			// удалим часть которая нам больше не нужна
			parts.splice(0, 1);
			// вспомогательный объект для отправки в web сокет
			const res = {};
			// перебираем все части и складываем в один объект
			// мы могли формировать сразу в esp8266 готовый json
			// или могли бы парсить данные на клиенте
			for (const el of parts) {
				// разбиваем на ключ - значение
				const tmp = el.split(':');
				// пишем в объект
				res[tmp[0]] = tmp[1];
			}
			// и отсылаем в web сокет
			io.emit('status', res);
			break;
		default:
			console.log('Error: unknown handler')
	}
}

// эту часть я поручаю nginx
// но для краткости и удобства для статьи я решил обойтись средствами node.js
app.get('/', (req, res) => {
	// отдаем статик файл в браузер
  res.sendFile(__dirname + '/public/index.html');
});

// событие при подключении нового web socket
io.on('connection', function(sock) {
	// если пришла команда status
  sock.on('status', () => {
		// просто отправляем ее на наш wifi модуль
		send_packet('status');
	}).on('toggle', (data) => {
		// для команды toggle меняем название команды
		// и добавляем параметр в том формате в котором его ожидает наш esp-01 модуль
		send_packet('load&' + (data ? 'on' : 'off'));
	});
});

// запускаем наш web сервер
http.listen(3000, () => {
  console.log('listening on *:3000');
});

Создадим каталог public и в нем файл index.html с кодом

<div>
  <div>Температура: <span id="field-t">---</span></div>
  <div>Влажность: <span id="field-h">---</span></div>
  <div>Нагрузка: <span id="field-l">---</span></div>

  <div>
    <button id="btn-toggle" disabled="disabled">Нагрузка...</button>
  </div>
</div>

<script src="/socket.io/socket.io.js"></script>
<script>
  var sock = io();

  var btnToggle = document.getElementById('btn-toggle');
  var fieldT = document.getElementById('field-t');
  var fieldH = document.getElementById('field-h');
  var fieldL = document.getElementById('field-l');
  var loadEnabled = 0;

  // обработчик события status
  sock.on('status', function (data) {
    if (data) {
      // если пришли данные температуры
      if (data.hasOwnProperty('t')) {
        // выводим ее
        fieldT.innerHTML = data.t;
      }
      // влажность
      if (data.hasOwnProperty('h')) {
        fieldH.innerHTML = data.h;
      }
      // статус нашего переключателя
      if (data.hasOwnProperty('l')) {
        loadEnabled = parseInt(data.l)
        fieldL.innerHTML = loadEnabled ? 'ВКЛ.' : 'ВЫКЛ.';

        // кнопка для изменения состояния
        btnToggle.innerHTML = loadEnabled ? 'ВЫКЛ.' : 'ВКЛ.';
        btnToggle.removeAttribute('disabled');
      }
    }
  })

  // при клике на кнопку
  btnToggle.addEventListener('click', function () {
    // отсылаем новое состояние
    sock.emit('toggle', !loadEnabled);
  });

  // обновляем статус оп таймеру 1 раз в секунду
  setTimeout(function () {
    sock.emit('status');
  }, 1000);
</script>

Прошивка модуля ESP-01 для связи с Node.js по сокету.

Для опроса датчика температуры и влажности DHT-11 я выбрал библиотеку DHT-sensor-library.

Скачиваем, и распаковываем ее, переименовываем в DHT и кладем в папку скетчей libraries.

Для ее работы необходимо установить Adafruit Unified Sensor Library через менеджер библиотек IDE Arduino.

Полный код прошивки wifi модуля на базе ESP8266:

#include 
#include "DHT.h"

// тип датчика
#define DHTTYPE DHT11

const char* ssid = "Имя WiFi сети";
const char* password = "пароль wifi сети";

// вспомогательная переменная для вывода debug информации в консоль
int wifiConnecting = false;

// позиция следующег бита в буфере для записи при получении из сокета
byte pos = 0;
// сам буфер
char packet[255];

// переменная для эмуляции переключения управления нагрузкой
bool loadEnabled = false;

// объявлеем клиент для подключения к node.js
WiFiClient client;
// указываем IP нашего nodejs сервера
IPAddress server(192, 168, 0, 102);

// номер ножки на которой висит наш датчик DHT11
const int DHTPin = 2;
DHT dht(DHTPin, DHTTYPE);

// переменные для хранения температуры и влажности
float h = 0, t = 0;

void setup() {
  // настройки порта для вывода отладочной информации
  Serial.begin(115200);
  delay(10);

  // инициализация dht11
  dht.begin();
 
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  // подключение по wifi для выхода в интернет
  WiFi.begin(ssid, password);
}

// функция для чтения данных с датчика DHT-11
void DHTRead() {
  h = dht.readHumidity();
  t = dht.readTemperature();
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
  } else {
    float hic = dht.computeHeatIndex(t, h, false);
    Serial.print("Humidity: ");
    Serial.println(h);
    Serial.print(" %\t Temperature: ");
    Serial.print(t);
    Serial.println(" *C ");
  }
}

// функция обработчик пакетов
void handler() {
  // получаем первую часть пакета
  char* part = strtok(packet, "&");
  Serial.print("Readed: ");
  Serial.println(part);
  // если это пакета статуса
  if (strcmp(part, "status") == 0) {
    Serial.println("Command read");
    читаем данные с датчика
    DHTRead();
    // шлет информацию в сокет
    // l - состояние переключателя
    client.print("status&l:");
    client.print(loadEnabled);
    // темература
    client.print("&t:");
    client.print(t);
    // и влажность
    client.print("&h:");
    client.println(h);
  } else if (strcmp(part, "load") == 0) { // если управление нашим перключателем
    Serial.println("Command load");
    // получаем вторую часть пакета
    part = strtok(0, "&");
    // если пришло on - значит включен, иначе выключен
    loadEnabled = strcmp(part, "on") == 0;
    // обновляем наш статус выключателя на клиенте
    client.print("status&l:");
    client.println(loadEnabled);
  } else {
    Serial.println("Invalid command");
  }
}

// чтение данных из сокета
void ClientRead() {
  // есть доступные данные?
  if (client.available()) {
    Serial.print("Read: ");
    // читаем байт
    byte c = client.read();
    Serial.println(c);
    // если это перевод строки
    if (c == '\n') {
      // закроем наш пакет символом нуля
      // иначе могут мешать данные от прошлого пакета
      packet[pos++] = 0;
      // обработаем пакет
      handler();
      // сбросим счетчик
      pos = 0;
    } else { // если это не конец строки
      // поместим символ в буфер и сдвинем позицию на 1
      packet[pos++] = c;
    }
  }
}

// основной цикл
void loop() {
  // если вайфай не подключен
  if (WiFi.status() != WL_CONNECTED) {
    if (!wifiConnecting) {
      wifiConnecting = true;
      Serial.print("WiFi connecting");
    }
    Serial.print(".");
    // ждем 1 сек и перепроверяем статус
    delay(1000);
  } else { // вайфай подключен 
    if (wifiConnecting) {
      wifiConnecting = false;
      Serial.println();
      Serial.println('WiFi connected');
    }

    // если нет подключения к nidejs серверу
    if (!client.connected()) {
      Serial.println("Connecting to server");
      client.stop();
      // переподключаемся
      client.connect(server, 3030);
    } else {
      // читаем данные из сокета
      ClientRead();
    }
   
    delay(1);
  }
}

Получился вот такой веб интерфейс:

esp8266-esp-01-nodejs-websocket-1 esp8266-esp-01-nodejs-websocket-2

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

Для клиентской части я буду использовать angular.js или react. Можно так сделать мобильное приложение, например на react native, ionic или  flutter.

При копировании материалов ссылка на https://terraideas.ru/ обязательна

Комментарии к статье: Связь с ESP8266 через Node.js

Артём около 1 года назад

а вы не написали что должно лежать в этом файле ?

Артём около 1 года назад

код не работает подскажите плиз какие порты надо юзать 3000 или 3030 ? а так же ругается на /socket.io/socket.io

DrobyshevAlex около 1 года назад

Порт 3000 указан для web сервера. Это связь web странички и nodejs сервера. А порт 3030 это просто TCP сокет, по нему подключается. ESP к серверу.
Оба порта используются.

DrobyshevAlex около 1 года назад

В каком файле? В /socket.io/socket.io.js?
Это файл из библиотеки socket.io. Его сервер автоматически отдаст из модулей.