Изменение сети Wi-Fi ESP8266 через точку доступа

У меня есть проект ESP8266, который требует установки SSID и пароля для сети через точку доступа. Все работает нормально, единственная проблема заключается в том, что если вы отправляете неверные учетные данные на точку доступа, сервер возвращает пустой ответ до истечения периода ожидания, а сообщение об ошибке никогда не отправляется клиенту.

Если соединение установлено успешно, клиенту отправляется соответствующее сообщение об успешном подключении. Это похоже на то, как если бы функция WiFi.begin() сама обнаруживала сбой соединения, и что-то мешало мне отправить собственный ответ.

%  curl -X POST "http://192.168.1.1/authenticate?ssid=FakeNetworkpassword=87654321" 
curl: (52) Empty reply from server

Провел небольшое исследование и увидел упоминания о том, что цикл должен работать, поэтому я попытался сделать это ниже, но безрезультатно. Буду признателен за любые идеи, спасибо.

#include "ESP8266WiFi.h"
#include "ESP8266HTTPClient.h"
#include "ESP8266WebServer.h"

HTTPClient http;
WiFiClient client;

ESP8266WebServer server(80);
IPAddress ipAP(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

unsigned long previousMillisConnection = 0;
unsigned long startingMillisConnection = 0;
const unsigned long CONNECTION_PERIOD = 1000;
const unsigned long CONNECTION_TIMEOUT = 10000;

int DELAY = 40;

char ssid[64] = { 0 };
char password[64] = { 0 };

char ssidNew[64];
char passNew[64];

const int STATE_DEFAULT = 0;
const int STATE_CREDS = 1;
int state = STATE_DEFAULT;

void setup() {
  Serial.begin(9600);
  
  WiFi.softAPConfig(ipAP, gateway, subnet);
  WiFi.softAP("ESP8266", "12345678");
  delay(100);
  
  server.on("/authenticate", HTTP_POST, onSetCredentials);
  server.begin();
  
  performWifiConnect(ssid, password);
  WiFi.waitForConnectResult(CONNECTION_TIMEOUT);
  
  Serial.println("Initialization complete");
}

void loop() {
  if (state == STATE_DEFAULT) {
    server.handleClient();
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected!");
    }
  }

  else if (state == STATE_CREDS) {
    whileConnecting();
  }
  
  delay(DELAY);
}

void performWifiConnect(char ssid[], char password[]) {
  Serial.println("Attempting to connect to " + String(ssid) + "...");
  WiFi.setAutoReconnect(false);
  WiFi.persistent(false);
  WiFi.begin(ssid, password);
}

void onWifiConnected() {
  Serial.println("WiFi connected " + WiFi.localIP().toString());
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
}

void onSetCredentials() {
  Serial.println("onSetCredentials");
  
  server.arg("ssid").toCharArray(ssidNew, 64);
  server.arg("password").toCharArray(passNew, 64);

  if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
    Serial.println("Already connected to " + String(ssidNew));
    server.send(200, "application/json", "{ \"success\": true, \"changed\": false }");
  } else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    state = STATE_CREDS;
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
  }
}

void whileConnecting() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillisConnection >= CONNECTION_PERIOD) {
    previousMillisConnection = currentMillis;

    Serial.println("checking connection...");
    
    // Success
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected to " + String(ssidNew) + " successfully!");
      
      strcpy(ssid, ssidNew);
      strcpy(password, passNew);
      
      server.send(200, "application/json", "{ \"success\": true, \"changed\": true }");

      state = STATE_DEFAULT;
    }
    
    // Failure
    if (currentMillis - startingMillisConnection >= CONNECTION_TIMEOUT) {
      Serial.println("connection failed, attempting to reconnect to old network!");
      Serial.println("ssidOld: " + String(ssid));
      Serial.println("passOld: " + String(password));

      String errorMessage = "Timed out while attempting to connect to " + String(ssidNew);
      Serial.println(errorMessage);
      server.send(500, "application/json", "{\"errorMessage\":\"" + errorMessage + "\"}");
      
      performWifiConnect(ssid, password);
      
      WiFi.waitForConnectResult(CONNECTION_TIMEOUT);

      state = STATE_DEFAULT;
      
      if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Could not return to old network!");
      }
    }
  }
}

, 👍2


2 ответа


Лучший ответ:

1

Я рекомендую вам использовать библиотеку WiFiManager. Настройка WiFi решена.


Чтобы присоединиться к точке доступа, esp8266 должен изменить радиоканал WiFi на канал точки доступа. Так как esp8266 имеет только одно радио, SoftAP также должен изменить канал. Это приводит к отключению клиентов SoftAP.

Решение состоит в том, чтобы ответить на запрос конфигурации перед подключением к точке доступа, а затем разрешить странице конфигурации выполнить новый запрос результата.

Для иллюстрации приведем простой пример:

#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif

void setup() {

  Serial.begin(115200);
  delay(500);

#ifdef ESP32
// WiFi.begin(); // использовать SSID и пароль, хранящиеся в SDK. закомментирован для проверки AP конфигурации
#else
// WiFi.отключить(); // забыть о постоянном соединении, чтобы проверить AP конфигурации
#endif

  // ожидание подключения к запомненной сети Wi-Fi
  Serial.println("Waiting for connection to WiFi");
  WiFi.waitForConnectResult();

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println();
    Serial.println("Could not connect to WiFi. Starting configuration AP...");
    configAP();
  } else {
    Serial.println("WiFi connected");
  }
}

void loop() {
}

void configAP() {

  WiFiServer configWebServer(80);

  WiFi.mode(WIFI_AP_STA); // запускает точку доступа по умолчанию (заводская установка по умолчанию или настройка как постоянная)

  Serial.print("Connect your computer to the WiFi network ");
#ifdef ESP32
  Serial.print("to SSID of you ESP32"); // нет геттера для SSID SoftAP
#else
  Serial.print(WiFi.softAPSSID());
#endif
  Serial.println();
  IPAddress ip = WiFi.softAPIP();
  Serial.print("and enter http://");
  Serial.print(ip);
  Serial.println(" in a Web browser");

  configWebServer.begin();

  while (true) {

    WiFiClient client = configWebServer.available();
    if (client) {
      char line[64];
      int l = client.readBytesUntil('\n', line, sizeof(line));
      line[l] = 0;
      client.find((char*) "\r\n\r\n");
      if (strncmp_P(line, PSTR("POST"), strlen("POST")) == 0) {
        l = client.readBytes(line, sizeof(line));
        line[l] = 0;

        // анализируем параметры, отправленные html-формой
        const char* delims = "=&";
        strtok(line, delims);
        const char* ssid = strtok(NULL, delims);
        strtok(NULL, delims);
        const char* pass = strtok(NULL, delims);

        // отправляем ответ перед попыткой подключения к сети WiFi
        // потому что это сбросит SoftAP и отключит клиентскую станцию
        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Connection: close"));
        client.println(F("Refresh: 10")); // отправить запрос через 10 секунд
        client.println();
        client.println(F("<html><body><h3>Configuration AP</h3><br>connecting...</body></html>"));
        client.stop();

        Serial.println();
        Serial.print("Attempting to connect to WPA SSID: ");
        Serial.println(ssid);
        WiFi.persistent(true);
        WiFi.setAutoConnect(true);
        WiFi.begin(ssid, pass);
        WiFi.waitForConnectResult();

        // настройка продолжается со следующего запроса

      } else {

        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Connection: close"));
        client.println();
        client.println(F("<html><body><h3>Configuration AP</h3><br>"));

        int status = WiFi.status();
        if (status == WL_CONNECTED) {
          client.println(F("Connection successful. Ending AP."));
        } else {
          client.println(F("<form action='/' method='POST'>WiFi connection failed. Enter valid parameters, please.<br><br>"));
          client.println(F("SSID:<br><input type='text' name='i'><br>"));
          client.println(F("Password:<br><input type='password' name='p'><br><br>"));
          client.println(F("<input type='submit' value='Submit'></form>"));
        }
        client.println(F("</body></html>"));
        client.stop();

        if (status == WL_CONNECTED) {
          delay(1000); // позволить SDK завершить связь
          Serial.println("Connection successful. Ending AP.");
          configWebServer.stop();
          WiFi.mode(WIFI_STA);
          return;
        }
      }
    }
  }
}
,

Использование второго запроса — такой приятный и простой ответ. Работает безупречно!, @Danny Buonocore


1

В этом вопросе несколько взаимосвязанных вопросов.

Во-первых, код setup() запускается раньше, чем должен.

Первая проблема здесь:

  server.on("/authenticate", HTTP_POST, onSetCredentials);
  server.begin();
  
  performWifiConnect(ssid, password);
  WiFi.waitForConnectResult(CONNECTION_TIMEOUT);

Вы уже установили для ssid и password значение null, а server.begin() еще не запустит сервер (я перейду к этому вопросу через минуту), так что это означает, что ваши учетные данные будут равны нулю при запуске программы. Тогда нет смысла запускать waitForConnectResult(), потому что не к чему подключаться. Однако в настройке performWifiConnect() может быть логика, поскольку, если вы когда-либо устанавливали учетные данные Wi-Fi с помощью wifi.begin() раньше при отключении постоянных учетных данных эти учетные данные по-прежнему запоминаются (просто не запоминаются новые).

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

void loop() {
  if (state == STATE_DEFAULT) {
    server.handleClient();
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected!");
    }
  }

  else if (state == STATE_CREDS) {
    whileConnecting();
  }
  
  delay(DELAY);
}

Как видите, server.handleclient() (фактический сервер; способность отвечать на запросы) работает только тогда, когда у него нет state == STATE_DEFAULT. Это затем изменяется, state = STATE_CREDS;, во время обратного вызова для получения учетных данных сети. Вероятно, вы получите ответ от работающего в данный момент обработчика запросов, но в противном случае это вызовет у вас проблемы, поскольку сервер никогда не будет отвечать ни на какие будущие запросы. Вы должны постоянно вызывать server.handleclient(), иначе ничего, связанное с сервером, работать не будет.

Но что-то еще не так. Вы можете передавать ответы обратно с сервера только в том случае, если вы фактически отправляете их во время обработки запроса сервером.

else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    state = STATE_CREDS;
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
  }

Как видите, в этой ветке нет server.send(), поэтому вы не получите ответа. Вероятно, это самая непосредственная причина вашей проблемы с пустым ответом.

Вместо этого у вас есть это в функции whileConnecting(). Как отмечалось ранее, сервер не может отправлять ответы, которых нет в функции, которую он вызывает при получении запроса, или в функциях, вызываемых этой функцией. Сервер не вызывает whileConnecting(), это делает loop(). Поэтому он не может увидеть или отправить этот ответ.

(Примечание: это требование сервера реализовано довольно странно, поскольку вы вызываете методы сервера, в то время как один из его методов вызывает ваш код, только для того, чтобы сервер спросил себя, что вы сказали ему, пока он был занят вашего запроса, возьмите этот буфер и верните его клиенту. Но он только выполняет эту проверку где-то внутри server.handleClient() и другие send() вызовы либо игнорируются, либо помещаются в очередь для отправки при следующем запросе клиента.)

Если бы вместо этого вы ждали соединения внутри обработчика запросов сервера, а затем также отправляли туда сообщения об ошибке/успешном выполнении, это могло бы сработать. Это по-прежнему плохая практика, так как сервер (эта часть, о которой я сказал, вы всегда хотите не задерживать) и весь остальной код будут приостановлены, пока он ожидает подключения, но вы вероятно пусть он работает пока не происходит ничего срочного или важного.

Лучшим подходом было бы, чтобы ваш клиент опрашивал второй адрес (или просто тот же адрес без значений POST, а затем сервер проверял, нет ли аргументов) и сообщал, подключен ли он в данный момент.

Кроме того, ваш код проверки соединения также имеет проблему. В loop() есть задержка, которая уже увязнет. Но у вас также есть функция whileConnecting для проверки прошедшего времени. Это это правильный способ координации нескольких независимых операций проверки и выполнения, так что вы знаете об этом, но это не было перенесено в остальную часть кода.

Кроме того, также во время подключения вы затем вызываете WiFi.waitForConnectResult(CONNECTION_TIMEOUT);. Это заблокирует эту функцию до тех пор, пока не будет получен результат, что, таким образом, остановит все остальное на время ожидания. Единственное место, где вы "должны" используйте это (опять же, вероятно, плохая практика) внутри сервера, если вы пытались получить от него прямой ответ со статусом соединения или, если не происходит ничего другого важного (что есть, так что это вопрос). Поскольку вы уже периодически запускаете код по таймеру, просто сделайте так, чтобы этот код проверял время ожидания напрямую (самый простой способ: вызовите функцию подключения с учетными данными, установите время проверки с помощью timeout, затем проверьте еще раз) и просто переподключитесь вместо того, чтобы заставлять ждать. Это оставит сервер и остальную часть вашего кода отзывчивыми.

А теперь со всем (ну, с большей частью) из этого вместе взятым: (ПРИМЕЧАНИЕ. Этот код потребует внесения некоторых изменений в остальную часть вашего кода и может потребовать корректировки самого себя, но он должен помочь вам очень близко. Я предлагаю сначала сделать резервную копию вашего кода на случай, если вам нужно сослаться или исправить что-то . Я также написал это в текстовом редакторе, так как в настоящее время у меня нет доступа к IDE, чтобы проверить это. Могут быть небольшие ошибки.)

Решение А (дайте серверу подождать и сообщить вам результаты): (ПРИМЕЧАНИЕ: также удалите whileConnecting() и все state, если вы используете этот метод; вы хотите убедиться, что server.handleClient() всегда вызывается до установления соединения. на самом деле успешно, или просто всегда.)

void onSetCredentials() {
  Serial.println("onSetCredentials");
  
  server.arg("ssid").toCharArray(ssidNew, 64);
  server.arg("password").toCharArray(passNew, 64);

  if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
    Serial.println("Already connected to " + String(ssidNew));
    server.send(200, "application/json", "{ \"success\": true, \"changed\": false }");
  } else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
    //код для определения состояния подключения к серверу и ответа начинается здесь//
    WiFi.waitForConnectResult(CONNECTION_TIMEOUT);
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected to " + String(ssidNew) + " successfully!");
      
      strcpy(ssid, ssidNew);
      strcpy(password, passNew);
      
      server.send(200, "application/json", "{ \"success\": true, \"changed\": true }");
    }
    
    // Отказ
    if (currentMillis - startingMillisConnection >= CONNECTION_TIMEOUT) {
      Serial.println("connection failed, attempting to reconnect to old network!");
      Serial.println("ssidOld: " + String(ssid));
      Serial.println("passOld: " + String(password));

      String errorMessage = "Timed out while attempting to connect to " + String(ssidNew);
      Serial.println(errorMessage);
      server.send(500, "application/json", "{\"errorMessage\":\"" + errorMessage + "\"}");
    }
  }
}

Это решение (A) вероятно вернет вам состояние подключения, если время попытки подключения Wi-Fi истекает раньше, чем запрос/сервер cURL. На самом деле я не проверял это и упомянул, что это плохая практика.

Решение Б (опросить сервер о состоянии подключения, а не ждать ответа): (Примечание. Опять же, удалите всю логику state и whileConnecting() из другого кода.)

void onSetCredentials() {
  Serial.println("onSetCredentials");
  if (!server.hasArg("ssid")){ //проверить запрос без аргументов и вернуть статус подключения//
    server.send("200", "text/plain", (WiFi.status() != WL_CONNECTED)? "Connected" : "Not Connected"); //или любые другие сообщения, которые вы хотите
    //Возможно, вы также захотите добавить здесь какую-то запись типа Serial.print(), поскольку я этого не сделал.//
  }
  else{ //здесь возобновляется работа существующего обработчика запроса на установку соединения//
    server.arg("ssid").toCharArray(ssidNew, 64);
    server.arg("password").toCharArray(passNew, 64);

    if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
      Serial.println("Already connected to " + String(ssidNew));
    } else {
      Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
      performWifiConnect(ssidNew, passNew);
      startingMillisConnection = millis();
    }
  }
}

С помощью Решения Б вы можете вызвать команду cURL для ESP8266 без набора ssid и пароль, и вы получите статус соединения обратно вместо этого. Это может занять несколько попыток, пока он все еще находится в процессе подключения, но если он по-прежнему не подключен, вы знаете, что учетные данные неверны.

,

Если вы хотите, чтобы веб-страница опрашивала этот сервер с помощью решения B, я предлагаю HTML-тег метаобновления (обратите внимание, что это также очистит все формы на текущей странице при ее обновлении, поэтому используйте фрейм или сделайте это в общем статусе страница без форм) или функцию JavaScript "setTimeout" для запуска кода позже/по расписанию., @RDragonrydr

Я не уверен, что все это правда. Например, я действительно могу ответить на запрос из другой функции. И я думаю, что причина этого именно в том, что я не вызываю handleClient(), пока состояние STATE_CREDS. Я хочу, чтобы это блокировалось. Единственная причина, по которой я использовал метод таймера, заключается в том, что, как я уже упоминал, некоторые люди предлагали оставить это соединение неблокирующим, но в любом случае это не сработало. Проголосовал за, потому что я ценю время, которое вы потратили, чтобы написать все это, хотя это не решило мою проблему. Я думаю, что метод с двумя запросами @Juraj - это то, что нужно., @Danny Buonocore

Это вполне справедливо. Я не видел другого решения (у меня была загружена страница накануне, а затем я работал над ней в более разумный час), и это «правильное» решение для этого без побочных эффектов. Надеюсь, подробности помогут? Ну, за исключением той части, где я ошибся насчет возможности ответить из другой функции; Надо будет посмотреть, потому что я не знал об этой функции..., @RDragonrydr

Да, я имею в виду, во всяком случае, это подтверждает, что на самом деле нет никакого способа сделать это, кроме как сделать несколько вызовов, один до переключения и один после., @Danny Buonocore