AP mode, Provisioning mode 활용 및 IoT를 위한 준비
Adafruit Feather M0 WiFi 보드를 이용해 IoT Device를 구성할 때 어려운 점이 하나 있습니다. WiFi를 통해 인터넷에 접속해야 하는데, Network SSID name, Password를 입력하기 위한 방법을 제공해야 하기 때문입니다. 또한, 입력 내용 등을 확인할 방법도 제공해야 합니다. 이제까지 처럼 프로그램 소스에 직접 입력하고 Serial monitor로 결과를 확인하는 건 개발 단계에서나 가능하니까요!
이를 해결하기 위해, 노트북이나 데스크탑 컴퓨터의 모니터, 키보드 등의 역할을 할 별도의 장치를 구성할 수 있습니다. LCD(character, graphic, TFT 등), Switch button, 가변 저항이나 엔코더 등을 이용한 수많은 방법이 가능하겠지만, 여기서는 오직 WiFi module만 가지고 해결하도록 하겠습니다.
AP mode
AP는 Access Point의 줄임말입니다. 흔히 사용하는 유무선공유기가 바로 무선 AP입니다. AP mode는 이 Board가 AP와 같은 역할을 하는 것입니다. 스스로 WiFi 네트워크를 구성하고, Client의 접속을 제공합니다. 일반적인 유무선 공유기에 WiFi 접속하고 Setting을 위해 관리자 페이지(192.168.0.1 같은...)에 접속하는 것과 비슷합니다. 반대되는 개념은 STA mode, Station mode입니다. 스마트폰 처럼 AP에 접속하는 장치들이 여기에 해당합니다. 그리고, Adafruit Feather M0 WiFi Board를 이용해 작성한 이제까지의 프로그램 소스들은 모두 STA mode를 이용한 것입니다. 이미 존재하는 무선 AP(무선공유기)의 SSID와 Password를 입력하여 WiFi에 접속하였습니다.
그런데, Adafruit Feather M0 WiFi 보드는 AP mode 기능도 소프트웨어적으로 제공하고 있습니다. 즉, 스마트폰 등을 이용해 접속할 수 있는 WiFi와 SSID 등을 공유기처럼 구성할 수 있어, 인터넷이 가능하지 않은 곳에서도 보드에 접속할 수 있습니다. 물론, 인터넷은 안되기에 이 기능 자체로 IoT에 적용할 수는 없지만, 네트워크에 접속될 준비가 되지 않은 상태에서도 스마트폰을 이용해 보드에 입력할 방법을 제공할 수 있게 됩니다.
AP mode상태에서도 Web Server 구성이 가능하기 때문에, 웹페이지를 통해 정보를 제공하고 데이터를 입력 받는 일이 가능합니다.
우선, WiFi101 Library의 AP 기본 예제 먼저 실행해 보겠습니다. 이 예제는 AP모드를 실행하고, 접속한 Client에게 간단한 웹페이지를 출력합니다. 또, 웹페이지를 통해 보드에 내장된 LED를 켜고 끄는 명령도 입력 받습니다. 전에 봤던 SimpleWebServerWiFi 예제를 AP mode 버전으로 변경한 것입니다.
#include <SPI.h>
#include <WiFi101.h>
//
int led = LED_BUILTIN;
char ssid[] = "wifi101-network"; // created AP name
char pass[] = "1234567890";
// AP password (needed only for WEP, must be exactly 10 or 26 characters in length)
int keyIndex = 0; // your network key Index number (needed only for WEP)
int status = WL_IDLE_STATUS;
WiFiServer server(80);
void setup() {
//Configure pins for Adafruit ATWINC1500 Feather
WiFi.setPins(8,7,4,2);
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
이제까지 해왔던 것처럼, 위 12, 13 라인만 추가하면 바로 실행 가능합니다.
프로그램 실행 후 Serial monitor 화면입니다. 형광색 표시된 wifi101-network가 AP모드의 SSID이며 원하는대로 변경 가능합니다. 스마트폰에서 해당 SSID로 접속한 후, http://192.168.1.1로 접속하면 웹페이지를 볼 수 있습니다.
그림과 같이 네트워크에 접속했습니다. 소스에는 password(1234567890)도 있지만 개방형으로 AP를 설정했기 때문에 바로 접속 가능합니다.
웹페이지 화면입니다. 예전에 봤던 것과 다른 점은 없고 LED on/off도 잘 작동합니다.
Provisioning mode
AP mode는 다른 WiFi newtwork가 없어도 웹서버로 구성이 가능하지만, 문제는 인터넷이 되지 않기 때문에 IoT에는 어울리지 않습니다. 다만, 이를 이용해서 WiFi 접속에 필요한 정보를 입력 받는 것에 이용할 수 있습니다. 이렇게 AP모드를 이용하여 기존 네트워크 접속에 필요한 정보를 입력받도록 하는 기능을 따로 만들어 WiFi 모듈속에 포함시켰는데 이를 Provisioning mode라고 합니다.
위 예제를 불러서 SPI 핀설정만 하고 실행하면 Provisioning mode를 볼 수 있습니다.
WiFi.beginProvision();
Provisioning mode는 WiFi network에 접속하는 과정을 말합니다. 위 코드가 Provisioning mode로 네트워크에 접속을 시작하는 명령입니다. 그 과정은 아래와 같습니다.
- 기존에 접속한 정보로 먼저 시도합니다.
- 접속에 실패할 경우, AP모드를 시작하고 Client 접속을 기다립니다.
- Client가 접속하면 네트워크 정보를 입력 받고 AP mode 종료 후 해당 네트워크로 접속합니다.
AP mode일 경우 SSID는 wifi101-XXXX이며, XXXX는 MAC 어드레스 뒤 4자리입니다.
스마트폰 등을 이용해서 해당 SSID에 접속하면 자동으로 위와 같은 로그인 화면이 나타납니다. 커피숍이나 호텔에서 흔히 접하는 방식입니다. 여기서 SSID와 Password를 입력하고 CONNECT 버튼을 누르면 AP mode를 종료하고 해당 네트워크에 접속합니다. 위 화면이 보이지 않으면 http://wifi101/ 주소로 직접 들어가도 동일한 화면을 볼 수 있습니다. 이 방식이 바로 Provisioning mode입니다.
AP mode를 활용한 IoT 준비
Provisioning mode가 편리하긴 하지만 사용에는 제한적입니다. 오직 WiFi 인터넷에 접속하는데만 사용할 수 있고, 로그인 웹페이지를 우리가 원하는 대로 변경하여 사용할 수 없습니다. WiFi가 안되는 상황에서도 AP mode를 통해 IoT 기기 설정 등이 가능하도록 하기위해 그냥 AP mode를 이용하여 처리하도록 하겠습니다.
- Station mode 접속에 실패할 경우 AP mode를 실행한다.
- Web page에서 네트워크 접속 정보를 입력 할 수 있다.
- 네트워크 접속이 끊길 경우 자동으로 위 동작을 반복한다.
제가 구현하고자 하는 기능은 위와 같습니다. IoT 기기를 실행하면 항상 WiFi 접속 상태를 유지하도록 연결이 끊길 경우 자동으로 재 접속합니다. 그리고, Station mode로 WiFi에 접속할 수 없을 때는 항상 AP mode를 실행합니다. 어떤 mode이든, 네트워크 접속에 관한 정보(SSID, Password)를 입력받을 수 있도록 하겠습니다.
Station mode 접속 코드 작성
우선, 기본적인 WiFi 접속(Station mode) 프로그램 먼저 작성한 후 조금씩 확장하도록 하겠습니다.
#include <SPI.h>
#include <WiFi101.h>
먼저 header file을 include하겠습니다. SPI 방식으로 연결되어 있는 WiFi module을 위해 위와 같이 두 개의 header file을 포함시켰습니다.
#include <SPI.h>
#include <WiFi101.h>
//
char ssid_AP[] = "Feather_WiFi";
String ssid_STA = "";
String pass_STA = "";
1개의 char array변수와 2개의 String type 변수를 선언했습니다. SSID는 AP mode, Station mode 두 개가 필요합니다. AP mode는 password 없이 개방형으로 연결 설정하기 때문에 Station mode만 password 변수를 선언하였습니다. 아직 사용자로부터 입력을 받지 않았기 때문에 STA mode 변수 두 개 모두 비워 두었습니다. AP mode의 SSID는 사용자가 지정할 수 있습니다.
ssid_AP 변수를 String type이 아닌 char[ ]
type으로 선언하였습니다. WiFi.begin()
함수는 String 인수에 대한 정의가 추가되었는데, WiFi.beginAP()
는 그렇지 않네요! 3개 변수 모두 String type으로 선언한다면 WiFi.beginAP(ssid_AP.c_str());
이런식으로 인수를 주고 호출하면 됩니다. WiFi.beginAP()
함수는 기존 C언어의 string type을 인수로 받을 수 있어 String class type을 받을 수 없습니다. ssid_AP.c_str()
함수는 String type을 C언의 string type으로 변환해주는 멤버 함수입니다.
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
}
setup()
함수를 구성합니다. 첫 번째 줄은 WiFi module의 SPI pin 설정을 위한 명령이고, 디버깅을 위해서 Serial monitor를 시작하였습니다. delay()
는 Serial monitor가 준비될 시간을 주기 위함입니다.
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
}
이 코드를 통해서 WiFi module이 정상적으로 동작하고 있는 지 확인할 수 있습니다.
status = WiFi.begin(ssid, pass); // WiFi에 연결 후, 상태값을 status에 저장
이제 WiFi에 연결하는 부분을 작성합니다. WiFi.begin()
함수는 인수로 주어진 정보대로 WiFi에 연결을 시도합니다. password가 필요없는 개방형 무선망이라면 인수는 SSID 하나면 됩니다. WiFi.begin()
함수는 네트워크에 연결을 시도한 후 그 결과값을 리턴합니다. status 변수는 이 리턴 값을 받아 저장합니다.
while ( status != WL_CONNECTED) {
Serial.print("Attempting to connect to Network named: ");
Serial.println(ssid_STA); // print the network name (SSID);
//
status = WiFi.begin(ssid_STA, pass_STA); // Connect to WPA/WPA2 network.
delay(2000); // wait 2 seconds for connection:
}
네트워크에 연결하는 작업이 한 번에 성공하면 좋겠지만, 그렇지 못한 경우를 위해 작업을 반복해줘야 합니다. 반복문은 종결 조건이 필요한데, 네트워크 연결에 성공하면 작업을 종결해야 합니다. WiFi.begin()
함수는 결과값을 리턴합니다. 리턴 값을 받은 status 변수의 값이 WL_CONNECTED
라면 연결에 성공하였으므로 반복문을 종결해야 합니다. 아니라면 작업을 다시 반복해야겠지요! status != WL_CONNECTED
로 검사하는 이유입니다. 매 시도마다 2초간 대기시간을 설정했습니다.
#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 status = WL_IDLE_STATUS;
//
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
while ( status != WL_CONNECTED) {
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);
// wait 2 seconds for connection:
delay(2000);
}
}
//
void loop() {
// put your main code here, to run repeatedly:
}
여기까지의 전체 소스입니다. 한 번 실행해 보겠습니다. loop() 함수에서는 아직까지 할 일이 없습니다.
업로드 후 실행한 모습입니다. 아직 ssid_STA, pass_STA 값을 주지 않았기 때문에 접속 시도만 계속 반복하고 있습니다.
void connect_STA() {
while ( status != WL_CONNECTED) {
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);
// wait 2 seconds for connection:
delay(2000);
}
}
코딩을 진행하기 전에, WiFi에 STA mode로 연결하는 부분을 위와 같이 connect_STA()
라는 함수로 묶었습니다. 연결이 끊길 때마다 반복해서 시도해야 하기 때문에 필요할 때마다 쉽게 불러 쓸 수 있는 함수 형태로 만든 것입니다.
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
//
if (ssid_STA.length() != 0) {
connect_STA();
} else {
Serial.println("SSID is not setted!");
}
}
프로그램이 처음 기동할 때는 아직 사용자의 입력을 받지 않았기 때문에 ssid_STA
가 공백입니다. 굳이 연결을 시도할 필요가 없기 때문에 위와 같이 IF문으로 검사하도록 했습니다. length()
함수는 문자열의 길이를 구하는 함수로 공백 즉, 0이 아닐 경우에만 접속을 시도하도록 코딩하였습니다.
#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 status = WL_IDLE_STATUS;
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
//
if (ssid_STA.length() != 0) {
connect_STA();
} else {
Serial.println("SSID is not setted!");
}
}
//
void loop() {
// put your main code here, to run repeatedly:
}
//
void connect_STA() {
while ( status != WL_CONNECTED) {
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);
// wait 2 seconds for connection:
delay(2000);
}
}
위 전체 소스를 업로드하여 실행하면 아무런 접속 시도 없이 바로 "SSID is not setted!" 메시지만 출력하게 됩니다.
void connect_STA() {
int count = 0;
while (status != WL_CONNECTED) {
count++;
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);
}
}
위에서 작성한 Station mode로 접속하는 함수인데 위와 같이 코드 세 줄을 추가하였습니다. 기존 코드는 네트워크에 연결될 때까지 계속해서 시도를 하게 됩니다. 네트워크의 서비스 자체가 중단되어 더이상 접속 시도를 할 필요가 없을 경우를 대비해서 총 3번만 연결 시도를 하도록 내용을 추가하였습니다. 3번 시도할 때까지 연결이 성공하지 못하면 AP mode로 넘어가도록 구성하겠습니다.
AP mode 접속 코드 작성
void connect_AP() {
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;
}
}
}
AP mode를 설정하는 부분도 위와 같이 함수로 작성하였습니다. 기본적인 구조는 connect_STA()
함수와 비슷합니다. AP mode가 설정되면 해당 AP로 Device가 접속할 때까지 리스닝 즉 대기합니다. 따라서 status값이 WL_AP_LISTENING라면 AP mode 설정에 성공한 것입니다. 역시 3번만 시도하도록 했습니다.
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
//
if (ssid_STA.length() != 0) {
connect_STA();
} else {
Serial.println("SSID is not setted!");
}
//
if (status != WL_CONNECTED) {
connect_AP();
}
}
AP mode를 실행하는 코드를 추가하였습니다. STA mode 시도가 성공했다면 status 변수값이 WL_CONNECTED로 설정되어 AP mode는 실행되지 않습니다.
여기까지 코딩 후 실행한 화면입니다. 왼쪽 Serial monitor 보시면 SSID가 설정되지 않았기 때문에 Station mode 는 건너 뛰었고, AP mode로 한 번 시도 후 성공하였습니다. Success 메시지가 따로 없지만, 오른쪽 그림처럼 스마트폰으로 WiFi 신호를 확인할 수 있습니다.
나머지 부분은 다음 글에서 구현하겠습니다. 여기까지의 전체 소스는 아래와 같습니다.
#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 status = WL_IDLE_STATUS;
//
void setup() {
WiFi.setPins(8,7,4,2); // SPI pin setting
Serial.begin(9600);
delay(1000);
//
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true);
}
//
if (ssid_STA.length() != 0) {
connect_STA();
} else {
Serial.println("SSID is not setted!");
}
//
if (status != WL_CONNECTED) {
connect_AP();
}
}
//
void loop() {
// put your main code here, to run repeatedly:
}
//
void connect_AP() {
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.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);
}
}