Sming framework은 가장 진보되고 생산성 높은 ESP8266 개발환경을 제공합니다.

.

본 페이지는 Sming 을 이용해 웹 관련 기술들을 적용하는 예제입니다.

.

HTTP Request – ThingSpeak 서버 업데이트

본격적으로 WiFi 통신을 하는 예제를 만들어 보겠습니다. WiFi 통신의 기본은 HTTP Request 죠. 쉽게 얘기해서 브라우저를 사용하듯 외부 서버에 접속해서 HTML/리소스를 받아오는 작업입니다. 또한 HTTP Request를 이용해서 외부 서버에 간단한 데이터를 전달할 수도 있습니다.

이번 예제에서는 IoT 서비스로 유명한 ThingSpeak.com 서버에 HTTP request를 이용해서 데이터를 전송하도록 해보겠습니다. 온습도 센서를 연결해서 데이터를 넘겨도 되지만 여기서는 아무런 외부 센서 없이 임의의 값을 일정 간격으로 전송하도록 할겁니다. 따라서 ESP8266 모듈만 있으면 테스트가 가능합니다.

먼저 ThingSpeak.com 서버에 계정이 하나 필요합니다. 회원가입을 하시고 로그인을 하시면 [My Channel] 을 보실 수 있습니다. 여기서 [New Channel] 버튼을 눌러 채널을 하나 생성합니다.

new_channel

채널 이름을 넣고 field1 이 활성화 된 상태여야 합니다. field1 의 이름도 넣어주세요.

new_channel_setting

채널 정보를 저장하시고 My Channel 화면으로 돌아가서 새로 생성된 채널의 API Key 탭을 클릭합니다. 채널의 API Key를 따로 메모해두세요.

channel_api_key

이제 ThingSpeak 채널을 사용할 준비는 다 되었습니다. ESP8266에서 주기적으로 센서값을 HTTP Request로 보내도록 만들어주면 됩니다. 아래 프로젝트 소스코드를 사용하세요.

프로젝트 파일들을 받으면 application.cpp 파일 상단에 있는 WIFI_SSID, WIFI_PWD 값을 설정해주세요. 그리고 앞서 메모해둔 API Key 도 적어줘야 하는데 아래 코드 분석하면서 언급하겠습니다.

application.cpp 파일에 필요한 코드들이 모두 있습니다. 초기화 함수인 init() 함수부터 보겠습니다.

void init()
{
	//spiffs_mount(); // Mount file system, in order to work with files

	Serial.begin(SERIAL_BAUD_RATE); // 115200 by default
	Serial.systemDebugOutput(false); // Disable debug output to serial

	WifiStation.config(WIFI_SSID, WIFI_PWD);
	WifiStation.enable(true);
	WifiAccessPoint.enable(false);

	// Run our method when station was connected to AP (or not connected)
	WifiStation.waitConnection(connectOk, 20, connectFail); // We recommend 20+ seconds for connection timeout at start
}

디버그 메시지 출력을 위해 Serial 통신을 초기화 했습니다. Serial.systemDebugOutput() 은 ESP8266 core에서 생성하는 디버그 메시지를 활성화하고 UART0 로 보내주는 함수입니다. UART0는 현재 PC랑 연결된 시리얼 라인입니다. 원래는 UART1으로 보내주도록 되어 있어서 FTDI 모듈을 하나 더 달아야 하기 때문에 편의상 이렇게 한겁니다.

HTTP Request를 보내야 하기 때문에 WifiStation.config() 함수로 공유기(AP)에 연결할 수 있도록 설정해 줬습니다. 그리고 WifiStation.enable(true) 로 설정합니다. AP 모드로 동작할 필요는 없기 때문에 WiFiAccessPoint.enable(false); 로 해줍니다.

아래 함수를 호출하면 공유기에 접속을 시도합니다. 그리고 결과는 connectOk, connectFail 콜백 함수로 알려줍니다.

  • WifiStation.waitConnection(connectOk, 20, connectFail);

공유기 접속에 성공하면 connectOk() 콜백 함수가 호출됩니다. 주기적으로 ThingSpeak 서버에 HTTP Request 를 보내줘야 하므로 여기서 타이머를 생성합니다.

void connectOk()
{
	Serial.println("I'm CONNECTED");

	// Start send data loop
	procTimer.initializeMs(25 * 1000, sendData).start(); // every 25 seconds
}

25초(25*1000) 마다 콜백 함수인 sendData() 함수가 호출되도록 설정했습니다. ThingSpeak 서버는 너무 짧은 간격으로 데이터를 보내면 받아주질 않습니다. 정확치는 않은데… 20초 이상의 간격을 지켜야합니다.

sendData() 함수는 ThingSpeak 서버에 센서값을 넣어서 HTTP Request를 보냅니다.

void sendData()
{
	if (thingSpeak.isProcessing()) return; // We need to wait while request processing was completed

	// Read our sensor value :)
	sensorValue++;
	if(sensorValue > 100) sensorValue=0;

	thingSpeak.downloadString("http://api.thingspeak.com/update?key=your_key&field1=" + String(sensorValue), onDataSent);
}

thingSpeak 는 HttpClient 인스턴스로 파일 상단에 선언되어 있습니다. thingSpeak 인스턴스를 이용해서 HTTP Request 를 보내는 코드가 마지막 라인입니다. 특정 ThingSpeak 서버 URL로 request를 보내고 HTTP Response 를 onDataSent() 콜백 함수로 받도록 설정했습니다.

여기서 URL 부분을 자세히 보시면 [update?key=your_key&field1=] 부분이 있습니다. URL을 이용해 key, field1 변수에 값을 넣어서 보내주는 겁니다. GET 방식의 데이터 전송 방법입니다. 당연히!! your_key 를 앞서 메모한 API Key 값으로 바꿔줘야 동작합니다!!

sendData() 함수는 서버에서 응답으로 보내준 데이터를 Serial 통신으로 표시해서 PC에서 볼 수 있도록 만들어 뒀습니다.

이제 코드를 빌드하고 펌웨어 올려서 동작을 확인해 보시죠. 제대로 동작한다면 ThingSpeak 의 [MyChannel – 생성한 채널 – Private View]를 눌렀을 때 아래처럼 그래프가 계속 업데이트 되어야 합니다.

thingspeak_result

.

Bootstrap HTTP server 만들기

Bootstrap HTTP 서버 만들기 예제는 Sming framework 이 가진 장점을 유감없이 보여주는, Sming의 꽃이라 할만한 예제입니다.

Bootstrap 자체는 Java script/CSS 기반한 웹 UI 프레임워크 정도로 볼 수 있는데, 사실 이 예제에서 Bootstrap 자체가 중요한 건 아닙니다. 임베디드 장치임에도 꽤 화려한 UI가 가능하다는 걸 보여주기 위한 용도로 사용되었을 뿐입니다. 자세한 내용은 구글에 물어보세요.

이 예제에서는 ESP8266 모듈을 HTTP 서버로 동작시킵니다. 그리고 외부에서 HTTP Request 요청이 오면 저장하고 있던 HTML 파일, Java Script 파일, CSS 파일을 전송해줍니다. PC나 모바일 폰에서 브라우저로 접속하면 아래처럼 웹 페이지 화면을 보여줄 수 있도록 만드는 겁니다. 그리고 웹 페이지에서 LED ON/OFF 버튼을 누르면 ESP8266 모듈의 LED가 제어되도록 할겁니다. 하나 더, ESP8266 모듈은 HTML, Java Script, CSS 파일을 외부 서버에서 다운로드 받아 자체 파일 시스템에 저장합니다!!

web_server

ESP8266 모듈은 웹 화면을 구성하는 HTML, JS, CSS 파일을 가지고 있어야 합니다. 물론 파일 형태가 아니라 소스코드에 텍스트 형태로 넣어두고 전송할 수도 있습니다만, 전송해야 할 텍스트 용량이 크기 때문에 파일로 관리하는 것이 더 좋습니다. Sming 에서는 이를 위해 spiffs 라는 파일 시스템을 지원하기 때문에 걱정하실 필요가 없습니다!!

전송할 HTML, JS, CSS 파일은 소스코드를 작성할 때 미리 넣어두고 컴파일해서 펌웨어 업데이트로 올릴 수 있습니다. 하지만 이 예제에서는 외부 서버에서 해당 파일들을 다운로드해서 파일 시스템에 저장합니다. 이 작업은 파일이 없을 때 딱 한 번만 수행하도록 되어 있는데, 원하신다면 코드를 수정해서 주기적으로 파일을 삭제 후 업데이트 된 파일을 다운로드 받으실 수도 있습니다!!

이 예제는 외부에서 HTTP Request를 받았을 때 URL에서 변수와 값을 추출하는 방법, Response를 전달할 때 변수와 값을 넣어 전달하는 방법도 포함하고 있습니다. 반드시 자세히 분석해 볼 필요가 있는 예제입니다.

본격적으로 프로젝트 셋팅을 시작해 보겠습니다. 아래 링크에서 프로젝트를 다운로드 받아서 이클립스에 불러옵니다.

먼저 살펴봐야 할 부분은 spiffs 파일 시스템 설정입니다. 이전의 해봤던 예제와는 달리 spiffs 파일 시스템을 사용할 경우 spiff_rom.bin 이라는 바이너리 파일을 생성합니다. 이 파일은 컴파일 하기 전에 미리 넣어둔 파일들을 하나의 파일로 묶어둔겁니다. 이게 ESP8266 flash 메모리에 올라가서 마치 PC의 하드디스크 처럼 폴더 구조를 만들어줍니다.

프로젝트 폴더의 files 폴더를 보면 dummy.html 파일이 있습니다. 아무데도 쓰이지 않는 파일인데 파일 시스템 구성을 테스트 하기위해 넣어둔겁니다. ESP8266 파일시스템에 미리 넣어둘 파일이 있다면 files 폴더에 넣어두면 됩니다.

그리고 Makefile-user.mk 파일을 열어보세요.

## Configure flash parameters (for ESP12-E and other new boards):
SPI_MODE = dio
SPI_SIZE = 4M

## SPIFFS options
DISABLE_SPIFFS = 0
#SPIFF_FILES = files
# Path to spiffy
SPIFFY = C:/tools/mingw64/bin/spiffy

윈도우 OS에서 이클립스를 사용하는 경우 Sming 에서 제공하는 공식 예제의 Makefile-user.mk 은 빌드할 때 에러를 발생시킵니다. 그래서 위와 같이 맞춰줘야 합니다. 제가 테스트 한 보드는 ESP12E 기반 NodeMCU 보드이기 때문에 flash 메모리가 4MByte 입니다. 만약 다른 모듈을 사용한다면 flash 메모리 사이즈를 그에 맞게 맞춰줘야 합니다. 사용가능한 flash 메모리 사이즈 값은 [C:/tools/sming/Sming/] 폴더에서 Makefile-project.mk 파일을 참고하세요.

그리고 윈도우에서는 Sming framework 디렉토리 인식이 안되는 문제가 발생하므로 아래 내용도 똑같이 맞춰주세요. (주석해제)

## SMING_HOME sets the path where Sming framework is located.
## Windows:
SMING_HOME = C:/tools/sming/Sming

Make 파일 설정이 모두  되었으면 일단 빌드를 해보세요. 빌드를 했을 때 에러가 발생하면 안됩니다. 그리고 아래처럼 [out/firmware] 폴더에 spiff_rom.bin 파일이 생성되어 들어가야 합니다.

out_files

spiff_rom.bin 파일이 프로젝트 루트 폴더에 생성되는 경우가 있는데, 이러면 빌드는 되어도 ESP8266 펌웨어 업로드가 안됩니다. spiff_rom.bin 파일을 [out/firmware] 폴더로 옮겨주세요.

주의!!! 윈도우에서는 경로 문제 때문에 파일이 spiff_rom.bin 파일에 제대로 들어가지 않는 문제가 발생하기도 합니다. 이 때는 아래 라인을 수정해야 합니다. 실제 파일이 위치한 곳의 경로를 지정해주세요. 그래도 안되면 프로젝트 폴더를 [C:\tools\sming\sming.example] 폴더 안으로 옮겨서 해보세요. (아직 정확한 해법을 못찾음…)

#SPIFF_FILES = files

이제 소스코드를 확인해 보겠습니다. application.cpp 파일을 엽니다. 그리고 늘 그랬듯… init() 초기화 함수부터 확인합니다.

void init() {
	spiffs_mount(); // Mount file system, in order to work with files

	pinMode(LED_PIN, OUTPUT);

	Serial.begin(SERIAL_BAUD_RATE); // 115200 by default
	Serial.systemDebugOutput(true); // Enable debug output to serial

	WifiStation.enable(true);
	WifiStation.config(WIFI_SSID, WIFI_PWD);
	WifiAccessPoint.enable(false);

	// Run our method when station was connected to AP
	WifiStation.waitConnection(connectOk);

	//Change CPU freq. to 160MHZ
	System.setCpuFrequency(eCF_160MHz);
	Serial.print("New CPU frequency is:");
	Serial.println((int)System.getCpuFrequency());
}

첫 라인에서 spiffs_mount() 함수를 호출합니다. spiffs 파일 시스템을 사용하게 해주는 함수입니다.

두 번째 라인에서 LED 출력을 위해 GPIO 12 핀을 초기화 합니다. LED를 GPIO 12 핀에 연결해 두세요.

디버그를 위해 Serial 통신 설정을 하고 WiFi Station 모드를 활성화 합니다. AP 모드는 필요 없으니 꺼두고, 공유기에 연결하는 작업이 해뒀습니다.

  • WifiStation.config(WIFI_SSID, WIFI_PWD);    // 공유기에 연결

그리고 공유기에 연결되면 connectOk() 콜백 함수가 호출됩니다.

  • WifiStation.waitConnection(connectOk);

공유기에 연결되어 IP를 받으면 connectOk() 콜백 함수가 호출됩니다.

void connectOk() {
	Serial.println("I'm CONNECTED");

	if (!fileExist("index.html") || !fileExist("bootstrap.css.gz") || !fileExist("jquery.js.gz")) 	{
		// Download server content at first
		downloadTimer.initializeMs(3000, downloadContentFiles).start();
	}
	else {
		startWebServer();
	}
}

할당받은 IP를 출력하고 파일 시스템에 HTML, CSS, JS 파일이 있는지 검사합니다. 만약 파일이 없다면 파일 다운로드부터 해야합니다. 그래서 downloadTimer를 시작해서 3초마다 downloadContentFiles() 콜백 함수가 실행되도록 해줬습니다.

  • downloadTimer.initializeMs(3000, downloadContentFiles).start();

downloadContentFiles() 함수는 3초마다 반복 실행되므로 현재 상태를 체크해서 파일 다운로드를 하나씩 실행합니다.

void downloadContentFiles() {
	if (downloadClient.isProcessing()) return; // Please, wait.

	if (downloadClient.isSuccessful())
		dowfid++; // Success. Go to next file!
	downloadClient.reset(); // Reset current download status

	if (dowfid == 0)
		downloadClient.downloadFile("http://simple.anakod.ru/templates/index.html");
	else if (dowfid == 1)
		downloadClient.downloadFile("http://simple.anakod.ru/templates/bootstrap.css.gz");
	else if (dowfid == 2)
		downloadClient.downloadFile("http://simple.anakod.ru/templates/jquery.js.gz");
	else {
		// Content download was completed
		downloadTimer.stop();
		startWebServer();
	}
}

파일 다운로드는 HttpClient 인스턴스인 downloadClient 를 사용합니다. 파일은 다운로드 후 spiffs 파일 시스템에 저장됩니다. 원하신다면 아래 URL을 바꿔 사용하시면 됩니다.

  • downloadClient.downloadFile(“http://simple.anakod.ru/templates/index.html“);

만약 3개의 파일 다운로드가 완료된 상태라면 downloadTimer를 중지시키고 startWebServer() 함수를 실행합니다.

		downloadTimer.stop();
		startWebServer();

startWebServer() 함수에서는 웹 서버를 만들기 위해 HttpServer 의 인스턴스인 server 를 사용합니다.

void startWebServer()
{
	server.listen(80);
	server.addPath("/", onIndex);
	server.addPath("/hello", onHello);
	server.setDefaultHandler(onFile);

	Serial.println("\r\n=== WEB SERVER STARTED ===");
	Serial.println(WifiStation.getIP());
	Serial.println("==============================\r\n");
}

기본 HTTP 포트인 80번 포트를 열고…

  • server.listen(80);

addPath() 함수를 이용해 외부에서 접근 가능한 경로를 지정해줍니다. 여기서는 [/], [/hello]를 지정해 줬습니다. 따라서 외부에서는 [http://IP_address/], [http://IP_address/hello] 경로로 접속이 가능합니다. 그리고 default handler를 지정해주기 때문에 [http://IP_address/file_name] 으로 특정 파일을 다운로드 할 수도 있습니다.

  • server.addPath(“/”, onIndex);
  • server.addPath(“/hello”, onHello);
  • server.setDefaultHandler(onFile);

addPath() 함수로 접근 가능한 경로를 지정할 때 해당 경로에 대한 요청을 처리해 줄 콜백 함수도 같이 지정해 줍니다. onIndex(), onHello(), onFile() 함수가 해당됩니다.

[http://IP_address/] 경로를 처리하는 onIndex() 함수에 중요한 내용들이 있습니다.

void onIndex(HttpRequest &request, HttpResponse &response)
{
	counter++;
	bool led = request.getQueryParameter("led") == "on";
	digitalWrite(LED_PIN, led);

	TemplateFileStream *tmpl = new TemplateFileStream("index.html");
	auto &vars = tmpl->variables();
	vars["counter"] = String(counter);
	//vars["ledstate"] = (*portOutputRegister(digitalPinToPort(LED_PIN)) & digitalPinToBitMask(LED_PIN)) ? "checked" : "";
	vars["IP"] = WifiStation.getIP().toString();
	vars["MAC"] = WifiStation.getMAC();
	response.sendTemplate(tmpl); // this template object will be deleted automatically
}

onIndex() 함수는 index.html 파일을 요청한 client 에게 전송해줍니다.

  • TemplateFileStream *tmpl = new TemplateFileStream(“index.html”);
  • ……
  • response.sendTemplate(tmpl); // this template object will be deleted automatically

그리고 HTTP URL에 포함된 변수(led)와 값(on, off)을 추출해서 LED를 on/off 시켜줍니다.

  • bool led = request.getQueryParameter(“led”) == “on”;
  • digitalWrite(LED_PIN, led);

예를들어 [http://IP_address/index.html?led=on] 이렇게 HTTP 요청을 보내면 LED가 켜집니다.

반대로 index.html 파일을 내용을 client 에게 응답으로 보내줄 때 ESP8266 모듈의 IP, MAC address 값을 같이 넣어 보내주기도 합니다. onIndex() 함수 코드를 유심히 보시면 vars 라는 변수를 이용해서 값을 담아 보냄을 알 수 있습니다.

이상의 코드를 빌드해서 펌웨어를 ESP8266 모듈에 올려보세요. 그리고 ESP8266 모듈과 같은 공유기에 연결된 PC 또는 모바일 장치의 브라우저로 [http://IP_address/] 접속해 보세요. 아래처럼 HTML, JS, CSS 가 결합된 웹 페이지가 보일겁니다.

web_server

PC나 모바일 장치의 브라우저는 HTML 파일을 기반으로 코드에 기재된 JS, CSS 파일을 다운로드 받아 화면을 구성합니다. 상단에 ESP8266 모듈의 IP, MAC address 도 보여주고 LED를 on/off 할 수 있는 버튼도 보여줍니다. 그리고 버튼을 누르면 led on, off 할 수 있도록  URL에 변수와 값을 붙여서 다시 요청합니다.

  • http://IP_address/index.html?led=on

html_code

이 예제는 웹 서버를 구성하기 위해 필요한 기본 코드들을 대부분 담고 있습니다. 반드시 정독하고 테스트 해볼만한 예제입니다!!

.

AJAX 를 이용한 동적 웹 페이지

HTML 을 이용한 웹 브라우징은 대단히 정적인 구조를 가지고 있습니다. 웹 브라우저는 서버에 특정 페이지를 요청하고 그 결과를 보여주지만 페이지 내에 뭔가 변화가 필요하면 서버에 다시 요청해서 화면을 다시 그려줘야 합니다.

그럼 페이지를 재로딩하지 않고 (채팅화면처럼) 동적으로 변화시켜 보여줄 수 없을까? 이런 문제는 AJAX(Asynchronous JavaScript and XML) 가 도입되면서 어느정도 해결되었습니다. Ajax 는 이름처럼 비동기식으로 자바 스크립트를 이용해 (XML 혹은 기타) 데이터를 교환하는 방법입니다. Ajax 는 화면 전체를 다시 그리지 않고도 자바 스크립트가 내부적으로 서버에 요청해 데이터를 받아와서 화면 일부만 바꿔줄 수 있습니다.

앞선 Bootstrap 예제는 ESP8266 모듈에 HTTP 접속을 하면 index.html 파일을 받아왔습니다. 접속한 브라우저는 HTML 파일을 보여주고, 화면에 있는 버튼을 누르면 HTML 파일을 다시 받아와서 업데이트된 화면을 보여줬습니다.

이번 예제는 동작 방식이 다릅니다. PC 등에서 브라우저는 ESP8266 모듈에 접속해서 index.html 파일을 받아와 보여줍니다. 그리고 사용자의 요청이 있을 때, index.html 파일 내부에 있는 자바 스크립트 (Ajax) 코드가 ESP8266 모듈에 요청을 보냅니다. 서버에서 응답을 받으면 데이터를 화면 일부에 업데이트 해줍니다. 즉, 브라우저는 index.html 파일 자체를 다시 받지 않고 화면 일부만 업데이트 할 수 있습니다.

이 작업을 주기적으로 반복하면 실시간으로 모니터링하는 화면처럼 페이지를 만들 수 있습니다. 흔히들 얘기하는 폴링(polling) 방식으로 데이터를 가져오는 겁니다.

ajax_index

이번 예제에서 사용할 index.html 파일에는 아래 자바 스크립트 코드가 들어가 있습니다.

......
(function worker() {
	$.getJSON('ajax/input', function(data) {
		$.each( data.gpio, function( key, val ) {
			var target = $('#gpio' + key);
			target.removeClass("label-default");
			if (val)
				target.removeClass("label-danger").addClass("label-success");
			else
				target.removeClass("label-success").addClass("label-danger");
		});
		setTimeout(worker, 400);
	});
})();

$(".switch-freq").click(function() {
	var freq = $(this).attr('data-value');
	$.getJSON('ajax/frequency', {value: freq}, function(data) {
		$("#display-freq").text(data.value);
	});
});

자바스크립트 부분을 보면 2개의 함수가 보입니다. 첫 번째 function worker() 함수는 HTML 페이지 로딩 후 실행됩니다. 아래 getJSON 부분이 서버로 Request를 보내는 부분입니다. [http://server_ip/ajax/input] 경로로 요청을 보내서 결과를 받으면 function(data) {} 부분이 실행됩니다.

  • $.getJSON(‘ajax/input‘, function(data) { …

결과는 json object 로 받습니다. 이걸 파싱해서 LED 상태값을 추출하고 id=GPIOxx 값을 가진 HTML 엘리먼트에 업데이트 합니다. 이상의 작업이 끝나면 마지막에 setTimeout(worker, 400);을 호출합니다. 400ms 마다 서버에 접속하도록 타이머를 설정하는 겁니다. 타이머가 호출할 콜백함수가 worker() 함수이니… 결국 스스로를 재귀적으로 계속 호출하는거죠.

worker 함수 아래에 정의된 함수도 유사한 작업을 합니다.

  • $(“.switch-freq”).click(function() { …

HTML 코드에는 2개의 버튼이 정의되어 있는데… ESP8266 CPU 동작 속도를 80/160 MHz 로 변경하기 위한 버튼입니다. 이 버튼이 눌러졌을 때 해야할 동작을 정의하는 부분이 위 함수입니다. 서버에 접근하는 경로 [http://server_ip/ajax/frequency] 가 다를 뿐 유사한 일을 합니다. 처리 결과는 버튼 아래쪽 영역, [div] 태그 영역에 표시될겁니다.

HTML/Ajax 설명이 길어졌는데.. 본격적으로 ESP8266에 올릴 코드를 보겠습니다. 아래 링크에서 프로젝트 파일들을 받아 이클립스로 불러옵니다.

application.cpp – init() 함수부터…

void init()
{
	spiffs_mount(); // Mount file system, in order to work with files

	Serial.begin(SERIAL_BAUD_RATE); // 115200 by default
	Serial.systemDebugOutput(true); // Enable debug output to serial

	WifiStation.enable(true);
	WifiStation.config(WIFI_SSID, WIFI_PWD);
	WifiAccessPoint.enable(false);

	for (int i = 0; i < countInputs; i++)
	{
		namesInput.add(String(inputs[i]));
		pinMode(inputs[i], INPUT);
	}

	// Run our method when station was connected to AP
	WifiStation.waitConnection(connectOk);
}

spiffs_mount() 함수로 파일 시스템을 사용합니다. Station 모드 활성화하고 공유기에 연결도 합니다. for 반복문은 미리 선언해둔 inputs[] 배열에 있는 GPIO 핀들을 초기화 해줍니다. ESP8266 모듈에 버튼을 2개 연결해두고 연결한 GPIO 핀을 inputs[] 배열에 기록해주면 됩니다.

공유기에 연결되면 connectOk() 콜백함수가 불립니다.

void connectOk()
{
	Serial.println("I'm CONNECTED");

	startFTP();
	startWebServer();
}

특이하게 이번 예제는 FTP 서비스를 실행합니다. startFTP() 함수에 있는 간단한 코드만으로 FTP 이용이 가능합니다.

void startFTP() {
	if (!fileExist("index.html"))
		fileSetContent("index.html", "<h3>Please connect to FTP and upload files from folder 'web/build' (details in code)</h3>");

	// Start FTP server
	ftp.listen(21);
	ftp.addUser("me", "123"); // FTP account
}

먼저 파일 시스템에 index.html 파일이 없으면 파일을 하나 생성해 줍니다. ftp.listen(21) 은 21번 포트로 FTP를 시작합니다. ftp.addUser() 를 이용해서 사용자 등록을 해줍니다. 나중에 펌웨어 올리고 구동하면 PC에서 FTP 프로그램을 이용해서 접속한 뒤 파일을 첨삭할 수 있습니다!! 제 경우 spiff_rom.bin 바이너리에 파일이 제대로 생성되지 않는 문제가 있어서 FTP로 파일들을 올리고 서버를 구동했습니다. 이럴 때 FTP 서비스를 유용하게 쓸 수 있습니다.

connectOk() 함수의 마지막 라인에 있는 startWebServer() 를 통해 웹 서버를 구동합니다.

void startWebServer()
{
	server.listen(80);
	server.addPath("/", onIndex);
	server.addPath("/ajax/input", onAjaxInput);
	server.addPath("/ajax/frequency", onAjaxFrequency);
	server.setDefaultHandler(onFile);

	......
}

80번 포트로 연결요청을 받습니다. 3개의 패스를 처리할 수 있도록 해줬습니다. [/] 패스는 index.html 첫 페이지를 보여주는 패스입니다. [/ajax/input] 패스는 Ajax를 통해 버튼 상태 데이터를 요청하면 처리해주는 패스입니다. [/ajax/frequency] 패스는 HTML 페이지에서 CPU 속도 버튼을 눌렀을 때, Ajax에서 오는 요청을 처리하는 패스입니다. 각각의 패스로 요청이 올 때마다 처리해주는 콜백함수가 연결되어 있습니다. onIndex(), onAjaxInput(), onAjaxFrequency() 함수입니다.

이 중에서 onAjaxFrequency() 콜백 함수만 자세히 보겠습니다. [/ajax/frequency] 패스로 요청이 오면 CPU 속도값을 추출해서 모듈의 동작 속도를 바꿔주고, 결과를 json 형태로 보내줍니다.

void onAjaxFrequency(HttpRequest &request, HttpResponse &response)
{
	int freq = request.getQueryParameter("value").toInt();
	System.setCpuFrequency((CpuFrequency)freq);

	JsonObjectStream* stream = new JsonObjectStream();
	JsonObject& json = stream->getRoot();
	json["status"] = (bool)true;
	json["value"] = (int)System.getCpuFrequency();

	response.sendJsonObject(stream);
}

앞서 Java script 코드 설명 때 언급하진 않았는데, frequency 변경을 위한 Ajax 요청을 보낼 때 value=xxx 값을 함께 보냅니다. 따라서 여기서 이 값을 추출해야 합니다.

  • int freq = request.getQueryParameter(value).toInt();

추출한 값에 맞게 CPU 속도를 바꿔주는 코드가 이어서 나옵니다.

이제 처리 결과를 응답 메시지에 담아 보내줘야 합니다. json 형식에 맞게 보내면 되는데, Sming에서는 이걸 위해 JsonObjectStream, JsonObject 클래스를 제공해줍니다. Json object 에 데이터를 담을 때는 [변수이름 = 값] 형태로 넣어주면 됩니다.

  • json[status] = (bool)true;
  • json[value] = (int)System.getCpuFrequency();

그럼 status=xx, value=xx 데이터가 브라우저로 전달되어 업데이트 된 값이 화면에 표시될겁니다.

프로젝트를 빌드하고 업로드해서 브라우저로 접속해서 테스트 해보세요.

ajax_index

.

WebSocket 을 활용한 동적 웹 페이지

이전 Ajax 예제에서는 폴링(polling) 방식으로, 주기적으로 서버에 요청을 해서 데이터를 가져왔습니다. 물론 이 방식도 굉장 유용하지만 브라우저 측에서 항상 데이터를 체크해야 하는 단점이 있습니다. 항상 서버-클라이언트가 백그라운드로 연결된 상태를 유지하면서 실시간-양방향으로 자유롭게 데이터를 주고 받는다면 더 진보된 방식이라 할 수 있을겁니다.

Web Socket 이 이런 자유로운 통신 방식을 제공해줍니다. HTML5에 포함된 Web Socket은 웹 서버와 클라이언트(브라우저의 JavaScript) 간 TCP/IP 연결을 유지하게 해 줌으로써 푸시(push) 방식의 데이터 전송도 지원합니다. 다만, 이 기능을 사용하기 위해서는 비교적 최신의 브라우저를 사용해야 하는 제약이 있습니다. 그리고 당연히 서버에는 WebSocket 통신을 위한 준비가 되어 있어야 합니다.

  • WebSocket 소개

이번에 다룰 예제에서는 ESP8266에서 WebSocket을 구현해 볼겁니다. 서버는 웹 소켓 시작 요청이 오면 TCP 연결을 만든 후 종료 될 때 까지 유지합니다.(브라우저 종료시 까지) 그리고 웹 소켓 시작과 종료시에 몇 개의 연결이 수립되어 있는지 모든 (연결된) 클라이언트에게 알려줍니다. 그리고 push 방식의 데이터 전송을 확인하기 위해 주기적으로 메시지를 클라이언트들에게 던져줍니다.

이번 예제에 포함된 index.html 파일은 아래와 같은 화면을 만들어줍니다. 웹 소켓이 연결 될 때마다 모든 클라이언트들은 현재 연결된 클라이언트 숫자를 메시지로 받습니다. ESP8266에서는 최대 5개의 TCP 연결이 가능하므로 5개의 브라우저 탭을 열어 확인해보세요.

websocket_index

이번 예제에 사용되는 소스는 아래 링크에 모두 들어있습니다.

[files/index.html] 파일을 열어 소스코드를 확인해보세요. 자바 스크립트를 포함하는 [script] 태그 안에서 웹 소켓 관련된 처리들을 해줍니다.

  var output;
  function init()  {
    output = document.getElementById("output");
    testWebSocket();
  }
  function testWebSocket()  {
	var wsUri = "ws://" + location.host + "/";
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }
  function onOpen(evt)  {
    writeToScreen("CONNECTED");
    doSend("Sming love WebSockets");
  }
  function onClose(evt)  {
    writeToScreen("DISCONNECTED");
  }
  function onMessage(evt)  {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
    //websocket.close();
  }
  function onError(evt)  {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }
  function doSend(message)  {
    writeToScreen("SENT: " + message); 
    websocket.send(message);
  }
  function writeToScreen(message)  {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }
  
  function doDisconnect()  {
	var disconnect = document.getElementById("disconnect");
	disconnect.disabled = true;
	websocket.close();
  }
  window.addEventListener("load", init, false);

HTML 문서가 시작될 때 init 콜백함수를 호출하도록 설정합니다.

  window.addEventListener("load", init, false);

init() 함수가 호출되면 testWebSocket() 호출합니다. 웹 소켓 관련된 설정을 여기서 해줍니다.

  function testWebSocket()  {
	var wsUri = "ws://" + location.host + "/";
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }

접속할 URI를 지정해서 웹 소켓을 생성하고 웹 소켓 시작, 종료, 메시지 수신 시 호출될 콜백함수를 지정합니다. 이 작업만 해주면 서버와 웹 소켓으로 연결됩니다.

웹 소켓으로 메시지를 보낼 때는  doSend() 함수를 메시지와 함께 호출하면 됩니다.

  function doSend(message)  {
    writeToScreen("SENT: " + message); 
    websocket.send(message);
  }

ESP8266에 올릴 코드를 보겠습니다. 아래 링크에서 프로젝트 파일들을 받아 이클립스로 불러옵니다.

application.cpp – init() 함수부터 보시면 됩니다. 앞선 Ajax 예제와 진행이 거의 동일하기 때문에 시작부분은 간단히 언급만 하겠습니다.

// Will be called when WiFi station was connected to AP
void connectOk() {
	......
	startFTP();
	startWebServer();
}

void init() {
	spiffs_mount(); // Mount file system, in order to work with files
	......
	WifiStation.enable(true);
	WifiStation.config(WIFI_SSID, WIFI_PWD);
	......
	WifiStation.waitConnection(connectOk);
}

init() 함수에서 파일 시스템 마운트하고 공유기에 접속합니다. 공유기에 접속되면 FTP를 시작하고 WebServer 를 시작합니다. 파일 시스템에 파일들이 정상적으로 담기지 않은 경우 FTP를 사용해서 올리세요.

startWebServer() 안에서 웹 서버와 웹 소켓을 시작합니다.

void startWebServer()
{
	server.listen(80);
	server.addPath("/", onIndex);
	server.setDefaultHandler(onFile);

	// Web Sockets configuration
	server.enableWebSockets(true);
	server.setWebSocketConnectionHandler(wsConnected);
	server.setWebSocketMessageHandler(wsMessageReceived);
	server.setWebSocketBinaryHandler(wsBinaryReceived);
	server.setWebSocketDisconnectionHandler(wsDisconnected);

	Serial.println("\r\n=== WEB SERVER STARTED ===");
	Serial.println(WifiStation.getIP());
	Serial.println("==============================\r\n");

	msgTimer.initializeMs(5 * 1000, wsSendMessage).start(); // every 5 seconds
}

그리고 웹 소켓으로 접속된 클라이언트 들에게 주기적으로 메시지를 보내기 위해 타이머를 5초 간격으로 동작 시켰습니다. 타이머 콜백 함수인 wsSendMessage() 함수에서 웹 소켓으로 데이터를 보내는 작업을 하겠죠.

int iMsgCount = 0;
void wsSendMessage() {
	iMsgCount++;
	// Notify everybody about new connection
	WebSocketsList &clients = server.getActiveWebSockets();
	for (int i = 0; i < clients.count(); i++)
		clients[i].sendString("Notify from server : message no. " + String(iMsgCount));
}

접속된 웹 소켓 리스트를 가져와 WebSocket 클래스의 sendString 함수로 메시지를 보냅니다. 다른 웹 소켓의 콜백 함수들도 비슷한 작업을 하기 때문에 별다른 어려움 없이 사용하실 수 있을겁니다.

프로젝트를 컴파일하고 업로드 해보세요. ESP8266 모듈이 공유기에 접속하고 웹 서버, 웹 소켓을 시작할겁니다. PC나 모바일 폰 등 같은 공유기에 물려있는 장치에서 브라우저로 ESP8266에 접속합니다. [http://192.168.x.x] 에 접속하면 아래와 같은 화면을 볼 수 있습니다. 그리고 주기적으로 메시지를 받아 뿌려줄겁니다.

websocket_index

탭을 여러개 실행해서 ESP8266 모듈에 접속해보세요.

.

참고자료