diff --git a/.github/workflows/eppp__build.yml b/.github/workflows/eppp__build.yml new file mode 100644 index 0000000000..eb10692346 --- /dev/null +++ b/.github/workflows/eppp__build.yml @@ -0,0 +1,28 @@ +name: "eppp_link: build-tests" + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + build_eppp: + if: contains(github.event.pull_request.labels.*.name, 'eppp') || github.event_name == 'push' + name: Build + strategy: + matrix: + idf_ver: ["latest"] + test: [ { app: host, path: "examples/host" }, { app: slave, path: "examples/slave" }, { app: test_app, path: "test/test_app" }] + runs-on: ubuntu-20.04 + container: espressif/idf:${{ matrix.idf_ver }} + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v3 + - name: Build ${{ matrix.test.app }} with IDF-${{ matrix.idf_ver }} + shell: bash + run: | + ${IDF_PATH}/install.sh --enable-pytest + . ${IDF_PATH}/export.sh + python ./ci/build_apps.py ./components/eppp_link/${{matrix.test.path}} -vv --preserve-all diff --git a/.github/workflows/publish-docs-component.yml b/.github/workflows/publish-docs-component.yml index 31a1ddcf6f..d00911690a 100644 --- a/.github/workflows/publish-docs-component.yml +++ b/.github/workflows/publish-docs-component.yml @@ -16,8 +16,6 @@ jobs: publish: name: Publish Tag, Release, Docs, Component runs-on: ubuntu-latest - # Skip running on forks since it won't have access to secrets - if: github.repository == 'espressif/esp-protocols' steps: - name: Checkout esp-protocols uses: actions/checkout@v3 @@ -61,29 +59,6 @@ jobs: fi fi done - - name: Deploying generated docs - shell: bash - run: | - source $GITHUB_WORKSPACE/docs/utils.sh - add_doc_server_ssh_keys $DOCS_DEPLOY_KEY $DOCS_DEPLOY_SERVER $DOCS_DEPLOY_SERVER_USER - export GIT_VER=$(git describe --always) - export GITHUB_REF_NAME=latest - for comp in `ls components`; do - if [[ -d $GITHUB_WORKSPACE/docs/${comp} ]]; then - echo "Deploying latest of ${comp}" - export DOCS_BUILD_DIR=$GITHUB_WORKSPACE/docs/${comp} - export DOCS_DEPLOY_PATH=$DOCS_DEPLOY_PATH_ORIG/${comp} - cd $GITHUB_WORKSPACE/docs/${comp} - deploy-docs - fi - done; - # Deploy docs with version path - if [[ "${{ env.BUMP_VERSION }}" != "" ]] && [[ -d $GITHUB_WORKSPACE/docs/${{ env.BUMP_COMPONENT }} ]]; then - echo "Deploying specific version of ${{ env.BUMP_COMPONENT }} (${{ env.BUMP_VERSION }})" - cd $GITHUB_WORKSPACE/docs/${{ env.BUMP_COMPONENT }} - export GITHUB_REF_NAME=${{ env.BUMP_VERSION }} - deploy-docs - fi - name: Upload components to component service uses: espressif/upload-components-ci-action@v1 with: @@ -92,6 +67,7 @@ jobs: components/esp_modem; components/esp_mqtt_cxx; components/esp_websocket_client; + components/eppp_link; components/mdns; components/console_simple_init; components/console_cmd_ping; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9261d60d5f..f60a43c21e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,8 +61,8 @@ repos: - repo: local hooks: - id: commit message scopes - name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common" - entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples)\)\:)' + name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common, eppp" + entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp)\)\:)' language: pygrep args: [--multiline] stages: [commit-msg] diff --git a/README.md b/README.md index f1e693df56..918eac47a7 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,7 @@ Please refer to instructions in [ESP-IDF](https://github.com/espressif/esp-idf) ### console_cmd_wifi * Brief introduction [README](components/console_cmd_wifi/README.md) + +### ESP PPP Link (eppp) + +* Brief introduction [README](components/eppp_link/README.md) diff --git a/components/eppp_link/.cz.yaml b/components/eppp_link/.cz.yaml new file mode 100644 index 0000000000..100ce71471 --- /dev/null +++ b/components/eppp_link/.cz.yaml @@ -0,0 +1,8 @@ +--- +commitizen: + bump_message: 'bump(eppp): $current_version -> $new_version' + pre_bump_hooks: python ../../ci/changelog.py eppp_link + tag_format: eppp-v$version + version: 0.0.1 + version_files: + - idf_component.yml diff --git a/components/eppp_link/CHANGELOG.md b/components/eppp_link/CHANGELOG.md new file mode 100644 index 0000000000..dfe0052344 --- /dev/null +++ b/components/eppp_link/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## [0.0.1](https://github.com/espressif/esp-protocols/commits/eppp-v0.0.1) + +### Features + +- Added CI job to build examples and tests ([8686977](https://github.com/espressif/esp-protocols/commit/8686977)) +- Added support for SPI transport ([18f8452](https://github.com/espressif/esp-protocols/commit/18f8452)) +- Added support for UART transport ([ad27414](https://github.com/espressif/esp-protocols/commit/ad27414)) +- Introduced ESP-PPP-Link component ([a761039](https://github.com/espressif/esp-protocols/commit/a761039)) diff --git a/components/eppp_link/CMakeLists.txt b/components/eppp_link/CMakeLists.txt new file mode 100644 index 0000000000..e7e9c1432e --- /dev/null +++ b/components/eppp_link/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "eppp_link.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES esp_netif esp_driver_spi esp_driver_gpio esp_timer driver) diff --git a/components/eppp_link/Kconfig b/components/eppp_link/Kconfig new file mode 100644 index 0000000000..4327cba334 --- /dev/null +++ b/components/eppp_link/Kconfig @@ -0,0 +1,35 @@ +menu "eppp_link" + + choice EPPP_LINK_DEVICE + prompt "Choose PPP device" + default EPPP_LINK_DEVICE_UART + help + Select which peripheral to use for PPP link + + config EPPP_LINK_DEVICE_UART + bool "UART" + help + Use UART. + + config EPPP_LINK_DEVICE_SPI + bool "SPI" + help + Use SPI. + endchoice + + config EPPP_LINK_CONN_MAX_RETRY + int "Maximum retry" + default 6 + help + Set the Maximum retry to infinitely avoid reconnecting + This is used only with the simplified API (eppp_connect() + and eppp_listen()) + + config EPPP_LINK_PACKET_QUEUE_SIZE + int "Packet queue size" + default 64 + help + Size of the Tx packet queue. + You can decrease the number for slower bit rates. + +endmenu diff --git a/components/eppp_link/LICENSE b/components/eppp_link/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/components/eppp_link/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/components/eppp_link/README.md b/components/eppp_link/README.md new file mode 100644 index 0000000000..9b517e7f6a --- /dev/null +++ b/components/eppp_link/README.md @@ -0,0 +1,52 @@ +# ESP PPP Link component (eppp_link) + +The component provides a general purpose connectivity engine between two microcontrollers, one acting as PPP server (slave), the other one as PPP client (host). +This component could be used for extending network using physical serial connection. Applications could vary from providing PRC engine for multiprocessor solutions to serial connection to POSIX machine. This uses a standard PPP protocol to negotiate IP addresses and networking, so standard PPP toolset could be used, e.g. a `pppd` service on linux. Typical application is a WiFi connectivity provider for chips that do not have WiFi + +## Typical application + +Using this component we can construct a WiFi connectivity gateway on PPP channel. The below diagram depicts an application where +PPP server is running on a WiFi capable chip with NAPT module translating packets between WiFi and PPPoS interface. +We usually call this node a SLAVE microcontroller. The "HOST" microcontroller runs PPP client and connects only to the serial line, +brings in the WiFi connectivity from the "SLAVE" microcontroller. + +``` + SLAVE micro HOST micro + \|/ +----------------+ +----------------+ + | | | serial line | | + +---+ WiFi NAT PPPoS |======== UART / SPI =======| PPPoS client | + | (server)| | | + +----------------+ +----------------+ +``` + +## API + +### Client + +* `eppp_connect()` -- Simplified API. Provides the initialization, starts the task and blocks until we're connected + +### Server + +* `eppp_listen()` -- Simplified API. Provides the initialization, starts the task and blocks until the client connects + +### Manual actions + +* `eppp_init()` -- Initializes one endpoint (client/server). +* `eppp_deinit()` -- Destroys the endpoint +* `eppp_netif_start()` -- Starts the network, could be called after startup or whenever a connection is lost +* `eppp_netif_stop()` -- Stops the network +* `eppp_perform()` -- Perform one iteration of the PPP task (need to be called regularly in task-less configuration) + +## Throughput + +Tested with WiFi-NAPT example, no IRAM optimizations + +### UART @ 3Mbauds + +* TCP - 2Mbits/s +* UDP - 2Mbits/s + +### SPI @ 20MHz + +* TCP - 6Mbits/s +* UDP - 10Mbits/s diff --git a/components/eppp_link/eppp_link.c b/components/eppp_link/eppp_link.c new file mode 100644 index 0000000000..a2a8e6a1b3 --- /dev/null +++ b/components/eppp_link/eppp_link.c @@ -0,0 +1,820 @@ +/* + * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_check.h" +#include "esp_event.h" +#include "esp_netif_ppp.h" +#include "eppp_link.h" + +#if CONFIG_EPPP_LINK_DEVICE_SPI +#include "driver/spi_master.h" +#include "driver/spi_slave.h" +#include "driver/gpio.h" +#include "esp_timer.h" +#include "esp_rom_crc.h" +#elif CONFIG_EPPP_LINK_DEVICE_UART +#include "driver/uart.h" +#endif + +static const int GOT_IPV4 = BIT0; +static const int CONNECTION_FAILED = BIT1; +#define CONNECT_BITS (GOT_IPV4|CONNECTION_FAILED) + +static EventGroupHandle_t s_event_group = NULL; +static const char *TAG = "eppp_link"; +static int s_retry_num = 0; +static int s_eppp_netif_count = 0; // used as a suffix for the netif key + + +struct packet { + size_t len; + uint8_t *data; +}; + +#if CONFIG_EPPP_LINK_DEVICE_SPI +#define MAX_PAYLOAD 1500 +#define MIN_TRIGGER_US 20 +#define SPI_HEADER_MAGIC 0x1234 + +static void timer_callback(void *arg); + +struct header { + uint16_t magic; + uint16_t size; + uint16_t next_size; + uint16_t check; +} __attribute__((packed)); + +enum blocked_status { + NONE, + MASTER_BLOCKED, + MASTER_WANTS_READ, + SLAVE_BLOCKED, + SLAVE_WANTS_WRITE, +}; + +#endif // CONFIG_EPPP_LINK_DEVICE_SPI + +struct eppp_handle { +#if CONFIG_EPPP_LINK_DEVICE_SPI + QueueHandle_t out_queue; + QueueHandle_t ready_semaphore; + spi_device_handle_t spi_device; + spi_host_device_t spi_host; + int gpio_intr; + uint16_t next_size; + uint16_t transaction_size; + struct packet outbound; + enum blocked_status blocked; + uint32_t slave_last_edge; + esp_timer_handle_t timer; +#elif CONFIG_EPPP_LINK_DEVICE_UART + QueueHandle_t uart_event_queue; + uart_port_t uart_port; +#endif + esp_netif_t *netif; + eppp_type_t role; + bool stop; + bool exited; + bool netif_stop; +}; + + +static esp_err_t transmit(void *h, void *buffer, size_t len) +{ + struct eppp_handle *handle = h; +#if CONFIG_EPPP_LINK_DEVICE_SPI + struct packet buf = { }; + uint8_t *current_buffer = buffer; + size_t remaining = len; + do { // TODO(IDF-9194): Refactor this loop to allocate only once and perform + // fragmentation after receiving from the queue (applicable only if MTU > MAX_PAYLOAD) + size_t batch = remaining > MAX_PAYLOAD ? MAX_PAYLOAD : remaining; + buf.data = malloc(batch); + if (buf.data == NULL) { + ESP_LOGE(TAG, "Failed to allocate packet"); + return ESP_ERR_NO_MEM; + } + buf.len = batch; + remaining -= batch; + memcpy(buf.data, current_buffer, batch); + current_buffer += batch; + BaseType_t ret = xQueueSend(handle->out_queue, &buf, 0); + if (ret != pdTRUE) { + ESP_LOGE(TAG, "Failed to queue packet to slave!"); + return ESP_ERR_NO_MEM; + } + } while (remaining > 0); + + if (handle->role == EPPP_SERVER && handle->blocked == SLAVE_BLOCKED) { + uint32_t now = esp_timer_get_time(); + uint32_t diff = now - handle->slave_last_edge; + if (diff < MIN_TRIGGER_US) { + esp_rom_delay_us(MIN_TRIGGER_US - diff); + } + gpio_set_level(handle->gpio_intr, 0); + } + +#elif CONFIG_EPPP_LINK_DEVICE_UART + ESP_LOG_BUFFER_HEXDUMP("ppp_uart_send", buffer, len, ESP_LOG_VERBOSE); + uart_write_bytes(handle->uart_port, buffer, len); +#endif + return ESP_OK; +} + +static void netif_deinit(esp_netif_t *netif) +{ + if (netif == NULL) { + return; + } + struct eppp_handle *h = esp_netif_get_io_driver(netif); + if (h == NULL) { + return; + } +#if CONFIG_EPPP_LINK_DEVICE_SPI + struct packet buf = { }; + while (xQueueReceive(h->out_queue, &buf, 0) == pdTRUE) { + if (buf.len > 0) { + free(buf.data); + } + } + vQueueDelete(h->out_queue); + if (h->role == EPPP_CLIENT) { + vSemaphoreDelete(h->ready_semaphore); + } +#endif + free(h); + esp_netif_destroy(netif); + if (s_eppp_netif_count > 0) { + s_eppp_netif_count--; + } +} + +static esp_netif_t *netif_init(eppp_type_t role) +{ + if (s_eppp_netif_count > 9) { // Limit to max 10 netifs, since we use "EPPPx" as the unique key (where x is 0-9) + ESP_LOGE(TAG, "Cannot create more than 10 instances"); + return NULL; + } + + // Create the object first + struct eppp_handle *h = calloc(1, sizeof(struct eppp_handle)); + if (!h) { + ESP_LOGE(TAG, "Failed to allocate eppp_handle"); + return NULL; + } + h->role = role; +#if CONFIG_EPPP_LINK_DEVICE_SPI + h->out_queue = xQueueCreate(CONFIG_EPPP_LINK_PACKET_QUEUE_SIZE, sizeof(struct packet)); + if (!h->out_queue) { + ESP_LOGE(TAG, "Failed to create the packet queue"); + free(h); + return NULL; + } + if (role == EPPP_CLIENT) { + h->ready_semaphore = xSemaphoreCreateBinary(); + if (!h->ready_semaphore) { + ESP_LOGE(TAG, "Failed to create the packet queue"); + vQueueDelete(h->out_queue); + free(h); + return NULL; + } + } + h->transaction_size = 0; + h->outbound.data = NULL; + h->outbound.len = 0; + if (role == EPPP_SERVER) { + esp_timer_create_args_t args = { + .callback = &timer_callback, + .arg = h, + .name = "timer" + }; + if (esp_timer_create(&args, &h->timer) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create the packet queue"); + vQueueDelete(h->out_queue); + vSemaphoreDelete(h->ready_semaphore); + free(h); + return NULL; + } + } + +#endif + + esp_netif_driver_ifconfig_t driver_cfg = { + .handle = h, + .transmit = transmit, + }; + const esp_netif_driver_ifconfig_t *ppp_driver_cfg = &driver_cfg; + + esp_netif_inherent_config_t base_netif_cfg = ESP_NETIF_INHERENT_DEFAULT_PPP(); + char if_key[] = "EPPP0"; // netif key needs to be unique + if_key[sizeof(if_key) - 2 /* 2 = two chars before the terminator */ ] += s_eppp_netif_count++; + base_netif_cfg.if_key = if_key; + if (role == EPPP_CLIENT) { + base_netif_cfg.if_desc = "pppos_client"; + } else { + base_netif_cfg.if_desc = "pppos_server"; + } + esp_netif_config_t netif_ppp_config = { .base = &base_netif_cfg, + .driver = ppp_driver_cfg, + .stack = ESP_NETIF_NETSTACK_DEFAULT_PPP + }; + + esp_netif_t *netif = esp_netif_new(&netif_ppp_config); + if (!netif) { + ESP_LOGE(TAG, "Failed to create esp_netif"); +#if CONFIG_EPPP_LINK_DEVICE_SPI + vQueueDelete(h->out_queue); + if (h->ready_semaphore) { + vSemaphoreDelete(h->ready_semaphore); + } +#endif + free(h); + return NULL; + } + return netif; + +} + +esp_err_t eppp_netif_stop(esp_netif_t *netif, int stop_timeout_ms) +{ + esp_netif_action_disconnected(netif, 0, 0, 0); + esp_netif_action_stop(netif, 0, 0, 0); + struct eppp_handle *h = esp_netif_get_io_driver(netif); + for (int wait = 0; wait < 100; wait++) { + vTaskDelay(pdMS_TO_TICKS(stop_timeout_ms) / 100); + if (h->netif_stop) { + break; + } + } + if (!h->netif_stop) { + return ESP_FAIL; + } + + return ESP_OK; +} + +esp_err_t eppp_netif_start(esp_netif_t *netif) +{ + esp_netif_action_start(netif, 0, 0, 0); + esp_netif_action_connected(netif, 0, 0, 0); + return ESP_OK; +} + +static int get_netif_num(esp_netif_t *netif) +{ + if (netif == NULL) { + return -1; + } + const char *ifkey = esp_netif_get_ifkey(netif); + if (strstr(ifkey, "EPPP") == NULL) { + return -1; // not our netif + } + int netif_cnt = ifkey[4] - '0'; + if (netif_cnt < 0 || netif_cnt > 9) { + ESP_LOGE(TAG, "Unexpected netif key %s", ifkey); + return -1; + } + return netif_cnt; +} + +static void on_ppp_event(void *arg, esp_event_base_t base, int32_t event_id, void *data) +{ + esp_netif_t **netif = data; + ESP_LOGD(TAG, "PPP status event: %" PRId32, event_id); + if (base == NETIF_PPP_STATUS && event_id == NETIF_PPP_ERRORUSER) { + ESP_LOGI(TAG, "Disconnected %d", get_netif_num(*netif)); + struct eppp_handle *h = esp_netif_get_io_driver(*netif); + h->netif_stop = true; + } +} + +static void on_ip_event(void *arg, esp_event_base_t base, int32_t event_id, void *data) +{ + ip_event_got_ip_t *event = (ip_event_got_ip_t *)data; + esp_netif_t *netif = event->esp_netif; + int netif_cnt = get_netif_num(netif); + if (netif_cnt < 0) { + return; + } + if (event_id == IP_EVENT_PPP_GOT_IP) { + ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s(%s)\" address: " IPSTR, esp_netif_get_desc(netif), + esp_netif_get_ifkey(netif), IP2STR(&event->ip_info.ip)); + xEventGroupSetBits(s_event_group, GOT_IPV4 << (netif_cnt * 2)); + } else if (event_id == IP_EVENT_PPP_LOST_IP) { + ESP_LOGI(TAG, "Disconnected"); + s_retry_num++; + if (s_retry_num > CONFIG_EPPP_LINK_CONN_MAX_RETRY) { + ESP_LOGE(TAG, "PPP Connection failed %d times, stop reconnecting.", s_retry_num); + xEventGroupSetBits(s_event_group, CONNECTION_FAILED << (netif_cnt * 2)); + } else { + ESP_LOGI(TAG, "PPP Connection failed %d times, try to reconnect.", s_retry_num); + eppp_netif_start(netif); + } + } +} + +#if CONFIG_EPPP_LINK_DEVICE_SPI + +#define SPI_ALIGN(size) (((size) + 3U) & ~(3U)) +#define TRANSFER_SIZE SPI_ALIGN((MAX_PAYLOAD + 6)) +#define NEXT_TRANSACTION_SIZE(a,b) (((a)>(b))?(a):(b)) /* next transaction: whichever is bigger */ + +static void IRAM_ATTR timer_callback(void *arg) +{ + struct eppp_handle *h = arg; + if (h->blocked == SLAVE_WANTS_WRITE) { + gpio_set_level(h->gpio_intr, 0); + } +} + +static void IRAM_ATTR gpio_isr_handler(void *arg) +{ + static uint32_t s_last_time; + uint32_t now = esp_timer_get_time(); + uint32_t diff = now - s_last_time; + if (diff < MIN_TRIGGER_US) { // debounce + return; + } + s_last_time = now; + struct eppp_handle *h = arg; + BaseType_t yield = false; + + // Positive edge means SPI slave prepared the data + if (gpio_get_level(h->gpio_intr) == 1) { + xSemaphoreGiveFromISR(h->ready_semaphore, &yield); + if (yield) { + portYIELD_FROM_ISR(); + } + return; + } + + // Negative edge (when master blocked) means that slave wants to transmit + if (h->blocked == MASTER_BLOCKED) { + struct packet buf = { .data = NULL, .len = -1 }; + xQueueSendFromISR(h->out_queue, &buf, &yield); + if (yield) { + portYIELD_FROM_ISR(); + } + } +} + +static esp_err_t deinit_master(esp_netif_t *netif) +{ + struct eppp_handle *h = esp_netif_get_io_driver(netif); + ESP_RETURN_ON_ERROR(spi_bus_remove_device(h->spi_device), TAG, "Failed to remove SPI bus"); + ESP_RETURN_ON_ERROR(spi_bus_free(h->spi_host), TAG, "Failed to free SPI bus"); + return ESP_OK; +} + +static esp_err_t init_master(struct eppp_config_spi_s *config, esp_netif_t *netif) +{ + struct eppp_handle *h = esp_netif_get_io_driver(netif); + h->spi_host = config->host; + h->gpio_intr = config->intr; + spi_bus_config_t bus_cfg = {}; + bus_cfg.mosi_io_num = config->mosi; + bus_cfg.miso_io_num = config->miso; + bus_cfg.sclk_io_num = config->sclk; + bus_cfg.quadwp_io_num = -1; + bus_cfg.quadhd_io_num = -1; + bus_cfg.max_transfer_sz = TRANSFER_SIZE; + bus_cfg.flags = 0; + bus_cfg.intr_flags = 0; + + // TODO: Init and deinit SPI bus separately (per Kconfig?) + if (spi_bus_initialize(config->host, &bus_cfg, SPI_DMA_CH_AUTO) != ESP_OK) { + return ESP_FAIL; + } + + spi_device_interface_config_t dev_cfg = {}; + dev_cfg.clock_speed_hz = config->freq; + dev_cfg.mode = 0; + dev_cfg.spics_io_num = config->cs; + dev_cfg.cs_ena_pretrans = config->cs_ena_pretrans; + dev_cfg.cs_ena_posttrans = config->cs_ena_posttrans; + dev_cfg.duty_cycle_pos = 128; + dev_cfg.input_delay_ns = config->input_delay_ns; + dev_cfg.pre_cb = NULL; + dev_cfg.post_cb = NULL; + dev_cfg.queue_size = 3; + + if (spi_bus_add_device(config->host, &dev_cfg, &h->spi_device) != ESP_OK) { + return ESP_FAIL; + } + + //GPIO config for the handshake line. + gpio_config_t io_conf = { + .intr_type = GPIO_INTR_ANYEDGE, + .mode = GPIO_MODE_INPUT, + .pull_up_en = 1, + .pin_bit_mask = BIT64(config->intr), + }; + + gpio_config(&io_conf); + gpio_install_isr_service(0); + gpio_set_intr_type(config->intr, GPIO_INTR_ANYEDGE); + gpio_isr_handler_add(config->intr, gpio_isr_handler, esp_netif_get_io_driver(netif)); + return ESP_OK; +} + +static void post_setup(spi_slave_transaction_t *trans) +{ + struct eppp_handle *h = trans->user; + h->slave_last_edge = esp_timer_get_time(); + gpio_set_level(h->gpio_intr, 1); + if (h->transaction_size == 0) { // If no transaction planned: + if (h->outbound.len == 0) { // we're blocked if we don't have any data + h->blocked = SLAVE_BLOCKED; + } else { + h->blocked = SLAVE_WANTS_WRITE; // we notify the master that we want to write + esp_timer_start_once(h->timer, MIN_TRIGGER_US); + } + } +} + +static void post_trans(spi_slave_transaction_t *trans) +{ + struct eppp_handle *h = trans->user; + h->blocked = NONE; + gpio_set_level(h->gpio_intr, 0); +} + +static esp_err_t deinit_slave(esp_netif_t *netif) +{ + struct eppp_handle *h = esp_netif_get_io_driver(netif); + ESP_RETURN_ON_ERROR(spi_slave_free(h->spi_host), TAG, "Failed to free SPI slave host"); + ESP_RETURN_ON_ERROR(spi_bus_remove_device(h->spi_device), TAG, "Failed to remove SPI device"); + ESP_RETURN_ON_ERROR(spi_bus_free(h->spi_host), TAG, "Failed to free SPI bus"); + return ESP_OK; +} + +static esp_err_t init_slave(struct eppp_config_spi_s *config, esp_netif_t *netif) +{ + struct eppp_handle *h = esp_netif_get_io_driver(netif); + h->spi_host = config->host; + h->gpio_intr = config->intr; + spi_bus_config_t bus_cfg = {}; + bus_cfg.mosi_io_num = config->mosi; + bus_cfg.miso_io_num = config->miso; + bus_cfg.sclk_io_num = config->sclk; + bus_cfg.quadwp_io_num = -1; + bus_cfg.quadhd_io_num = -1; + bus_cfg.flags = 0; + bus_cfg.intr_flags = 0; + + //Configuration for the SPI slave interface + spi_slave_interface_config_t slvcfg = { + .mode = 0, + .spics_io_num = config->cs, + .queue_size = 3, + .flags = 0, + .post_setup_cb = post_setup, + .post_trans_cb = post_trans, + }; + + //Configuration for the handshake line + gpio_config_t io_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(config->intr), + }; + + gpio_config(&io_conf); + gpio_set_pull_mode(config->mosi, GPIO_PULLUP_ONLY); + gpio_set_pull_mode(config->sclk, GPIO_PULLUP_ONLY); + gpio_set_pull_mode(config->cs, GPIO_PULLUP_ONLY); + + //Initialize SPI slave interface + if (spi_slave_initialize(config->host, &bus_cfg, &slvcfg, SPI_DMA_CH_AUTO) != ESP_OK) { + return ESP_FAIL; + } + return ESP_OK; +} + +typedef esp_err_t (*perform_transaction_t)(struct eppp_handle *h, size_t len, const void *tx_buffer, void *rx_buffer); + +static esp_err_t perform_transaction_master(struct eppp_handle *h, size_t len, const void *tx_buffer, void *rx_buffer) +{ + spi_transaction_t t = {}; + t.length = len * 8; + t.tx_buffer = tx_buffer; + t.rx_buffer = rx_buffer; + return spi_device_transmit(h->spi_device, &t); +} + +static esp_err_t perform_transaction_slave(struct eppp_handle *h, size_t len, const void *tx_buffer, void *rx_buffer) +{ + spi_slave_transaction_t t = {}; + t.user = h; + t.length = len * 8; + t.tx_buffer = tx_buffer; + t.rx_buffer = rx_buffer; + return spi_slave_transmit(h->spi_host, &t, portMAX_DELAY); +} + +esp_err_t eppp_perform(esp_netif_t *netif) +{ + static WORD_ALIGNED_ATTR uint8_t out_buf[TRANSFER_SIZE] = {}; + static WORD_ALIGNED_ATTR uint8_t in_buf[TRANSFER_SIZE] = {}; + + struct eppp_handle *h = esp_netif_get_io_driver(netif); + + // Perform transaction for master and slave + const perform_transaction_t perform_transaction = h->role == EPPP_CLIENT ? perform_transaction_master : perform_transaction_slave; + + if (h->stop) { + return ESP_ERR_TIMEOUT; + } + + BaseType_t tx_queue_stat; + bool allow_test_tx = false; + uint16_t next_tx_size = 0; + if (h->role == EPPP_CLIENT) { + // SPI MASTER only code + if (xSemaphoreTake(h->ready_semaphore, pdMS_TO_TICKS(1000)) != pdTRUE) { + // slave might not be ready, but maybe we just missed an interrupt + allow_test_tx = true; + } + if (h->outbound.len == 0 && h->transaction_size == 0 && h->blocked == NONE) { + h->blocked = MASTER_BLOCKED; + xQueueReceive(h->out_queue, &h->outbound, portMAX_DELAY); + h->blocked = NONE; + if (h->outbound.len == -1) { + h->outbound.len = 0; + h->blocked = MASTER_WANTS_READ; + } + } else if (h->blocked == MASTER_WANTS_READ) { + h->blocked = NONE; + } + } + struct header *head = (void *)out_buf; + if (h->outbound.len <= h->transaction_size && allow_test_tx == false) { + // sending outbound + head->size = h->outbound.len; + if (h->outbound.len > 0) { + memcpy(out_buf + sizeof(struct header), h->outbound.data, h->outbound.len); + free(h->outbound.data); + ESP_LOG_BUFFER_HEXDUMP(TAG, out_buf + sizeof(struct header), head->size, ESP_LOG_VERBOSE); + h->outbound.data = NULL; + h->outbound.len = 0; + } + do { + tx_queue_stat = xQueueReceive(h->out_queue, &h->outbound, 0); + } while (tx_queue_stat == pdTRUE && h->outbound.len == -1); + if (h->outbound.len == -1) { // used as a signal only, no actual data + h->outbound.len = 0; + } + } else { + // outbound is bigger, need to transmit in another transaction (keep this empty) + head->size = 0; + } + next_tx_size = head->next_size = h->outbound.len; + head->magic = SPI_HEADER_MAGIC; + head->check = esp_rom_crc16_le(0, out_buf, sizeof(struct header) - sizeof(uint16_t)); + esp_err_t ret = perform_transaction(h, sizeof(struct header) + h->transaction_size, out_buf, in_buf); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "spi_device_transmit failed"); + h->transaction_size = 0; // need to start with HEADER only transaction + return ESP_FAIL; + } + head = (void *)in_buf; + uint16_t check = esp_rom_crc16_le(0, in_buf, sizeof(struct header) - sizeof(uint16_t)); + if (check != head->check || head->magic != SPI_HEADER_MAGIC) { + h->transaction_size = 0; // need to start with HEADER only transaction + if (allow_test_tx) { + return ESP_OK; + } + ESP_LOGE(TAG, "Wrong checksum or magic"); + return ESP_FAIL; + } + if (head->size > 0) { + ESP_LOG_BUFFER_HEXDUMP(TAG, in_buf + sizeof(struct header), head->size, ESP_LOG_VERBOSE); + esp_netif_receive(netif, in_buf + sizeof(struct header), head->size, NULL); + } + h->transaction_size = NEXT_TRANSACTION_SIZE(next_tx_size, head->next_size); + return ESP_OK; +} + +#elif CONFIG_EPPP_LINK_DEVICE_UART +#define BUF_SIZE (1024) + +static esp_err_t init_uart(struct eppp_handle *h, eppp_config_t *config) +{ + h->uart_port = config->uart.port; + uart_config_t uart_config = {}; + uart_config.baud_rate = config->uart.baud; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + uart_config.source_clk = UART_SCLK_DEFAULT; + + ESP_RETURN_ON_ERROR(uart_driver_install(h->uart_port, config->uart.rx_buffer_size, 0, config->uart.queue_size, &h->uart_event_queue, 0), TAG, "Failed to install UART"); + ESP_RETURN_ON_ERROR(uart_param_config(h->uart_port, &uart_config), TAG, "Failed to set params"); + ESP_RETURN_ON_ERROR(uart_set_pin(h->uart_port, config->uart.tx_io, config->uart.rx_io, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE), TAG, "Failed to set UART pins"); + ESP_RETURN_ON_ERROR(uart_set_rx_timeout(h->uart_port, 1), TAG, "Failed to set UART Rx timeout"); + return ESP_OK; +} + +static void deinit_uart(struct eppp_handle *h) +{ + uart_driver_delete(h->uart_port); +} + +esp_err_t eppp_perform(esp_netif_t *netif) +{ + static uint8_t buffer[BUF_SIZE] = {}; + struct eppp_handle *h = esp_netif_get_io_driver(netif); + uart_event_t event = {}; + if (h->stop) { + return ESP_ERR_TIMEOUT; + } + + if (xQueueReceive(h->uart_event_queue, &event, pdMS_TO_TICKS(100)) != pdTRUE) { + return ESP_OK; + } + if (event.type == UART_DATA) { + size_t len; + uart_get_buffered_data_len(h->uart_port, &len); + if (len) { + len = uart_read_bytes(h->uart_port, buffer, BUF_SIZE, 0); + ESP_LOG_BUFFER_HEXDUMP("ppp_uart_recv", buffer, len, ESP_LOG_VERBOSE); + esp_netif_receive(netif, buffer, len, NULL); + } + } else { + ESP_LOGW(TAG, "Received UART event: %d", event.type); + } + return ESP_OK; +} + +#endif // CONFIG_EPPP_LINK_DEVICE_SPI / UART + +static void ppp_task(void *args) +{ + esp_netif_t *netif = args; + while (eppp_perform(netif) != ESP_ERR_TIMEOUT) {} + struct eppp_handle *h = esp_netif_get_io_driver(netif); + h->exited = true; + vTaskDelete(NULL); +} + +static bool have_some_eppp_netif(esp_netif_t *netif, void *ctx) +{ + return get_netif_num(netif) > 0; +} + +static void remove_handlers(void) +{ + esp_netif_t *netif = esp_netif_find_if(have_some_eppp_netif, NULL); + if (netif == NULL) { + // if EPPP netif in the system, we cleanup the statics + vEventGroupDelete(s_event_group); + s_event_group = NULL; + esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event); + esp_event_handler_unregister(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_ppp_event); + } +} + +void eppp_deinit(esp_netif_t *netif) +{ + if (netif == NULL) { + return; + } +#if CONFIG_EPPP_LINK_DEVICE_SPI + struct eppp_handle *h = esp_netif_get_io_driver(netif); + if (h->role == EPPP_CLIENT) { + deinit_master(netif); + } else { + deinit_slave(netif); + } +#elif CONFIG_EPPP_LINK_DEVICE_UART + deinit_uart(esp_netif_get_io_driver(netif)); +#endif + netif_deinit(netif); +} + +esp_netif_t *eppp_init(eppp_type_t role, eppp_config_t *config) +{ + esp_netif_t *netif = netif_init(role); + if (!netif) { + ESP_LOGE(TAG, "Failed to initialize PPP netif"); + remove_handlers(); + return NULL; + } + esp_netif_ppp_config_t netif_params; + ESP_ERROR_CHECK(esp_netif_ppp_get_params(netif, &netif_params)); + netif_params.ppp_our_ip4_addr = config->ppp.our_ip4_addr; + netif_params.ppp_their_ip4_addr = config->ppp.their_ip4_addr; + netif_params.ppp_error_event_enabled = true; + ESP_ERROR_CHECK(esp_netif_ppp_set_params(netif, &netif_params)); +#if CONFIG_EPPP_LINK_DEVICE_SPI + if (role == EPPP_CLIENT) { + init_master(&config->spi, netif); + } else { + init_slave(&config->spi, netif); + + } +#elif CONFIG_EPPP_LINK_DEVICE_UART + init_uart(esp_netif_get_io_driver(netif), config); +#endif + return netif; +} + +esp_netif_t *eppp_open(eppp_type_t role, eppp_config_t *config, int connect_timeout_ms) +{ +#if CONFIG_EPPP_LINK_DEVICE_UART + if (config->transport != EPPP_TRANSPORT_UART) { + ESP_LOGE(TAG, "Invalid transport: UART device must be enabled in Kconfig"); + return NULL; + } +#endif +#if CONFIG_EPPP_LINK_DEVICE_SPI + if (config->transport != EPPP_TRANSPORT_SPI) { + ESP_LOGE(TAG, "Invalid transport: SPI device must be enabled in Kconfig"); + return NULL; + } +#endif + + if (config->task.run_task == false) { + ESP_LOGE(TAG, "task.run_task == false is invalid in this API. Please use eppp_init()"); + return NULL; + } + if (s_event_group == NULL) { + s_event_group = xEventGroupCreate(); + if (esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to register IP event handler"); + remove_handlers(); + return NULL; + } + if (esp_event_handler_register(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_ppp_event, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to register PPP status handler"); + remove_handlers(); + return NULL; + } + } + esp_netif_t *netif = eppp_init(role, config); + if (!netif) { + remove_handlers(); + return NULL; + } + + eppp_netif_start(netif); + + if (xTaskCreate(ppp_task, "ppp connect", config->task.stack_size, netif, config->task.priority, NULL) != pdTRUE) { + ESP_LOGE(TAG, "Failed to create a ppp connection task"); + eppp_deinit(netif); + return NULL; + } + int netif_cnt = get_netif_num(netif); + if (netif_cnt < 0) { + eppp_close(netif); + return NULL; + } + ESP_LOGI(TAG, "Waiting for IP address %d", netif_cnt); + EventBits_t bits = xEventGroupWaitBits(s_event_group, CONNECT_BITS << (netif_cnt * 2), pdFALSE, pdFALSE, pdMS_TO_TICKS(connect_timeout_ms)); + if (bits & (CONNECTION_FAILED << (netif_cnt * 2))) { + ESP_LOGE(TAG, "Connection failed!"); + eppp_close(netif); + return NULL; + } + ESP_LOGI(TAG, "Connected! %d", netif_cnt); + return netif; +} + +esp_netif_t *eppp_connect(eppp_config_t *config) +{ + return eppp_open(EPPP_CLIENT, config, portMAX_DELAY); +} + +esp_netif_t *eppp_listen(eppp_config_t *config) +{ + return eppp_open(EPPP_SERVER, config, portMAX_DELAY); +} + +void eppp_close(esp_netif_t *netif) +{ + struct eppp_handle *h = esp_netif_get_io_driver(netif); + if (eppp_netif_stop(netif, 60000) != ESP_OK) { + ESP_LOGE(TAG, "Network didn't exit cleanly"); + } + h->stop = true; + for (int wait = 0; wait < 100; wait++) { + vTaskDelay(pdMS_TO_TICKS(10)); + if (h->exited) { + break; + } + } + if (!h->exited) { + ESP_LOGE(TAG, "Cannot stop ppp_task"); + } + eppp_deinit(netif); + remove_handlers(); +} diff --git a/components/eppp_link/examples/host/CMakeLists.txt b/components/eppp_link/examples/host/CMakeLists.txt new file mode 100644 index 0000000000..3405abc526 --- /dev/null +++ b/components/eppp_link/examples/host/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following four 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(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/iperf) + + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(pppos_host) diff --git a/components/eppp_link/examples/host/README.md b/components/eppp_link/examples/host/README.md new file mode 100644 index 0000000000..a6592627c6 --- /dev/null +++ b/components/eppp_link/examples/host/README.md @@ -0,0 +1,9 @@ + +# Client side demo of ESP-PPP-Link + +This is a basic demo of using esp-mqtt library, but connects to the internet using a PPPoS client. To run this example, you would need a PPP server that provides connectivity to the MQTT broker used in this example (by default a public broker accessible on the internet). + +If configured, this example could also run a ping session and an iperf console. + + +The PPP server could be a Linux computer with `pppd` service or an ESP32 acting like a connection gateway with PPPoS server (see the "slave" project). diff --git a/components/eppp_link/examples/host/main/CMakeLists.txt b/components/eppp_link/examples/host/main/CMakeLists.txt new file mode 100644 index 0000000000..0198ddd323 --- /dev/null +++ b/components/eppp_link/examples/host/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS app_main.c register_iperf.c + INCLUDE_DIRS ".") diff --git a/components/eppp_link/examples/host/main/Kconfig.projbuild b/components/eppp_link/examples/host/main/Kconfig.projbuild new file mode 100644 index 0000000000..5029beb568 --- /dev/null +++ b/components/eppp_link/examples/host/main/Kconfig.projbuild @@ -0,0 +1,53 @@ +menu "Example Configuration" + + config EXAMPLE_GLOBAL_DNS + hex "Set global DNS server" + range 0 0xFFFFFFFF + default 0x08080808 + help + Global DNS server address. + + config EXAMPLE_MQTT + bool "Run mqtt example" + default y + help + Run MQTT client after startup. + + config EXAMPLE_BROKER_URL + string "Broker URL" + depends on EXAMPLE_MQTT + default "mqtt://mqtt.eclipseprojects.io" + help + URL of the broker to connect to. + + config EXAMPLE_IPERF + bool "Run iperf" + default y + help + Init and run iperf console. + + config EXAMPLE_UART_TX_PIN + int "TXD Pin Number" + depends on EPPP_LINK_DEVICE_UART + default 10 + range 0 31 + help + Pin number of UART TX. + + config EXAMPLE_UART_RX_PIN + int "RXD Pin Number" + depends on EPPP_LINK_DEVICE_UART + default 11 + range 0 31 + help + Pin number of UART RX. + + config EXAMPLE_UART_BAUDRATE + int "Baudrate" + depends on EPPP_LINK_DEVICE_UART + default 2000000 + range 0 4000000 + help + Baudrate used by the PPP over UART + +endmenu diff --git a/components/eppp_link/examples/host/main/app_main.c b/components/eppp_link/examples/host/main/app_main.c new file mode 100644 index 0000000000..85ae186362 --- /dev/null +++ b/components/eppp_link/examples/host/main/app_main.c @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#include "esp_system.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "eppp_link.h" +#include "esp_log.h" +#include "mqtt_client.h" +#include "console_ping.h" + +void register_iperf(void); + +static const char *TAG = "eppp_host_example"; + +#if CONFIG_EXAMPLE_MQTT +static void mqtt_event_handler(void *args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id); + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + + msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1"); + ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); + } + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +static void mqtt_app_start(void) +{ + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = "mqtt://mqtt.eclipseprojects.io", + }; + + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */ + esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); + esp_mqtt_client_start(client); +} +#endif // MQTT + + +void app_main(void) +{ + 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_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* Sets up the default EPPP-connection + */ + eppp_config_t config = EPPP_DEFAULT_CLIENT_CONFIG(); +#if CONFIG_EPPP_LINK_DEVICE_SPI + config.transport = EPPP_TRANSPORT_SPI; +#else + config.transport = EPPP_TRANSPORT_UART; + config.uart.tx_io = CONFIG_EXAMPLE_UART_TX_PIN; + config.uart.rx_io = CONFIG_EXAMPLE_UART_RX_PIN; + config.uart.baud = CONFIG_EXAMPLE_UART_BAUDRATE; +#endif + esp_netif_t *eppp_netif = eppp_connect(&config); + if (eppp_netif == NULL) { + ESP_LOGE(TAG, "Failed to connect"); + return ; + } + // Setup global DNS + esp_netif_dns_info_t dns; + dns.ip.u_addr.ip4.addr = esp_netif_htonl(CONFIG_EXAMPLE_GLOBAL_DNS); + dns.ip.type = ESP_IPADDR_TYPE_V4; + ESP_ERROR_CHECK(esp_netif_set_dns_info(eppp_netif, ESP_NETIF_DNS_MAIN, &dns)); + + // Initialize console REPL + ESP_ERROR_CHECK(console_cmd_init()); + +#if CONFIG_EXAMPLE_IPERF + register_iperf(); + + printf("\n =======================================================\n"); + printf(" | Steps to Test EPPP-host bandwidth |\n"); + printf(" | |\n"); + printf(" | 1. Wait for the ESP32 to get an IP |\n"); + printf(" | 2. Server: 'iperf -u -s -i 3' (on host) |\n"); + printf(" | 3. Client: 'iperf -u -c SERVER_IP -t 60 -i 3' |\n"); + printf(" | |\n"); + printf(" =======================================================\n\n"); + +#endif // CONFIG_EXAMPLE_IPERF + + // Register the ping command + ESP_ERROR_CHECK(console_cmd_ping_register()); + // start console REPL + ESP_ERROR_CHECK(console_cmd_start()); + +#if CONFIG_EXAMPLE_MQTT + mqtt_app_start(); +#endif +} diff --git a/components/eppp_link/examples/host/main/idf_component.yml b/components/eppp_link/examples/host/main/idf_component.yml new file mode 100644 index 0000000000..675c135995 --- /dev/null +++ b/components/eppp_link/examples/host/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + espressif/eppp_link: + version: "*" + override_path: "../../.." + console_cmd_ping: + version: "*" diff --git a/components/eppp_link/examples/host/main/register_iperf.c b/components/eppp_link/examples/host/main/register_iperf.c new file mode 100644 index 0000000000..63fded10c5 --- /dev/null +++ b/components/eppp_link/examples/host/main/register_iperf.c @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "sys/socket.h" // for INADDR_ANY +#include "esp_netif.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_netif_ppp.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" + +#include "esp_console.h" +#include "esp_event.h" +#include "esp_bit_defs.h" +#include "argtable3/argtable3.h" +#include "iperf.h" +#include "sdkconfig.h" + +/* "iperf" command */ + +static struct { + struct arg_str *ip; + struct arg_lit *server; + struct arg_lit *udp; + struct arg_lit *version; + struct arg_int *port; + struct arg_int *length; + struct arg_int *interval; + struct arg_int *time; + struct arg_int *bw_limit; + struct arg_lit *abort; + struct arg_end *end; +} iperf_args; + +static int ppp_cmd_iperf(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&iperf_args); + // ethernet iperf only support IPV4 address + iperf_cfg_t cfg = {.type = IPERF_IP_TYPE_IPV4}; + + if (nerrors != 0) { + arg_print_errors(stderr, iperf_args.end, argv[0]); + return 0; + } + + /* iperf -a */ + if (iperf_args.abort->count != 0) { + iperf_stop(); + return 0; + } + + if (((iperf_args.ip->count == 0) && (iperf_args.server->count == 0)) || + ((iperf_args.ip->count != 0) && (iperf_args.server->count != 0))) { + ESP_LOGE(__func__, "Wrong mode! ESP32 should run in client or server mode"); + return 0; + } + + /* iperf -s */ + if (iperf_args.ip->count == 0) { + cfg.flag |= IPERF_FLAG_SERVER; + } + /* iperf -c SERVER_ADDRESS */ + else { + cfg.destination_ip4 = esp_ip4addr_aton(iperf_args.ip->sval[0]); + cfg.flag |= IPERF_FLAG_CLIENT; + } + + if (iperf_args.length->count == 0) { + cfg.len_send_buf = 0; + } else { + cfg.len_send_buf = iperf_args.length->ival[0]; + } + + cfg.source_ip4 = INADDR_ANY; + + /* iperf -u */ + if (iperf_args.udp->count == 0) { + cfg.flag |= IPERF_FLAG_TCP; + } else { + cfg.flag |= IPERF_FLAG_UDP; + } + + /* iperf -p */ + if (iperf_args.port->count == 0) { + cfg.sport = IPERF_DEFAULT_PORT; + cfg.dport = IPERF_DEFAULT_PORT; + } else { + if (cfg.flag & IPERF_FLAG_SERVER) { + cfg.sport = iperf_args.port->ival[0]; + cfg.dport = IPERF_DEFAULT_PORT; + } else { + cfg.sport = IPERF_DEFAULT_PORT; + cfg.dport = iperf_args.port->ival[0]; + } + } + + /* iperf -i */ + if (iperf_args.interval->count == 0) { + cfg.interval = IPERF_DEFAULT_INTERVAL; + } else { + cfg.interval = iperf_args.interval->ival[0]; + if (cfg.interval <= 0) { + cfg.interval = IPERF_DEFAULT_INTERVAL; + } + } + + /* iperf -t */ + if (iperf_args.time->count == 0) { + cfg.time = IPERF_DEFAULT_TIME; + } else { + cfg.time = iperf_args.time->ival[0]; + if (cfg.time <= cfg.interval) { + cfg.time = cfg.interval; + } + } + + /* iperf -b */ + if (iperf_args.bw_limit->count == 0) { + cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT; + } else { + cfg.bw_lim = iperf_args.bw_limit->ival[0]; + if (cfg.bw_lim <= 0) { + cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT; + } + } + + printf("mode=%s-%s sip=" IPSTR ":%" PRIu16 ", dip=%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 ":%" PRIu16 ", interval=%" PRIu32 ", time=%" PRIu32 "\r\n", + cfg.flag & IPERF_FLAG_TCP ? "tcp" : "udp", + cfg.flag & IPERF_FLAG_SERVER ? "server" : "client", + (uint16_t) cfg.source_ip4 & 0xFF, + (uint16_t) (cfg.source_ip4 >> 8) & 0xFF, + (uint16_t) (cfg.source_ip4 >> 16) & 0xFF, + (uint16_t) (cfg.source_ip4 >> 24) & 0xFF, + cfg.sport, + cfg.destination_ip4 & 0xFF, (cfg.destination_ip4 >> 8) & 0xFF, + (cfg.destination_ip4 >> 16) & 0xFF, (cfg.destination_ip4 >> 24) & 0xFF, cfg.dport, + cfg.interval, cfg.time); + + iperf_start(&cfg); + return 0; +} + +void register_iperf(void) +{ + + iperf_args.ip = arg_str0("c", "client", "", + "run in client mode, connecting to "); + iperf_args.server = arg_lit0("s", "server", "run in server mode"); + iperf_args.udp = arg_lit0("u", "udp", "use UDP rather than TCP"); + iperf_args.version = arg_lit0("V", "ipv6_domain", "use IPV6 address rather than IPV4"); + iperf_args.port = arg_int0("p", "port", "", + "server port to listen on/connect to"); + iperf_args.length = arg_int0("l", "len", "", "set read/write buffer size"); + iperf_args.interval = arg_int0("i", "interval", "", + "seconds between periodic bandwidth reports"); + iperf_args.time = arg_int0("t", "time", "