From 3c17ef663b85d220653a056cde8bdf56a2e0aec3 Mon Sep 17 00:00:00 2001 From: Chiranjeevi Srikakulapu Date: Thu, 17 Aug 2023 15:23:34 +0530 Subject: [PATCH] samples: wifi: Add TWT sample This is based on STA sample, for now it only does Wi-Fi connection. Also, update changelog and add a codeowners entry. Signed-off-by: Chiranjeevi Srikakulapu --- CODEOWNERS | 1 + .../releases/release-notes-changelog.rst | 1 + samples/wifi/twt/CMakeLists.txt | 16 ++ samples/wifi/twt/Kconfig | 50 ++++ samples/wifi/twt/README.rst | 177 ++++++++++++ samples/wifi/twt/prj.conf | 88 ++++++ samples/wifi/twt/sample.yaml | 11 + samples/wifi/twt/src/main.c | 269 ++++++++++++++++++ 8 files changed, 613 insertions(+) create mode 100644 samples/wifi/twt/CMakeLists.txt create mode 100644 samples/wifi/twt/Kconfig create mode 100644 samples/wifi/twt/README.rst create mode 100644 samples/wifi/twt/prj.conf create mode 100644 samples/wifi/twt/sample.yaml create mode 100644 samples/wifi/twt/src/main.c diff --git a/CODEOWNERS b/CODEOWNERS index ec38891bc529..c69e398854d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -186,6 +186,7 @@ Kconfig* @tejlmand /samples/wifi/shell/ @krish2718 @sachinthegreen @rado17 @rlubos /samples/wifi/sta/ @D-Triveni @bama-nordic /samples/wifi/sr_coex/ @muraliThokala @bama-nordic +/samples/wifi/twt/ @chiranjeevi2776 @krish2718 /scripts/ @mbolivar-nordic @tejlmand @nrfconnect/ncs-test-leads /scripts/hid_configurator/ @MarekPieta /scripts/tools-versions-*.txt @tejlmand @grho @shanthanordic @ihansse diff --git a/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst b/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst index cec3fc358a99..a2e9cc285540 100644 --- a/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst +++ b/doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst @@ -480,6 +480,7 @@ Wi-Fi samples * Added :ref:`wifi_wfa_qt_app_sample` that demonstrates how to use the WFA QuickTrack (WFA QT) library needed for Wi-Fi Alliance QuickTrack certification. * Added :ref:`wifi_shutdown_sample` that demonstrates how to configure the Wi-Fi driver to shut down the Wi-Fi hardware when the Wi-Fi interface is not in use. * Added support for the Wi-Fi driver to several upstream Zephyr networking samples. +* Added :ref:`wifi_twt_sample` that demonstrates how to establish TWT flow and transfer data conserving power. Other samples ------------- diff --git a/samples/wifi/twt/CMakeLists.txt b/samples/wifi/twt/CMakeLists.txt new file mode 100644 index 000000000000..46d75e83cd4d --- /dev/null +++ b/samples/wifi/twt/CMakeLists.txt @@ -0,0 +1,16 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(nrf_wifi_twt) + +target_include_directories(app PUBLIC ${ZEPHYR_BASE}/subsys/net/ip) + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/wifi/twt/Kconfig b/samples/wifi/twt/Kconfig new file mode 100644 index 000000000000..e417ef58dd4f --- /dev/null +++ b/samples/wifi/twt/Kconfig @@ -0,0 +1,50 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +source "Kconfig.zephyr" + +menu "Nordic TWT sample" + +config CONNECTION_IDLE_TIMEOUT + int "Time to be waited for a station to connect" + default 30 + +config TWT_SAMPLE_SSID + string "SSID" + help + Specify the SSID to connect + +choice TWT_STA_KEY_MGMT_SELECT + prompt "Security Option" + default TWT_STA_KEY_MGMT_WPA3 + +config TWT_STA_KEY_MGMT_NONE + bool "Open Security" + help + Enable for Open Security + +config TWT_STA_KEY_MGMT_WPA2 + bool "WPA2 Security" + help + Enable for WPA2 Security + +config TWT_STA_KEY_MGMT_WPA2_256 + bool "WPA2 SHA 256 Security" + help + Enable for WPA2-PSK-256 Security + +config TWT_STA_KEY_MGMT_WPA3 + bool "WPA3 Security" + help + Enable for WPA3 Security +endchoice + +config TWT_SAMPLE_PASSWORD + string "Passphrase (WPA2) or password (WPA3)" + help + Specify the Password to connect + +endmenu diff --git a/samples/wifi/twt/README.rst b/samples/wifi/twt/README.rst new file mode 100644 index 000000000000..f4109ee42b75 --- /dev/null +++ b/samples/wifi/twt/README.rst @@ -0,0 +1,177 @@ +.. _wifi_twt_sample: + +Wi-Fi: TWT +########## + +.. contents:: + :local: + :depth: 2 + +The TWT sample demonstrates how to use TWT power save feature. + +Requirements +************ + +The sample supports the following development kit: + +.. table-from-sample-yaml:: + +Overview +******** + +The sample can perform Wi-Fi operations such as connect and disconnect in the 2.4 GHz and 5 GHz bands depending on the capabilities of an access point. + +Using this sample, the development kit can connect to the specified access point in :abbr:`STA (Station)` mode and setup TWT flow with the access point. + +Configuration +************* + +|config| + +You must configure the following Wi-Fi credentials in the :file:`prj.conf` file: + +* Network name (SSID) +* Key management +* Password + +.. note:: + You can also use ``menuconfig`` to enable ``Key management`` option. + +See :ref:`zephyr:menuconfig` in the Zephyr documentation for instructions on how to run ``menuconfig``. + +Configuration options +===================== + +The following application-specific Kconfig option is used in this sample (located in :file:`samples/wifi/twt/Kconfig`) : + +.. options-from-kconfig:: + :show-type: + +IP addressing +************* +The sample uses DHCP to obtain an IP address for the Wi-Fi interface. +It starts with a default static IP address to handle networks without DHCP servers, or if the DHCP server is not available. +Successful DHCP handshake will override the default static IP configuration. + +You can change the following default static configuration in the :file:`prj.conf` file: + +.. code-block:: console + + CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.98" + CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0" + CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1" + +Building and running +******************** + +.. |sample path| replace:: :file:`samples/wifi/twt` + +.. include:: /includes/build_and_run_ns.txt + +Currently, only the nRF7002 DK is supported. + +To build for the nRF7002 DK, use the ``nrf7002dk_nrf5340_cpuapp`` build target. +The following is an example of the CLI command: + +.. code-block:: console + + west build -b nrf7002dk_nrf5340_cpuapp + +Testing +======= + +|test_sample| + +#. |connect_kit| +#. |connect_terminal| + + The sample shows the following output: + + .. code-block:: console + + [00:00:02.016,235] sta: Connection requested + [00:00:02.316,314] sta: ================== + [00:00:02.316,314] sta: State: SCANNING + [00:00:02.616,424] sta: ================== + [00:00:02.616,424] sta: State: SCANNING + [00:00:02.916,534] sta: ================== + [00:00:02.916,534] sta: State: SCANNING + [00:00:03.216,613] sta: ================== + [00:00:03.216,613] sta: State: SCANNING + [00:00:03.516,723] sta: ================== + [00:00:03.516,723] sta: State: SCANNING + [00:00:03.816,802] sta: ================== + [00:00:03.816,802] sta: State: SCANNING + [00:00:04.116,882] sta: ================== + [00:00:04.116,882] sta: State: SCANNING + [00:00:04.416,961] sta: ================== + [00:00:04.416,961] sta: State: SCANNING + [00:00:04.717,071] sta: ================== + [00:00:04.717,071] sta: State: SCANNING + [00:00:05.017,150] sta: ================== + [00:00:05.017,150] sta: State: SCANNING + [00:00:05.317,230] sta: ================== + [00:00:05.317,230] sta: State: SCANNING + [00:00:05.617,309] sta: ================== + [00:00:05.617,309] sta: State: SCANNING + [00:00:05.917,419] sta: ================== + [00:00:05.917,419] sta: State: SCANNING + [00:00:06.217,529] sta: ================== + [00:00:06.217,529] sta: State: SCANNING + [00:00:06.517,639] sta: ================== + [00:00:06.517,639] sta: State: SCANNING + [00:00:06.817,749] sta: ================== + [00:00:06.817,749] sta: State: SCANNING + [00:00:07.117,858] sta: ================== + [00:00:07.117,858] sta: State: SCANNING + [00:00:07.336,730] wpa_supp: wlan0: SME: Trying to authenticate with aa:bb:cc:dd:ee:ff (SSID='' freq=5785 MHz) + [00:00:07.353,027] wifi_nrf: wifi_nrf_wpa_supp_authenticate:Authentication request sent successfully + + [00:00:07.417,938] sta: ================== + [00:00:07.417,938] sta: State: AUTHENTICATING + [00:00:07.606,628] wpa_supp: wlan0: Trying to associate with aa:bb:cc:dd:ee:ff (SSID='' freq=5785 MHz) + [00:00:07.609,680] wifi_nrf: wifi_nrf_wpa_supp_associate: Association request sent successfully + + [00:00:07.621,978] wpa_supp: wpa_drv_zep_get_ssid: SSID size: 5 + + [00:00:07.622,070] wpa_supp: wlan0: Associated with aa:bb:cc:dd:ee:ff + [00:00:07.622,192] wpa_supp: wlan0: CTRL-EVENT-CONNECTED - Connection to aa:bb:cc:dd:ee:ff completed [id=0 id_str=] + [00:00:07.622,192] sta: Connected + [00:00:07.623,779] wpa_supp: wlan0: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0 + [00:00:07.648,406] net_dhcpv4: Received: 192.168.119.6 + [00:00:07.648,468] net_config: IPv4 address: 192.168.119.6 + [00:00:07.648,498] net_config: Lease time: 3599 seconds + [00:00:07.648,498] net_config: Subnet: 255.255.255.0 + [00:00:07.648,529] net_config: Router: 192.168.119.147 + [00:00:07.648,559] sta: DHCP IP address: 192.168.119.6 + [00:00:07.720,153] sta: ================== + [00:00:07.720,153] sta: State: COMPLETED + [00:00:07.720,153] sta: Interface Mode: STATION + [00:00:07.720,184] sta: Link Mode: WIFI 6 (802.11ax/HE) + [00:00:07.720,184] sta: SSID: + [00:00:07.720,214] sta: BSSID: aa:bb:cc:dd:ee:ff + [00:00:07.720,214] sta: Band: 5GHz + [00:00:07.720,214] sta: Channel: 157 + [00:00:07.720,245] sta: Security: OPEN + [00:00:07.720,245] sta: MFP: UNKNOWN + [00:00:07.720,245] sta: RSSI: -57 + [00:00:07.720,245] sta: Static IP address: + +Power management testing +************************ + +You can use this sample to measure the current consumption of both the nRF5340 SoC and the nRF7002 device independently by using two separate Power Profiler Kit II (PPK2) devices. +The nRF5340 SoC is connected to the first PPK2 and the nRF7002 DK is connected to the second PPK2. + +See `Measuring current`_ for more information about how to set up and measure the current consumption of both the nRF5340 SoC and the nRF7002 device. + +The average current consumption in an idle case can be around ~1-2 mA in the nRF5340 SoC and ~20 µA in the nRF7002 device. + +See :ref:`app_power_opt` for more information on power management testing and usage of the PPK2. + +Dependencies +************ + +This sample uses the following library: + +* :ref:`nrf_security` diff --git a/samples/wifi/twt/prj.conf b/samples/wifi/twt/prj.conf new file mode 100644 index 000000000000..7188e8085bee --- /dev/null +++ b/samples/wifi/twt/prj.conf @@ -0,0 +1,88 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +CONFIG_WIFI=y +CONFIG_WIFI_NRF700X=y + +# WPA supplicant +CONFIG_WPA_SUPP=y + +# Below configs need to be modified based on security +# CONFIG_TWT_STA_KEY_MGMT_NONE=y +# CONFIG_TWT_STA_KEY_MGMT_WPA2=y +# CONFIG_TWT_STA_KEY_MGMT_WPA2_256=y +# CONFIG_TWT_STA_KEY_MGMT_WPA3=y +CONFIG_TWT_SAMPLE_SSID="Myssid" +CONFIG_TWT_SAMPLE_PASSWORD="Mypassword" + +# System settings +CONFIG_NEWLIB_LIBC=y +CONFIG_NEWLIB_LIBC_NANO=n + +# Networking +CONFIG_NETWORKING=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_LOG=y +CONFIG_NET_IPV4=y +CONFIG_NET_UDP=y +CONFIG_NET_TCP=y +CONFIG_NET_DHCPV4=y + +CONFIG_NET_PKT_RX_COUNT=8 +CONFIG_NET_PKT_TX_COUNT=8 + +# Below section is the primary contributor to SRAM and is currently +# tuned for performance, but this will be revisited in the future. +CONFIG_NET_BUF_RX_COUNT=16 +CONFIG_NET_BUF_TX_COUNT=16 +CONFIG_NET_BUF_DATA_SIZE=128 +CONFIG_HEAP_MEM_POOL_SIZE=153600 +CONFIG_NET_TC_TX_COUNT=0 + +CONFIG_NET_IF_UNICAST_IPV4_ADDR_COUNT=1 +CONFIG_NET_MAX_CONTEXTS=5 +CONFIG_NET_CONTEXT_SYNC_RECV=y + +CONFIG_INIT_STACKS=y + +CONFIG_NET_L2_ETHERNET=y + +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_INIT_TIMEOUT=0 + +CONFIG_NET_SOCKETS_POLL_MAX=4 + +# Memories +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_NET_TX_STACK_SIZE=4096 +CONFIG_NET_RX_STACK_SIZE=4096 + +# Debugging +CONFIG_STACK_SENTINEL=y +CONFIG_DEBUG_COREDUMP=y +CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y +CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y +CONFIG_SHELL_CMDS_RESIZE=n + + +# Kernel options +CONFIG_ENTROPY_GENERATOR=y + +# Logging +CONFIG_LOG=y +CONFIG_LOG_BUFFER_SIZE=2048 +CONFIG_POSIX_CLOCK=y + +CONFIG_PM=y + +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.99" +CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0" +CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1" + +# printing of scan results puts pressure on queues in new locking +# design in net_mgmt. So, use a higher timeout for a crowded +# environment. +CONFIG_NET_MGMT_EVENT_QUEUE_TIMEOUT=5000 diff --git a/samples/wifi/twt/sample.yaml b/samples/wifi/twt/sample.yaml new file mode 100644 index 000000000000..1d61db6e546c --- /dev/null +++ b/samples/wifi/twt/sample.yaml @@ -0,0 +1,11 @@ +sample: + description: Wi-Fi TWT sample + application + name: Wi-Fi TWT sample +tests: + sample.wifi.twt: + build_only: true + integration_platforms: + - nrf7002dk_nrf5340_cpuapp + platform_allow: nrf7002dk_nrf5340_cpuapp + tags: ci_build diff --git a/samples/wifi/twt/src/main.c b/samples/wifi/twt/src/main.c new file mode 100644 index 000000000000..07aa617a3132 --- /dev/null +++ b/samples/wifi/twt/src/main.c @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** @file + * @brief WiFi TWT sample + */ + +#include +LOG_MODULE_REGISTER(twt, CONFIG_LOG_DEFAULT_LEVEL); + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "net_private.h" + +#define WIFI_SHELL_MODULE "wifi" + +#define WIFI_SHELL_MGMT_EVENTS (NET_EVENT_WIFI_CONNECT_RESULT | \ + NET_EVENT_WIFI_DISCONNECT_RESULT) + +#define MAX_SSID_LEN 32 +#define DHCP_TIMEOUT 70 +#define CONNECTION_TIMEOUT 100 +#define STATUS_POLLING_MS 300 + +static struct net_mgmt_event_callback wifi_shell_mgmt_cb; +static struct net_mgmt_event_callback net_shell_mgmt_cb; + +static struct { + const struct shell *sh; + union { + struct { + uint8_t connected : 1; + uint8_t connect_result : 1; + uint8_t disconnect_requested : 1; + uint8_t _unused : 5; + }; + uint8_t all; + }; +} context; + +static int cmd_wifi_status(void) +{ + struct net_if *iface = net_if_get_default(); + struct wifi_iface_status status = { 0 }; + int ret; + + ret = net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS, iface, &status, + sizeof(struct wifi_iface_status)); + if (ret) { + LOG_INF("Status request failed: %d\n", ret); + return ret; + } + + LOG_INF("=================="); + LOG_INF("State: %s", wifi_state_txt(status.state)); + + if (status.state >= WIFI_STATE_ASSOCIATED) { + uint8_t mac_string_buf[sizeof("xx:xx:xx:xx:xx:xx")]; + + LOG_INF("Interface Mode: %s", + wifi_mode_txt(status.iface_mode)); + LOG_INF("Link Mode: %s", + wifi_link_mode_txt(status.link_mode)); + LOG_INF("SSID: %-32s", status.ssid); + LOG_INF("BSSID: %s", + net_sprint_ll_addr_buf( + status.bssid, WIFI_MAC_ADDR_LEN, + mac_string_buf, sizeof(mac_string_buf))); + LOG_INF("Band: %s", wifi_band_txt(status.band)); + LOG_INF("Channel: %d", status.channel); + LOG_INF("Security: %s", wifi_security_txt(status.security)); + LOG_INF("MFP: %s", wifi_mfp_txt(status.mfp)); + LOG_INF("RSSI: %d", status.rssi); + LOG_INF("TWT: %s", status.twt_capable ? "Supported" : "Not supported"); + } + return 0; +} + +static void handle_wifi_connect_result(struct net_mgmt_event_callback *cb) +{ + const struct wifi_status *status = + (const struct wifi_status *) cb->info; + + if (context.connected) { + return; + } + + if (status->status) { + LOG_ERR("Connection failed (%d)", status->status); + } else { + LOG_INF("Connected"); + context.connected = true; + } + + context.connect_result = true; +} + +static void handle_wifi_disconnect_result(struct net_mgmt_event_callback *cb) +{ + const struct wifi_status *status = + (const struct wifi_status *) cb->info; + + if (!context.connected) { + return; + } + + if (context.disconnect_requested) { + LOG_INF("Disconnection request %s (%d)", + status->status ? "failed" : "done", + status->status); + context.disconnect_requested = false; + } else { + LOG_INF("Received Disconnected"); + context.connected = false; + } + + cmd_wifi_status(); +} + +static void wifi_mgmt_event_handler(struct net_mgmt_event_callback *cb, + uint32_t mgmt_event, struct net_if *iface) +{ + switch (mgmt_event) { + case NET_EVENT_WIFI_CONNECT_RESULT: + handle_wifi_connect_result(cb); + break; + case NET_EVENT_WIFI_DISCONNECT_RESULT: + handle_wifi_disconnect_result(cb); + break; + default: + break; + } +} + +static void print_dhcp_ip(struct net_mgmt_event_callback *cb) +{ + /* Get DHCP info from struct net_if_dhcpv4 and print */ + const struct net_if_dhcpv4 *dhcpv4 = cb->info; + const struct in_addr *addr = &dhcpv4->requested_ip; + char dhcp_info[128]; + + net_addr_ntop(AF_INET, addr, dhcp_info, sizeof(dhcp_info)); + + LOG_INF("DHCP IP address: %s", dhcp_info); +} + +static void net_mgmt_event_handler(struct net_mgmt_event_callback *cb, + uint32_t mgmt_event, struct net_if *iface) +{ + switch (mgmt_event) { + case NET_EVENT_IPV4_DHCP_BOUND: + print_dhcp_ip(cb); + break; + default: + break; + } +} + +static int __wifi_args_to_params(struct wifi_connect_req_params *params) +{ + params->timeout = SYS_FOREVER_MS; + + /* SSID */ + params->ssid = CONFIG_TWT_SAMPLE_SSID; + params->ssid_length = strlen(params->ssid); + +#if defined(CONFIG_TWT_STA_KEY_MGMT_WPA2) + params->security = 1; +#elif defined(CONFIG_TWT_STA_KEY_MGMT_WPA2_256) + params->security = 2; +#elif defined(CONFIG_TWT_STA_KEY_MGMT_WPA3) + params->security = 3; +#else + params->security = 0; +#endif + +#if !defined(CONFIG_TWT_STA_KEY_MGMT_NONE) + params->psk = CONFIG_TWT_SAMPLE_PASSWORD; + params->psk_length = strlen(params->psk); +#endif + params->channel = WIFI_CHANNEL_ANY; + + /* MFP (optional) */ + params->mfp = WIFI_MFP_OPTIONAL; + + return 0; +} + +static int wifi_connect(void) +{ + struct net_if *iface = net_if_get_default(); + static struct wifi_connect_req_params cnx_params; + + context.connected = false; + context.connect_result = false; + __wifi_args_to_params(&cnx_params); + + if (net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, + &cnx_params, sizeof(struct wifi_connect_req_params))) { + LOG_ERR("Connection request failed"); + + return -ENOEXEC; + } + + LOG_INF("Connection requested"); + + return 0; +} + +int main(void) +{ + int i; + + memset(&context, 0, sizeof(context)); + + net_mgmt_init_event_callback(&wifi_shell_mgmt_cb, + wifi_mgmt_event_handler, + WIFI_SHELL_MGMT_EVENTS); + + net_mgmt_add_event_callback(&wifi_shell_mgmt_cb); + + net_mgmt_init_event_callback(&net_shell_mgmt_cb, + net_mgmt_event_handler, + NET_EVENT_IPV4_DHCP_BOUND); + + net_mgmt_add_event_callback(&net_shell_mgmt_cb); + + LOG_INF("Starting %s with CPU frequency: %d MHz", CONFIG_BOARD, SystemCoreClock/MHZ(1)); + k_sleep(K_SECONDS(1)); + + LOG_INF("Static IP address (overridable): %s/%s -> %s", + CONFIG_NET_CONFIG_MY_IPV4_ADDR, + CONFIG_NET_CONFIG_MY_IPV4_NETMASK, + CONFIG_NET_CONFIG_MY_IPV4_GW); + + while (1) { + + wifi_connect(); + + for (i = 0; i < CONNECTION_TIMEOUT; i++) { + k_sleep(K_MSEC(STATUS_POLLING_MS)); + cmd_wifi_status(); + if (context.connect_result) { + break; + } + } + if (context.connected) { + k_sleep(K_FOREVER); + } else if (!context.connect_result) { + LOG_ERR("Connection Timed Out"); + } + } + + return 0; +}