diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9868e18 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: ["**"] + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Conan + id: conan + uses: turtlebrowser/get-conan@main + + - name: Conan version + run: echo "${{ steps.conan.outputs.version }}" + + + - name: Create default Conan profile + run: conan profile detect + + - name: Install Rust toolchain + run: rustup component add rustfmt clippy + + - name: Create up-cpp Conan package + shell: bash + run: | + git clone https://github.com/eclipse-uprotocol/up-cpp.git + cd up-cpp + git clone -b uprotocol-core-api-1.5.6 https://github.com/eclipse-uprotocol/up-core-api.git + git submodule update --init --recursive + conan create . --build=missing + + - name: Build and install Zenoh-C + shell: bash + run: | + git clone https://github.com/eclipse-zenoh/zenoh-c.git + cd zenoh-c && mkdir -p build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + cmake --build . --target install --config Release -- -j + + - name: Create up-client-zenoh-cpp Conan package + shell: bash + run: | + git clone https://github.com/eclipse-uprotocol/up-client-zenoh-cpp.git + cd up-client-zenoh-cpp + conan create . --build=missing + + - name: Build up-zenoh-example-cpp + shell: bash + run: | + mkdir build_samples + cd build_samples + conan install ../ + cmake ../ -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release + cmake --build . + + + # NOTE: In GitHub repository settings, the "Require status checks to pass + # before merging" branch protection rule ensures that commits are only merged + # from branches where specific status checks have passed. These checks are + # specified manually as a list of workflow job names. Thus we use this extra + # job to signal whether all CI checks have passed. + ci: + name: CI status checks + runs-on: ubuntu-latest + needs: build + if: always() + steps: + - name: Check whether all jobs pass + run: echo '${{ toJson(needs) }}' | jq -e 'all(.result == "success")' diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bc85302 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (c) 2024 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2024 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) +project(samples VERSION 0.1.0 LANGUAGES CXX) + +add_subdirectory(pubsub) +add_subdirectory(rpc) diff --git a/README.md b/README.md index aec3e87..ba1310a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # up-zenoh-example-cpp C++ Example application and service that utilizes up-client-zenoh-cpp + +## Getting Started +### Requirements: +- Compiler: GCC/G++ 11 or Clang 13 +- Ubuntu 22.04 +- conan : 1.59 or latest 2.X + +#### up-client-zenoh-cpp dependencies + +1. install up-client-zenoh-cpp library https://github.com/eclipse-uprotocol/up-client-zenoh-cpp + +## How to Build +``` +$ cd up-zenoh-example-cpp +$ mkdir build +$ cd build + +$ conan install .. +$ cmake -S .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release +$ cmake --build . +``` diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..571da94 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,32 @@ +from conan import ConanFile + +class HelloWorldRecipe(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps", "CMakeToolchain", "PkgConfigDeps", "VirtualRunEnv", "VirtualBuildEnv" + options = { + "shared": [True, False], + "fPIC": [True, False], + "build_testing": [True, False], + "build_unbundled": [True, False], + "build_cross_compiling": [True, False], + } + + default_options = { + "shared": False, + "fPIC": False, + "build_testing": False, + "build_unbundled": True, + "build_cross_compiling": False, + } + + # def configure(self): + # self.options["spdlog"].shared = self.options.shared + + def requirements(self): + self.requires("up-client-zenoh-cpp/0.1.0-dev") + self.requires("protobuf/3.21.12" + ("@cross/cross" if self.options.build_cross_compiling else "")) + + + def imports(self): + if self.options.build_testing: + self.copy("*.so*", dst="lib", keep_path=False) diff --git a/pubsub/CMakeLists.txt b/pubsub/CMakeLists.txt new file mode 100644 index 0000000..6cb9d8e --- /dev/null +++ b/pubsub/CMakeLists.txt @@ -0,0 +1,38 @@ +# Copyright (c) 2024 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2024 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) +project(pubsub VERSION 0.1.0 LANGUAGES CXX) + +find_package(up-client-zenoh-cpp REQUIRED) + +# sub +add_executable(sub src/main_sub.cpp) +target_link_libraries(sub + PRIVATE + up-client-zenoh-cpp::up-client-zenoh-cpp) + +# pub +add_executable(pub src/main_pub.cpp) +target_link_libraries(pub + PRIVATE + up-client-zenoh-cpp::up-client-zenoh-cpp) diff --git a/pubsub/src/main_pub.cpp b/pubsub/src/main_pub.cpp new file mode 100644 index 0000000..90219c1 --- /dev/null +++ b/pubsub/src/main_pub.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 General Motors GTO LLC + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * SPDX-FileType: SOURCE + * SPDX-FileCopyrightText: 2024 General Motors GTO LLC + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include // For sleep + +#include +#include +#include +#include +#include +#include + +using namespace uprotocol::utransport; +using namespace uprotocol::uri; +using namespace uprotocol::uuid; +using namespace uprotocol::v1; + +const std::string TIME_URI_STRING = "/test.app/1/milliseconds"; +const std::string RANDOM_URI_STRING = "/test.app/1/32bit"; +const std::string COUNTER_URI_STRING = "/test.app/1/counter"; + +bool gTerminate = false; + +void signalHandler(int signal) { + if (signal == SIGINT) { + std::cout << "Ctrl+C received. Exiting..." << std::endl; + gTerminate = true; + } +} + +std::uint8_t* getTime() { + + auto currentTime = std::chrono::system_clock::now(); + auto duration = currentTime.time_since_epoch(); + auto timeMilli = std::chrono::duration_cast(duration).count(); + static std::uint8_t buf[8]; + std::memcpy(buf, &timeMilli, sizeof(timeMilli)); + + return buf; +} + +std::uint8_t* getRandom() { + + int32_t val = std::rand(); + static std::uint8_t buf[4]; + std::memcpy(buf, &val, sizeof(val)); + + return buf; +} + +std::uint8_t* getCounter() { + + static std::uint8_t counter = 0; + ++counter; + + return &counter; +} + +UCode sendMessage(ZenohUTransport *transport, + UUri &uri, + std::uint8_t *buffer, + size_t size) { + + auto uuid = Uuidv8Factory::create(); + + UAttributesBuilder builder(uuid, UMessageType::PUBLISH, UPriority::STANDARD); + UAttributes attributes = builder.build(); + + UPayload payload(buffer, size, UPayloadType::VALUE); + + UStatus status = transport->send(uri, payload, attributes); + if (UCode::OK != status.code()) { + spdlog::error("send.send failed"); + return UCode::UNAVAILABLE; + } + return UCode::OK; +} + +/* The sample pub applications demonstrates how to send data using uTransport - + * There are three topics that are published - random number, current time and a counter */ +int main(int argc, char **argv) { + + signal(SIGINT, signalHandler); + + UStatus status; + ZenohUTransport *transport = &ZenohUTransport::instance(); + + /* Initialize zenoh utransport */ + status = transport->init(); + if (UCode::OK != status.code()) { + spdlog::error("ZenohUTransport init failed"); + return -1; + } + + /* Create URI objects from string URI*/ + auto timeUri = LongUriSerializer::deserialize(TIME_URI_STRING); + auto randomUri = LongUriSerializer::deserialize(RANDOM_URI_STRING); + auto counterUri = LongUriSerializer::deserialize(COUNTER_URI_STRING); + + while (!gTerminate) { + /* send current time in milliseconds */ + if (UCode::OK != sendMessage(transport, timeUri, getTime(), 8)) { + spdlog::error("sendMessage failed"); + break; + } + + /* send random number */ + if (UCode::OK != sendMessage(transport, randomUri, getRandom(), 4)) { + spdlog::error("sendMessage failed"); + break; + } + + /* send counter */ + if (UCode::OK != sendMessage(transport, counterUri, getCounter(), 1)) { + spdlog::error("sendMessage failed"); + break; + } + + sleep(1); + } + + /* Terminate zenoh utransport */ + status = transport->term(); + if (UCode::OK != status.code()) { + spdlog::error("ZenohUTransport term failed"); + return -1; + } + + return 0; +} diff --git a/pubsub/src/main_sub.cpp b/pubsub/src/main_sub.cpp new file mode 100644 index 0000000..3d7cb71 --- /dev/null +++ b/pubsub/src/main_sub.cpp @@ -0,0 +1,148 @@ + +/* + * Copyright (c) 2024 General Motors GTO LLC + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * SPDX-FileType: SOURCE + * SPDX-FileCopyrightText: 2024 General Motors GTO LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include // For sleep +#include +#include + +using namespace uprotocol::utransport; +using namespace uprotocol::uri; + +const std::string TIME_URI_STRING = "/test.app/1/milliseconds"; +const std::string RANDOM_URI_STRING = "/test.app/1/32bit"; +const std::string COUNTER_URI_STRING = "/test.app/1/counter"; + +bool gTerminate = false; + +void signalHandler(int signal) { + if (signal == SIGINT) { + std::cout << "Ctrl+C received. Exiting..." << std::endl; + gTerminate = true; + } +} + +class CustomListener : public UListener { + + public: + /* in this example the same onReceive callback implementation is used to receive + * the three different messages , each message is differntiated by the URI + * it is possible to provide a different onReceive callback for each topic */ + UStatus onReceive(const UUri& uri, + const UPayload& payload, + const UAttributes& attributes) const override { + + if (TIME_URI_STRING == LongUriSerializer::serialize(uri)) { + + const uint64_t *timeInMilliseconds = reinterpret_cast(payload.data()); + + spdlog::info("time = {}", *timeInMilliseconds); + + } else if (RANDOM_URI_STRING == LongUriSerializer::serialize(uri)) { + + const uint32_t *random = reinterpret_cast(payload.data()); + + spdlog::info("random = {}", *random); + + } else if (COUNTER_URI_STRING == LongUriSerializer::serialize(uri)) { + + const uint8_t *counter = reinterpret_cast(payload.data()); + + spdlog::info("counter = {}", *counter); + } + + UStatus status; + + status.set_code(UCode::OK); + + return status; + } +}; + +/* The sample sub applications demonstrates how to consume data using uTransport - + * There are three topics that are received - random number, current time and a counter */ +int main(int argc, char** argv) { + + signal(SIGINT, signalHandler); + + UStatus status; + ZenohUTransport *transport = &ZenohUTransport::instance(); + + /* init zenoh utransport */ + status = transport->init(); + if (UCode::OK != status.code()){ + spdlog::error("ZenohUTransport init failed"); + return -1; + } + + std::vector> listeners; + listeners.emplace_back(std::make_unique()); + listeners.emplace_back(std::make_unique()); + listeners.emplace_back(std::make_unique()); + + const std::vector uriStrings = { + TIME_URI_STRING, + RANDOM_URI_STRING, + COUNTER_URI_STRING + }; + + /* create URI objects from URI strings */ + std::vector uris; + for (const auto& uriString : uriStrings) { + uris.push_back(LongUriSerializer::deserialize(uriString)); + } + + /* register listeners - in this example the same listener is used for three seperate topics */ + for (size_t i = 0; i < uris.size(); ++i) { + status = transport->registerListener(uris[i], *listeners[i]); + if (UCode::OK != status.code()){ + spdlog::error("registerListener failed for {}", uriStrings[i]); + return -1; + } + } + + while (!gTerminate) { + sleep(1); + } + + for (size_t i = 0; i < uris.size(); ++i) { + status = transport->unregisterListener(uris[i], *listeners[i]); + if (UCode::OK != status.code()){ + spdlog::error("unregisterListener failed for {}", uriStrings[i]); + return -1; + } + } + + /* term zenoh utransport */ + status = transport->term(); + if (UCode::OK != status.code()) { + spdlog::error("ZenohUTransport term failed"); + return -1; + } + + return 0; +} diff --git a/rpc/CMakeLists.txt b/rpc/CMakeLists.txt new file mode 100644 index 0000000..d3b7a00 --- /dev/null +++ b/rpc/CMakeLists.txt @@ -0,0 +1,38 @@ +# Copyright (c) 2024 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2024 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) +project(rpc VERSION 0.1.0 LANGUAGES CXX) + +find_package(up-client-zenoh-cpp REQUIRED) + +# rpc server +add_executable(rpc_server src/main_rpc_server.cpp) +target_link_libraries(rpc_server + PRIVATE + up-client-zenoh-cpp::up-client-zenoh-cpp) + +# rpc client +add_executable(rpc_client src/main_rpc_client.cpp) +target_link_libraries(rpc_client + PRIVATE + up-client-zenoh-cpp::up-client-zenoh-cpp) diff --git a/rpc/src/main_rpc_client.cpp b/rpc/src/main_rpc_client.cpp new file mode 100644 index 0000000..a7f7910 --- /dev/null +++ b/rpc/src/main_rpc_client.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 General Motors GTO LLC + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * SPDX-FileType: SOURCE + * SPDX-FileCopyrightText: 2024 General Motors GTO LLC + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace uprotocol::utransport; +using namespace uprotocol::uuid; +using namespace uprotocol::uri; + +bool gTerminate = false; + +void signalHandler(int signal) { + if (signal == SIGINT) { + std::cout << "Ctrl+C received. Exiting..." << std::endl; + gTerminate = true; + } +} + +UPayload sendRPC(UUri& uri) { + + auto uuid = Uuidv8Factory::create(); + + UAttributesBuilder builder(uuid, UMessageType::REQUEST, UPriority::STANDARD); + UAttributes attributes = builder.build(); + + constexpr uint8_t BUFFER_SIZE = 1; + uint8_t buffer[BUFFER_SIZE] = {0}; + + UPayload payload(buffer, sizeof(buffer), UPayloadType::VALUE); + /* send the RPC request , a future is returned from invokeMethod */ + std::future result = ZenohRpcClient::instance().invokeMethod(uri, payload, attributes); + + if (!result.valid()) { + spdlog::error("Future is invalid"); + return UPayload(nullptr, 0, UPayloadType::UNDEFINED); + } + /* wait for the future to be fullfieled - it is possible also to specify a timeout for the future */ + result.wait(); + + return result.get(); +} + +/* The sample RPC client applications demonstrates how to send RPC requests and wait for the response - + * The response in this example will be the current time */ +int main(int argc, char** argv) { + + signal(SIGINT, signalHandler); + + UStatus status; + ZenohRpcClient *rpcClient = &ZenohRpcClient::instance(); + + /* init RPC client */ + status = rpcClient->init(); + if (UCode::OK != status.code()) { + spdlog::error("init failed"); + return -1; + } + + auto rpcUri = LongUriSerializer::deserialize("/test_rpc.app/1/rpc.milliseconds"); + + while (!gTerminate) { + + auto response = sendRPC(rpcUri); + + uint64_t milliseconds = 0; + + if (response.data() != nullptr && response.size() >= sizeof(uint64_t)) { + memcpy(&milliseconds, response.data(), sizeof(uint64_t)); + spdlog::info("Received = {}", milliseconds); + } + + sleep(1); + } + + /* term RPC client */ + status = rpcClient->term(); + if (UCode::OK != status.code()) { + spdlog::error("term failed"); + return -1; + } + + return 0; +} diff --git a/rpc/src/main_rpc_server.cpp b/rpc/src/main_rpc_server.cpp new file mode 100644 index 0000000..32660fc --- /dev/null +++ b/rpc/src/main_rpc_server.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 General Motors GTO LLC + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * SPDX-FileType: SOURCE + * SPDX-FileCopyrightText: 2024 General Motors GTO LLC + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace uprotocol::utransport; +using namespace uprotocol::uuid; +using namespace uprotocol::uri; + +bool gTerminate = false; + +void signalHandler(int signal) { + if (signal == SIGINT) { + std::cout << "Ctrl+C received. Exiting..." << std::endl; + gTerminate = true; + } +} + +class RpcListener : public UListener { + + public: + UStatus onReceive(const UUri& uri, + const UPayload& payload, + const UAttributes& attributes) const override { + + /* Construct response payload with the current time */ + auto currentTime = std::chrono::system_clock::now(); + auto duration = currentTime.time_since_epoch(); + uint64_t currentTimeMilli = std::chrono::duration_cast(duration).count(); + + UPayload responsePayload(reinterpret_cast(¤tTimeMilli), sizeof(currentTimeMilli), UPayloadType::VALUE); + + /* Build response attributes - the same UUID should be used to send the response + * it is also possible to send the response outside of the callback context */ + UAttributesBuilder builder(attributes.id(), UMessageType::RESPONSE, UPriority::STANDARD); + UAttributes responseAttributes = builder.build(); + + /* Send the response */ + return ZenohUTransport::instance().send(uri, responsePayload, responseAttributes); + } +}; + +/* The sample RPC server applications demonstrates how to receive RPC requests and send a response back to the client - + * The response in this example will be the current time */ +int main(int argc, char** argv) { + + RpcListener listener; + + signal(SIGINT, signalHandler); + + UStatus status; + ZenohUTransport *transport = &ZenohUTransport::instance(); + + /* init zenoh utransport */ + status = transport->init(); + if (UCode::OK != status.code()) { + spdlog::error("ZenohUTransport init failed"); + return -1; + } + + auto rpcUri = LongUriSerializer::deserialize("/test_rpc.app/1/rpc.milliseconds"); + + /* register listener to handle RPC requests */ + status = transport->registerListener(rpcUri, listener); + if (UCode::OK != status.code()) { + spdlog::error("registerListener failed"); + return -1; + } + + while (!gTerminate) { + sleep(1); + } + + status = transport->unregisterListener(rpcUri, listener); + if (UCode::OK != status.code()) { + spdlog::error("unregisterListener failed"); + return -1; + } + + /* term zenoh utransport */ + status = transport->term(); + if (UCode::OK != status.code()) { + spdlog::error("ZenohUTransport term failed"); + return -1; + } + + return 0; +}