Pull to refresh

Comments 15

Мне кажется у DeepSeek лучше получилось. =)

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

/* Network Configuration */
const char* ssid = "ESP32";
const char* password = "12345678";
IPAddress local_ip(192,168,1,1);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

/* LED Pins */
const uint8_t LED1pin = 4;
const uint8_t LED2pin = 5;
bool LED1status = false;
bool LED2status = false;

AsyncWebServer server(80);

String generateHTML(bool led1, bool led2) {
  String html = R"rawliteral(
<!DOCTYPE html><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>LED Control</title>
  <style>
    html {font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}
    body {margin-top: 50px;} 
    h1 {color: #444444; margin: 50px auto 30px;} 
    h3 {color: #444444; margin-bottom: 50px;}
    .button {
      display: block; width: 80px; background-color: #3498db; border: none;
      color: white; padding: 13px 30px; text-decoration: none; font-size: 25px;
      margin: 0px auto 35px; cursor: pointer; border-radius: 4px;
    }
    .button-on {background-color: #3498db;}
    .button-on:active {background-color: #2980b9;}
    .button-off {background-color: #34495e;}
    .button-off:active {background-color: #2c3e50;}
    p {font-size: 14px; color: #888; margin-bottom: 10px;}
  </style>
</head>
<body>
  <h1>ESP32 Web Server</h1>
  <h3>Using Access Point (AP) Mode</h3>
)rawliteral";

  html += (led1) ? 
    "<p>LED1 Status: ON</p><a class=\"button button-off\" href=\"/led1off\">OFF</a>\n" : 
    "<p>LED1 Status: OFF</p><a class=\"button button-on\" href=\"/led1on\">ON</a>\n";

  html += (led2) ? 
    "<p>LED2 Status: ON</p><a class=\"button button-off\" href=\"/led2off\">OFF</a>\n" : 
    "<p>LED2 Status: OFF</p><a class=\"button button-on\" href=\"/led2on\">ON</a>\n";

  html += "</body></html>";
  return html;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED1pin, OUTPUT);
  pinMode(LED2pin, OUTPUT);
  digitalWrite(LED1pin, LOW);
  digitalWrite(LED2pin, LOW);

  // Configure WiFi AP
  WiFi.softAP(ssid, password);
  WiFi.softAPConfig(local_ip, gateway, subnet);
  Serial.print("AP IP address: ");
  Serial.println(WiFi.softAPIP());

  // Route handlers
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    LED1status = false;
    LED2status = false;
    Serial.println("GPIO4 Status: OFF | GPIO5 Status: OFF");
    request->send(200, "text/html", generateHTML(LED1status, LED2status));
  });

  server.on("/led1on", HTTP_GET, [](AsyncWebServerRequest *request){
    LED1status = true;
    Serial.println("GPIO4 Status: ON");
    request->send(200, "text/html", generateHTML(true, LED2status));
  });

  server.on("/led1off", HTTP_GET, [](AsyncWebServerRequest *request){
    LED1status = false;
    Serial.println("GPIO4 Status: OFF");
    request->send(200, "text/html", generateHTML(false, LED2status));
  });

  server.on("/led2on", HTTP_GET, [](AsyncWebServerRequest *request){
    LED2status = true;
    Serial.println("GPIO5 Status: ON");
    request->send(200, "text/html", generateHTML(LED1status, true));
  });

  server.on("/led2off", HTTP_GET, [](AsyncWebServerRequest *request){
    LED2status = false;
    Serial.println("GPIO5 Status: OFF");
    request->send(200, "text/html", generateHTML(LED1status, false));
  });

  server.onNotFound([](AsyncWebServerRequest *request){
    request->send(404, "text/plain", "Not found");
  });

  server.begin();
  Serial.println("HTTP async server started");
}

void loop() {
  // Update LED states
  digitalWrite(LED1pin, LED1status ? HIGH : LOW);
  digitalWrite(LED2pin, LED2status ? HIGH : LOW);
  
  // No need for server.handleClient() with async server
}

Лямбда (не именованные, анонимные, безымянные) функции хорошо выглядят когда они короткие.

А в коде светодиоды будут гаснуть при каждом новом обращении к корневой страничке. ИИ за тебя накодит кучку кода, но логику сам проверяй.

Вы к кому сейчас обращались? Я лишь тактично попытался намекнуть автору статьи, что асинхронный код лучше синхронного. И на брудершафт мы вроде не пили пока чтобы тыкать.

В норме русского языка, говорить "ты", когда имеется в виду абстрактный пример, говорящий про действие не обращенное конкретно к кому то, а само по себе. То есть, как в данном случае: стоит самому проверить, прежде чем доверять коду который сгенерировал ИИ. И это не конкретно к вам обращение, а к любому кто так делает. О чем, кстати, люто подтверждаю ибо почти каждый день встречаюсь с вопросами ко мне как к эксперту в определённой области, по такому сгенерированному коду. Не надо сразу все сразу на свой счет воспринимать.

Ещё бы эту простыню HTML кода в константы вынести.

В статье много чего не расмотрено, так сказать очень поверхностно...

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

/*
Generic ESP8266 Module

Питание: 3,3 В до 250 мА
Объём Flash-памяти: 2 МБ
Беспроводной интерфейс: Wi-Fi 802.11 b/g/n 2,4 ГГц
Тактовая частота: 80 МГц
список команд http://wiki.amperka.ru/_media/%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D1%8B:esp8266-wifi-module:esp8266_at_commands.pdf 

воспринимает с UART порта передачу данных в формате
<set_term=33.1>
передает в UART порт статус в формате
<status=OK>, или <status=E1>

*/

#include <FS.h>
#include <ESP8266WiFi.h> 

#include "params.h" 

// на рабочей компиляции закоментировать !
// #define ON_DEBUG_ECHO

#define BUFER_WIFI_COUNT 250
// -----------------------------------------------------------
// буферы, нужны для реализации асинхронности чтения данных в главном цикле
char buf_wifi[BUFER_WIFI_COUNT + 1];
uint8_t wifi_tec_tec_size = 0; // текущая заполненая длинна данных, от 0 до BUFER_WIFI_COUNT 

// описание зачитанных параметров WiFi запроса, глобальные для поддержки асинхронности
String Request_Host = ""; // имя сервера
String Request_Type = ""; // GET, POST
String Request = "";	  // строка запроса, начинается с /

unsigned long time_loop_Request = 0; // время последней операции с клиентом
long speedOfLight = 9600;			 // скорость порта UART
char sid[21];
char pass[21];


// -----------------------------------------------------------

// Create an instance of the server
// specify the port to listen on as an argument
WiFiServer server(80);
WiFiClient client;					  // подразумеваем, что у нас будет только одна сесия
unsigned long time_loop_new = 0;	  // время последнего окончания чтения первой строки запроса

void clear_buf(char *buf, uint8_t start_num);


void setup()
{
	Serial.begin(57600);

	while (!SPIFFS.begin())
	{ 
		delay(100);
		Serial.println(F("ERROR INIT SPIFFS."));
		delay(10000);
	}

	while (!Read_Param_SD()) 
	{
		delay(100);
		Serial.println(F("ERROR READ setup.ini"));
		delay(10000);
	}

	// параметры speedOfLight, sid, pass считаны из файла setup.ini

	// Connect to WiFi network
	Serial.println();
	Serial.print(F("Connecting to "));
	Serial.println(sid);

	WiFi.mode(WIFI_STA);
	WiFi.begin(&(sid[0]), &(pass[0]));

/*        case STATION_GOT_IP:
            return WL_CONNECTED;
        case STATION_NO_AP_FOUND:
            return WL_NO_SSID_AVAIL;
        case STATION_CONNECT_FAIL:
        case STATION_WRONG_PASSWORD:
            return WL_CONNECT_FAILED;
        case STATION_IDLE:
            return WL_IDLE_STATUS;
        default:
            return WL_DISCONNECTED;
*/
	while (WiFi.status() != WL_CONNECTED)
	{
		delay(5000);
		Serial.print(F("."));
		Serial.print(sid);
//		Serial.print(F("."));
//		Serial.print(pass);
		Serial.println(F("."));
	}
	Serial.println();
	Serial.println(F("WiFi connected"));

	// Start the server
	server.begin();
	Serial.println(F("Server started"));

	// Print the IP address
	Serial.println(WiFi.localIP());

	clear_buf(&(buf_wifi[0]), 0);
	uart_value_tec[0] = 0;
	uart_status_tec[0] = 0;
	wifi_value_tec[0] = 0;
	wifi_status_tec[0] = 0;

	// инициализация массивов
	for (uint8_t i = 0; i < PARAM_COUNT; i++)
	{
		for (uint8_t ii = 0; ii < 20; ii++)
		{
			if (out_id[i][ii] == 0)
			{
				out_id_length[i] = ii;
				break;
			}
		}

		out_value[i][0] = 0;
		out_status[i][0] = 0;
	}
}

void loop() {
	LoopReadData();

	if (client)	{
		if (time_pause(time_loop_new, millis()) > 10000) {
			// держим не более 10 сек
			client.stop();
			Request_Host = "";
			Request_Type = "";
			Request = "";
		}
	}

	if (!client) {
		// попытка создание клиентской сессии
		client = server.available();
		if (client)	{
			time_loop_new = millis();
		}
	}

	if (client)	{
		LoopReadWiFi();
	}
}

// -----------------------------------------------------------
// процедуры формирования страниц вывода

void out_page_file(WiFiClient *client, char *FileName, char *Content)
{
	File f = SPIFFS.open(FileName, "r");
	if (!f)
	{
		out_404(client);
	}
	else
	{
		client->print(F(
			"HTTP/1.1 200 OK\r\n"
			"Cache-Control: max-age=31536000\r\n"
			"Content-Type: "));
		client->print(Content);
		out_br(client);
		client->print(F("Content-Length: "));
		client->print(f.size(), DEC);
		out_br(client);
		out_br(client);
		while (f.available())
		{
			client->write(f.read());
		}
		out_br(client);
		f.close();
	}
}

void out_ajax_data(WiFiClient *client)
{
	//
	// формат: {"t_home_1":{"value":{"data-v":"030"},"status":{"data-st":"OK"}}}
	// в этом примере меняются только: t_home_1,030,OK
	// можно менять/добавлять и value, status, data-v, data-st но тогда надо менять HTML и CSS файлы
	//

	String str = "{";
	for (uint8_t i = 0; i < PARAM_COUNT; i++)
	{
		str = str + (String)("\"") + (String)(out_id[i]) + (String)("\":{\"value\":{\"data-v\":\"") + (String)(out_value[i]) + (String)("\"},\"status\":{\"data-st\":\"") + (String)(out_status[i]) + (String)("\"}},");
	}
	str[str.length() - 1] = '}';

	client->print(F(
		"HTTP/1.1 200 OK\r\n"
		"Content-Type: application/json\r\n"));
	client->print(F("Content-Length: "));
	client->print(str.length(), DEC);
	out_br(client);
	out_br(client);
	client->print(str);
	out_br(client);
}

void out_favicon(WiFiClient *client)
{

	client->print(F(
		"HTTP/1.1 200 OK\r\n"
		"Cache-Control: max-age=31536000\r\n"
		"Transfer-Encoding: chunked\r\n"
		"Content-Type: image/x-icon\r\n"));
	out_br(client);
	out_br(client);
}

void out_404(WiFiClient *client)
{

	client->print(F(
		"HTTP/1.1 200 OK\r\n"
		"Transfer-Encoding: chunked\r\n"
		"\r\n"
		"<!DOCTYPE html>\r\n"
		"<html>\r\n"
		"<head>\r\n"
		"<title>404</title>\r\n"
		"</head>\r\n"
		"<body>\r\n"
		"<h1>ERROR 404</h1>\r\n"
		"<div>\r\n"
		"</div>\r\n"
		"</body>\r\n"
		"</html>\r\n"));
	out_br(client);
}

// -----------------------------------------------------------
// процедуры логики главного цикла

void LoopReadWiFi() {
	uint8_t c;
	unsigned long time_loop = millis();
	while (client.available())	{
		time_loop_new = millis(); // обновим данные о времени сесии

		if (time_pause(time_loop, millis()) > 100) {
			// если читаем данные более 0.1 сек - надо прерватся и поделится ресурсами с другими задачами
			break;
		}

		c = client.read(); 
		if (c > 31) {
			if (c > 127) { 
				// символы с кодом > 127 пропускаем
				continue;
			}
			if (wifi_tec_tec_size >= BUFER_WIFI_COUNT) {
				// если нам пришла строка больше размера нашего буфера мы ее усекаем 
				// будем читать буфер и пока не найдем символ с кодом < 32
				// или пока не пройдет таймаут
				continue;
			}

			// запишем в буфер значение и увеличим переменную считанных данных
			*((uint8_t *)(buf_wifi + wifi_tec_tec_size)) = c;
			wifi_tec_tec_size++;
			// и зафиксируем время для расчета таймаута
			time_loop_Request = millis();
		} else {
			// нам пришел символ меньше 32, надо разбирать то, что лежит в buf_wifi
			if (wifi_tec_tec_size > 0) {
				parse_buf_wifi();
			}
		}
	}

	// если не было данных более 0.1 сек, значит запрос закончился
	if 	(time_pause(time_loop_Request, millis()) > 100) {
		// если в буфере есть какие-то данные, то их будем анализировать
		if (wifi_tec_tec_size > 0) {
			parse_buf_wifi();
		}
		// код зачистки по таймауту для следующих итераций, 
		// если не все 3 параметра заполнены, значит весь пакет не валидный
		if ((Request.length() == 0) ||
			(Request_Type.length() == 0) ||
			(Request_Host.length() == 0)) {
			
			Request = "";
			Request_Type = "";
			Request_Host = "";
		}
	}


	// если мы получили 3 параметра - значит что-то надо делать
	// если не все параметры заполнены возвращаемся в главный цикл на последующие заходы
	if ((Request.length() > 0) &&
		(Request_Type.length() > 0) &&
		(Request_Host.length() > 0)) {

		// формируем ответ
		if (Request == "/")	{

			out_page_file(&client, "/index.html", "text/html");

		} else if (Request == "/setup.ini")	{

			out_404(&client); // запрет на скачку этого файла, в нем настройки сети

		} else if (Request == "/ajax_data")	{

			out_ajax_data(&client);

		} else if (Request == "/favicon.ico") {

			out_favicon(&client); // уменьшаем запросы на выдачу этого файла
								  // если положить иконку можно будет и показывать
		} else {

			int16_t inum = Request.indexOf('.');
			if (inum >= 0) {
				String file_ext = Request.substring(inum + 1);
				// все остальные файлы по расширениям
				if (file_ext == "css")
				{
					out_page_file(&client, (char *)(Request.c_str()), "text/css");
				}
				else if (file_ext == "png")
				{
					out_page_file(&client, (char *)(Request.c_str()), "image/png");
				}
				else if (file_ext == "jpg")
				{
					out_page_file(&client, (char *)(Request.c_str()), "image/jpg");
				}
				else if (file_ext == "html")
				{
					out_page_file(&client, (char *)(Request.c_str()), "text/html");
				}
			} else {
				out_404(&client);
			}
		}

		// очистка
		Request_Host = ""; // имя сервера
		Request_Type = ""; // GET, POST
		Request = "";	   // строка запроса, начинается с /
	}
}

void LoopReadData()
{
	//
	// ожидаемый формат: <id=value;status>
	//
	uint8_t c;
	unsigned long time_loop = millis();
	while (Serial.available())
	{
		if (time_pause(time_loop, millis()) > 100) {
			// если читаем данные более 0.1 сек - надо прерватся и поделится ресурсами с другими задачами
			break;
		}

		c = Serial.read(); 

		if (c < 32) { // эти символы пропускаем и считаем сначала
			uart_read_0_31();
		}
		else if (uart_step == 0)	{ // ждем начала блока "<"
			uart_read_param_step_0(c); 
		}
		else if (uart_step == 1) { // читаем и ищем идентификатор, до символа "="
			uart_read_param_step_1(c);
		}
		else if (uart_step == 2) { // читаем VALUE, до символа ";"
			uart_read_param_step_2(c);
		}
		else if (uart_step == 3) { // читаем STATUS, до символа ">"
			uart_read_param_step_3(c);
		}

		if (uart_step == 4)	{ // копируем текущие параметры на постоянное место
			uart_read_param_step_4();
		}
	}
}

boolean Read_Param_SD() 
{
	// читаем файл инициализации setup.ini, и устанавливаем параметры
	char c;
	String inString = "";
	String s_name = "";
	String s_value = "";
	uint8_t ii;

	clear_buf(&(buf_wifi[0]), 0); // этот буфер будем использовать для чтения параметров (только в setup)

	File f = SPIFFS.open("/setup.ini", "r");
	if (!f)	{
		Serial.println(F("ERROR READ setup.ini"));
		return false;
	} else {
		while (f.available()) {
			c = (char)f.read();

			if ((uint8_t)c < 32)
			{ // эти символы не могут быть в данных, надо анализировать чего в буфере
				// и потом заполнять заново

				ii = find_byte_buf(&(buf_wifi[0]), '=');
				if (ii != 0) {
					inString = buf_wifi;
					s_name = inString.substring(0, ii);
					s_value = inString.substring(ii + 1);
					if (s_name == "sid") {
						if (s_value.length() <= 20) {
							s_value.toCharArray(&(sid[0]), s_value.length()+1);
							sid[s_value.length()] = 0;
						} else {
							s_value.toCharArray(&(sid[0]), 20);
							sid[20] = 0;
						}
					}
					if (s_name == "pass") {
						if (s_value.length() <= 20)	{
							s_value.toCharArray(&(pass[0]), s_value.length()+1);
							pass[s_value.length()] = 0;
						} else	{
							s_value.toCharArray(&(pass[0]), 20);
							pass[20] = 0;
						}
					}
					if (s_name == "serial") {
						speedOfLight = (long)(s_value.toInt());
					}
				}

				clear_buf(&(buf_wifi[0]), 0);
			} else {
				ii = find_byte_buf(&(buf_wifi[0]), 0);
				*((uint8_t *)(buf_wifi + ii)) = c;
			}
		}
		f.close(); //Close file
	}

	return true;
}

// -----------------------------------------------------------
// вспомогательные процедуры

void parse_buf_wifi() {
	String inString = "";
	String file_ext = "";

	// для начала поставим нулевой символ в конец данных буфера
	*((uint8_t *)(buf_wifi + wifi_tec_tec_size)) = 0;

	inString = buf_wifi;

	if (inString.startsWith("Host: ")) {
		Request_Host = inString.substring(6);
	}
	else if (inString.startsWith("GET "))
	{
		Request = inString.substring(4);
		Request_Host = "";
		Request_Type = "GET";
		if (Request.indexOf(' ') != -1)
		{
			Request.remove(Request.indexOf(' '));
		}
	}
	else if (inString.startsWith("POST "))
	{
		Request = inString.substring(5);
		Request_Host = "";
		Request_Type = "POST";
		if (Request.indexOf(' ') != -1)
		{
			Request.remove(Request.indexOf(' '));
		}
	}

	wifi_tec_tec_size = 0;
}

void clear_buf(char *buf, uint8_t start_num = 0)
{
	for (uint8_t i = start_num; i < 250; i++)
	{
		*((uint8_t *)(buf + i)) = 0;
	}
	*((uint8_t *)(buf + 250)) = 0;
}

void out_br(WiFiClient *client)
{
	client->print(F("\r\n"));
}

unsigned long time_pause(unsigned long tlo, unsigned long tln) {
	if (tlo <= tln)	{
		return (tln - tlo);
	} else {
		return (4294967295 - tln + tlo);
	}
}

uint8_t find_byte_buf(char *buf, uint8_t b)
{
	for (uint8_t i = 0; i < 250; i++)
	{
		uint8_t a = *((uint8_t *)(buf + i));
		if (a == b) {
			return i;
		}
		if (a == 0)	{
			return 0;
		}
	}
	return 0;
}



Количество сессий вероятно ограничивается настройками TCPIP стека. В нативном ESP-IDF фреймворке настраиваемо через переменную CONFIG_LWIP_MAX_SOCKETS, по умолчанию равную 8.

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

Для этого нужно перейти с уровня Arduino Core на уровень ESP-IDF SDK.

а почему крошечный, вот вполне себе полноценный и на esp8266

Какие-то совсем неудачные решения учитывая недостаток ресурсов у ESP32
Будет правильнее (с моей точки зрения):
- хранить статическую html zip-ованую страницу на fs контроллера
- в самом html описывать всю логику работы на javascript
- принимать и отправлять данные на МК через web-socket в формате json например.
Такой подход позволит существенно сократить использование памяти у МК, снимет с него нагрузку по генерации html, сократит объем передаваемых данных и ускорит скорость их обработки.

Может я не заметил, но как идет подключение к Wi-Fi?

Я как-то делал решение, когда информация о подключенном Wi-Fi хранится в ПЗУ, а если информации нет или пользователь нажал кнопку BOOT более чем 8 секунд, то информация о Wi-Fi сбрасывается и ESP переходит в режим Access Point.

А, еще фишка: Учитывая, что у ESP нет дисплея, то кратковременное нажатие на кнопку BOOT выдавало последний актет IP адреса морзянкой. Не супер удобно, зато деревянно надежно. Особенно для тех, кто в течение 4 лет барабанил "ба-а-ааки текут".

Возможно вы говорите о работе с модулем посредством AT команд и стандартной прошивке. Тут приведены примеры "своих" прошивок, где явно описано какой способ использовать: wifi точка или клиент, а по нажатию на boot, не должно производить какой то эффект на работу устройства с этой прошивкой (если не указано иного)

Тут подключение к существующей точке

//connect to your local wi-fi network
  WiFi.begin(ssid, password);

А тут создание wifi точки

WiFi.softAP(ssid, password);
WiFi.softAPConfig(local_ip, gateway, subnet);

Оба используют библиотеки WiFi, в ардуино ide, можно их установить.

Сайт амперки в помощь

(Мне помог, другим советую =) )

Можно ли как-то обращаться к ESP32 не находясь в общей сети вай фай и не имея белого ip адреса, например находясь в другом городе, по аналогии с другими умными устройствами?

без "костыля" никак. даже если на Вашем роутере белый IP, что по моему редкость, то надо будет всё равно дополнительно его настраивать на "проброс портов" и esp32 надо внутри сети постоянный адрес давать. ещё есть вариант использовать сайт iocontrol.ru. на нём вроде можно завести учётку указав почту и используя простенький API гонять пакеты между устройствами находящимися за NATами.

Sign up to leave a comment.