From 5cb04f3e139663841eaca2072b40d02ea1c76328 Mon Sep 17 00:00:00 2001 From: Marius Vikhammer Date: Tue, 15 Oct 2019 17:27:47 +0800 Subject: [PATCH] websocket_client: added example_test with a local websocket server - Added a example test that connects to a local python websocket server. - Added readme for websocket_client example. Closes IDF-907 --- examples/protocols/websocket/README.md | 57 ++++++ examples/protocols/websocket/example_test.py | 181 +++++++++++++++++- .../websocket/main/Kconfig.projbuild | 14 ++ .../websocket/main/websocket_example.c | 45 ++++- examples/protocols/websocket/sdkconfig.ci | 3 + 5 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 examples/protocols/websocket/sdkconfig.ci diff --git a/examples/protocols/websocket/README.md b/examples/protocols/websocket/README.md index 2969444fd..c434a7c13 100644 --- a/examples/protocols/websocket/README.md +++ b/examples/protocols/websocket/README.md @@ -1 +1,58 @@ # Websocket Sample application + +(See the README.md file in the upper level 'examples' directory for more information about examples.) +This example will shows how to set up and communicate over a websocket. + +## How to Use Example + +### Hardware Required + +This example can be executed on any ESP32 board, the only required interface is WiFi and connection to internet or a local server. + +### Configure the project + +* Open the project configuration menu (`idf.py menuconfig`) +* Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. +* Configure the websocket endpoint URI under "Example Configuration", if "WEBSOCKET_URI_FROM_STDIN" is selected then the example application will connect to the URI it reads from stdin (used for testing) + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (482) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE +I (2492) example_connect: Ethernet Link Up +I (4472) tcpip_adapter: eth ip: 192.168.2.137, mask: 255.255.255.0, gw: 192.168.2.2 +I (4472) example_connect: Connected to Ethernet +I (4472) example_connect: IPv4 address: 192.168.2.137 +I (4472) example_connect: IPv6 address: fe80:0000:0000:0000:bedd:c2ff:fed4:a92b +I (4482) WEBSOCKET: Connecting to ws://echo.websocket.org... +I (5012) WEBSOCKET: WEBSOCKET_EVENT_CONNECTED +I (5492) WEBSOCKET: Sending hello 0000 +I (6052) WEBSOCKET: WEBSOCKET_EVENT_DATA +W (6052) WEBSOCKET: Received=hello 0000 + +I (6492) WEBSOCKET: Sending hello 0001 +I (7052) WEBSOCKET: WEBSOCKET_EVENT_DATA +W (7052) WEBSOCKET: Received=hello 0001 + +I (7492) WEBSOCKET: Sending hello 0002 +I (8082) WEBSOCKET: WEBSOCKET_EVENT_DATA +W (8082) WEBSOCKET: Received=hello 0002 + +I (8492) WEBSOCKET: Sending hello 0003 +I (9152) WEBSOCKET: WEBSOCKET_EVENT_DATA +W (9162) WEBSOCKET: Received=hello 0003 + +``` + diff --git a/examples/protocols/websocket/example_test.py b/examples/protocols/websocket/example_test.py index a91b4ab6c..37c2ec339 100644 --- a/examples/protocols/websocket/example_test.py +++ b/examples/protocols/websocket/example_test.py @@ -1,15 +1,158 @@ +from __future__ import print_function +from __future__ import unicode_literals import re import os +import socket +import hashlib +import base64 +from threading import Thread import ttfw_idf -@ttfw_idf.idf_example_test(env_tag="Example_WIFI", ignore=True) +def get_my_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + s.connect(('10.255.255.255', 1)) + IP = s.getsockname()[0] + except Exception: + IP = '127.0.0.1' + finally: + s.close() + return IP + + +# Simple Websocket server for testing purposes +class Websocket: + HEADER_LEN = 6 + + def __init__(self, port): + self.port = port + self.socket = socket.socket() + self.socket.settimeout(10.0) + + def __enter__(self): + try: + self.socket.bind(('', self.port)) + except socket.error as e: + print("Bind failed:{}".format(e)) + raise + + self.socket.listen(1) + self.server_thread = Thread(target=self.run_server) + self.server_thread.start() + + def __exit__(self, exc_type, exc_value, traceback): + self.server_thread.join() + self.socket.close() + self.conn.close() + + def run_server(self): + self.conn, address = self.socket.accept() # accept new connection + self.conn.settimeout(10.0) + print("Connection from: {}".format(address)) + + self.establish_connection() + + # Echo data until client closes connection + self.echo_data() + + def establish_connection(self): + while True: + try: + # receive data stream. it won't accept data packet greater than 1024 bytes + data = self.conn.recv(1024).decode() + if not data: + # exit if data is not received + raise + + if "Upgrade: websocket" in data and "Connection: Upgrade" in data: + self.handshake(data) + return + except socket.error as err: + print("Unable to establish a websocket connection: {}, {}".format(err)) + raise + + def handshake(self, data): + # Magic string from RFC + MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + headers = data.split("\r\n") + + for header in headers: + if "Sec-WebSocket-Key" in header: + client_key = header.split()[1] + + if client_key: + resp_key = client_key + MAGIC_STRING + resp_key = base64.standard_b64encode(hashlib.sha1(resp_key.encode()).digest()) + + resp = "HTTP/1.1 101 Switching Protocols\r\n" + \ + "Upgrade: websocket\r\n" + \ + "Connection: Upgrade\r\n" + \ + "Sec-WebSocket-Accept: {}\r\n\r\n".format(resp_key.decode()) + + self.conn.send(resp.encode()) + + def echo_data(self): + while(True): + try: + header = bytearray(self.conn.recv(self.HEADER_LEN, socket.MSG_WAITALL)) + if not header: + # exit if data is not received + return + + # Remove mask bit + payload_len = ~(1 << 7) & header[1] + + payload = bytearray(self.conn.recv(payload_len, socket.MSG_WAITALL)) + frame = header + payload + + decoded_payload = self.decode_frame(frame) + + echo_frame = self.encode_frame(decoded_payload) + self.conn.send(echo_frame) + except socket.error as err: + print("Stopped echoing data: {}".format(err)) + + def decode_frame(self, frame): + # Mask out MASK bit from payload length, this len is only valid for short messages (<126) + payload_len = ~(1 << 7) & frame[1] + + mask = frame[2:self.HEADER_LEN] + + encrypted_payload = frame[self.HEADER_LEN:self.HEADER_LEN + payload_len] + payload = bytearray() + + for i in range(payload_len): + payload.append(encrypted_payload[i] ^ mask[i % 4]) + + return payload + + def encode_frame(self, payload): + # Set FIN = 1 and OP_CODE = 1 (text) + header = (1 << 7) | (1 << 0) + + frame = bytearray(header) + frame.append(len(payload)) + frame += payload + + return frame + + +def test_echo(dut): + dut.expect("WEBSOCKET_EVENT_CONNECTED") + for i in range(0, 10): + dut.expect(re.compile(r"Received=hello (\d)")) + dut.expect("Websocket Stopped") + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") def test_examples_protocol_websocket(env, extra_data): """ - steps: | + steps: 1. join AP - 2. connect to ws://echo.websocket.org + 2. connect to uri specified in the config 3. send and receive data """ dut1 = env.get_dut("websocket", "examples/protocols/websocket") @@ -18,15 +161,33 @@ def test_examples_protocol_websocket(env, extra_data): bin_size = os.path.getsize(binary_file) ttfw_idf.log_performance("websocket_bin_size", "{}KB".format(bin_size // 1024)) ttfw_idf.check_performance("websocket_bin_size", bin_size // 1024) + + try: + if "CONFIG_WEBSOCKET_URI_FROM_STDIN" in dut1.app.get_sdkconfig(): + uri_from_stdin = True + else: + uri = dut1.app.get_sdkconfig()["CONFIG_WEBSOCKET_URI"].strip('"') + uri_from_stdin = False + + except Exception: + print('ENV_TEST_FAILURE: Cannot find uri settings in sdkconfig') + raise + # start test dut1.start_app() - dut1.expect("Waiting for wifi ...") - dut1.expect("Connection established...", timeout=30) - dut1.expect("WEBSOCKET_EVENT_CONNECTED") - for i in range(0, 10): - dut1.expect(re.compile(r"Sending hello (\d)")) - dut1.expect(re.compile(r"Received=hello (\d)")) - dut1.expect("Websocket Stopped") + + if uri_from_stdin: + server_port = 4455 + with Websocket(server_port): + uri = "ws://{}:{}".format(get_my_ip(), server_port) + print("DUT connecting to {}".format(uri)) + dut1.expect("Please enter uri of websocket endpoint", timeout=30) + dut1.write(uri) + test_echo(dut1) + + else: + print("DUT connecting to {}".format(uri)) + test_echo(dut1) if __name__ == '__main__': diff --git a/examples/protocols/websocket/main/Kconfig.projbuild b/examples/protocols/websocket/main/Kconfig.projbuild index 7790aa248..0613b9033 100644 --- a/examples/protocols/websocket/main/Kconfig.projbuild +++ b/examples/protocols/websocket/main/Kconfig.projbuild @@ -1,7 +1,21 @@ menu "Example Configuration" + choice WEBSOCKET_URI_SOURCE + prompt "Websocket URI source" + default WEBSOCKET_URI_FROM_STRING + help + Selects the source of the URI used in the example. + + config WEBSOCKET_URI_FROM_STRING + bool "From string" + + config WEBSOCKET_URI_FROM_STDIN + bool "From stdin" + endchoice + config WEBSOCKET_URI string "Websocket endpoint URI" + depends on WEBSOCKET_URI_FROM_STRING default "ws://echo.websocket.org" help URL of websocket endpoint this example connects to and sends echo diff --git a/examples/protocols/websocket/main/websocket_example.c b/examples/protocols/websocket/main/websocket_example.c index 8cd756150..73f69c743 100644 --- a/examples/protocols/websocket/main/websocket_example.c +++ b/examples/protocols/websocket/main/websocket_example.c @@ -26,23 +26,36 @@ #include "esp_event_loop.h" static const char *TAG = "WEBSOCKET"; -static const char *WEBSOCKET_ECHO_ENDPOINT = CONFIG_WEBSOCKET_URI; +#if CONFIG_WEBSOCKET_URI_FROM_STDIN +static void get_string(char *line, size_t size) +{ + int count = 0; + while (count < size) { + int c = fgetc(stdin); + if (c == '\n') { + line[count] = '\0'; + break; + } else if (c > 0 && c < 127) { + line[count] = c; + ++count; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */ static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - // esp_websocket_client_handle_t client = (esp_websocket_client_handle_t)handler_args; esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; switch (event_id) { case WEBSOCKET_EVENT_CONNECTED: ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); - - break; case WEBSOCKET_EVENT_DISCONNECTED: ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED"); break; - case WEBSOCKET_EVENT_DATA: ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA"); ESP_LOGI(TAG, "Received opcode=%d", data->op_code); @@ -56,11 +69,23 @@ static void websocket_event_handler(void *handler_args, esp_event_base_t base, i static void websocket_app_start(void) { - ESP_LOGI(TAG, "Connectiong to %s...", WEBSOCKET_ECHO_ENDPOINT); + esp_websocket_client_config_t websocket_cfg = {}; - const esp_websocket_client_config_t websocket_cfg = { - .uri = WEBSOCKET_ECHO_ENDPOINT, // or wss://echo.websocket.org for websocket secure - }; + #if CONFIG_WEBSOCKET_URI_FROM_STDIN + char line[128]; + + ESP_LOGI(TAG, "Please enter uri of websocket endpoint"); + get_string(line, sizeof(line)); + + websocket_cfg.uri = line; + ESP_LOGI(TAG, "Endpoint uri: %s\n", line); + + #else + websocket_cfg.uri = CONFIG_WEBSOCKET_URI; + + #endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */ + + ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri); esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg); esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client); @@ -76,6 +101,8 @@ static void websocket_app_start(void) } vTaskDelay(1000 / portTICK_RATE_MS); } + // Give server some time to respond before closing + vTaskDelay(3000 / portTICK_RATE_MS); esp_websocket_client_stop(client); ESP_LOGI(TAG, "Websocket Stopped"); esp_websocket_client_destroy(client); diff --git a/examples/protocols/websocket/sdkconfig.ci b/examples/protocols/websocket/sdkconfig.ci new file mode 100644 index 000000000..a0b7712a2 --- /dev/null +++ b/examples/protocols/websocket/sdkconfig.ci @@ -0,0 +1,3 @@ +CONFIG_WEBSOCKET_URI_FROM_STDIN=y +CONFIG_WEBSOCKET_URI_FROM_STRING=n +