examples: http_server - add websocket echo server example with test
This commit is contained in:
parent
e983042af2
commit
d7b3a051f0
10
examples/protocols/http_server/ws_echo_server/CMakeLists.txt
Normal file
10
examples/protocols/http_server/ws_echo_server/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||||
|
# in this exact order for cmake to work correctly
|
||||||
|
cmake_minimum_required(VERSION 3.5)
|
||||||
|
|
||||||
|
# (Not part of the boilerplate)
|
||||||
|
# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection.
|
||||||
|
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(ws_echo_server)
|
11
examples/protocols/http_server/ws_echo_server/Makefile
Normal file
11
examples/protocols/http_server/ws_echo_server/Makefile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#
|
||||||
|
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
|
||||||
|
# project subdirectory.
|
||||||
|
#
|
||||||
|
|
||||||
|
PROJECT_NAME := ws_echo_server
|
||||||
|
|
||||||
|
EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common
|
||||||
|
|
||||||
|
include $(IDF_PATH)/make/project.mk
|
||||||
|
|
24
examples/protocols/http_server/ws_echo_server/README.md
Normal file
24
examples/protocols/http_server/ws_echo_server/README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# WS test
|
||||||
|
|
||||||
|
The Example consists of HTTPD server demo with demostration of URI handling :
|
||||||
|
1. URI \hello for GET command returns "Hello World!" message
|
||||||
|
2. URI \echo for POST command echoes back the POSTed message
|
||||||
|
|
||||||
|
* Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details.
|
||||||
|
|
||||||
|
* In order to test the HTTPD server persistent sockets demo :
|
||||||
|
1. compile and burn the firmware `idf.py -p PORT flash`
|
||||||
|
2. run `idf.py -p PORT monitor` and note down the IP assigned to your ESP module. The default port is 80
|
||||||
|
3. test the example :
|
||||||
|
* run the test script : "python scripts/client.py \<IP\> \<port\> \<MSG\>"
|
||||||
|
* the provided test script first does a GET \hello and displays the response
|
||||||
|
* the script does a POST to \echo with the user input \<MSG\> and displays the response
|
||||||
|
* or use curl (asssuming IP is 192.168.43.130):
|
||||||
|
1. "curl 192.168.43.130:80/hello" - tests the GET "\hello" handler
|
||||||
|
2. "curl -X POST --data-binary @anyfile 192.168.43.130:80/echo > tmpfile"
|
||||||
|
* "anyfile" is the file being sent as request body and "tmpfile" is where the body of the response is saved
|
||||||
|
* since the server echoes back the request body, the two files should be same, as can be confirmed using : "cmp anyfile tmpfile"
|
||||||
|
3. "curl -X PUT -d "0" 192.168.43.130:80/ctrl" - disable /hello and /echo handlers
|
||||||
|
4. "curl -X PUT -d "1" 192.168.43.130:80/ctrl" - enable /hello and /echo handlers
|
||||||
|
|
||||||
|
See the README.md file in the upper level 'examples' directory for more information about examples.
|
|
@ -0,0 +1,2 @@
|
||||||
|
idf_component_register(SRCS "ws_echo_server.c"
|
||||||
|
INCLUDE_DIRS ".")
|
|
@ -0,0 +1,5 @@
|
||||||
|
#
|
||||||
|
# "main" pseudo-component makefile.
|
||||||
|
#
|
||||||
|
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* WebSocket Echo Server Example
|
||||||
|
|
||||||
|
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, this
|
||||||
|
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <esp_wifi.h>
|
||||||
|
#include <esp_event.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
#include <nvs_flash.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_eth.h"
|
||||||
|
#include "protocol_examples_common.h"
|
||||||
|
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
|
||||||
|
/* A simple example that demonstrates using websocket echo server
|
||||||
|
*/
|
||||||
|
|
||||||
|
static const char *TAG = "ws_echo_server";
|
||||||
|
|
||||||
|
|
||||||
|
static esp_err_t ws_ping_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
uint8_t buf[128] = { 0 };
|
||||||
|
httpd_ws_frame_t ws_pkt;
|
||||||
|
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||||
|
ws_pkt.payload = buf;
|
||||||
|
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
|
||||||
|
|
||||||
|
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 128);
|
||||||
|
ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload);
|
||||||
|
ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type);
|
||||||
|
|
||||||
|
ret = ret ? : httpd_ws_send_frame(req, &ws_pkt);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const httpd_uri_t ws = {
|
||||||
|
.uri = "/ws",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = ws_ping_handler,
|
||||||
|
.user_ctx = NULL,
|
||||||
|
.is_websocket = true
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static httpd_handle_t start_webserver(void)
|
||||||
|
{
|
||||||
|
httpd_handle_t server = NULL;
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
|
||||||
|
// Start the httpd server
|
||||||
|
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
|
||||||
|
if (httpd_start(&server, &config) == ESP_OK) {
|
||||||
|
// Set URI handlers
|
||||||
|
ESP_LOGI(TAG, "Registering URI handlers");
|
||||||
|
httpd_register_uri_handler(server, &ws);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Error starting server!");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stop_webserver(httpd_handle_t server)
|
||||||
|
{
|
||||||
|
// Stop the httpd server
|
||||||
|
httpd_stop(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disconnect_handler(void* arg, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void* event_data)
|
||||||
|
{
|
||||||
|
httpd_handle_t* server = (httpd_handle_t*) arg;
|
||||||
|
if (*server) {
|
||||||
|
ESP_LOGI(TAG, "Stopping webserver");
|
||||||
|
stop_webserver(*server);
|
||||||
|
*server = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void connect_handler(void* arg, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void* event_data)
|
||||||
|
{
|
||||||
|
httpd_handle_t* server = (httpd_handle_t*) arg;
|
||||||
|
if (*server == NULL) {
|
||||||
|
ESP_LOGI(TAG, "Starting webserver");
|
||||||
|
*server = start_webserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void app_main(void)
|
||||||
|
{
|
||||||
|
static httpd_handle_t server = NULL;
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
|
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
|
||||||
|
* Read "Establishing Wi-Fi or Ethernet Connection" section in
|
||||||
|
* examples/protocols/README.md for more information about this function.
|
||||||
|
*/
|
||||||
|
ESP_ERROR_CHECK(example_connect());
|
||||||
|
|
||||||
|
/* Register event handlers to stop the server when Wi-Fi or Ethernet is disconnected,
|
||||||
|
* and re-start it upon connection.
|
||||||
|
*/
|
||||||
|
#ifdef CONFIG_EXAMPLE_CONNECT_WIFI
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server));
|
||||||
|
#endif // CONFIG_EXAMPLE_CONNECT_WIFI
|
||||||
|
#ifdef CONFIG_EXAMPLE_CONNECT_ETHERNET
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &connect_handler, &server));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, &disconnect_handler, &server));
|
||||||
|
#endif // CONFIG_EXAMPLE_CONNECT_ETHERNET
|
||||||
|
|
||||||
|
/* Start the server for the first time */
|
||||||
|
server = start_webserver();
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright 2020 Espressif Systems (Shanghai) PTE LTD
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from builtins import range
|
||||||
|
import re
|
||||||
|
from tiny_test_fw import Utility
|
||||||
|
import ttfw_idf
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
OPCODE_TEXT = 0x1
|
||||||
|
OPCODE_BIN = 0x2
|
||||||
|
OPCODE_PING = 0x9
|
||||||
|
OPCODE_PONG = 0xa
|
||||||
|
|
||||||
|
|
||||||
|
class WsClient:
|
||||||
|
def __init__(self, ip, port):
|
||||||
|
self.port = port
|
||||||
|
self.ip = ip
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.client_key = "abcdefghjk"
|
||||||
|
self.socket.settimeout(10.0)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.socket.connect((self.ip, self.port))
|
||||||
|
self._handshake()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
def _handshake(self):
|
||||||
|
MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||||
|
client_key = self.client_key + MAGIC_STRING
|
||||||
|
expected_accept = base64.standard_b64encode(hashlib.sha1(client_key.encode()).digest())
|
||||||
|
request = 'GET /ws HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: ' \
|
||||||
|
'Upgrade\r\nSec-WebSocket-Key: {}\r\n' \
|
||||||
|
'Sec-WebSocket-Version: 13\r\n\r\n'.format(self.client_key)
|
||||||
|
self.socket.send(request.encode('utf-8'))
|
||||||
|
response = self.socket.recv(1024)
|
||||||
|
ws_accept = re.search(b'Sec-WebSocket-Accept: (.*)\r\n', response, re.IGNORECASE)
|
||||||
|
if ws_accept and ws_accept.group(1) is not None and ws_accept.group(1) == expected_accept:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise("Unexpected Sec-WebSocket-Accept, handshake response: {}".format(response))
|
||||||
|
|
||||||
|
def _masked(self, data):
|
||||||
|
mask_key = os.urandom(4)
|
||||||
|
out = mask_key
|
||||||
|
for i in range(len(data)):
|
||||||
|
if six.PY3:
|
||||||
|
out += (data[i] ^ mask_key[i % 4]).to_bytes(1, byteorder="little")
|
||||||
|
else:
|
||||||
|
out += chr(ord(data[i]) ^ ord(mask_key[i % 4]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _ws_encode(self, data="", opcode=OPCODE_TEXT, mask=1):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
length = len(data)
|
||||||
|
if length >= 126:
|
||||||
|
raise("Packet length of {} not supported!".format(length))
|
||||||
|
frame_header = chr(1 << 7 | opcode)
|
||||||
|
frame_header += chr(mask << 7 | length)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
if not mask:
|
||||||
|
return frame_header + data
|
||||||
|
return frame_header + self._masked(data)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
header = self.socket.recv(2)
|
||||||
|
if not six.PY3:
|
||||||
|
header = [ord(character) for character in header]
|
||||||
|
opcode = header[0] & 15
|
||||||
|
length = header[1] & 127
|
||||||
|
payload = self.socket.recv(length)
|
||||||
|
return opcode, payload.decode('utf-8')
|
||||||
|
|
||||||
|
def write(self, data="", opcode=OPCODE_TEXT, mask=1):
|
||||||
|
return self.socket.sendall(self._ws_encode(data=data, opcode=opcode, mask=mask))
|
||||||
|
|
||||||
|
|
||||||
|
@ttfw_idf.idf_example_test(env_tag="Example_WIFI")
|
||||||
|
def test_examples_protocol_http_ws_echo_server(env, extra_data):
|
||||||
|
# Acquire DUT
|
||||||
|
dut1 = env.get_dut("http_server", "examples/protocols/http_server/ws_echo_server", dut_class=ttfw_idf.ESP32DUT)
|
||||||
|
|
||||||
|
# Get binary file
|
||||||
|
binary_file = os.path.join(dut1.app.binary_path, "ws_echo_server.bin")
|
||||||
|
bin_size = os.path.getsize(binary_file)
|
||||||
|
ttfw_idf.log_performance("http_ws_server_bin_size", "{}KB".format(bin_size // 1024))
|
||||||
|
ttfw_idf.check_performance("http_ws_server_bin_size", bin_size // 1024)
|
||||||
|
|
||||||
|
# Upload binary and start testing
|
||||||
|
Utility.console_log("Starting ws-echo-server test app based on http_server")
|
||||||
|
dut1.start_app()
|
||||||
|
|
||||||
|
# Parse IP address of STA
|
||||||
|
Utility.console_log("Waiting to connect with AP")
|
||||||
|
got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)IPv4 address: (\d+.\d+.\d+.\d+)"), timeout=30)[0]
|
||||||
|
got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0]
|
||||||
|
|
||||||
|
Utility.console_log("Got IP : " + got_ip)
|
||||||
|
Utility.console_log("Got Port : " + got_port)
|
||||||
|
|
||||||
|
# Start ws server test
|
||||||
|
with WsClient(got_ip, 80) as ws:
|
||||||
|
DATA = 'Espressif'
|
||||||
|
for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]:
|
||||||
|
Utility.console_log("Testing opcode {}".format(expected_opcode))
|
||||||
|
ws.write(data=DATA, opcode=expected_opcode)
|
||||||
|
opcode, data = ws.read()
|
||||||
|
if expected_opcode == OPCODE_PING:
|
||||||
|
dut1.expect("Got a WS PING frame, Replying PONG")
|
||||||
|
if opcode != OPCODE_PONG or data != DATA:
|
||||||
|
raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data))
|
||||||
|
continue
|
||||||
|
dut_data = dut1.expect(re.compile(r"Got packet with message: ([A-Za-z0-9_]*)"))[0]
|
||||||
|
dut_opcode = int(dut1.expect(re.compile(r"Packet type: ([0-9]*)"))[0])
|
||||||
|
if opcode != expected_opcode or data != DATA or opcode != dut_opcode or data != dut_data:
|
||||||
|
raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_examples_protocol_http_ws_echo_server()
|
Loading…
Reference in a new issue