Системный администратор, программист
0,0
рейтинг
11 декабря 2014 в 09:12

Работа с ESP8266: Пишем прошивку для управления системой nooLite recovery mode

В прошлых статьях мы рассмотрели работу с SoC Espressif ESP8266 на примере работы с базовой AT-прошивкой, сборкой компилятора и написания собственной прошивки с реализацией TCP-сервера (клиента). В этой статье мы рассмотрим пример написания прошивки для работы с модулем передатчика nooLite MT1132 и попробуем управлять освещением в реальной квартире. Всем кому это интересно, прошу под хабракат.


Для написания прошивки мы будем использовать Eclipse Luna и мою сборку Unofficial Development Kit for Espressif ESP8266 (стоит отметить, что недавно я её обновил, теперь для её работы не требуется Python, обновлена libhal.a, а так же было добавлено много примеров: 1wire_ds18b20, dht22, blinky, blinky2, i2c_24xx16, i2c_htu21d и др.), о ней я рассказывал в прошлой статье.

Для работы нам потребуются следующие материалы и устройства:
1. Плата ESP-01 с чипом ESP8266.
2. Модуль MT1132, приобрести можно в этом интернет-магазине или этом.
3. Спецификация на чип ESP8266.
4. Описание SDK ESP8266.
5. Руководство по эксплуатации модуля nooLite MT1132.

Разработчики nooLite достаточно подробно описали модуль MT1132 в документации, а так же привели пример скретча для работы модуля с Arduino, поэтому сложностей с подключением модуля к плате ESP-01 не возникло.

Итоговая схема подключения:

В прошивке реализуем 2 режима работы ESP-01:
1. Режим конфигурации. Через Web-интерфейс в данном режиме можно будет установить параметры подключения ESP-01 к нашей AP, установить уникальный ID нашей платы. ID будет использоваться только для идентификации платы, если у нас в сети их будет несколько штук.
2. Основной режим. Через Web-интерфейс в данном режиме можно будет привязывать-отвязывать модуль MT1132 от силовых блоков, включать-выключать силовые блоки nooLite.

Краткий алгоритм работы прошивки такой:
1. После включения питания читаем из памяти настройки и конфигурацию wi-fi, если настроек нет или wi-fi в режиме STA, то переводим wi-fi в режим STA-AP, даём AP имя NOOLITE_%MACADDRESS%, пароль %MACADDRESS%, запускаем web-сервер в режим конфигурации.
2. Через простенький Web-интерфейс устанавливаем параметры подключения к AP, к этой AP плата ESP-01 будет подключаться в основном режиме. Устанавливаем ID платы. Сохраняем настройки во flash. Перезапускаем плату в основной режим.
3. В основном режиме запускается простенький Web-сервер, через который можно привязывать-отвязывать модуль MT1132 от силовых блоков, включать-выключать силовые блоки nooLite.
4. К GPIO0 подключаем обычную кнопку, замыкание кнопки на землю переводит ESP-01 назад в режим конфигурации.
5. К GPIO16 (на самом деле китайцы обманули и это не вывод GPIO, а вывод RESET) подключаем обычную кнопку, замыкание кнопки на землю производит аппаратный reset платы.

Теперь приступим к написанию кода (сразу скажу, что реализация web-сервера была позаимствована из проекта elektronika-ba):
Прошивка состоит из 6 основных файлов:
driver\uart.c — драйвер UART, в нем модифицирована процедура uart0_rx_intr_handler для приема и парсинга ответа от MT1132;
user\user_main.c — основной файл, но не главный;
user\flash_param.c — работа с flash-памятью;
user\noolite_platform.c — основных функций, вся логика работы по переключению между разными режимами;
user\noolite_config_server.c — web-сервер конфигурации;
user\noolite_control_server.c — web-сервер основного режима, отправка команд MT1132;

Разберем каждый файл по порядку, под спулером будет много коду с комментариями:

Файл user_main.c
#include "ets_sys.h"
#include "osapi.h"
#include "noolite_platform.h"
#include "driver/uart.h"

extern int ets_uart_printf(const char *fmt, ...);

void user_init(void)
{
	// Инициализация UART на скорости 9600, т.к. модуль MT1132 работает только с такой скоростью
	uart_init(BIT_RATE_9600, BIT_RATE_9600);

	#ifdef NOOLITE_LOGGING
	ets_uart_printf("nooLite platform starting...\r\n");
	#endif

	// Вызов основной процедуры запуска из noolite_platform.c
	noolite_platform_init();

	#ifdef NOOLITE_LOGGING
	ets_uart_printf("nooLite platform started!\r\n");
	#endif
}

Файл noolite_platform.c
#include "ets_sys.h"
#include "osapi.h"
#include "user_interface.h"
#include "mem.h"
#include "espconn.h"
#include "os_type.h"
#include "driver/uart.h"
#include "gpio.h"

#include "flash_param.h"
#include "noolite_platform.h"
#include "noolite_config_server.h"
#include "noolite_control_server.h"

// Таймер для проверка состояния wi-fi соединения с AP
os_timer_t WiFiLinker;
// Таймер для устранения дребезга контактов
os_timer_t DebounceTimer;
static tConnState connState = WIFI_CONNECTING;
uint16_t wifiErrorConnect = 0;
uint16_t controlServerStatus = 0;

LOCAL void input_intr_handler(void *arg);
void ICACHE_FLASH_ATTR debounce_timer_cb(void *arg);

// Процедура проверки состояния wi-fi соединения с AP
static void ICACHE_FLASH_ATTR noolite_platform_check_ip(void *arg)
{
    // Структура с инф. о конфигурации ip
    struct ip_info ipconfig;

    // Откл. таймер проверки
    os_timer_disarm(&WiFiLinker);

    // Получаем настройки IP
    wifi_get_ip_info(STATION_IF, &ipconfig);

    // Проверяем файт подключения к AP и назначения IP адреса
    // если соединение установлено, то запускаем web-сервер в основном режиме
    if (wifi_station_get_connect_status() == STATION_GOT_IP && ipconfig.ip.addr != 0)
    {
        connState = WIFI_CONNECTED;
        #ifdef NOOLITE_LOGGING
        char temp[80];
        ets_uart_printf("WiFi connected\r\n");
        os_sprintf(temp, "Client IP address: " IPSTR "\r\n", IP2STR(&ipconfig.ip));
        ets_uart_printf(temp);
        os_sprintf(temp, "Client IP netmask: " IPSTR "\r\n", IP2STR(&ipconfig.netmask));
        ets_uart_printf(temp);
        os_sprintf(temp, "Client IP gateway: " IPSTR "\r\n", IP2STR(&ipconfig.gw));
        ets_uart_printf(temp);
        #endif
        wifiErrorConnect = 0;
        if(controlServerStatus == 0) {
        	controlServerStatus++;
		#ifdef NOOLITE_LOGGING
		ets_uart_printf("Init noolite control server...\r\n");
		#endif
		// Запуск web-сервера в основном режиме
		noolite_control_server_init();
        }
        // Если нужно периодически проверять wi-fi соединение, то раскомментируйте строки ниже
        //os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
        //os_timer_arm(&WiFiLinker, 10000, 0);
    }
    else
    {
		// Wi-Fi соединение не установлено, неправильный пароль, запускаем проверку подключения с интервалом 1,5 сек.
		if(wifi_station_get_connect_status() == STATION_WRONG_PASSWORD)
		{
			connState = WIFI_CONNECTING_ERROR;
			#ifdef PLATFORM_DEBUG
			ets_uart_printf("WiFi connecting error, wrong password\r\n");
			#endif
			wifiErrorConnect++;
			os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
			os_timer_arm(&WiFiLinker, 1500, 0);
		}
		// Wi-Fi соединение не установлено, не найдена AP, запускаем проверку подключения с интервалом 1 сек.
		else if(wifi_station_get_connect_status() == STATION_NO_AP_FOUND)
		{
			connState = WIFI_CONNECTING_ERROR;
			#ifdef PLATFORM_DEBUG
			ets_uart_printf("WiFi connecting error, ap not found\r\n");
			#endif
			wifiErrorConnect++;
			os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
			os_timer_arm(&WiFiLinker, 1000, 0);
		}
		// Wi-Fi соединение не установлено, ошибка, запускаем проверку подключения с интервалом 1 сек.
		else if(wifi_station_get_connect_status() == STATION_CONNECT_FAIL)
		{
			connState = WIFI_CONNECTING_ERROR;
			#ifdef PLATFORM_DEBUG
			ets_uart_printf("WiFi connecting fail\r\n");
			#endif
			wifiErrorConnect++;
			os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
			os_timer_arm(&WiFiLinker, 1000, 0);
		}
		// Установка Wi-Fi соединения, ждите...
		else
		{
			os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
			os_timer_arm(&WiFiLinker, 1000, 0);
			connState = WIFI_CONNECTING;
			wifiErrorConnect++;
			#ifdef PLATFORM_DEBUG
			ets_uart_printf("WiFi connecting...\r\n");
			#endif
		}
		controlServerStatus = 0;
    }
}

// Вход в режим конфигурации, осужествляется путем перевода ESP-01 в режим STATIONAP и стиранием конфигурации
static void ICACHE_FLASH_ATTR noolite_platform_enter_configuration_mode(void)
{
	wipe_flash_param(ESP_PARAM_SAVE_1);
	wifi_set_opmode(STATIONAP_MODE);
	#ifdef NOOLITE_LOGGING
	ets_uart_printf("Restarting in STATIONAP mode...\r\n");
	#endif
	system_restart();
}

// Инициализация кнопки CONFMODE (GPIO0)
void BtnInit() {
	// Уст. GPIO 0 на ввод-вывод
	PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO0);
	// Вкл. подтяг. резистор
	PIN_PULLUP_EN(PERIPHS_IO_MUX_MTDI_U);
	// Откл. глоб.прерываний
	ETS_GPIO_INTR_DISABLE();
	// Подкл. процедуры обраб. прерываний
	ETS_GPIO_INTR_ATTACH(input_intr_handler, NULL);
	GPIO_DIS_OUTPUT(BTN_CONFIG_GPIO);
	// Очистка статуса
	GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, BIT(0));
	// Вкл.прерываний
	gpio_pin_intr_state_set(GPIO_ID_PIN(BTN_CONFIG_GPIO), GPIO_PIN_INTR_NEGEDGE);
	// Вкл.глоб.прерываний
	ETS_GPIO_INTR_ENABLE();
	// Таймер
	os_timer_disarm(&DebounceTimer);
	os_timer_setfn(&DebounceTimer, &debounce_timer_cb, 0);
}

// Процедура обраб. прерываний
LOCAL void input_intr_handler(void *arg)
{
	// Состояние GPIO
	uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS);
	// Очистка
	GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status);
	// Откл.прерываний
	ETS_GPIO_INTR_DISABLE();
	gpio_pin_intr_state_set(GPIO_ID_PIN(BTN_CONFIG_GPIO), GPIO_PIN_INTR_DISABLE);
	// Вкл.прерываний
	ETS_GPIO_INTR_ENABLE();
	// Установка таймера
	os_timer_arm(&DebounceTimer, 200, FALSE);
}

// Выполняется в случае нажатия кнопки CONFMODE
void ICACHE_FLASH_ATTR debounce_timer_cb(void *arg)
{
	ETS_GPIO_INTR_DISABLE();
	gpio_pin_intr_state_set(GPIO_ID_PIN(BTN_CONFIG_GPIO), GPIO_PIN_INTR_NEGEDGE);
	ETS_GPIO_INTR_ENABLE();
	#ifdef NOOLITE_LOGGING
	ets_uart_printf("Button CONFMODE pressed, wiping configuration and restart in configuration mode...\r\n");
	#endif
	noolite_platform_enter_configuration_mode();
}

// Инициализация
void ICACHE_FLASH_ATTR noolite_platform_init(void)
{
	// Структура для хранения конфигурации, размер структуры должен быть выровнен до 4
	tSetup nooLiteSetup;

	// Загружаем настройки из flash
	load_flash_param(ESP_PARAM_SAVE_1, (uint32 *)&nooLiteSetup, sizeof(tSetup));

	// Инициализация кнопки CONFMODE
	BtnInit();

	// Если во flash не было сохранено настроек или плата не в режиме STA, то запускаем web-сервер конфигурации
	if(nooLiteSetup.SetupOk != SETUP_OK_KEY || wifi_get_opmode() != STATION_MODE)
	{
		// Чистим память
		wipe_flash_param(ESP_PARAM_SAVE_1);

		// Если плата не в режиме STA-AP, то уст. это режим
		if(wifi_get_opmode() != STATIONAP_MODE)
		{
			wifi_set_opmode(STATIONAP_MODE);
			#ifdef NOOLITE_LOGGING
			ets_uart_printf("Restarting in STATIONAP mode...\r\n");
			#endif
			system_restart();
		}

		char macaddr[6];
		// Читаем MAC адрес интерфейса AP
		wifi_get_macaddr(SOFTAP_IF, macaddr);

		// Устанавливаем для AP платы настройки IP
		struct ip_info ipinfo;
		IP4_ADDR(&ipinfo.ip, 10, 10, 10, 1);
		IP4_ADDR(&ipinfo.gw, 10, 10, 10, 1);
		IP4_ADDR(&ipinfo.netmask, 255, 255, 255, 0);
		wifi_set_ip_info(SOFTAP_IF, &ipinfo);

		// Устанавливаем настройки AP платы
		struct softap_config apConfig;
		os_memset(apConfig.ssid, 0, sizeof(apConfig.ssid));
		os_sprintf(apConfig.ssid, "NOOLITE_%02x%02x%02x%02x%02x%02x", MAC2STR(macaddr));
		os_memset(apConfig.password, 0, sizeof(apConfig.password));
		os_sprintf(apConfig.password, "%02x%02x%02x%02x%02x%02x", MAC2STR(macaddr));
		apConfig.authmode = AUTH_WPA_WPA2_PSK;
		apConfig.channel = 7;
		apConfig.max_connection = 255;
		apConfig.ssid_hidden = 0;
		wifi_softap_set_config(&apConfig);

		// Вывод настроек, если включен режим отладки, см.noolite_platform.h
		#ifdef NOOLITE_LOGGING
		char temp[80];
		os_sprintf(temp, "OPMODE: %u\r\n", wifi_get_opmode());
		ets_uart_printf(temp);
		os_sprintf(temp, "SSID: %s\r\n", apConfig.ssid);
		ets_uart_printf(temp);
		os_sprintf(temp, "PASSWORD: %s\r\n", apConfig.password);
		ets_uart_printf(temp);
		os_sprintf(temp, "CHANNEL: %u\r\n", apConfig.channel);
		ets_uart_printf(temp);
		os_sprintf(temp, "CONFIGURATION WEB SERVER IP: " IPSTR "\r\n", IP2STR(&ipinfo.ip));
		ets_uart_printf(temp);
		ets_uart_printf("Starting nooLite configuration web server...\r\n");
		#endif

		// Запуск сервера конфигурации
		noolite_config_server_init();
	}
	else // Обычный режим работы платы
	{
		#ifdef NOOLITE_LOGGING
		ets_uart_printf("Starting in normal mode...\r\n");
		#endif

		// Переводим платы в режим STA
		if(wifi_get_opmode() != STATION_MODE)
		{
			#ifdef NOOLITE_LOGGING
			ets_uart_printf("Restarting in STATION mode...\r\n");
			#endif
			wifi_set_opmode(STATION_MODE);
			system_restart();
		}

		// Вывод отлад. инф о режиме STA, в том числе клиентского MAC адреса
		#ifdef NOOLITE_LOGGING
		char temp[80];
		uint8 bssid[6];
		struct station_config stationConf;
		wifi_station_get_config(&stationConf);
		os_sprintf(temp, "OPMODE: %u\r\n", wifi_get_opmode());
		ets_uart_printf(temp);
		os_sprintf(temp, "AP SSID: %s\r\n", stationConf.ssid);
		ets_uart_printf(temp);
		os_sprintf(temp, "AP PASSWORD: %s\r\n", stationConf.password);
		ets_uart_printf(temp);
		wifi_get_macaddr(STATION_IF, bssid);
		os_sprintf(temp, "STA MACADDR: " MACSTR "\r\n", MAC2STR(bssid));
		ets_uart_printf(temp);
		#endif

		#ifdef NOOLITE_LOGGING
		ets_uart_printf("System initialization done!\r\n");
		#endif

		// Запуск таймера проверки состояния wi-fi соединения с AP
		os_timer_disarm(&WiFiLinker);
		os_timer_setfn(&WiFiLinker, (os_timer_func_t *)noolite_platform_check_ip, NULL);
		os_timer_arm(&WiFiLinker, 100, 0);

	}
}

Описывать файл noolite_config_server.c я не вижу смысла, в нем идет запуск web-сервера в режиме конфигурации, по принципу работы он аналогичен работе в обычном режиме, который мы и рассмотрим ниже.
Реализация web-сервера очень простая и не претендует на гибкость и функциональность, и более того, реализовавать web-сервер на ESP8266 не совсем правильно, более правильный подход — это организация на ESP8266 TCP-клиента, который будет держать соединение с сервером управления, а на сервере управления уже организовать полноценный интерфейс взаимодействия (RESTfull или JSON) с внешней средой, под внешней средой я подразумеваею Web-интерфейс, разного рода мобильные и стационарные клиенты (Windows, Android, iOS и т.п.)

Внешний вид страниц web-интерфейса получился такой, интерфейс очень простой:

Режим конфигурации, основная страница

При нажатии на Return to normal mode, происходит возврат платы к нормальному режиму.

Режим конфигурации, страница настройки WiFi соединения

Режим конфигурации, страница настройки deviveID

Основной режим, главная страница

Страница deviceID предназначена для получения номера нашего устройства для каких-то вспомогательных задач, она просто выдает номер без какого-либо оформления

Основной режим, страница deviceID

Основной режим, страница привязки-отвязки силовых блоков

Основной режим, страница управления силовыми блоками

Реализация всего этого в коде:

Файл noolite_control_server.c
#include "ets_sys.h"
#include "osapi.h"
#include "user_interface.h"
#include "mem.h"
#include "espconn.h"
#include "driver/uart.h"

#include "noolite_control_server.h"
#include "flash_param.h"
#include "noolite_platform.h"

static struct espconn esp_conn;
static esp_tcp esptcp;
static unsigned char killConn;
// http заголовок 404
static const char *http404Header = "HTTP/1.0 404 Not Found\r\nServer: nooLite-Control-Server\r\nContent-Type: text/plain\r\n\r\nNot Found (or method not implemented).\r\n";
// http заголовок 200
static const char *http200Header = "HTTP/1.0 200 OK\r\nServer: nooLite-Control-Server/0.1\r\nContent-Type: text/html\r\n";
// html страница, верхняя часть
static const char *pageStart = "<html><head><title>nooLite Control Panel</title><style>body{font-family: Arial}</style></head><body><form method=\"get\" action=\"/\"><input type=\"hidden\" name=\"set\" value=\"1\">\r\n";
// html страница, нижняя часть
static const char *pageEnd = "</form><hr>(c) 2014 by <a href=\"mailto:sleuthhound@gmail.com\" target=\"_blank\">Mikhail Grigorev</a>, <a href=\"http://programs74.ru\" target=\"_blank\">programs74.ru</a>\r\n</body></html>\r\n";
// основная html страница
static const char *pageIndex = "<h2>Welcome to nooLite Control Panel</h2>nooLite Control deviceID: {deviceid}<ul><li><a href=\"?page=devid\">Get deviceID</a></li><li><a href=\"?page=bind\">nooLite bind-unbind channel</a></li><li><a href=\"?page=control\">nooLite control</a></li></ul>\r\n";
// html страница привязки каналов к силовым блокам
static const char *pageSetBindChannel = "<h2><a href=\"/\">Home</a> / nooLite bind-unbind channel</h2><input type=\"hidden\" name=\"page\" value=\"bind\"><table border=\"0\"><tr><td><b>Channel #:</b></td><td><input type=\"text\" name=\"channel\" value=\"{channel}\" size=\"5\"> 0 .. 32</td></tr></tr><tr><td><b>Status:</b></td><td>{status}</td></tr><tr><td></td><td><input type=\"submit\" name = \"action\" value=\"Bind\"><input type=\"submit\" name = \"action\" value=\"Unbind\"></td></tr></table>\r\n";
// html страница управления силовыми блоками
static const char *pageSetNooliteControl = "<h2><a href=\"/\">Home</a> / nooLite control</h2><input type=\"hidden\" name=\"page\" value=\"control\"><table border=\"0\"><tr><td><b>Channel #:</b></td><td><input type=\"text\" name=\"channel\" value=\"{channel}\" size=\"5\"> 0 .. 32</td></tr><tr><td><b>Status:</b></td><td>{status}</td></tr><tr><td></td><td><input type=\"submit\" name = \"action\" value=\"On\"><input type=\"submit\" name = \"action\" value=\"Off\"></td></tr></table>\r\n";
static const char *pageCommandSent = "<br><b style=\"color: green\">Command sent!</b>\r\n";
// html страница ID устройства, простая как угол дома
static const char *pageDevID = "{deviceid}";
int recvOK = 0;

static void recvTask(os_event_t *events);

// Функция отправки данных по UART модулю nooLite MT1132
// Описание протокола обмена можно найти здесь http://www.noo.com.by/assets/files/PDF/MT1132.pdf
static int ICACHE_FLASH_ATTR noolite_sendCommand(unsigned char channel, unsigned char command, unsigned char data, unsigned char format)
{
	unsigned char buf[12];
	unsigned int i;
	int checkSum = 0;
	recvOK = 0;
	os_memset(buf, 0, 12);
	buf[0] = 85;
	buf[1] = 80;
	buf[2] = command;
	buf[3] = format;
	buf[5] = channel;
	buf[6] = data;
	for(i = 0;i<10;i++){
	  checkSum+= buf[i];
	}
	buf[10] = lowByte(checkSum);
	buf[11] = 170;
	uart0_tx_buffer(&buf, 12);
	sleepms(300);
	return recvOK;
}

#ifdef NOOLITE_LOGGING
static void ICACHE_FLASH_ATTR noolite_control_server_recon(void *arg, sint8 err)
{
    ets_uart_printf("noolite_control_server_recon\r\n");
}
#endif

#ifdef NOOLITE_LOGGING
static void ICACHE_FLASH_ATTR noolite_control_server_discon(void *arg)
{
    ets_uart_printf("noolite_control_server_discon\r\n");
}
#endif

// Получение данных 
static void ICACHE_FLASH_ATTR noolite_control_server_recv(void *arg, char *data, unsigned short len)
{
	struct espconn *ptrespconn = (struct espconn *)arg;

	// Принимаем только GET запросы
	if(os_strncmp(data, "GET ", 4) == 0)
	{
		char page[16];
		os_memset(page, 0, sizeof(page));
		// Парсим GET запрос в поисках параметра page
		noolite_control_server_get_key_val("page", sizeof(page), data, page);
		// Если page=devid, то отдаем страницу с device id
		if(os_strncmp(page, "devid", 5) == 0 && strlen(page) == 5) {
			noolite_control_server_deviceid_page(ptrespconn, page, data);
		} else { // Иначе другие страницы
			noolite_control_server_process_page(ptrespconn, page, data);
		}
		return;
	}
	else // Если пришел другой тип запроса, выдаем страницу http404Header
	{
		espconn_sent(ptrespconn, (uint8 *)http404Header, os_strlen(http404Header));
		killConn = 1;
		return;
	}
}

// Процедура отдачи страницы с device id
static void ICACHE_FLASH_ATTR noolite_control_server_deviceid_page(struct espconn *ptrespconn, char *page, char *request)
{
	// Читаем параметры из flash
	tSetup nooliteSetup;
	load_flash_param(ESP_PARAM_SAVE_1, (uint32 *)&nooliteSetup, sizeof(tSetup));

	// Парсинг pageDevID в поисках конструкции вида {переменная}, эта конструкция заменяется на нужное значение и отсылается клиенту
	char *stream = (char *)pageDevID;
	char templateKey[16];
	os_memset(templateKey, 0, sizeof(templateKey));
	unsigned char templateKeyIdx;
	while(*stream)
	{
		if(*stream == '{')
		{
			templateKeyIdx = 0;
			stream++;
			while(*stream != '}')
			{
				templateKey[templateKeyIdx++] = *stream;
				stream++;
			}

			// подмена ключа {deviceid} на значение device id и отправка его клиенту
			if(os_strncmp(templateKey, "deviceid", 6) == 0)
			{
				char deviceid[3];
				char i;
				for(i=0; i<16; i++)
				{
					os_sprintf(deviceid, "%02x", nooliteSetup.deviceId[i]);
					espconn_sent(ptrespconn, (uint8 *)deviceid, os_strlen(deviceid));
				}
			}
		}
		else
		{
			espconn_sent(ptrespconn, (uint8 *)stream, 1);
		}

		stream++;
	}
	killConn = 1;
}

// Процедура отдачи других страниц
static void ICACHE_FLASH_ATTR noolite_control_server_process_page(struct espconn *ptrespconn, char *page, char *request)
{
	// Отправляем http200Header
	espconn_sent(ptrespconn, (uint8 *)http200Header, os_strlen(http200Header));
	espconn_sent(ptrespconn, (uint8 *)"\r\n", 2);

	// Отправляем pageStart
	espconn_sent(ptrespconn, (uint8 *)pageStart, os_strlen(pageStart));

	// подготовка данных
	char set[2] = {'0', '\0'};
	noolite_control_server_get_key_val("set", sizeof(set), request, set);
	char channel_num[2] = "0";
	char status[32] = "[status]";
	char action[10] = "unbind";
	os_memset(channel_num, 0, sizeof(channel_num));
	os_memset(status, 0, sizeof(status));
	os_memset(action, 0, sizeof(action));

	// Отдача страницы pageSetBindChannel
	if(os_strncmp(page, "bind", 4) == 0 && strlen(page) == 4)
	{
		// Проверка значения переменной set в форме на странице pageSetBindChannel
		// Если значение 1, то подразумевается, что форма с данными о канале и операции привязки или отвязки отправлена, нужно прочитать данные и распарсить
		if(set[0] == '1')
		{
			// Парсим данные формы, переменные канала и действия (привязка или отвязка)
			noolite_control_server_get_key_val("channel", sizeof(channel_num), request, channel_num);
			noolite_control_server_get_key_val("action", sizeof(action), request, action);
			// Отправка команды модуля MT1132
			// noolite_sendCommand(channel, command, data, format)
			// channel = 0 .. 32
			// command = 15 - bind
			// command = 9 - unbind
			// command = 2 - on
			// command = 0 - off
			// После успешной отправки данных силовым блокам, модуль MT1132 должен дать ответ OK по UART
			// Прием и парсинг данных от модуля идет в uart.c, в основной программе мы запускаем системную задачу (см. в конце вызов system_os_task), которой из uart.c отправляется сигнал 0
			// Проверяем номер канала, правильный от 0 до 32
			if(atoi(channel_num) >= 0 && atoi(channel_num) <= 32) {
				// Привязка
				if(os_strncmp(action, "Bind", 4) == 0 && strlen(action) == 4) {
					if(noolite_sendCommand(atoi(channel_num), 15, 0, 0)) {
						os_sprintf(status, "Bind OK");
					}
					else {
						os_sprintf(status, "Bind ERR");
					}
				// Отвязка
				} else if(os_strncmp(action, "Unbind", 6) == 0 && strlen(action) == 6) {
					if(noolite_sendCommand(atoi(channel_num), 9, 0, 0)) {
						os_sprintf(status, "Unbind OK");
					}
					else {
						os_sprintf(status, "Unbind ERR");
					}
				} else {
					os_sprintf(status, "Err");
				}
			}
			else {
				os_sprintf(status, "Err");
			}
		}

		// Подготовка к отправке страницы
		char *stream = (char *)pageSetBindChannel;
		char templateKey[16];
		os_memset(templateKey, 0, sizeof(templateKey));
		unsigned char templateKeyIdx;
		while(*stream)
		{
			if(*stream == '{')
			{
				templateKeyIdx = 0;
				stream++;
				while(*stream != '}')
				{
					templateKey[templateKeyIdx++] = *stream;
					stream++;
				}
				// Замена конструкйи вида {переменная} на нужные значения
				if(os_strncmp(templateKey, "channel", 7) == 0)
				{
					if(strlen(channel_num) == 0)
						os_sprintf(channel_num, "0");
					espconn_sent(ptrespconn, (uint8 *)channel_num, os_strlen(channel_num));
				}
				else if(os_strncmp(templateKey, "status", 6) == 0)
				{
					if(strlen(status) == 0) {
						os_sprintf(status, "[status]");
					}
					espconn_sent(ptrespconn, (uint8 *)status, os_strlen(status));
				}
			}
			else
			{
				espconn_sent(ptrespconn, (uint8 *)stream, 1);
			}

			stream++;
		}

		// Отправка pageCommandSent
		if(set[0] == '1')
		{
			espconn_sent(ptrespconn, (uint8 *)pageCommandSent, os_strlen(pageCommandSent));
		}
	}
	// Отдача страницы pageSetNooliteControl, логмка аналогична как и выше
	else if(os_strncmp(page, "control", 7) == 0 && strlen(page) == 7)
	{
		if(set[0] == '1')
		{
			noolite_control_server_get_key_val("channel", sizeof(channel_num), request, channel_num);
			noolite_control_server_get_key_val("action", sizeof(action), request, action);
			// noolite_sendCommand(channel, command, data, format)
			// channel = 0 .. 32
			// command = 15 - bind
			// command = 9 - unbind
			// command = 2 - on
			// command = 0 - off
			if(atoi(channel_num) >= 0 && atoi(channel_num) <= 32) {
				if(os_strncmp(action, "On", 2) == 0 && strlen(action) == 2) {
					if(noolite_sendCommand(atoi(channel_num), 2, 0, 0)) {
						os_sprintf(status, "On");
					}
					else {
						os_sprintf(status, "On ERR");
					}
				} else if(os_strncmp(action, "Off", 3) == 0 && strlen(action) == 3) {
					if (noolite_sendCommand(atoi(channel_num), 0, 0, 0)) {
						os_sprintf(status, "Off");
					}
					else {
						os_sprintf(status, "Off ERR");
					}
				} else {
					os_sprintf(status, "Err");
				}
			}
			else {
				os_sprintf(status, "Err");
			}
		}

		char *stream = (char *)pageSetNooliteControl;
		char templateKey[16];
		os_memset(templateKey, 0, sizeof(templateKey));
		unsigned char templateKeyIdx;
		while(*stream)
		{
			if(*stream == '{')
			{
				templateKeyIdx = 0;
				stream++;
				while(*stream != '}')
				{
					templateKey[templateKeyIdx++] = *stream;
					stream++;
				}
				// send the replacing value now
				if(os_strncmp(templateKey, "channel", 7) == 0)
				{
					if(strlen(channel_num) == 0 )
						os_sprintf(channel_num, "0");
					espconn_sent(ptrespconn, (uint8 *)channel_num, os_strlen(channel_num));
				}
				else if(os_strncmp(templateKey, "status", 6) == 0)
				{
					if(strlen(status) == 0) {
						os_sprintf(status, "[status]");
					}
					espconn_sent(ptrespconn, (uint8 *)status, os_strlen(status));
				}
			}
			else
			{
				espconn_sent(ptrespconn, (uint8 *)stream, 1);
			}

			stream++;
		}
		if(set[0] == '1')
		{
			espconn_sent(ptrespconn, (uint8 *)pageCommandSent, os_strlen(pageCommandSent));
		}
	}
	else // отдача главной страницы pageIndex с device id
	{
		tSetup nooliteSetup;
		load_flash_param(ESP_PARAM_SAVE_1, (uint32 *)&nooliteSetup, sizeof(tSetup));

		char *stream = (char *)pageIndex;
		char templateKey[16];
		os_memset(templateKey, 0, sizeof(templateKey));
		unsigned char templateKeyIdx;
		while(*stream)
		{
			if(*stream == '{')
			{
				templateKeyIdx = 0;
				stream++;
				while(*stream != '}')
				{
					templateKey[templateKeyIdx++] = *stream;
					stream++;
				}

				// замена {deviceid}
				if(os_strncmp(templateKey, "deviceid", 6) == 0)
				{
					char deviceid[3];
					char i;
					for(i=0; i<16; i++)
					{
						os_sprintf(deviceid, "%02x", nooliteSetup.deviceId[i]);
						espconn_sent(ptrespconn, (uint8 *)deviceid, os_strlen(deviceid));
					}
				}
			}
			else
			{
				espconn_sent(ptrespconn, (uint8 *)stream, 1);
			}

			stream++;
		}
	}

	// отдача pageEnd
	espconn_sent(ptrespconn, (uint8 *)pageEnd, os_strlen(pageEnd));
	killConn = 1;
}

// Процедура поиска и извлечения из запроса конструкций вида key=value
static unsigned char ICACHE_FLASH_ATTR noolite_control_server_get_key_val(char *key, unsigned char maxlen, char *str, char *retval)
{
	unsigned char found = 0;
	char *keyptr = key;
	char prev_char = '\0';
	*retval = '\0';

	while( *str && *str!='\r' && *str!='\n' && !found )
	{
		if(*str == *keyptr)
		{
			if(keyptr == key && !( prev_char == '?' || prev_char == '&' ) )
			{
				str++;
				continue;
			}

			keyptr++;

			if (*keyptr == '\0')
			{
				str++;
				keyptr = key;
				if (*str == '=')
				{
					found = 1;
				}
			}
		}
		else
		{
			keyptr = key;
		}
		prev_char = *str;
		str++;
	}
	if(found == 1)
	{
		found = 0;
		while( *str && *str!='\r' && *str!='\n' && *str!=' ' && *str!='&' && maxlen>0 )
		{
			*retval = *str;
			maxlen--;
			str++;
			retval++;
			found++;
		}
		*retval = '\0';
	}
	return found;
}

// callback процедура, выполняется после отправки данных
static void ICACHE_FLASH_ATTR noolite_control_server_sent(void *arg)
{
    struct espconn *pesp_conn = (struct espconn *)arg;

	if (pesp_conn == NULL)
	{
		return;
	}

	if(killConn)
	{
		espconn_disconnect(pesp_conn);
	}
}

// callback процедура, выполняется после установки соединения
static void ICACHE_FLASH_ATTR noolite_control_server_connect(void *arg)
{
    struct espconn *pesp_conn = (struct espconn *)arg;

	#ifdef NOOLITE_LOGGING
		ets_uart_printf("noolite_control_server_connect\r\n");
	#endif

    // регистрируем различные callback процедуры
    espconn_regist_recvcb(pesp_conn, noolite_control_server_recv);
    espconn_regist_sentcb(pesp_conn, noolite_control_server_sent);
	#ifdef NOOLITE_LOGGING
    espconn_regist_reconcb(pesp_conn, noolite_control_server_recon);
    espconn_regist_disconcb(pesp_conn, noolite_control_server_discon);
	#endif
}

// Инициализация и запуск we-сервера
void ICACHE_FLASH_ATTR noolite_control_server_init()
{
	#ifdef NOOLITE_LOGGING
		ets_uart_printf("noolite_control_server_init()\r\n");
	#endif
    esptcp.local_port = 80;

    esp_conn.type = ESPCONN_TCP;
    esp_conn.state = ESPCONN_NONE;
    esp_conn.proto.tcp = &esptcp;
    // регистрируем callback процедуру
    espconn_regist_connectcb(&esp_conn, noolite_control_server_connect);

    espconn_accept(&esp_conn);

    // Запуск системной задачи для обработки приема данных по UART
    // В uart.c при приеме данных осуществляется парсинг и поиск наличия ответа от платы MT1132 (см. uart0_rx_intr_handler)
    // Если ответ OK, то отправляется сигнал 0 с помощью функции system_os_post(recvTaskPrio, 0, atHead);
    os_event_t *recvTaskQueue = (os_event_t *)os_malloc(sizeof(os_event_t) * recvTaskQueueLen);
    system_os_task(recvTask, recvTaskPrio, recvTaskQueue, recvTaskQueueLen);
}

// Процедура приема сигнала
static void ICACHE_FLASH_ATTR recvTask(os_event_t *events)
{
	switch (events->sig) {
		case 0: // Если принят сигнал 0, то устанавливаем флаг приема ответа от платы recvOK = 1
			#ifdef NOOLITE_LOGGING
			ets_uart_printf("nooLite MT1132 sent OK\r\n");
			#endif
			recvOK = 1;
			break;
		default:
			break;
	}
}

И в заключении небольшое видео с демонстрацией работы платы:
Демонстрация работы Espressif ESP8266 и модуля nooLite MT1132

В целом, мы в очередной раз видим, что возможности платы на чипе ESP8266 достаточно велики.

В заключении я бы хотел поблагодарить компанию Ноотехника, а так же администрацию сайта Thinking-Home.RU за предоставленное для тестирования оборудование nooLite.

Репозитарий проекта на Github

P.S. Если у кого-то будут пожелания относительно следующей статьи про ESP8266, то я готов их выслушать. Можно написать про работу с различными сенсорами: DS18B20, DHT22, SHT21, BMP180, ИК-приемник/передатчик, внешние мс памяти, например серия Microchip 24xx16 и т.д.
Михаил Григорьев @Sleuthhound
карма
9,0
рейтинг 0,0
Системный администратор, программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое

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

  • 0
    Если кто-то не знает, где купить:

    image
    ESP8266 разных версий и аналоги
    по цене от 3 долларов за штуку.
    • +1
      на али тоже за 3$ (по скидке)

      ru.aliexpress.com/item/Free-shipping-ESP8266-serial-WIFI-wireless-module-wireless-transceiver/2031529724.html

      Отложил, всё думал брать/не брать. Но раз на Хабре/Гиктаймс про него статья, пойду куплю ))
  • +1
    Очень-очень хорошие у вас статьи!
    Интересна статья о скорости работы процессора модуля, какой частоты меандр можно получить, например, на ножке.
    Также будут полезны примеры работы с внешними АЦП, многоканальными…
  • 0
    Про DHT22 напишите. Едут как раз они мне. Может ваша прошивка будет лучше (в смысле энергосбережения).
  • +2
    Автор молодец. Очень доходчиво. Еще с первой его статьи перешел на виндошный SDK. До этого — стандарно — линуксовый под виртуалкой. И раз автор принимает заказы на следующую статью отправляю заказ :-)
    Было бы отлично допилить до логического завершения схему работы с DHT-22, как самобытного автономного устройства. Т.е. связка DHT22 + ESP8266 + 18650 (можно в принципе любой другой аккум) = автономная работа выносным датчиком в течении ГОДА. Т.е. интересует именно варианты с глубоким сном, собакой по прерыванию раз в пять минут, проверка валидности полученных от датчика значений (все помнят, что внутри DHT22 китайский проц, отдающий значения неторопливо, кроме того пляска с 2 секундами после пробуждения, и т.д. и т.п.), отсылка данных на сервер и получения подтверждения от него, контроль разряда батареи и алармы по этому поводу, ватчдог на понижение питания, на зависание…
    Sleuthhound — слабо? ;-)

    • 0
      хотя бы тоже самое с сохранением на ftp. Также мысль о выносной метеостации, но боюсь замерзнет 18650 (.
    • 0
      Пока в наличии есть только датчик DS18B20 и BMP180, DHT22 едет из Китая.
      Как все соберу, так напишу обзор. Хотя примеры кода для DS18B20, DHT22, SHT21, BMP180, HTU21D, 24xx16 уже есть в моей сборке DevKit.
    • 0
      Мне кажется не особо выгодным использование esp8266 в режиме питания от батарей со спящим режимом… Уж очень модуль прожорливый по сравнению с nRF24LE1…
      Как будет время буду реализовывать мост в виде esp8266+nRF24L01, в качестве клиентов могут быть поделки на nRF24L01+м/к или nRF24LE1…
      Про DHT-22 уже есть примеры работы с возможностью отправки на удаленный сервер и веб мордой в интернете. А так же недавно я реализовал и опубликовал беспроводной клиент на базе esp8266 c датчиками DHT-22, DS18B20, BMP180 c возможностью отправки данных на narodmon.ru. Исходники пока не доступны…
      • +1
        >>Уж очень модуль esp8266 прожорливый по сравнению с nRF24LE1…

        У них совершенно разные ниши на рынке и назначение, поэтому сравнивать их не имеет смысла.

        >>Про DHT-22 уже есть примеры работы с возможностью отправки на удаленный сервер и веб мордой в интернете.

        Примеры есть, но там не расписано по-русски, для чего что нужно, а для новичков это важно. Нужны простые примеры, без web-морды, с простым выводом результатов на консоль, остальное человек сам допишет если будет нужно. У меня в DevKit как раз такие примеры и есть.

        >> А так же недавно я реализовал и опубликовал беспроводной клиент на базе esp8266 c датчиками DHT-22, DS18B20, BMP180 c возможностью отправки данных на narodmon.ru. Исходники пока не доступны…

        Ключевое слово: Исходники пока не доступны… Так что, увы, лично мне эта прошивка не интересна. Откройте исходники, там посмотрим.
        • 0
          Вот простейший пример работы с датчиком dht22 с выводом на удаленный сервер — хотя там вроде нет комментариев, но все банально просто…

          >>У них совершенно разные ниши на рынке и назначение, поэтому сравнивать их не имеет смысла.
          Может и разные, но все мы тут используем их для IoT…

          >>Примеры есть, но там не расписано по-русски, для чего что нужно, а для новичков это важно
          Если новичок хочет разобраться с программированием, то он просто обязан знать хотя бы английский, хотя бы на уровне умения пользования переводчиком…
          • 0
            >>Вот простейший пример работы с датчиком dht22 с выводом на удаленный сервер — хотя там вроде нет комментариев, но все банально просто…

            Да это понятно, что там все понятно и просто, для меня и для Вас. А новичкам хочется чтобы все рассказали и показали на пальцах.

            >>Может и разные, но все мы тут используем их для IoT

            Боюсь наше понимание IoT, а в особенности методы использования долеки от правильных, начнем хотя бы с того, что организовавать web-сервер на ESP не имеет смысла, это лишняя нагрузка на ESP и увеличение кода. Обратите внимание на проект github.com/elektronika-ba, я считаю что человек выбрал правильный подход, слежу за этим проектом давно, тестирую, очень неплохо, идея хорошая, конечно многое еще предстоит сделать, надеюсь человек не забросит это дело.

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

            Фигня это все, никто не обязан знать чужой язык. Я русский и разговариваю по-русски и пишу по-русски, английский я знаю на уровне, hello, my name is mikhail и все и я не понимаю почему моему ребёнку со 2 класса в школе начинают втюхивать, что он должен учить и знать английский язык, он русский язык еще толком не знает, а тут учи английский. Может ему (ребенку) стоит выучить китайский, или немецкий, пусть он сам выбирает. Я вот в школе выбрал немецкий и учил его, потом в техникуме и универе учил английский.
            P.S. Это было лирическое отступление о наболевшем.

            • 0
              web сервер иногда может быть актуальнее, особенно для законченных устройств. Простой легкий веб интерфейс esp8266 тянет без проблем — у её достаточно для этого ресурсов. Если веб морду делают на всяких ардуино с 2кб ОЗУ, то чем esp8266 хуже? у этого модуля намного больше ресурсов!

              К сожалению понимать английский язык желательно для программирования- написание всех функций же на английском… Исключение только — язык программирования 1с бухгалтерии )))

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