mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
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…
x
Reference in New Issue
Block a user