WiFi를 통한 아두이노 활용 (16) : AP mode, IoT #3

2017. 8. 15. 12:35

Arduino/Wireless

AP mode 활용 및 IoT를 위한 준비

이번 글에선 이전 글에서 만든 소스를 좀더 다듬어 보고자 합니다.

우선, 웹페이지 상에서 AP mode와 Staiton mode를 표시하는 코드를 추가하겠습니다.

웹페이지 출력은 webService() 함수에서 수행합니다. 그리고, 이 함수가 실행된다면, 네트워크는 정상적으로 연결되어 있다는 뜻이므로 status 변수는 WL_CONNECTED, WL_AP_LISTENING, WL_AP_CONNECTED 이 세 개의 값중 하나입니다. 또, Client가 접속하여 웹 응답을 요청한 상태이므로 AP mode라면 Device가 이미 AP로 접속한 상태이므로 WL_AP_LISTENING 상태는 제외됩니다.

따라서, WL_CONNECTED 값이면 Station mode이고 아니라면 AP mode입니다.

client.print("<p>Network : ");
if (status == WL_CONNECTED) {
client.println("Station mode");
} else {
client.println("AP mode");
}

웹페이지 출력 부분에 위 코드를 삽입하였습니다.

SSID name 자동으로 출력하기

Wifi망에 접속하기 위해 필요한 SSID name과 Password를 입력 받기 위해, 웹 페이지에서 text input box를 사용했기 때문에, SSID를 직접 타이핑해서 입력해야 합니다. 이를 스마트폰 처럼 미리 만들어진 리스트에서 선택할 수 있도록 하겠습니다.

WiFi101 Library의 예제 ScanNetworks를 실행해보면 주변의 사용 가능한 WiFi 신호를 찾아서 출력합니다.

// scan for nearby networks:
  Serial.println("** Scan Networks **");
  int numSsid = WiFi.scanNetworks();
  if (numSsid == -1)
  {
    Serial.println("Couldn't get a wifi connection");
    while (true);
  }

예제 중간에 위와 같은 코드가 있습니다. 여기서 WiFi.scanNetworks() 함수가 바로 WiFi 리스트를 찾아서 내부적으로 저장해 줍니다. 이 함수를 이용하여 위에서 말한 내용을 해결하도록 하겠습니다. 우선, 아래 주의사항을 참고하세요!

  • AP mode 에선 실행되지 않습니다.
  • STA mode가 시작되면 이전 스캔 정보는 삭제됩니다.

이 보드는 dual mode가 안됩니다. 즉, Ap mode와 Station mode 둘 중 하나만 가능해서, AP mode 상태에선 다른 WiFi 신호를 잡아내지 못합니다. 그래서 scanNetworks() 함수가 아무런 값도 리턴하지 않습니다.

Station mode가 실행되면 우선 WiFi module을 초기화 하는 듯 합니다. 이전에 스캔한 값은 모두 사라집니다. 따라서, Station mode가 begin되고 나서 scanNetworks() 함수를 실행해야 합니다!

#include <SPI.h>
#include <WiFi101.h>
//
char ssid_AP[] = "Feather_WiFi";
String ssid_STA = "TURTLE";      //  your network SSID (name)
String pass_STA = "yesican1";   // your network password
int numSsid = 0;    // Count of ScanNetworks list
int status = WL_IDLE_STATUS;
WiFiServer server(80);

변수가 하나 필요해서 위와 같이 선언하였습니다. scanNetworks() 함수는 리턴값으로 스캔한 리스트의 Count를 반환하는데, 이를 받아 저장할 변수입니다. 스캔된 리스트 수만큼 반복해서 출력할 때 사용합니다.

void connectSet() {
  status = WiFi.status();
  //  
  if (ssid_STA.length() != 0) {
    connect_STA();
  } else {
    Serial.println("SSID is not setted!");
  }
  //
  numSsid = WiFi.scanNetworks();
  for (int i = 0; i < numSsid; i++) {
    Serial.println(WiFi.SSID(i));  
  }
  //
  if (status != WL_CONNECTED) {
    connect_AP();
  }
  //
  if (status == WL_CONNECTED || status == WL_AP_LISTENING) {
    server.begin();
    IPAddress ip = WiFi.localIP();
    Serial.print("IP Address: ");
    Serial.println(ip);
  }
}

위와 같이 네트워크 설정을 위한 connectSet() 함수내에서 connect_STA()connect_AP() 사이에 scanNetworks() 함수를 위치시키면 됩니다. 바로 밑 FOR문은 제대로 출력되는지 Serial monitor로 확인하기 위한 코드이므로 필요없을 때 삭제합니다.

WiFi.SSID()
WiFi.SSID(argument)

위 두 함수는 이름은 동일하지만 인수 유무로 구별됩니다. 인수가 없이 WiFi.SSID()로 사용하면 현재의 SSID 이름이 리턴되고, 인수를 사용하면 스캔된 SSID 이름이 리턴됩니다. scanNetworks() 함수는 스캔한 WiFi 신호 리스트를 내부 배열로 저장합니다. 그리고, 이에 접근하기 위해서 WiFi.SSID(인수) 형태로 사용합니다. 인수는 정수로 배열 인덱스값입니다. 인수가 0이라면 첫 번째 SSID name이 리턴되는 것입니다. 위의 출력 FOR문을 보면 SSID(i) 형태로 사용하고 있음을 확인할 수 있습니다.

<select name="ssid">
  <option value=WiFi.SSID(0)>WiFi.SSID(0)</option>
  <option value=WiFi.SSID(1)>WiFi.SSID(1)</option>
  <option value=WiFi.SSID(2)>WiFi.SSID(2)</option>
  ......
  <option value=WiFi.SSID(i - 1)>WiFi.SSID(i - 1)</option>
</select>

이제 웹페이지에 출력하는 HTML 코드를 추가해야 합니다. 이전 예제와 같이 Drop-Down List 형태로 출력하기 위해 위와 같이 구성하였습니다. <select>, </select> 태그는 드롭다운 리스트의 시작과 끝을 알립니다. <select> tag의 name attribute는 서버로 전송되는 값이 어떤 Form 요소인지 구별하기 위한 이름이고, <option> 태그는 리스트 항목 하나를 표시합니다. <option> tag의 value attribute는 어떤 리스트가 선택되었는지 전송되는 값이고 <option>, </option> 태그 사이에 오는 값은 웹 화면에 표시되는 내용입니다. 리스트 수만큼 <option> 존재해야하고, 전송되는 값과 화면에 표시되는 값은 동일하게 처리하였습니다. 항목이 몇 개인지 미리 알 수 없기 때문에, numSsid 변수를 참조하여 반복시켜 출력하여야 합니다.

scanNetworks() 함수에 의해 저장된 첫 번째 SSID가 WiFi.SSID(0)이고, 0부터 시작하기 때문에 마지막이 i -1 입니다. 즉, 만약 5개라면 0부터 4까지 출력되도록 루프를 돌려야 합니다.

client.println("<form>");
if (numSsid != -1) {
  client.println("<select name=\"ssid\">");
  for (int i = 0; i < numSsid; i++) {
    client.print("<option value=\"");
    client.print(WiFi.SSID(i));
    client.print("\">");
    client.print(WiFi.SSID(i));
    client.println("</option>");
  }
  client.println("</select><br>");
}
//client.println("<input type=\"text\" name=\"ssid\" placeholder=\"SSID\"><br>");
client.println("<input type=\"password\" name=\"pass\" placeholder=\"PASSWORD\"><br>");
client.println("<input type=\"submit\" value=\"Submit\">");
client.println("<input type=\"reset\" value=\"Reset\">");
client.println("</form>");

반복되는 부분과 그렇지 않은 부분을 구별하여 위와 같이 HTML 코드를 추가하였습니다. 그리고, SSID명을 입력 받던 Text input 항목은 삭제하였습니다.

여기까지의 코드를 적용한 결과 화면입니다. 이제 SSID명을 직접 입력하지 않아도 됩니다.

Contents Width setting

<meta name="viewport" content="width=device-width, initial-scale=1.0">

소스를 실행한 후 웹페이지에 접속해보면 화면 내용이 아주 작게 나옵니다. 위에서 캡처된 화면들은 모두 확대해서 얻은 결과이고, 위 메타 태그를 적용하면 Device 사이즈에 맞게 컨텐츠 비율을 조절해서 보여 줍니다.

client.println("<!DOCTYPE html>");
client.println("<html>");
client.println("<head>");
client.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
client.println("<title>Network Info</title>");
client.println("</head>");
client.println("<body>");
client.println("<h1>WiFi Connection Status</h1>");

위와 같이 추가해 주면 좀더 보기 쉽게 화면 출력이 됩니다. 서버 예제에서 다루었던 부분입니다.

코드를 적용한 화면입니다. 왼쪽이 적용 전 원래의 화면이고 오른쪽이 적용한 화면입니다.

Network Reconnect

지금까지 코딩한 내용대로 네트워크 접속 과정을 보면,

  1. STA mode로 우선 접속할 것
  2. STA mode가 실패하면 AP mode로 접속할 것
  3. 접속이 유실될 경우 자동으로 위 과정을 반복할 것

위와 같은 내용으로 동작하도록 코딩했습니다. 만약, WiFi망 연결이 끊기면 다시 해당 WiFi(STA 모드)로 접속을 시도하고 실패하면 AP mode로 들어갑니다. 그리고 그 상태를 유지하죠. AP mode는 Station mode 즉 WiFi에 접속할 수 없을 때 Device 접속을 위해서 필요한 기능이고, IoT Device 구현을 위해서는 다시 인터넷에 연결되어야 합니다.

그래서, 당장은 WiFi 신호 이상으로 접속할 수 없더라도 일정 시간 이후에 다시 접속을 시도하도록 구성하는 게 좋을 듯 합니다. 이를 해결하기 위해, AP mode에 들어 가면 일정 시간 이후에 AP mode를 종료하고 다시 네트워크에 연결을 시도하도록 수정하겠습니다.

시간 간격은 1분이든 5분이든 원하는 만큼 하면 되지만, 여기서는 테스트를 위해 10초로 설정하겠습니다.

#include <SPI.h>
#include <WiFi101.h>
//
char ssid_AP[] = "Feather_WiFi";
String ssid_STA = "TURTLE";      //  your network SSID (name)
String pass_STA = "yesican1";   // your network password
int numSsid = 0;
long reConnect;
int status = WL_IDLE_STATUS;
WiFiServer server(80);

우선 AP mode가 시작되는 시간을 저장할 변수 reConnect를 선언하였습니다. 시간단위가 밀리 초라서 10초면 10000의 값입니다. int type 변수로 선언해도 충분하지만, 시간 간격을 더 길게 잡을 수도 있으므로 넉넉히 long type으로 선언하였습니다.

if (status == WL_CONNECTED || status == WL_AP_LISTENING) {
    server.begin();
    if (status == WL_AP_LISTENING) {
      reConnect = millis();
    }
    IPAddress ip = WiFi.localIP();
    Serial.print("IP Address: ");
    Serial.println(ip);
  }

다음으로 AP mode로 들어가면 현재 시간을 저장하는 부분입니다. 네트워크에 연결을 시도하는 connectSet() 함수의 마지막 부분에 위와 같이 STA mode나 AP mode로 연결이 성공했는지 체크하고 Server 서비스를 begin() 하는 코드가 있습니다. STA mode가 실패하고 AP mode로 성공했다면 status 변수는 WL_AP_LISTENING값을 가지고 있습니다. Device가 접속하길 기다리고 있는 상태이기 때문입니다. 따라서, 위 값을 가지고 있다면 현재 시간을 저장합니다.

millis() 함수는 아두이노 보드가 실행되고 얼마의 시간이 흘렀는지 1000분의 1초 단위로 시간값을 리턴하는 함수입니다. 상대적인 시간 간격만 체크하면 되므로, reConnect에 현재 시간을 저장합니다. 얼마의 값이 저장되었는지는 알 필요가 없습니다. 앞으로 10초의 시간이 지나는 시점만 알아 내면 됩니다.

void loop() {
  //
  if (WiFi.SSID() == 0) {
    connectSet();
  } else {
    webService();
  }
  //
  if (WiFi.status() == WL_AP_LISTENING) {
    if (millis() - reConnect > 10000) {
      WiFi.end();
    }
  }
}

매우 간단했던 loop()함수에 내용이 더 추가되었습니다. status의 값이 WL_AP_CONNECTED 일 때는 재접속하지 않습니다. Device가 AP에 연결된 상태이기 때문입니다. millis()는 현재 시간입니다. reConnect는 이전의 시간을 저장했습니다. 따라서, 현재 시간(millis())에서 reConnect를 빼면 얼마의 시간이 흘렀는지 알 수 있습니다.

millis()에서 reConnect를 뺀 값이 '1000'이라면 1초가 흘렀음을 의미합니다. 10초이므로 '10000'의 값보다 큰지 검사하면 됩니다. 10초가 지났다면 WiFi.end() 함수를 호출해 연결을 끊습니다. 연결이 끊어지면 WiFi.SSID() 값이 0이기 때문에 다시 네트워크에 접속을 시도합니다. 10초는 테스트하기에 조금 짧은 시간이네요!

이제 AP mode로 진입 후 Device 접속 없이 10초가 지나면 다시 기존 네트워크에 접속을 시도합니다. Feather_WiFi에 접속한 Device가 있다면 status = WL_AP_CONNECTED가 되어 재접속하지 않습니다.

테스트 영상입니다. 아래와 같은 순서로 진행이 됩니다.

  1. network credentials not setted -> AP mode Start
  2. go 192.168.1.1 ( Web page in AP mode)
  3. network credentials input and submit -> AP mode Off
  4. In STA mode, connect Router(TURTLE)
  5. go 192.168.1.65 (Web page in STA mode)
  6. WiFi Lost (Router off) -> auto Reconnect try
  7. by router off, STA mode fail -> AP mode start
  8. In AP mode, every 10s try STA mode

이상으로 이번 글을 마치고, 다음 글에선 아이폰과 간단하게 데이터를 주고 받는 테스트를 해보겠습니다.

전체 소스는 아래를 참고하세요!

#include <SPI.h>
#include <WiFi101.h>
//
char ssid_AP[] = "Feather_WiFi";
String ssid_STA = "";      //  your network SSID (name)
String pass_STA = "";   // your network password
int numSsid = 0;
long reConnect;
int status = WL_IDLE_STATUS;
WiFiServer server(80);
//
void setup() {
  delay(10000);
  WiFi.setPins(8,7,4,2); // SPI pin setting
  Serial.begin(9600);
  delay(2000);
  //
  if (WiFi.status() == WL_NO_SHIELD) {
        Serial.println("WiFi shield not present");
        while (true);
  }
  //
  connectSet();
}
//
void loop() { 
  if (WiFi.SSID() == 0) {
    Serial.println("Network Losted!\n");
    connectSet();
  } else {
    webService();
  }
  //
  if (WiFi.status() == WL_AP_LISTENING) {
    if (millis() - reConnect > 10000) {
      Serial.println("\nWiFi Reconneting Start.....!");
      WiFi.end();
    }
  }
}
//
void connectSet() {
  status = WiFi.status();
  Serial.println("*** Starting Network Connection!***\n");
  if (ssid_STA.length() != 0) {
    connect_STA();
  } else {
    Serial.println("SSID is not setted!");
  }
  //
  numSsid = WiFi.scanNetworks();
  Serial.println("WiFi Scan complete.");
  if (status != WL_CONNECTED) {
    connect_AP();
  }
  //
  if (status == WL_CONNECTED || status == WL_AP_LISTENING) {
    server.begin();
    if (status == WL_AP_LISTENING) {
      reConnect = millis();
    }
    IPAddress ip = WiFi.localIP();
    Serial.print("Connect IP Address: ");
    Serial.println(ip);
    Serial.println();
  }
}
//
void webService() {
  WiFiClient client = server.available();
  //
  if (client) {
    Serial.println("new client");
    String currentLine = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        //Serial.write(c);
        if (c == '\n') {
          if (currentLine.length() == 0) {
            // request ended, web page print out
            //Serial.println("Client Request Ended!");
            Serial.println("Web page transfer Start!");
            //
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            //
            client.println("<!DOCTYPE html>");
            client.println("<html>");
            client.println("<head>");
            client.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
            client.println("<title>Network Info</title>");
            client.println("</head>");
            client.println("<body>");
            client.println("<h1>WiFi Connection Status</h1>");
            //
            client.print("<p>Network : ");
            if (status == WL_CONNECTED) {
              client.println("Station mode");
            } else {
              client.println("AP mode");
            }
            //
            client.println("<p>IP Address : ");
            IPAddress ip = WiFi.localIP();
            client.println(ip);
            client.println("</p>");
            client.print("<p>SSID : ");
            client.print(WiFi.SSID());
            client.print("</p>");
            client.print("<p>RSSI : ");
            client.print(WiFi.RSSI());
            client.println(" dBm</p>");
            client.println("<hr>");
            client.println("<h2>Network Credentials</h2>");
            client.print("<p>Current SSID : ");
            client.println(WiFi.SSID());
            client.println("<form>");
            if (numSsid != -1) {
              client.println("<select name=\"ssid\">");
              for (int i = 0; i < numSsid; i++) {
                client.print("<option value=\"");
                client.print(WiFi.SSID(i));
                client.print("\">");
                client.print(WiFi.SSID(i));
                client.println("</option>");
              }
              client.println("</select><br>");
            }
            client.println("<input type=\"password\" name=\"pass\" placeholder=\"PASSWORD\"><br>");
            client.println("<input type=\"submit\" value=\"Submit\">");
            client.println("<input type=\"reset\" value=\"Reset\">");
            client.println("</form>");
            client.println("</body>");
            client.println("</html>");
            break;
          } else {
            bool ssidSet = false;
            bool passSet = false; 
            int indexSsid = currentLine.indexOf("ssid=");
            int indexAmp = currentLine.indexOf("&");
            if (indexSsid != -1 && indexAmp != -1) {
              ssid_STA = currentLine.substring(indexSsid + 5, indexAmp);
              ssidSet = true;
            }
            int indexPass = currentLine.indexOf("pass=");
            int indexHttp = currentLine.indexOf("HTTP/1.1");
            if (indexPass != -1 && indexHttp != -1) {
              pass_STA = currentLine.substring(indexPass + 5, indexHttp);
              pass_STA.trim();
              passSet = true;
            }
            if (ssidSet == true || passSet == true) {
              Serial.println("Network Credentials Updated!!");
              Serial.print("WiFi Reconnect to ");
              Serial.println(ssid_STA);
              client.stop();
              WiFi.end();
            }
            // new line, clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {
          currentLine += c;
        }
      }
    }
    client.stop(); // close the connection
    Serial.println("client disconnected");
  }
}
//
void connect_AP() {
  Serial.println("STA mode Failed!");
  int count = 0;
  while (status != WL_AP_LISTENING) {
    count++;
    Serial.print(count);
    Serial.print(" Creating access point named: ");
    Serial.println(ssid_AP);
    //
    status = WiFi.beginAP(ssid_AP);
    //
    if (count >= 3) {
      Serial.println("Creating access point failed!");
      return;
    }
  }
}
//
void connect_STA() {
  int count = 0;
  while (status != WL_CONNECTED) {
    count++;
    Serial.println();
    Serial.print(count);
    Serial.print(" Attempting to connect to Network named: ");
    Serial.println(ssid_STA);                   // print the network name (SSID);
    //
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid_STA, pass_STA);
    //
    if (count >= 3) return;
    //
    // wait 2 seconds for connection:
    delay(2000);
  }
}

Comments