Skip to content

feat(lws): Add lws server example #779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/lws_server_build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: "lws: build-tests-server"

on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened, labeled]

jobs:
build_lws_server:
if: contains(github.event.pull_request.labels.*.name, 'lws') || github.event_name == 'push'
name: Libwebsockets server build
strategy:
matrix:
idf_ver: ["latest", "release-v5.3", "release-v5.4"]
test: [ { app: example, path: "examples/server-echo" }]
runs-on: ubuntu-22.04
container: espressif/idf:${{ matrix.idf_ver }}
env:
TEST_DIR: components/libwebsockets/${{ matrix.test.path }}
steps:
- name: Checkout esp-protocols
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }}
shell: bash
run: |
. ${IDF_PATH}/export.sh
python -m pip install idf-build-apps
python ./ci/build_apps.py ${TEST_DIR}
cd ${TEST_DIR}
for dir in `ls -d build_esp32_*`; do
$GITHUB_WORKSPACE/ci/clean_build_artifacts.sh `pwd`/$dir
zip -qur artifacts.zip $dir
done
- uses: actions/upload-artifact@v4
with:
name: lws_target_esp32_${{ matrix.idf_ver }}_${{ matrix.test.app }}
path: ${{ env.TEST_DIR }}/artifacts.zip
if-no-files-found: error

run-target-lws-server:
if: |
github.repository == 'espressif/esp-protocols' &&
( contains(github.event.pull_request.labels.*.name, 'lws') || github.event_name == 'push' )
name: Target server test
needs: build_lws_server
strategy:
fail-fast: false
matrix:
idf_ver: ["latest", "release-v5.3", "release-v5.4"]
idf_target: ["esp32"]
test: [ { app: example, path: "examples/server-echo" }]
runs-on:
- self-hosted
- ESP32-ETHERNET-KIT
env:
TEST_DIR: components/libwebsockets/${{ matrix.test.path }}

steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: lws_target_esp32_${{ matrix.idf_ver }}_${{ matrix.test.app }}
path: ${{ env.TEST_DIR }}/ci/
- name: Install Python packages
env:
PIP_EXTRA_INDEX_URL: "https://www.piwheels.org/simple"
run: |
pip install --only-binary cryptography --extra-index-url https://dl.espressif.com/pypi/ -r $GITHUB_WORKSPACE/ci/requirements.txt websocket-client
- name: Run Example Test on target
working-directory: ${{ env.TEST_DIR }}
run: |
unzip ci/artifacts.zip -d ci
for dir in `ls -d ci/build_*`; do
rm -rf build sdkconfig.defaults
mv $dir build
python -m pytest --log-cli-level DEBUG --junit-xml=./results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=${{ matrix.idf_target }}
done
- uses: actions/upload-artifact@v4
if: always()
with:
name: results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}.xml
path: components/libwebsockets/${{ matrix.test.path }}/*.xml
7 changes: 7 additions & 0 deletions components/libwebsockets/examples/server-echo/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 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.16)
set(requirements 1)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(server_echo_example)
66 changes: 66 additions & 0 deletions components/libwebsockets/examples/server-echo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Websocket LWS server example

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.

### Server Certificate Verification


### Generating a self signed Certificates with OpenSSL


### 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


## Python Flask echo server

By default, the `ws://echo.websocket.events` endpoint is used. You can setup a Python websocket echo server locally and try the `ws://<your-ip>:5000` endpoint. To do this, install Flask-sock Python package

```
pip install flask-sock
```

and start a Flask websocket echo server locally by executing the following Python code:

```python
from flask import Flask
from flask_sock import Sock

app = Flask(__name__)
sock = Sock(app)


@sock.route('/')
def echo(ws):
while True:
data = ws.receive()
ws.send(data)


if __name__ == '__main__':
# To run your Flask + WebSocket server in production you can use Gunicorn:
# gunicorn -b 0.0.0.0:5000 --workers 4 --threads 100 module:app
app.run(host="0.0.0.0", debug=True)
```
12 changes: 12 additions & 0 deletions components/libwebsockets/examples/server-echo/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
set(SRC_FILES "lws-server-echo.c") # Initialize SRC_FILES as an empty list
set(INCLUDE_DIRS ".") # Define include directories
set(EMBED_FILES) # Initialize EMBED_FILES as an empty list

list(APPEND EMBED_FILES
"certs/server_cert.pem"
"certs/ca_cert.pem"
"certs/server_key.pem")

idf_component_register(SRCS "${SRC_FILES}"
INCLUDE_DIRS "${INCLUDE_DIRS}"
EMBED_TXTFILES "${EMBED_FILES}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
menu "Example Configuration"
config WEBSOCKET_PORT
int "Websocket endpoint PORT"
default 80
help
Port of websocket endpoint this example connects to and sends echo
config WS_OVER_TLS
bool "Enable WebSocket over TLS with Server Certificate"
default y
help
Enables WebSocket connections over TLS (WSS)
endmenu
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUL04QhbSEt5oNbV4f7CeLLqTCw2gwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA2MjVaFw0zNDAy
MjAwODA2MjVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDjc78SuXAmJeBc0el2/m+2lwtk3J/VrNxHYkhjHa8K
/ybU89VvKGuv9+L3IP67WMguFTaMgivJYUePjfMchtNJLJ+4cR9BkBKH4JnyXDae
s0a5181LxRo8rqcaOw9hmJTgt9R4dIRTR3GN2/VLhlR+L9OTYA54RUtMyMMpyk5M
YIJbcOwiwkVLsIYnexXDfgz9vQGl/2vBQ/RBtDBvbSyBiWox9SuzOrya1HUBzJkM
Iu5L0bSa0LAeXHT3i3P1Y4WPt9ub70OhUNfJtHC+XbGFSEkkQG+lfbXU75XLoMWa
iATMREOcb3Mq+pn1G8o1ZHVc6lBHUkfrNfxs5P/GQcSvAgMBAAGjUzBRMB0GA1Ud
DgQWBBQGkdK2gR2HrQTnZnbuWO7I1+wdxDAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn
ZnbuWO7I1+wdxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx
G0hFtMwV/agIwC3ZaYC36ZWiijFzWkJSZG+fqAy32mSoVL2uQvOT8vEfF0ZnAcPc
JI4oI059dBhAVlwqv6uLHyD4Gf2bF4oSLljdTz3X23llF+/wrTC2LLqMrm09aUC0
ac74Q0FVwVJJcqH1HgemCMVjna5MkwNA6B+q7uR3eQ692VqXk6vjd4fRLBg1bBO1
hXjasfNxA8A9quORF5+rjYrwyUZHuzcs0FfSClckIt4tHKtt4moLufOW6/PM4fRe
AgdDfiTupxYLJFz4hFPhfgCh4TjQ+f9+uP4IAjW42dJmTVZjLEku/hm5lxCFObAq
RgfaNwH8Ug1r1xswjSZG
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWjCCAkKgAwIBAgIUUPCOgMA2v09E29fCkogx3RUBRtEwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA3MzFaFw0zNDAy
MjAwODA3MzFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCrNeomxI2aoP+4iUy5SiA+41oHUDZDFeJOBjv5JCsK
mlvFqxE9zynmPOVpuABErOJwzerPTa4NYKvuvs5CxVJUV5CXtWANuu9majioZNzj
f877MDNX/GnZHK2gnkxVrZCPaDmx9yiMsFMXgmfdrDhwoUpXbdgSyeU/al9Ds2kF
0hrHOH2LBWt/mVeLbONU5CC1HOdVVw+uRlhVlxnfhTPd/Nru3rJx7R0sN7qXcZpJ
PL87WvrszLVOux24DeaOz9oiD2b7egFyUuq1BM25iCwi8s/Ths8xd0Ca1d8mEcHW
FVd4w2+nUMXFE+IbP+wo6FXuiSaOBNri3rztpvCCMaWjAgMBAAGjQjBAMB0GA1Ud
DgQWBBSOlA+9Vfbcfy8iS4HSd4V0KPtm4jAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn
ZnbuWO7I1+wdxDANBgkqhkiG9w0BAQsFAAOCAQEAOmzm/MwowKTrSpMSrmfA3MmW
ULzsfa25WyAoTl90ATlg4653Y7pRaNfdvVvyi2V2LlPcmc7E0rfD53t1NxjDH1uM
LgFMTNEaZ9nMRSW0kMiwaRpvmXS8Eb9PXfvIM/Mw0co/aMOtAQnfTGIqsgkQwKyk
1GG7QKQq3p4QGu5ZaTnjnaoa79hODt+0xQDD1wp6C9xwBY0M4gndAi3wkOeFkGv+
OmGPtaCBu5V9tJCZ9dfZvjkaK44NGwDw0urAcYRK2h7asnlflu7cnlGMBB0qY4kQ
BX5WI8UjN6rECBHbtNRvEh06ogDdHbxYV+TibrqkkeDRw6HX1qqiEJ+iCgWEDQ==
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrNeomxI2aoP+4
iUy5SiA+41oHUDZDFeJOBjv5JCsKmlvFqxE9zynmPOVpuABErOJwzerPTa4NYKvu
vs5CxVJUV5CXtWANuu9majioZNzjf877MDNX/GnZHK2gnkxVrZCPaDmx9yiMsFMX
gmfdrDhwoUpXbdgSyeU/al9Ds2kF0hrHOH2LBWt/mVeLbONU5CC1HOdVVw+uRlhV
lxnfhTPd/Nru3rJx7R0sN7qXcZpJPL87WvrszLVOux24DeaOz9oiD2b7egFyUuq1
BM25iCwi8s/Ths8xd0Ca1d8mEcHWFVd4w2+nUMXFE+IbP+wo6FXuiSaOBNri3rzt
pvCCMaWjAgMBAAECggEAOTWjz16AXroLmRMv8v5E9h6sN6Ni7lnCrAXDRoYCZ+Ga
Ztu5wCiYPJn+oqvcUxZd+Ammu6yeS1QRP468h20+DHbSFw+BUDU1x8gYtJQ3h0Fu
3VqG3ZC3odfGYNRkd4CuvGy8Uq5e+1vz9/gYUuc4WNJccAiBWg3ir6UQviOWJV46
LGfdEd9hVvIGl5pmArMBVYdpj9+JHunDtG4uQxiWla5pdLjlkC2mGexD18T9d718
6I+o3YHv1Y9RPT1d4rNhYQWx6YdTTD2rmS7nTrzroj/4fXsblpXzR+/l7crlNERY
67RMPwgDR1NiAbCAJKsSbMS66lRCNlhTM4YffGAN6QKBgQDkIdcNm9j49SK5Wbl5
j8U6UbcVYPzPG+2ea+fDfUIafA0VQHIuX6FgA17Kp7BDX9ldKtSBpr0Z8vetVswr
agmXVMR/7QdvnZ9NpL66YA/BRs67CvsryVu4AVAzThFGySmlcXGlPq47doWDQ3B9
0BOEnVoeDXR3SabaNsEbhDYn1wKBgQDAIAUyhJcgz+LcgaAtBwdnEN57y66JlRVZ
bsb6cEG/MNmnLjQYsplJjNbz4yrB5ukTChPTGRF/JQRqHoXh6DGQFHvobukwwA6x
RAIIq0NLJ5HUipfOi+VpCbWUHdoUNhwjAB2qVtD4LXE2Lyn46C8ET5eRtRjUKpzV
lpsq63KHFQKBgFB+cDbpCoGtXPcxZXQy+lA9jPAKLKmXHRyMzlX32F8n7iXVe3RJ
YdNS3Rt8V4EuTK/G8PxeLNL/G80ZlyiqXX/79Ol+ZOVJJHBs9K8mPejgZwkwMrec
cLRYIkg3/3iOehdaE9NOboOkqi9KmGKMDJb6PlXkQXflkO3l6/UdjU45AoGAen0v
sxiTncjMU1eVfn+nuY8ouXaPbYoOFXmqBItDb5i+e3baohBj6F+Rv+ZKIVuNp6Ta
JNErtYstOFcDdpbp2nkk0ni71WftNhkszsgZ3DV7JS3DQV0xwvj8ulUZ757b63is
cShujHu0XR5OvTGSoEX6VVxHWyVb3lTp0sBPwU0CgYBe2Ieuya0X8mAbputFN64S
Kv++dqktTUT8i+tp07sIrpDeYwO3D89x9kVSJj4ImlmhiBVGkxFWPkpGyBLotdse
Ai/E6f5I7CDSZZC0ZucgcItNd4Yy459QY+dFwFtT3kIaD9ml8fnqQ83J9W8DWtv9
6mY9FnUUufbJcpHxN58RTw==
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWjCCAkKgAwIBAgIUUPCOgMA2v09E29fCkogx3RUBRtAwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjMwODA2NTlaFw0zNDAy
MjAwODA2NTlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC8WWbDxnLzTSfuQaO+kQnnzbwjhUHWn58s+BIEaO8M
GG6bX+8r/SH9XjMfFS36qAN3qxgRun3YoRTaHc2QByiGjf5IL4EAPDnLN+NzUIL5
7Gi2QPQP/GksAsOGKWk/nMRPk1vcMptkFVIWSp474SQ0A92Z9z0dUIqBpjRa34kr
HsAIcT59/EG7YBBadMk0fQIxQVLh3Vosky85q+0waFihe47Ef5U2UftexoUx4Vcz
6EtP60Wx+4qN+FLsr+n2B7Oz2ITqfwgqLzjNLZwm9bMjcLZ0fWm1A/W1C989MXwI
w6DAPEZv7pbgp8r9phyrNieSDuuRaCvFsaXh6troAjLxAgMBAAGjQjBAMB0GA1Ud
DgQWBBRJCYAQG2+1FN5P/wyAR1AsrAyb4DAfBgNVHSMEGDAWgBQGkdK2gR2HrQTn
ZnbuWO7I1+wdxDANBgkqhkiG9w0BAQsFAAOCAQEAmllul/GIH7RVq85mM/SxP47J
M7Z7T032KuR3n/Psyv2iq/uEV2CUje3XrKNwR2PaJL4Q6CtoWy7xgIP+9CBbjddR
M7sdNQab8P2crAUtBKnkNOl/na/5KnXnjwi/PmWJJ9i2Cqt0PPkaykTWp/MLfYIw
RPkY2Yo8f8gEiqXQd+0qTuMgumbgkPq3V8Lk1ocy62F5/qUhXxH+ifAXEoUQS6EG
8DlgwdZlfUY+jeM6N56WzYmxD1syjNW7faPio+qXINfpYatROhqphaMQ5SA6TRj6
jcnLa31TdDdWmWYDcYgZntAv6yGi3rh0MdYqeNS0FKlMKmaH81VHs7V1UUXwUQ==
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8WWbDxnLzTSfu
QaO+kQnnzbwjhUHWn58s+BIEaO8MGG6bX+8r/SH9XjMfFS36qAN3qxgRun3YoRTa
Hc2QByiGjf5IL4EAPDnLN+NzUIL57Gi2QPQP/GksAsOGKWk/nMRPk1vcMptkFVIW
Sp474SQ0A92Z9z0dUIqBpjRa34krHsAIcT59/EG7YBBadMk0fQIxQVLh3Vosky85
q+0waFihe47Ef5U2UftexoUx4Vcz6EtP60Wx+4qN+FLsr+n2B7Oz2ITqfwgqLzjN
LZwm9bMjcLZ0fWm1A/W1C989MXwIw6DAPEZv7pbgp8r9phyrNieSDuuRaCvFsaXh
6troAjLxAgMBAAECggEACNVCggTxCCMCr+RJKxs/NS1LWPkbZNbYjrHVmnpXV6Bf
s460t0HoUasUx6zlGp+9heOyvcYat8maIj6KkOodBu5q0fTUXm/0n+ivlI1ejxz8
ritupr9GKWe5xrVzd6XA+SBmivWenvt2/Y+jSxica4oQ3vMe3RyVWk4yn15jXu+9
7B9lNyNeZtOBr6OozHGLYw4dwWcBNv2S6wevRKfHPwn/Ch5yTH1uAskgoMxUuyK2
ynNVHWUhyS4pFU7Tex5ENDel15VYdbxV/2lQ2W6fHMLtC5GWKJXXbigCX7pfOpzC
BFJEfZl7ze/qptE9AR7DkLFYyMtrS7OlebYbLDOM9wKBgQD+rTdwULZibpKwlI3a
9Y22d4N/EDFvuu8LnuEiVQnXgwg9M+tlaa2liP18j1a7y/FCfoXf5sjUWCsdYR6d
C0TuiOGI59hYGI94NvVLAmOutR+vJ/3jhbv5wyqEQLhJ42Yz9kWBrDCI+V3q3TdO
H7wcH6suUIZpeLEJF4qHzY/1dwKBgQC9U/Pvswiww8sfysmd5shUNo4ofAZnTM1A
ak6pWE3lSyiOkSm+3B2GqxYWLRoo1v+pTyhhXDtRRmxGtMNrKCsmlHef/o3c6kkG
cuC2h/DiSmoITHy3BYKJoDeE54E8ubXUUKqHo41LYUs+D7M/IGxeiO13MUoIrEtF
AwzVWPBU1wKBgH8barD2x6Bm+XWCHy6qIZlxGsMfDN1r2gTdvhWJhcj3D/Sj5heO
X+lfbsxtKee+yOHcDesK3y8D9jjKkSHmTvgSfyX6OML3NxvTqidOwPugUHj2J8QX
qhLk8mJhftj50reacWRf0TV76ADhecnXEuaic6hA7mTTpOAZzL0svm3PAoGBALWF
r6VLX3KzVqZVtLb7FWmAoQ35093pCgXPpznAW3cTd4Axd/fxbTG4CUYb2i/760X2
ij3Gw2yqe5fTKmYsLisgQA2bb4K28msHa6I2dmNQe5cXVp/X3Y98mJ6JpCSH3ekB
qm7ABfGXCCApx28n9B8zY5JbJKNqJgS15vELA+ojAoGAAkaV2w46+3iQ6gJtQepr
zGNybiYBx/Wo5fDdTS5u0xN+ZdC9fl2Zs0n7sMmUT8bWdDLcMnntHHO+oDIKyRHs
TQh1n68vQ4JoegQv3Z9Z/TLEKqr9gyJC1Ao6M4bZpPhUWQwupfHColtsr2TskcnJ
Nf2FpJZ7z6fQEShGlK1yTXM=
-----END PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies:
espressif/libwebsockets:
version: "*"
override_path: "../../../"
protocol_examples_common:
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
301 changes: 301 additions & 0 deletions components/libwebsockets/examples/server-echo/main/lws-server-echo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* ESP libwebsockets 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 <libwebsockets.h>
#include <stdio.h>

#include "esp_wifi.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "esp_log.h"
#include "esp_netif.h"
#include "esp_netif_ip_addr.h"

#define RING_DEPTH 4096
#define LWS_MAX_PAYLOAD 1024

static int callback_minimal_server_echo(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
/* one of these created for each message */
struct msg {
void *payload; /* is malloc'd */
size_t len;
};

/* one of these is created for each client connecting to us */

struct per_session_data__minimal {
struct per_session_data__minimal *pss_list;
struct lws *wsi;
int last; /* the last message number we sent */
unsigned char buffer[RING_DEPTH];
size_t buffer_len;
int is_receiving_fragments;
int is_ready_to_send;
};

/* one of these is created for each vhost our protocol is used with */

struct per_vhost_data__minimal {
struct lws_context *context;
struct lws_vhost *vhost;
const struct lws_protocols *protocol;

struct per_session_data__minimal *pss_list; /* linked-list of live pss*/

struct msg amsg; /* the one pending message... */
int current; /* the current message number we are caching */
};

static struct lws_protocols protocols[] = {
{
.name = "lws-minimal-server-echo",
.callback = callback_minimal_server_echo,
.per_session_data_size = sizeof(struct per_session_data__minimal),
.rx_buffer_size = RING_DEPTH,
.id = 0,
.user = NULL,
.tx_packet_size = RING_DEPTH
},
LWS_PROTOCOL_LIST_TERM
};

static int options;
static const char *TAG = "lws-server-echo", *iface = "";

/* pass pointers to shared vars to the protocol */
static const struct lws_protocol_vhost_options pvo_options = {
NULL,
NULL,
"options", /* pvo name */
(void *) &options /* pvo value */
};

static const struct lws_protocol_vhost_options pvo_interrupted = {
&pvo_options,
NULL,
"interrupted", /* pvo name */
NULL /* pvo value */
};

static const struct lws_protocol_vhost_options pvo = {
NULL, /* "next" pvo linked-list */
&pvo_interrupted, /* "child" pvo linked-list */
"lws-minimal-server-echo", /* protocol name we belong to on this vhost */
"" /* ignored */
};

int app_main(int argc, const char **argv)
{
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);

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());

/* Create LWS Context - Server. */
struct lws_context_creation_info info;
struct lws_context *context;
int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;

lws_set_log_level(logs, NULL);
ESP_LOGI(TAG, "LWS minimal ws server echo\n");

memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */
info.port = CONFIG_WEBSOCKET_PORT;
info.iface = iface;
info.protocols = protocols;
info.pvo = &pvo;
info.pt_serv_buf_size = 64 * 1024;

#ifdef CONFIG_WS_OVER_TLS
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;

/* Configuring server certificates for mutual authentification */
extern const char cert_start[] asm("_binary_server_cert_pem_start"); // Server certificate
extern const char cert_end[] asm("_binary_server_cert_pem_end");
extern const char key_start[] asm("_binary_server_key_pem_start"); // Server private key
extern const char key_end[] asm("_binary_server_key_pem_end");
extern const char cacert_start[] asm("_binary_ca_cert_pem_start"); // CA certificate
extern const char cacert_end[] asm("_binary_ca_cert_pem_end");

info.server_ssl_cert_mem = cert_start;
info.server_ssl_cert_mem_len = cert_end - cert_start - 1;
info.server_ssl_private_key_mem = key_start;
info.server_ssl_private_key_mem_len = key_end - key_start - 1;
info.server_ssl_ca_mem = cacert_start;
info.server_ssl_ca_mem_len = cacert_end - cacert_start;
#endif

context = lws_create_context(&info);
if (!context) {
ESP_LOGE(TAG, "lws init failed\n");
return 1;
}

while (n >= 0) {
n = lws_service(context, 100);
}

lws_context_destroy(context);

return 0;
}

static int callback_minimal_server_echo(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct per_session_data__minimal *pss = (struct per_session_data__minimal *)user;
struct per_vhost_data__minimal *vhd = (struct per_vhost_data__minimal *)
lws_protocol_vh_priv_get(lws_get_vhost(wsi), lws_get_protocol(wsi));
char client_address[128];

switch (reason) {
case LWS_CALLBACK_PROTOCOL_INIT:
vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
lws_get_protocol(wsi),
sizeof(struct per_vhost_data__minimal));
if (!vhd) {
ESP_LOGE("LWS_SERVER", "Failed to allocate vhost data.");
return -1;
}
vhd->context = lws_get_context(wsi);
vhd->protocol = lws_get_protocol(wsi);
vhd->vhost = lws_get_vhost(wsi);
vhd->current = 0;
vhd->amsg.payload = NULL;
vhd->amsg.len = 0;
break;

case LWS_CALLBACK_ESTABLISHED:
lws_get_peer_simple(wsi, client_address, sizeof(client_address));
ESP_LOGI("LWS_SERVER", "New client connected: %s", client_address);
lws_ll_fwd_insert(pss, pss_list, vhd->pss_list);
pss->wsi = wsi;
pss->last = vhd->current;
pss->buffer_len = 0;
pss->is_receiving_fragments = 0;
pss->is_ready_to_send = 0;
memset(pss->buffer, 0, RING_DEPTH);
break;

case LWS_CALLBACK_CLOSED:
lws_get_peer_simple(wsi, client_address, sizeof(client_address));
ESP_LOGI("LWS_SERVER", "Client disconnected: %s", client_address);
lws_ll_fwd_remove(struct per_session_data__minimal, pss_list, pss, vhd->pss_list);
break;

case LWS_CALLBACK_RECEIVE:
lws_get_peer_simple(wsi, client_address, sizeof(client_address));

bool is_binary = lws_frame_is_binary(wsi); /* Identify if it is binary or text */

ESP_LOGI("LWS_SERVER", "%s fragment received from %s (%d bytes)",
is_binary ? "Binary" : "Text", client_address, (int)len);

if (lws_is_first_fragment(wsi)) { /* First fragment: reset the buffer */
pss->buffer_len = 0;
}

if (pss->buffer_len + len < RING_DEPTH) {
memcpy(pss->buffer + pss->buffer_len, in, len);
pss->buffer_len += len;
} else {
ESP_LOGE("LWS_SERVER", "Fragmented message exceeded buffer limit.");
return -1;
}

/* If it is the last part of the fragment, process the complete message */
if (lws_is_final_fragment(wsi)) {
ESP_LOGI("LWS_SERVER", "Complete %s message received from %s (%d bytes)",
is_binary ? "binary" : "text", client_address, (int)pss->buffer_len);

if (!is_binary) {
ESP_LOGI("LWS_SERVER", "Complete text message: %.*s", (int)pss->buffer_len, (char *)pss->buffer);
} else {
char hex_output[pss->buffer_len * 2 + 1]; /* Display the binary message as hexadecimal */
for (int i = 0; i < pss->buffer_len; i++) {
snprintf(&hex_output[i * 2], 3, "%02X", pss->buffer[i]);
}
ESP_LOGI("LWS_SERVER", "Complete binary message (hex): %s", hex_output);
}

/* Respond to the client */
int write_type = is_binary ? LWS_WRITE_BINARY : LWS_WRITE_TEXT;
int m = lws_write(wsi, (unsigned char *)pss->buffer, pss->buffer_len, write_type);
pss->buffer_len = 0;

if (m < (int)pss->buffer_len) {
ESP_LOGE("LWS_SERVER", "Failed to send %s message.", is_binary ? "binary" : "text");
return -1;
}
break;
}

/* If the message is not fragmented, process JSON, echo, and other messages */

/* JSON */
if (strstr((char *)in, "{") && strstr((char *)in, "}")) {
ESP_LOGI("LWS_SERVER", "JSON message received from %s: %.*s", client_address, (int)len, (char *)in);
int m = lws_write(wsi, (unsigned char *)in, len, LWS_WRITE_TEXT);
if (m < (int)len) {
ESP_LOGE("LWS_SERVER", "Failed to send JSON message.");
return -1;
}
break;
}

/* Echo */
if (!is_binary) {
ESP_LOGI("LWS_SERVER", "Text message received from %s (%d bytes): %.*s",
client_address, (int)len, (int)len, (char *)in);
int m = lws_write(wsi, (unsigned char *)in, len, LWS_WRITE_TEXT);
if (m < (int)len) {
ESP_LOGE("LWS_SERVER", "Failed to send text message.");
return -1;
}
break;
}

/* Bin */
ESP_LOGI("LWS_SERVER", "Binary message received from %s (%d bytes)", client_address, (int)len);
int m = lws_write(wsi, (unsigned char *)in, len, LWS_WRITE_BINARY);
if (m < (int)len) {
ESP_LOGE("LWS_SERVER", "Failed to send binary message.");
return -1;
}

ESP_LOGI("LWS_SERVER", "Message sent back to client.");
break;

default:
break;
}

return 0;
}
225 changes: 225 additions & 0 deletions components/libwebsockets/examples/server-echo/pytest_lws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import json
import random
import re
import ssl
import string
import time

import websocket


def get_esp32_ip(dut):
"""Retrieve ESP32 IP address from ESP-IDF logs."""
ip_regex = re.compile(r'IPv4 address:\s*(\d+\.\d+\.\d+\.\d+)')
timeout = time.time() + 60

while time.time() < timeout:
try:
match = dut.expect(ip_regex, timeout=5)
if match:
ip_address = match.group(1).decode()
print(f'ESP32 IP found: {ip_address}')
return ip_address
except Exception:
pass

print('Error: ESP32 IP not found in logs.')
raise RuntimeError('ESP32 IP not found.')


def wait_for_websocket_server(dut):
"""Waits for the ESP32 WebSocket server to be ready."""
server_ready_regex = re.compile(r'LWS minimal ws server echo')
timeout = time.time() + 20

while time.time() < timeout:
try:
dut.expect(server_ready_regex, timeout=5)
print('WebSocket server is up!')
return
except Exception:
pass

print('Error: WebSocket server did not start.')
raise RuntimeError('WebSocket server failed to start.')


def connect_websocket(uri, use_tls):
"""Attempts to connect to the WebSocket server with retries, handling TLS if enabled."""
for attempt in range(5):
try:
print(f'Attempting to connect to {uri} (Try {attempt + 1}/5)')

if use_tls:
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ssl_context.load_cert_chain(certfile='main/certs/client_cert.pem',
keyfile='main/certs/client_key.pem')

try:
ssl_context.load_verify_locations(cafile='main/certs/ca_cert.pem')
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.check_hostname = False
except Exception:
print('Warning: CA certificate not found. Skipping mutual authentication.')
ssl_context.verify_mode = ssl.CERT_NONE

ws = websocket.create_connection(uri, sslopt={'context': ssl_context}, timeout=10)

else:
ws = websocket.create_connection(uri, timeout=10)

print('WebSocket connected successfully!')
return ws
except Exception as e:
print(f'Connection failed: {e}')
time.sleep(3)

print('Failed to connect to WebSocket.')
raise RuntimeError('WebSocket connection failed.')


def send_fragmented_message(ws, message, is_binary=False, fragment_size=1024):
"""Sends a message in fragments."""
opcode = websocket.ABNF.OPCODE_BINARY if is_binary else websocket.ABNF.OPCODE_TEXT
total_length = len(message)

for i in range(0, total_length, fragment_size):
fragment = message[i: i + fragment_size]
ws.send(fragment, opcode=opcode)


def test_examples_protocol_websocket(dut):
"""Tests WebSocket communication."""

esp32_ip = get_esp32_ip(dut)
wait_for_websocket_server(dut)

# Retrieve WebSocket configuration from SDKCONFIG
try:
port = int(dut.app.sdkconfig['WEBSOCKET_PORT']) # Gets WebSocket port
use_tls = dut.app.sdkconfig.get('WS_OVER_TLS', False) # Gets TLS setting
except KeyError:
print('Error: WEBSOCKET_PORT or WS_OVER_TLS not found in sdkconfig.')
raise

protocol = 'wss' if use_tls else 'ws'
uri = f'{protocol}://{esp32_ip}:{port}'

print(f'\nWebSocket Configuration:\n - TLS: {use_tls}\n - Port: {port}\n - URI: {uri}\n')

ws = connect_websocket(uri, use_tls)

def test_echo():
"""Sends and verifies an echo message."""
ws.send('Hello ESP32!')
response = ws.recv()
assert response == 'Hello ESP32!', 'Echo response mismatch!'
print('Echo test passed!')

def test_send_receive_long_msg(msg_len=1024):
"""Sends and verifies a long text message."""
send_msg = ''.join(random.choices(string.ascii_letters + string.digits, k=msg_len))
print(f'Sending long message ({msg_len} bytes)...')
ws.send(send_msg)
response = ws.recv()
assert response == send_msg, 'Long message mismatch!'
print('Long message test passed!')

def test_send_receive_binary():
"""Sends and verifies binary data."""
expected_binary_data = bytearray([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
print('Sending binary data...')
ws.send(expected_binary_data, opcode=websocket.ABNF.OPCODE_BINARY)
received_data = ws.recv()
assert received_data == expected_binary_data, 'Binary data mismatch!'
print('Binary data test passed!')

def test_json():
"""Sends and verifies a JSON message."""
json_data = {'id': '1', 'name': 'test_user'}
json_string = json.dumps(json_data)
print('Sending JSON message...')
ws.send(json_string)
response = ws.recv()
received_json = json.loads(response)
assert received_json == json_data, 'JSON data mismatch!'
print('JSON test passed!')

def test_recv_fragmented_msg1():
"""Verifies reception of the first text fragment."""
print('Waiting for first fragment log...')
dut.expect(re.compile(r'I \(\d+\) LWS_SERVER: .* fragment received from .* \(1024 bytes\)'), timeout=20)
print('Fragmented message part 1 received correctly.')

def test_recv_fragmented_msg2():
"""Verifies reception of the second text fragment."""
print('Waiting for second fragment log...')
dut.expect(re.compile(r'I \(\d+\) LWS_SERVER: .* fragment received from .* \(.* bytes\)'), timeout=20)
print('Fragmented message part 2 received correctly.')

def test_fragmented_txt_msg():
"""Tests sending and receiving a fragmented text message."""
part1 = ''.join(random.choices(string.ascii_letters + string.digits, k=1024))
part2 = ''.join(random.choices(string.ascii_letters + string.digits, k=976))
message = part1 + part2

send_fragmented_message(ws, message, is_binary=False, fragment_size=1024)

print('Waiting for Complete text message log...')

escaped_message_start = re.escape(message[:30])
escaped_message_end = re.escape(message[-30:])

dut.expect(
re.compile(
rb"I \(\d+\) LWS_SERVER: Complete text message:.*?" + escaped_message_start.encode() + rb".*?" + escaped_message_end.encode(),
re.DOTALL
),
timeout=20
)

print('Fragmented text message received correctly.')

def test_fragmented_binary_msg():
"""Tests sending and receiving a fragmented binary message."""
expected_data = bytearray([0, 0, 0, 0, 0, 1, 1, 1, 1, 1] * 5)
send_fragmented_message(ws, expected_data, is_binary=True, fragment_size=10)

print('Waiting for Complete binary message log...')
dut.expect(re.compile(r'I \(\d+\) LWS_SERVER: Complete binary message \(hex\): '), timeout=10)
print('Fragmented binary message received correctly.')

def test_close():
"""Closes WebSocket connection and verifies closure."""
print('Closing WebSocket...')
ws.close()

try:
close_regex = re.compile(rb"websocket: Received closed message with code=(\d+)")
disconnect_regex = re.compile(rb"LWS_SERVER: Client disconnected")

match = dut.expect([close_regex, disconnect_regex], timeout=5)

if match == 0:
close_code = match.group(1).decode()
print(f'WebSocket closed successfully with code: {close_code}')
else:
print('WebSocket closed successfully (client disconnect detected).')

except Exception:
print('WebSocket close message not found.')
raise

test_echo()
test_send_receive_long_msg()
test_send_receive_binary()
test_json()
test_recv_fragmented_msg1()
test_recv_fragmented_msg2()
test_fragmented_txt_msg()
test_fragmented_binary_msg()
test_close()

ws.close()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_IDF_TARGET_LINUX=n
CONFIG_WEBSOCKET_PORT=433
CONFIG_EXAMPLE_CONNECT_ETHERNET=y
CONFIG_EXAMPLE_CONNECT_WIFI=n
CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_EXAMPLE_ETH_PHY_IP101=y
CONFIG_EXAMPLE_ETH_MDC_GPIO=23
CONFIG_EXAMPLE_ETH_MDIO_GPIO=18
CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5
CONFIG_EXAMPLE_ETH_PHY_ADDR=1
CONFIG_EXAMPLE_CONNECT_IPV6=y
CONFIG_WS_OVER_TLS=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8584
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_IDF_TARGET_LINUX=n
CONFIG_WEBSOCKET_PORT=8080
CONFIG_EXAMPLE_CONNECT_ETHERNET=y
CONFIG_EXAMPLE_CONNECT_WIFI=n
CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_EXAMPLE_ETH_PHY_IP101=y
CONFIG_EXAMPLE_ETH_MDC_GPIO=23
CONFIG_EXAMPLE_ETH_MDIO_GPIO=18
CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5
CONFIG_EXAMPLE_ETH_PHY_ADDR=1
CONFIG_EXAMPLE_CONNECT_IPV6=y
CONFIG_WS_OVER_TLS=n
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8584