ESP8266 SDK 프로그래밍 – WiFi 통신
코드 구조 & 초기화 과정
ESP8266이 동작하기 위한 핵심 로직은 ESP8266 core에 담겨 있습니다. 그리고 개발자가 작업할 수 있도록 모듈의 동작 단계별로 사용할 수 있는 함수들을 제공합니다. 개발자는 제공된 함수 안에서 필요한 작업들을 수행하도록 코딩하면 됩니다.
ESP8266 모듈을 제어하기 위해서는 ESP8266 core에서 제공하는 함수(API)들을 사용하면 됩니다. 하지만 ESP8266의 핵심 기능인 WiFi 컨트롤은 대부분 사용자의 요청에 즉각적인 답을 보내기 힘들기 마련입니다. 그래서 ESP8266이 제공하는 함수들은 호출하더라도 작업이 끝날때까지 기다리지 않고 바로 return 되는 non-blocking 함수들입니다. 대신 작업의 결과를 callback 함수 또는 이벤트로 알려줍니다.
따라서 ESP8266 펌웨어를 만들때는 다양한 콜백함수들을 만들어두고, 콜백 등록함수를 이용해 미리 알려줘야 합니다. 그리고 이벤트를 처리하는 콜백함수도 만들어서 핸들링 해줘야 합니다.
많은 경우(특히 WiFi 통신 관련된), 특정 기능을 수행하는 함수를 호출하더라도 콜백 함수로 응답을 받기 전까지는 실행 결과를 알 수 없음을 염두에 둬야 합니다.
# 기본 구조
C/C++로 펌웨어 제작할 때 user_main.c 파일을 작성해야 합니다. 이 파일은 user_init() 함수를 포함하는데 어플리케이션 코드의 시작점 역할을 합니다.
- void user_init(void)
이 함수는 모듈이 동작할 때 처음 1번만 호출됩니다. 이 함수가 호출될 때는 아직 모듈의 환경 초기화 과정이 완전히 끝나지 않은 상태입니다. 따라서 모든 기능이 동작할 환경이 갖추어진 후 동작해야 할 코드가 있다면 아래 callback 함수를 사용하는 것이 좋습니다.
- system_init_done_cb()
RF 초기화 과정은 아래 함수를 통해 설정할 수 있습니다.
- void user_rf_pre_init(void)
ESP8266 모듈은 하나의 쓰레드만 동작하기 때문에 사용자가 작성한 코드가 실행된다는 말은 ESP8266 모듈의 원래 목적인 네트워크 기능이 멈춘다는 뜻입니다. 따라서 가급 적 사용자의 코드는 효율적이어야하며, 10msec 이내의 시간에 처리되는 것을 권장합니다.
# Defining the operating mode
ESP8266 모듈은 STA, AP, STA+AP 모드 중 하나로 동작합니다. 아래 함수로 변경할 수 있는데 변경한 값은 flash에 저장되므로 재부팅 후에도 적용됩니다.
- wifi_set_opmode()
단순히 현재 모드만 변경한다면 아래 함수를 사용하세요.
- wifi_set_opmode_current()
현재 모드를 알고 싶으면 wifi_get_opmode() 함수로 확인하고, flash에 저장된 기본 모드를 알고 싶으면 wifi_get_opmode_default() 함수를 쓰면 됩니다.
Working with WiFi
# AP scanning
ESP8266 모듈이 시작할 때 자동으로 이전에 접속한 AP를 찾아 접속합니다. 이 기능은 때로는 불필요할 때도 있는데, 이 때는 아래 두 함수를 사용해서 컨트롤 할 수 있습니다.
- wifi_station_set_auto_connect()
- wifi_station_get_auto_connect()
AP scanning : STA 모드로 모듈이 동작하면 주변의 AP를 스캔할 수 있다. 이때는 아래 함수를 사용한다.
- wifi_station_scan()
이 함수의 파라미터 중 하나는 callback 함수의 포인터를 받는데, 스캔이 끝날을 때 호출될 함수를 의미합니다. 스캔 callback 함수는 BSS 구조체의 linked list를 파라미터로 받습니다. BSS 구조체에는 아래 정보들이 들어가 있습니다.
- The SSID for the network
- The BSSID for the access point
- The channel
- The signal strength
- … others
예를 들어 아래 코드처럼 동작할 수 있습니다.
void scanCB(void *arg, STATUS status) { struct bss_info *bssInfo; bssInfo = (struct bss_info *)arg; // skip the first in the chain … it is invalid bssInfo = STAILQ_NEXT(bssInfo, next); while(bssInfo != NULL) { os_printf("ssid: %s\n", bssInfo->ssid); bssInfo = STAILQ_NEXT(bssInfo, next); } } //... { // Ensure we are in station mode wifi_set_opmode_current(STATION_MODE); // Request a scan of the network calling "scanCB" on completion wifi_station_scan(NULL, scanCB); }
STAILQ_NEXT() 매크로는 다음 BASS 구조체를 리턴하는 매크로이다. 리스트의 끝을 넘어서면 NULL 을 리턴한다.
# Handling WiFi events
ESP8266 모듈이 동작하면서 발생하는 WiFi 이벤트는 언제 발생할지 알 수 없습니다. 그래서 callback 함수를 등록하고 이벤트가 발생했을 때 해당 코드가 실행되도록 해야합니다. callback 함수는 아래 함수를 이용해서 등록합니다.
- wifi_set_event_handler_cb()
아래와 같은 이벤트가 발생했을 때 callback 함수가 호출되도록 할 수 있습니다.
- AP(access point)에 접속했을 때
- AP 접속이 끊어졌을 때
- 인증 모드가 변경되었을 때
- DHCP 를 통해 IP address 를 받았을 때
- AP 모드일 때 기기가 접속된 경우
- AP 모드일 때 기기가 접속 해제된 경우
WiFi event callback 함수를 사용하는 예제는 아래와 같습니다.
void eventHandler(System_Event_t *event) { switch(event->event) { case EVENT_STAMODE_CONNECTED: os_printf("Event: EVENT_STAMODE_CONNECTED\n"); break; case EVENT_STAMODE_DISCONNECTED: os_printf("Event: EVENT_STAMODE_DISCONNECTED\n"); break; case EVENT_STAMODE_AUTHMODE_CHANGE: os_printf("Event: EVENT_STAMODE_AUTHMODE_CHANGE\n"); break; case EVENT_STAMODE_GOT_IP: os_printf("Event: EVENT_STAMODE_GOT_IP\n"); break; case EVENT_SOFTAPMODE_STACONNECTED: os_printf("Event: EVENT_SOFTAPMODE_STACONNECTED\n"); break; case EVENT_SOFTAPMODE_STADISCONNECTED: os_printf("Event: EVENT_SOFTAPMODE_STADISCONNECTED\n"); break; default: os_printf("Unexpected event: %d\n", event->event); break; } }
callbak 함수는 user_init() 함수가 실행될 때 아래처럼 등록하면 됩니다.
- wifi_set_event_handler_cb(eventHandler);
# Station configuration
WiFi 모듈은 오직 하나의 AP에만 접속할 수 있습니다. 그리고 접속한 AP 에 대한 정보는 station_config 구조체에 저장됩니다.
AP 접속을 위해서는 2개의 공유한 값이 필요한데, AP를 구부하는 구분자인 SSID와 비밀번호인 password 입니다.
ESP8266 모듈은 마지막 접속한 station_config 데이터를 저장하고 있는데 이 값을 명시적으로 지정해서 바꿀 수 있다. 이때 아래 함수를 사용합니다.
- wifi_station_set_config()
이 함수를 사용하면 현재 설정을 저장해서 다음번 부팅때 사용하도록 합니다. 단순히 현재의 접속 상태만 바꾸고 싶다면 아래 함수를 사용하면 됩니다.
- wifi_station_set_config_current()
이 작업은 WiFi 관련 초기화가 완전히 끝난 뒤에만 사용할 수 있습니다. 따라서 system_init_done_cb()를 이용해 callback 함수를 등록한 뒤 사용해야 합니다. 예를들면 아래처럼 AP를 변경하는 코드를 작성할 수 있습니다.
void initDone() { wifi_set_opmode_current(STATION_MODE); struct station_config stationConfig; strncpy(stationConfig.ssid, "myssid", 32); strncpy(stationConfig.password, "mypassword", 64); wifi_station_set_config(&stationConfig); }
# Connecting to an access point
SSID, password 정보가 있다면 wifi_station_connect() 함수로 AP 접속이 가능합니다. wifi_station_connect() 호출 하더라도 바로 AP 접속이 이루어지지는 않기 때문에 일정 시간후에 Event로 알려주게 됩니다.
Event callback 함수에서는 두 개의 이벤트를 받게 되는데 순서대로 EVENT_STAMODE_CONNECTED, EVENT_STAMODE_GOT_IP 입니다. EVENT_STAMODE_GOT_IP 이벤트까지 받아야 WiFi 통신을 위한 IP 주소까지 받은 상태가 됩니다.
ESP8266 모듈은 flash 메모리에 부팅시 자동으로 AP에 접속할지 판단하는 flag 값을 저장합니다. 이 값을 아래 함수를 이용해 바꿀수 있습니다.
- wifi_station_set_auto_connect()
# Control and data flows when connecting as a station
앞서서 펌웨어를 만들때 사용하게 되는 함수와 WiFi 기능 제어 방법을 설명했습니다. 종합하자면 ESP8266 SDK를 이용해 펌웨어를 만든다면 반드시 아래의 함수들은 직접 구현해야 합니다.
- user_init – 펌웨어 개발자 코드의 시작점
- initDoneCB – 모듈 초기화가 완료된 시점에 호출되는 Callback 함수 (사용자의 초기화 코드를 여기에 정의)
- eventCB – WiFi 관련 이벤트가 생겼을 때 호출되는 Callback 함수
모듈이 구동되면서 이상의 함수가 구동되는 순서는 아래와 같습니다.
(이미지)
# Being an access point
ESP8266 모듈은 AP로 동작할 수 있는데 AP 기능을 사용하기 위해서는 동작 모드를 아래 함수로 변경해줘야 합니다.
- wifi_set_opmode()
- wifi_set_opmode_current()
그리고 AP의 고유 ID인 SSID, 장치들이 연결할 때 필요한 패스워드와 인증 모드를 설정해야 합니다. 실제로는 아래 예제코드처럼 사용하면 됩니다.
// Define our mode as an Access Point wifi_set_opmode_current(SOFTAP_MODE); // Build our Access Point configuration details os_strcpy(config.ssid, "ESP8266"); os_strcpy(config.password, "password"); config.ssid_len = 0; config.authmode = AUTH_OPEN; config.ssid_hidden = 0; config.max_connection = 4; wifi_softap_set_config_current(&config);
AP 기능이 활성화 되고 다른 WiFi 장치가 연결되면 아래와 같은 디버그 메시지가 UART1으로 들어옵니다.
- station: f0:25:b7:ff:12:c5 join, AID = 1
6자리 16진수 숫자는 장치의 MAC address 입니다. 이 장치의 연결이 끊어지면 아래 디버그 메시지가 출력됩니다.
- station: f0:25:b7:ff:12:c5 leave, AID = 1
현재 AP에 몇 개의 장치들이 연결되어 있는지는 아래 함수로 확인할 수 있습니다.
- wifi_softap_get_station_num()
이 장치들의 세부정보는 아래 함수로 조회할 수 있습니다.
- wifi_softap_get_station_info()
이 함수를 사용하면 struct station_info 구조체의 리스트(linked list)를 리턴해줍니다. 장치들 정보를 담은 이 리스트는 약간의 메모리를 소모하기 때문에 아래 함수를 이용해서 메모리 사용을 해제할 수 있습니다.
- wifi_softap_free_station_info()
현재 연결된 장치들에 할당된 IP를 출력하는 코드 예제는 아래와 같습니다.
if (stationInfo != NULL) { while (stationInfo != NULL) { os_printf("Station IP: %d.%d.%d.%d\n", IP2STR(&(stationInfo->ip))); stationInfo = STAILQ_NEXT(stationInfo, next); } wifi_softap_free_station_info(); }
주의!!! ESP8266 모듈이 AP 모드로 동작할 때 WiFi 장치들은 AP에 접속해서 AP 모듈과 통신할 수 있습니다. 하지만 AP에 연결된 장치들끼리 통신은 할 수 없습니다. 예를 들어, A 모듈이 AP 모드로 동작하고 여기에 B, C 모듈이 접속을 했다고 하죠. 이 상태에서 B와 C가 직접 통신을 할 수는 없습니다. B에서 C에 ping 커맨드로 상태를 알아보는 등의 작업은 할 수 없다는 겁니다.
오직 A-B, A-C 간의 직접 통신만 가능합니다. 원래 ESP8266의 AP 기능은 모바일이나 PC등의 장치와 공유기 없이 직접 통신할 수 있는 방법을 제공하기 위한 기능이기 때문입니다.
# The DHCP server
ESP8266 모듈이 AP 기능을 수행할 때 접속하는 장치들이 자동으로 IP 주소와 subnet mask, gateway를 알 수 잇도록 DHCP 서버 기능을 사용할 수 있습니다. DHCP 서버 기능은 아래 함수로 시작, 종료가 가능합니다.
- wifi_softap_dhcps_start()
- wifi_softap_dhcps_stop()
현재 DHCP 서버의 상태는 아래 함수로 조회할 수 있습니다.
- wifi_softap_dhcps_status()
DHCP로 할당되는 IP 주소는 192.168.4.1 부터 시작합니다. 이 중 192.168.4.1 은 AP로 동작하는 모듈 자신에게 할당됩니다.
# Current IP Address, netmask and gateway
ESP8266 모듈이 할당받은 IP 주소는 아래 함수를 사용하면 조회할 수 있습니다.
- wifi_get_ip_info()
그리고 아래 함수를 사용하면 값을 변경할 수도 있습니다.
- wifi_set_ip_info()
항상 같은 IP를 사용하기위해 IP 주소를 지정하려면(static IP) 아래와 같은 과정을 거치면 됩니다. 우리가 IP를 직접 지정한 상태에서 AP에 접속해서 DHCP로 IP를 할당받는 과정을 거치면 지정한 IP가 사용 가능한지 알려주는 이벤트가 발생합니다.
init_done() callbak 함수에서 wifi_station_dhcpc_stop() 함수를 호출해서 DHCP client 를 중지시킵니다. 그리고 wifi_station_connect() 함수를 호출해서 AP에 접속합니다. AP에 접속이 되었다는 이벤트가 발생하면(EVENT_STAMODE_CONNECTED) wifi_set_ip_info() 함수를 이용해서 사용하고 싶은 IP, netmask, gateway 값을 할당합니다.
주의해야 할 점이 있는데, static IP 주소를 사용할 때는 이 주소가 할당되었는지를 나타내는 이벤트(EVENT_STAMODE_GOT_IP)를 받을 수 없습니다.
# WiFi Protected Setup – WPS
ESP8266 모듈은 Station 모드로 동작할 때 WPS(WiFi Protected Setup)를 지원합니다. 만약 AP에서 WPS를 지원하면 ESP8266 모듈은 패스워드 없이 접속이 가능합니다. 현재는 Push button mode 만 지원하기 때문에 AP에서 물리적인 버튼을 눌렀을 때 약 2분간 WPS 프로토콜을 사용한 접속을 할 수 있습니다. 이 기능을 사용하려면 AP의 WPS 버튼을 누르고 ESP8266 모듈에서 아래 함수가 실행되도록 하면 됩니다.
- wifi_wps_enable()
- wifi_wps_start()
Working with TCP/IP
TCP/IP는 인터넷에서 사용되는 네트워크 프로토콜로 ESP8266에서 기본으로 사용합니다. 이미 자세한 설명은 인터넷에서 쉽게 찾을 수 있으므로 여기서는 몇 가지 필요한 사항들만 언급합니다.
IP 어드레스는 인터넷 상의 각 장치를 구분하는 주소입니다. 32bit(4byte) 를 사용하며 0~255 까지의 10진수 4개로 표시합니다. 일반적으로 어플리케이션 레벨에서는 IP 주소 대신 “google.com” 같은 텍스트 주소를 사용하는데 이는 TCP/IP 레벨에서는 사용되지 않습니다. 그래서 IP주소와 텍스트 주소를 매칭, 검색해주는 장치가 필요한데 이를 Domain Name System(DNS)이라 부릅니다.
TCP/IP 는 실질적으로 3개의 구분된 프로토콜로 나눌 수 있습니다. IP 프로토콜이 가장 하위에 위치하며 데이터를 전송할 때 원하는 목적지까지 길을 찾는 역할을 해줍니다. 그리고 그 위에 TCP(Transmission Control Protocol) 프로토콜과 UDP(User Datagram Protocol) 프로토콜이 위치합니다. TCP, UDP는 데이터 전송을 위한 연결 방법론입니다. 보통 TCP/IP라 부르는 상황에서는 IP, TCP, UDP 프로토콜을 모두 포함하는 경우가 많으며 때로는 관련된(더 상위의) DNS, HTTP, FTP, Telnet 등의 프로토콜을 포함하기도 합니다.
# The espconn architecture
개발자가 작성하는 코드가 ESP8266 의 동작을 긴 시간 멈추게 해서는 안되기 때문에 긴 시간이 걸리는 작업이나 비동기(asynchronous)식 이벤트는 callback 함수로 처리하도록 되어 있습니다.
예를들어 네트워크 접속 요청이 들어오길 기다리고 싶을 때, 무한정 접속 요청이 오는지 확인만 하고 있을수는 없습니다. 따라서 callback 함수를 작성해서 미리 등록하고 요청이 있을 때만 callback 함수가 호출되어 우리가 작성한 루틴이 실행되도록 해야합니다. 나머지 시간은 OS에서 컨트롤 하는겁니다.
ESP8266 에서는 TCP 관련된 기능과 이벤트를 처리하기 위해 아래와 같은 함수를 지원합니다. 이 함수들로 callback 함수를 등록할 수 있습니다.
(표 103 p)
Register Function Callback Description
espconn_regist_connectcb espconn_connect_callback TCP connected successfully
espconn_regist_disconcb espconn_disconnect_callback TCP disconnected successfully
espconn_regist_reconcb espconn_reconnect_callback Error detected or TCP disconnected
espconn_regist_sentcb espconn_sent_callback Sent TCP or UDP data
espconn_regist_recvcb espconn_recv_callback Received TCP or UDP data
espconn_regist_write_finish espconn_write_finish_callback Write data into TCP-send-buffer
TCP
TCP 는 양방향으로 데이터가 흐를수 있도록 파이프를 생성해주는 프로코톨입니다. TCP 연결이 만들어지려면 우선 한 쪽은 TCP server 역할을 하면서 연결요청을 기다렸다가 처리해줘야 합니다. 그리고 다른 한쪽에서(client) 연결 요청을 보내면서 연결이 만들어집니다. 일단 연결이 맺어지면 이후로는 양쪽에서 데이터를 주고받을 수 있습니다. 클라이언트(client)에서 연결 요청을 보내기 위해서는 IP와 Port를 알아야 합니다. IP가 하나의 건물이라고하면 Port는 사무실에 가깝습니다. 하나의 서버에 여러개의 어플리케이션이 동작하기 때문에 보다 세밀하게 구분을 해주는 것이 Port 입니다.
ESP8266 에서는 struct espconn 구조체를 통해 TCP/IP 연결 작업을 시작합니다. espconn 구조체는 대부분의 TCP API 에서 참고하는 구조체이기 때문에 매우 중요합니다. espconn 구조체의 필드 중 아래 필드는 초기화를 위해 꼭 필요합니다.
- type – 사용할 연결 타입 TCP/UDP를 지정할 수 있으며 TCP의 경우 ESPCONN_TCP 로 지정
- state – 연결 상태를 나타내는 파라미터. 연결 상태에 따라 변화하는 값이며 초기화 때는 ESPCONN_NONE 로 지정.
espconn 구조체를 초기화 하는 코드는 아래와 같습니다.
struct espconn conn1; void init() { conn1.type = ESPCONN_TCP; conn1.state = ESPCONN_NONE; }
TCP 연결을 위해서는 struct esp_tcp 라는 특별한 구조체가 하나 더 필요합니다. TCP 요청을 받을 포트 넘버처럼 연결에 필수적인 정보가 local_port 파라미터에 저장됩니다.
esp_tcp tcp1; void init() { tcp1.local_port = 25867; }
espconn 구조체에는 proto 라는 필드가 있는데 TCP/UDP 데이터 구조체의 포인터를 저장합니다. 즉, 앞서 생성한 esp_tcp 구조체를 espconn 구조체와 연결할 때 사용합니다. 아래 코드처럼요.
struct espconn conn1; esp_tcp tcp1; void init() { tcp1.local_port = 25867; conn1.type = ESPCONN_TCP; conn1.state = ESPCONN_NONE; conn1.proto.tcp = &tcp1; }
TCP 연결을 위한 기본 파리미터가 준비되었으므로 espconn_accept() 함수를 이용해 TCP 연결 요청을 받도록 할 수 있습니다. 이 함수는 파리미터로 espconn 구조체 데이터가 필요합니다. 그래야 어떤 포트로 요청을 받을지 알 수 있습니다.
- espconn_accept(&conn1);
이제 ESP8266 모듈은 local_port로 들어오는 TCP 연결 요청을 기다리면서 TCP 서버 역할을 합니다. 기억해야할 점은 TCP 서버 관련된 작업은 ESP8266 core에서 처리하며 espconn_accept() 함수를 호출한다고해서 개발자의 코드가 여기서 멈추지는 않습니다. ESP8266 core는 연결 요청이 들어올 때 처럼 중요한 이벤트가 발생했을 때만 콜백함수를 통해 알려줍니다. 따라서 우리는 중요한 이벤트를 처리할 콜백함수를 미리 등록해줘야 합니다. 콜백함수를 등록하는 함수가 espconn_regist_connectcb() 입니다.
정리하면, TCP 서버로 동작해서 관련된 이벤트를 처리하기 위해선 아래처럼 코드를 작성해야 합니다.
void connectCB(void *arg) { struct espconn *pNewEspConn = (struct espconn *)arg; //… Do something with the new connection } { ... espconn_regist_connectcb(&conn1, connectCB); espconn_accept(&conn1); }
이 과정을 sequence flow diagram으로 보면 아래와 같습니다.
(그림 106 p)
espconn_accept() 함수로 TCP 서버를 시작할 때 espconn 구조체 데이터를 같이 넣어줬습니다. 만약 외부에서 TCP 연결 요청이 오면 espconn 구조체에는 요청을 보낸 장치의 IP가 기록됩니다.
예를들어 우리가 원하지 않는 IP에서 접속요청이 온다면 IP를 확인하고 espconn_disconnect() 함수로 연결을 거부할 수 있습니다. 매번 연결 요청이 올 때 espconn 데이터가 새롭게 쓰여지므로 새로운 연결을 기다리려면 espconn 구조체를 새로 생성해서 전달해야 합니다.
앞서 소개한 과정은 TCP 서버 역할을 하면서 TCP 연결을 기다리는 과정입니다. 그러면 반대로 외부 TCP 서버에 연결 요청을 보낼때는?? 이때는 espconn_connect() 함수를 사용합니다.
이 요청을 보내기 전에 우리는 먼저 TCP 구조체에 적절한 데이터를 넣어줘야 합니다. 특히 remote_port 필드와 remote_ip 필드에 정확한 대상 서버 IP, port 값을 넣어줘야 합니다. 그리고 local_port 항목도 반드시 할당해줘야 서버에서 되돌아오는 응답을 받을 수 있습니다. 응답을 받을 포트인 local_port 는 espconn_port() 함수로 설정해야 합니다. local_ip 필드도 할당되어야 하는데 이를 위한 wifi_get_ip_info() 함수가 준비되어 있습니다.
TCP 요청은 espconn_connect() 함수를 이용해 시작하게 되며 ESP8266 core에서 처리합니다.
struct espconn conn1; esp_tcp tcp1; void init() { tcp1.remote_port = 25867; tcp1.remote_ip = ipAddress; tcp1.local_port = espconn_port(); struct ip_info ipconfig; wifi_get_ip_info(STATION_IF, &ipconfig); os_memcpy(tcp.local_ip, &ipconfig.ip, 4); conn1.type = ESPCONN_TCP; conn1.state = ESPCONN_NONE; conn1.proto.tcp = &tcp1; }
TCP 요청을 보낸 대상 서버가 연결을 거부할 경우를 위해 espconn_regist_disconcb() 함수로 콜백 함수를 등록해줘야 합니다. 이때 struct espconn 구조체는 CLOSE 상태값을 가지게 됩니다.
만약 TCP 연결이 생성되고 10초 이상 데이터 흐름이 없다면 연결은 ESP8266 코어에 의해 자동으로 종료됩니다. 여기에 사용되는 timeout 시간을 변경하고 싶은 경우 espconn_regist_time() 함수를 사용할 수 있습니다.
ESP8266은 최대 5개의 TCP 연결을 지원합니다.
# Sending and receiving TCP data
ESP8266 모듈과 외부 서버가 TCP로 연결되어 있다고 가정해보죠. 그럼 TCP 데이터 전송과 수신, 두 가지 형태의 대화를 처리해야 합니다. 이 상태에서 데이터를 보내는 쪽(transmitter)과 받는 쪽(receiver)의 구분은 Application 차원에서만 의미 있을 뿐, TCP 에서는 양방향으로 얼마든지 데이터가 오갈 수 있습니다.
TCP로 연결된 장치에서 데이터를 받았을 때 호출될 콜백 함수를 espconn_regist_recvcb() 를 이용해서 등록할 수 있습니다. 이때 espconn 구조체를 파라미터로 넘기는데, TCP 연결이 되었을 때 호출되는 connected 콜백 함수에서 얻을 수 있는 구조체입니다. espconn_regist_recvcb() 함수로 등록된 콜백 함수는 호출될 때 수신된 데이터를 담은 버퍼와 버퍼에 담겨진 데이터의 길이를 알려주는 값을 파라미터로 넘겨줍니다. TCP 데이터 수신시 호출될 콜백 함수는 아래처럼 작성할 수 있습니다.
void recvCB(void *arg, char *pData, unsigned short len) { struct espconn *pEspConn = (struct espconn *)arg; os_printf("Received data!! - length = %d\n", len); int i=0; for (i=0; i<len; i++) { os_printf("%c", pData[i]); } os_printf("\n"); } // End of recvCB
recvCB 콜백 함수는 상대편에서 데이터를 전송해줄 때마다 호출되는데, 만약 상대편에서 1460 bytes 크기가 넘는 데이터를 보내면 1460byte 단위로 끊어서 여러번 호출됩니다. 예를 들어 5000byte 데이터를 수신하면 recvCB() 함수는 4회 호출됩니다. 처음 3번의 호출시엔 1460 byte를 넘겨주고 마지막 호출시에는 620 byte를 넘겨줍니다.
이것은 ESP8266이 굉장히 작은 RAM 을 운용하고 다중 TCP 접속을 관리하기 때문입니다. 수신한 데이터를 빠르게 처리하지 못하면 다음 데이터 처리에 곤란을 겪습니다.
그리고 TCP 연결은 일종의 steamed – 스트림화 된 데이터 통신입니다. 무슨 말이냐면, TCP 전송의 데이터 단위라는게 정해져 있지 않고 전송할 데이터가 생기면 사이즈가 어떻든 그저 보내고 받을 뿐이라는겁니다. 그래서 전송측에서 5byte 씩 두 번의 데이터를 전송 하더라도 수신측에서는 10byte를 한 번에 받거나 예측할 수 없는 사이즈로 분할해서 받을 수도 있습니다.
ESP8266에서 데이터를 TCP로 보낼때는 espconn_send() 함수를 사용합니다.
이 함수는 어떤 TCP 연결을 이용해 데이터를 보낼지 판단하기 위해 espconn 구조체를 파라미터로 받습니다. 그리고 전송할 데이터 버퍼와 사이즈도 파라미터로 받습니다.
다른 ESP8266 함수와 마찬가지로 espconn_send() 함수도 호출 후 바로 데이터가 전송되지는 않습니다. 보통 수 밀리세컨드가 소요되겠지만 더 길어질 수도 있습니다. 대신 전송이 완료되면 콜백 함수를 호출해주기 때문에 사전에 espconn_regist_sentcb() 함수로 콜백함수를 등록할 수 있습니다. 일단 이 콜백함수가 호출되어야 다음번 espconn_send() 요청을 보낼 수 있습니다.
이 때 반드시 지켜야 할 점이 있습니다. espconn_send() 함수를 호출하더라도 ESP8266은 데이터를 바로 전송하지 않습니다. 따라서 데이터를 담고 있는 버퍼는 전송이 완료될 때까지 보낼 데이터를 계속 유지하고 있어야 합니다.
# Flow control
만약 TCP로 연결된 장치가 초당 5KByte의 데이터를 전송하는데 ESP8266은 초당 1KB의 데이터만 처리할 수 있는 경우라면 어떻게 될까요? 금방 문제가 생길거란걸 짐작할 수 있습니다. 따라서 데이터를 전송하는 장치가 ESP8266 이 처리할 수 있을 정도로 데이터량을 조절할 수 있도록 flow control 로직을 구현해줘야 합니다. 단순히 ESP8266에서 데이터의 일부를 무시하거나, 처리 가능한 용량을 알려주는 형태로 구현할 수 있을겁니다.
# TCP Error Handling
두 장치가 TCP 연결이 맺어지더라도 실제 두 장치 사이에 물리적인 연결이 생기는 것은 아닙니다. 대신 IP 프로토콜에 기반한 데이터 흐름을 논리적으로 인식할 뿐입니다. 따라서 한쪽 장치가 연결이 끊어지더라도 상대편은 그 사실을 바로 알지 못합니다. 내가 사용하는 ESP8266 모듈의 TCP 연결이 끊어지면 상대편은 내 모듈이 메시지를 주기를 계속 기다리고 있을겁니다.
이런 상황이 발생하지 않도록 TCP는 “keep-alive” 라는 개념을 가지고 있습니다. Keep-alive 는 단순히 두 장치가 생존 신호(heartbeat signal)를 주기적으로 교환함으로써 구현합니다. 따라서 한 쪽 장치의 연결이 끊어지면 반대쪽 장치가 생존 신호를 보내도 응답을 받을 수 없겠죠. 이 경우 상대방의 TCP 연결이 끊어졌다고 판단해서 연결을 종료하는 clean up and shut down 과정을 수행할 수 있습니다.
ESP8266은 Keep-alive 를 통해 연결을 제어하는 espconn_set_keepalive() 함수를 제공합니다. 이 함수는 아래와 같은 속성값이 있습니다.
- 마지막으로 생존 신호를 받은 후 얼마나 기다렸다 다시 생존 신호를 보낼 것인가?
- If no response, how long between subsequent heartbeats?
- 상대방의 연결이 끊어졌음을 확정하기 위해 몇 번 더 생존 신호를 보낼 것인가?
이런 속성들은 espconn_set_opt() 함수로 지정할 수 있습니다. 만약 keep-alive 속성을 사용한다면 connect 콜백 함수의 핸들러 안에서 사용해야 합니다. 아래 함수를 통해 콜백함수를 등록함으로써 연결이 끊어졌는지 알 수 있습니다.
- espconn_reconnect_callback()
UDP (User Datagram Protocol)
두 장치간 연결이라는 관점에서 볼 때 TCP는 전화에 가깝습니다. 한쪽에서 전화를 걸어 연결이 완료되어야 이후 통화(데이터 전송)가 가능하죠. 반면에 UDP는 편지에 가깝습니다. 데이터를 전송하기 전 연결 설정 작업이 필요치 않으며 목적지만 알면 바로 데이터를 보낼 수 있습니다. 대신 데이터를 전송 후 제대로 도착했는지 확인할 수 없습니다. TCP의 경우는 데이터를 전송후 handshaking(상호 확인)을 함으로써 데이터가 도착했는지 확인하고 문제가 발생하면 재시도하거나 포기합니다.
UDP를 이용할 때 최대 데이터 사이즈는 64KB 입니다.
UDP로 전송되는 데이터를 받기 위해서는 포트를 설정해줘야 합니다. espconn 구조체의 local_port 파라미터에 값을 지정하고 espconn_create() 함수를 이용하면 됩니다. espconn_create() 함수를 호출한 뒤에는 espconn_regist_recvcb() 함수로 데이터가 도착했을 때 실행될 콜백 함수를 지정해줘야 합니다. 아래는 이 과정을 수행하는 예제코드 입니다.
struct espconn conn1; esp_udp udp1; void setupUDP() { sint8 err; conn1.type = ESPCONN_UDP; conn1.state = ESPCONN_NONE; udp1.local_port = 25867; conn1.proto.udp = &udp1; err = espconn_create(&conn1); err = espconn_regist_recvcb(&conn1, recvCB); } // End of setupUDP
UDP 데이터 수신을 종료하고 싶을 때는 espconn_delete() 함수를 호출하면 됩니다.
UDP 를 이용해 데이터를 보낼때도 espconn 구조체가 사용됩니다. espconn 구조체에는 가장 중요한 정보인 상대방 IP 주소와 포트를 넣어줘야 합니다. 그리고 espconn_create() 함수를 이용해 내부 connection을 생성하면 데이터를 보낼 준비가 됩니다. espconn_sent() 함수를 이용하면 데이터를 보낼 수 있고 espconn_delete() 함수를 호출하면 데이터 전송에 사용된 ESP8266 리소스를 모두 해제합니다.
struct espconn sendResponse; esp_udp udp; void sendDatagram(char *datagram, uint16 size) { sendResponse.type = ESPCONN_UDP; sendResponse.state = ESPCONN_NONE; sendResponse.proto.udp = &udp; IP4_ADDR((ip_addr_t *)sendResponse.proto.udp->remote_ip, 192, 168, 1, 7); sendResponse.proto.udp->remote_port = 9876; // Remote port err = espconn_create(&sendResponse); err = espconn_send(&sendResponse, "hi123", 5); err = espconn_delete(&sendResponse); }
# Broadcast with UDP
UDP로 가능한 유용한 컨셉 중 하나가 broadcast 입니다. Broadcast는 같은 subnet에 있는 모든 장치들에게 같은 데이터를 전송할 수 있는 기능입니다. 데이터를 수신할 장치들은 일반적인 UDP 통신처럼 UDP 포트를 열고 데이터가 오기를 기다리면 됩니다. 송신측은 UDP 포트를 기재하고 broadcast를 위한 IP 주소를 사용하면 됩니다.
예를들어 255.255.255.0 netmask를 사용하고 네트워크의 주소가 192.168.1.x 라면 192.168.1.255 가 broadcast 주소가 됩니다.
ESP8266에서 broadcast 를 사용하기 위해서는 wifi_set_broadcast_if() 함수를 이용해 인터페이스를 설정해줘야 합니다. 현재 인터페이스가 station 인지, AP인지 혹은 station + AP 인지 선택해야 합니다. wifi_get_broadtcast_if() 함수를 사용하면 현재 인터페이스 상태를 알 수 있습니다.
기타 네트웍 도구들
# Ping request
TCP/IP 레벨에서는 IP 주소를 가진 장치가 다른 IP 주소를 가진 장치에게 ping 명령을 보낼 수 있습니다. Ping은 네트워크를 통해 상대 장치까지 연결이 되는지, 상대 장치가 응답이 가능한 상태인지를 확인하는 명령입니다.
ping_option 구조체는 ping 명령을 보내기 위한 정보를 담습니다. 이 구조체를 ping_start() 함수의 파라미터로 넣어 호출하면 ping 명령을 수행할 수 있습니다. ping_start() 함수를 사용하기 전에 IP 주소와 ping request 횟수를 미리 지정해줘야 합니다.
ping_regist_recv() 함수를 이용해 응답을 받을 콜백 함수를 등록합니다. 그리고 ping_regist_sent() 함수로 ping 명령을 보낼 수 있습니다.
# Name Service
인터넷에서 사용하는 “google.com” 같은 주소는 사람이 읽기 편하도록 설계된 주소입니다. 이걸 4개의 숫자로 구성된 IP로 변환하기 위해서 제공되는 서비스가 Domain Name Service (DNS) 입니다. 따라서 주소->IP 변환을 위해 ESP8266 모듈은 하나 이상의 외부 DNS 서버를 알고 있어야 합니다.
만약 DHCP 를 사용한다면 DHCP 서버가 자동으로 DNS 서버 주소를 제공해주기 때문에 신경쓸 필요가 없습니다. 하지만 DHCP 를 사용하지 않는다면 DNS 서버를 수동으로 지정해줘야 합니다. 이 작업은 espconn_dns_setserver() 함수로 할 수 있습니다. 이 함수는 하나 혹은 두 개의 IP 주소를 받습니다.
구글에서는 공용으로 쓸 수 있도록 두 개의 네임서버 주소를 제공합니다.
- 8.8.8.8 / 8.8.4.4
일단 네임서버가 설정되면 espconn_gethostbyname() 함수로 IP 주소를 얻을 수 있습니다. 이때 결과로 받게 되는 값은 주의해서 볼 필요가 있습니다. 현재 상태에 따라 우리가 기대한 결과와는 다른 값이 나올수도 있기 때문입니다. 자세한 내용은 SDK 매뉴얼을 참고하세요.
# Multicast Domain Name Systems
Local area network 에 여러 장치가 동적으로 참가, 탈퇴하는 경우에는 한 장치가 다른 장치의 IP 주소를 알기 쉽지 않습니다. 왜냐면 보통 IP 주소는 AP의 DHCP 기능에 의해서 동적으로 할당되기 때문입니다. 이런 경우 IP 대신 이름으로 장치를 찾을 수 했도록 해주는 기능이 Multicast Domain Name System(mDNS) 입니다.
mDNS로 장치를 찾을 때는 broadcast 채널로 요청을 보냅니다. 그럼 각 장치들이 자신의 이름과 맞을 때 IP주소와 함께 응답하고, 원하는 장치가 아닐 때는 이 과정을 반복합니다.
mDNS에 참여하는 장치들은 전용 UDP 포트인 5353을 사용합니다. 이 포트를 이용하면 ESP8266 도 IP 주소 검색을 시도할 수 있습니다.
# Working with SNTP
SNTP (Simple Network Time Protocol)는 인터넷을 통해 정확한 시간을 받기위한 프로토콜입니다. SNTP 사용을 위해서는 하나 이상의 SNTP 서버를 사용해야 합니다. US NIST에서 운용하는 SNTP 서버 리스트를 아래 링크에서 보실 수 있습니다.
- http://tf.nist.gov/tf-cgi/servers.cgi
SNTP 서버이름이나 IP 주소를 확보했으면 sntp_setservername(), sntp_setserver() 함수를 이용해서 시간을 받아올 수 있습니다. ESP8266에서는 3개의 타임 서버를 설정할 수 있습니다.
ESP8266 은 모듈이 동작하는 timezone을 sntp_set_timezone() 함수로 설정할 수 있습니다. 이 함수는 UTC 로부터의 시간차(hours offset)를 입력값으로 받습니다. 예를 틀어 서울의 경우는 9이고, 미국 텍사스의 경우는 -5 입니다.
sntp_init() 함수를 호출하면 SNTP 서비스가 시작됩니다. 물론 이 작업은 일정 시간이 걸리므로 바로 현재 시간이 나오지 않습니다. 일정 시간이 지나고 작업이 완료되면 sntp_get_current_timestamp() 함수로 현재 시간의 timestamp 값을 얻을 수 있습니다. timestamp는 UTC-1970-1-1 00:00 이후로 지나온 초 단위 시간입니다. sntp_get_real_time() 함수를 사용하면 읽기 편한 문자열 형태의 시간을 얻을 수 있습니다.
# ESP-NOW
ESP-NOW 는 ESP8266 장치간 사용되도록 고안된 프로토콜입니다. ESP-NOW 는 하나의 컨트롤러에 여러개의 슬레이브가 ‘항상’ 연결되어 제어가 가능하도록 하고자 만들어 졌습니다. 따라서 슬레이브는 전원만 넣으면 컨트롤러에 연결되어 제어될 수 있습니다.
ESP-NOW 에 참여하고자 하는 ESP8266 모듈은 esp_now_init()를 호출하면 됩니다. 그리고 esp_now_deinit() 함수로 종료할 수 있습니다. ESP-NOW 에 참여한 이후 통신을 하기 위해서는 esp_now_add_peer()를 호출해서 peer 등록을 해줘야 합니다.
각각의 ESP8266 장치는 esp_now_set_self_role()을 호출해서 자신의 역할을 지정할 수 있습니다. esp_now_delete_peer() 함수는 특정 peer의 등록을 취소하는데 사용됩니다. 데이터 전송시 길이는 256 byte로 제한됩니다.
데이터가 전송되었을 때, 수신했을 때를 위해 두 개의 콜백함수를 등록할 수 있습니다.
- esp_now_register_recv_cb()
- esp_now_register_send_cb()
데이터 전송에는 esp_now_send() 함수를 사용합니다.
# IP 출력
IP 주소를 디버그 메시지로 출력할 때는 아래 코드를 사용하세요.
- os_printf(IPSTR, IP2STR(pIpAddrVar))
==> os_printf(“%d.%d.%d.%d”, IP2STR(pIpAddrVar))
# Fatal exception
동작중 처리할 수 없는 오류가 발생할 경우 아래와 같은 메시지를 UART1으로 출력하며 Reboot 됩니다. (일반적인 USB연결에 사용하는 핀은 UART0, UART1은 디버깅용 시리얼포트)
Fatal exception (28): epc1=0x40243182, epc2=0x00000000, epc3=0x00000000, excvaddr=0x00000050, depc=0x00000000 exccause – Code describing the cause epc1 – Exception program counter excvaddr – Virtual address that caused the most recent fetch, load or store exception.
문제가 발생한 코드 찾을 때 윈도우 쉘에서 아래 명령을 이용하세요.
- xtensa-lx106-elf-objdump -x app.out -d
# 네트웍 테스트용 어플리케이션
- Android – Socket Protocol
- Android – UDP Sender/Receiver
- Windows – Hercules
- Curl (command line tool)
- Eclipse – TCP/MON (add on)
- httpbin.org (hosting service)