Pārlūkot izejas kodu

Merge branch 'feature/ws_local_server_test_v4.0' into 'release/v4.0'

websocket_client: added example_test with a local websocket server (backport v4.0)

See merge request espressif/esp-idf!7200
Angus Gratton 6 gadi atpakaļ
vecāks
revīzija
3f8a89504f

+ 57 - 0
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
+
+```
+

+ 171 - 10
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__':

+ 14 - 0
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

+ 36 - 9
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 = {};
+
+    #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 */
 
-    const esp_websocket_client_config_t websocket_cfg = {
-        .uri = WEBSOCKET_ECHO_ENDPOINT, // or wss://echo.websocket.org for websocket secure
-    };
+    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);

+ 3 - 0
examples/protocols/websocket/sdkconfig.ci

@@ -0,0 +1,3 @@
+CONFIG_WEBSOCKET_URI_FROM_STDIN=y
+CONFIG_WEBSOCKET_URI_FROM_STRING=n
+