diff --git a/src/viam/examples/modules/CMakeLists.txt b/src/viam/examples/modules/CMakeLists.txt index 30723bd06..1714eb933 100644 --- a/src/viam/examples/modules/CMakeLists.txt +++ b/src/viam/examples/modules/CMakeLists.txt @@ -14,3 +14,4 @@ add_subdirectory(tflite) add_subdirectory(simple) +add_subdirectory(complex) diff --git a/src/viam/examples/modules/complex/CMakeLists.txt b/src/viam/examples/modules/complex/CMakeLists.txt new file mode 100644 index 000000000..0d5e1d8c0 --- /dev/null +++ b/src/viam/examples/modules/complex/CMakeLists.txt @@ -0,0 +1,150 @@ +# Copyright 2023 Viam Inc. +# +# 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. + +# Much of this `CMakeLists.txt` file is dedicated to generating the correct C++ +# files from `proto/gizmo.proto` and proto/summation.proto`. We use `buf +# generate` with a custom `buf.gen.yaml` (there are two versions in the config +# directory depending on whether you've opted for offline or online proto +# generation), add the generated files as sources for our executables, and +# `target_include` the directory to get `#include` statements to work. +# +# Note that this `CMakeLists.txt` file is part of the larger cmake system for +# the Viam C++ SDK and you'll have to adjust the logic below based on your +# local build system setup. + +set(MODULE_PROTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto) +set(MODULE_PROTO_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/gen) +set(MODULE_PROTO_OUTPUT_FILES + ${MODULE_PROTO_GEN_DIR}/gizmo.grpc.pb.cc + ${MODULE_PROTO_GEN_DIR}/gizmo.grpc.pb.h + ${MODULE_PROTO_GEN_DIR}/gizmo.pb.cc + ${MODULE_PROTO_GEN_DIR}/gizmo.pb.h + ${MODULE_PROTO_GEN_DIR}/summation.grpc.pb.cc + ${MODULE_PROTO_GEN_DIR}/summation.grpc.pb.h + ${MODULE_PROTO_GEN_DIR}/summation.pb.cc + ${MODULE_PROTO_GEN_DIR}/summation.pb.h +) + +# Look for the `buf` command in the usual places, and use it if found. If we +# can't find it, try to download it and use that. +find_program(BUF_COMMAND buf) +if (NOT BUF_COMMAND) + file( + DOWNLOAD + https://github.com/bufbuild/buf/releases/latest/download/buf-${CMAKE_HOST_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR} + ${CMAKE_CURRENT_BINARY_DIR}/buf_latest + STATUS buf_status + ) + list(GET buf_status 0 buf_status_code) + list(GET buf_status 1 buf_status_string) + + if(NOT buf_status_code EQUAL 0) + message(FATAL_ERROR "No local `buf` program found (try setting PATH?) and failed to download: ${buf_status_string}") + endif() + + set(BUF_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/buf_latest) + file(CHMOD ${BUF_COMMAND} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +endif() + +if ((NOT VIAMCPPSDK_OFFLINE_PROTO_GENERATION) AND (VIAMCPPSDK_GRPCXX_VERSION VERSION_GREATER 1.51.1)) + configure_file( + config/buf.gen.remote.plugin.yaml.in + buf.gen.yaml + ) +else() + configure_file( + config/buf.gen.local.yaml.in + buf.gen.yaml + ) +endif() + +add_custom_command( + OUTPUT + # Unfortunately, there isn't a good way to know in advance what files will be + # generated by invoking `buf generate`. Instead, we just include all proto + # output files here that we know we need in the `add_executable` calls below. + ${MODULE_PROTO_OUTPUT_FILES} + + # We must run `buf mod update` before generating to download google APIs. + COMMAND ${BUF_COMMAND} mod update ${MODULE_PROTO_DIR} + COMMAND ${BUF_COMMAND} generate ${MODULE_PROTO_DIR} --template buf.gen.yaml + MAIN_DEPENDENCY buf.gen.yaml +) + +add_custom_target( + generate_complex_module_protos + # This must be one of the files listed in `add_custom_command` above, but it + # doesn't matter which one, we just need to have a dependency edge into the + # files produced by that command. + DEPENDS ${MODULE_PROTO_GEN_DIR}/gizmo.grpc.pb.cc +) + +add_executable(complex_module) +# We have to include the generated proto files in order to #include the files. +target_include_directories(complex_module + PUBLIC + ${MODULE_PROTO_GEN_DIR} +) +target_sources(complex_module + PRIVATE + main.cpp + base/impl.cpp + base/impl.hpp + gizmo/impl.cpp + gizmo/impl.hpp + gizmo/api.cpp + gizmo/api.hpp + summation/api.cpp + summation/api.hpp + summation/impl.cpp + summation/impl.hpp + + ${MODULE_PROTO_OUTPUT_FILES} +) + +add_executable(complex_module_client) +# We have to include the generated proto files in order to #include the files. +target_include_directories(complex_module_client + PUBLIC + ${MODULE_PROTO_GEN_DIR} +) +target_sources(complex_module_client + PRIVATE + client.cpp + gizmo/api.hpp + gizmo/api.cpp + summation/api.hpp + summation/api.cpp + + ${MODULE_PROTO_OUTPUT_FILES} +) + +target_link_libraries(complex_module + PRIVATE Threads::Threads + viam-cpp-sdk::viamsdk +) + +target_link_libraries(complex_module_client + viam-cpp-sdk::viamsdk +) + +install( + TARGETS complex_module + COMPONENT examples +) + +install( + TARGETS complex_module_client + COMPONENT examples +) diff --git a/src/viam/examples/modules/complex/README.md b/src/viam/examples/modules/complex/README.md new file mode 100644 index 000000000..77cf36dbe --- /dev/null +++ b/src/viam/examples/modules/complex/README.md @@ -0,0 +1,89 @@ +# VIAM Complex Module Example +This example goes through how to create custom modular resources using Viam's C++ SDK and how to connect them to a Robot. + +This is a limited document. For a more in-depth understanding of modules, see the [documentation](https://docs.viam.com/program/extend/modular-resources/). + +## Purpose +Modular resources allow you to define custom components and services, and add them to your robot. Viam ships with many component types, but you're not limited to only using those types -- you can create your own using modules. + +For more information, see the [documentation](https://docs.viam.com/program/extend/modular-resources/). For a simpler example, take a look at the [simple module example](https://github.com/viamrobotics/viam-cpp-sdk/tree/main/src/viam/examples/modules/simple), which only contains one custom resource model in one file. + +For a fully fleshed-out example of a C++ module that uses Github CI to upload to the Viam Registry, take a look at [module-example-cpp](https://github.com/viamrobotics/module-example-cpp). For a list of example modules in different Viam SDKs, take a look [here](https://github.com/viamrobotics/upload-module/#example-repos). + +## Project structure +The complex module example defines three new resources: a Gizmo component, a Summation service, and a custom Base component. + +The `proto` directory contains the `gizmo.proto` and `summation.proto` definitions of all the message types and calls that can be made to the Gizmo component and Summation service. These proto files are compiled automatically with `buf` through our cmake build system generation. See the `CMakeLists.txt` file in this directory for more information. + +The `config` directory contains two buf.gen.yaml files that are used for compiling the files in `proto` to C++. `config/buf.gen.local.yaml.in` is used when offline generation is enabled and `config/buf.gen.remote.plugin.yaml.in` is used when offline proto generation is disabled. Offline proto generation is enabled by default and can be set with `VIAMCPPSDK_OFFLINE_PROTO_GENERATION`. + +The `gizmo` directory contains all the necessary definitions for creating a custom `Gizmo` component type. `api.cpp` and `api.hpp` define what a `Gizmo` can do (mirroring the `proto` definition), implement the gRPC `GizmoServer` for receiving calls, and implement the gRPC `GizmoClient` for making calls. See the [API docs](https://docs.viam.com/program/extend/modular-resources/#apis) for more info. `impl.cpp` and `impl.hpp` contain the unique implementation of a `Gizmo`. This is defined as a specific `Model`. See the [Model docs](https://docs.viam.com/program/extend/modular-resources/#models) for more info. + +Similarly, the `summation` directory contains the analogous definitions for the `Summation` service type. The files in this directory mirror the files in the `gizmo` directory. + +The `base` directory contains all the necessary definitions for creating a custom modular `Base` component type. Since it is inheriting an already existing component supported by the Viam SDK, there is no need for `api` files. + +In the main directory, there is also a `main.cpp` file, which creates a module, registers the above resources, and starts the module. It also handles SIGTERM and SIGINT OS signals using the `SignalManager` class. Read further to learn how to connect this module to your robot. + +Finally, the `client.cpp` file can be used to test the module once you have connected to your robot and configured it. You will have to update the credentials and robot address in that file before building. After building, the `build/install/bin/complex_module_client` generated binary can be called to run the client. + +## Configuring and using the module + +The `complex_module` binary generated after building is the entrypoint for this module. To connect this module with your robot, you must add this module's entrypoint to the robot's config. For example, the entrypoint file may be at `/home/viam-cpp-sdk/build/install/bin/complex_module` and you must add this file path to your configuration. See the [documentation](https://docs.viam.com/program/extend/modular-resources/#use-a-modular-resource-with-your-robot) for more details. + +Once the module has been added to your robot, add a `gizmo` component that uses the `viam:gizmo:mygizmo` model. See the [documentation](https://docs.viam.com/program/extend/modular-resources/#configure-a-component-instance-for-a-modular-resource) for more details. You can also add a `summation` service that uses the `viam:summation:mysummation` model and a `base` component that uses the `viam:base:mybase` model in a similar manner. + +An example configuration for a gizmo component, a summation service, and a base component could look like this: +```json +{ + "components": [ + { + "name": "gizmo1", + "type": "gizmo", + "namespace": "viam", + "model": "viam:gizmo:mygizmo", + "attributes": { + "arg1": "arg1", + "motor": "motor1" + } + }, + { + "name": "base1", + "type": "base", + "namespace": "viam", + "model": "viam:base:mybase", + "attributes": { + "left": "motor1", + "right": "motor2" + } + }, + { + "name": "motor1", + "type": "motor", + "model": "fake" + }, + { + "name": "motor2", + "type": "motor", + "model": "fake" + } + ], + "services": [ + { + "name": "mysum1", + "type": "summation", + "namespace": "viam", + "model": "viam:summation:mysummation", + "attributes": { + "subtract": false + } + } + ], + "modules": [ + { + "name": "MyModule", + "executable_path": "/home/viam-cpp-sdk/build/install/bin/complex_module" + } + ] +} +``` diff --git a/src/viam/examples/modules/complex/base/impl.cpp b/src/viam/examples/modules/complex/base/impl.cpp new file mode 100644 index 000000000..487fefe07 --- /dev/null +++ b/src/viam/examples/modules/complex/base/impl.cpp @@ -0,0 +1,111 @@ +#include "impl.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +using namespace viam::sdk; + +std::string find_motor(ResourceConfig cfg, std::string motor_name) { + auto base_name = cfg.name(); + auto motor = cfg.attributes()->find(motor_name); + if (motor == cfg.attributes()->end()) { + std::ostringstream buffer; + buffer << base_name << ": Required parameter `" << motor_name + << "` not found in configuration"; + throw std::invalid_argument(buffer.str()); + } + const auto* const motor_string = motor->second->get(); + if (!motor_string || motor_string->empty()) { + std::ostringstream buffer; + buffer << base_name << ": Required non-empty string parameter `" << motor_name + << "` is either not a string " + "or is an empty string"; + throw std::invalid_argument(buffer.str()); + } + return *motor_string; +} + +void MyBase::reconfigure(Dependencies deps, ResourceConfig cfg) { + // Downcast `left` and `right` dependencies to motors. + auto left = find_motor(cfg, "left"); + auto right = find_motor(cfg, "right"); + for (const auto& kv : deps) { + if (kv.first.short_name() == left) { + left_ = std::dynamic_pointer_cast(kv.second); + } + if (kv.first.short_name() == right) { + right_ = std::dynamic_pointer_cast(kv.second); + } + } +} + +std::vector MyBase::validate(ResourceConfig cfg) { + // Custom validation can be done by specifying a validate function at the + // time of resource registration (see complex/main.cpp) like this one. + // Validate functions can `throw` exceptions that will be returned to the + // parent through gRPC. Validate functions can also return a vector of + // strings representing the implicit dependencies of the resource. + // + // Here, we return the names of the "left" and "right" motors as found in + // the attributes as implicit dependencies of the base. + return {find_motor(cfg, "left"), find_motor(cfg, "right")}; +} + +bool MyBase::is_moving() { + return left_->is_moving() || right_->is_moving(); +} + +grpc::StatusCode MyBase::stop(const AttributeMap& extra) { + auto left_stop = left_->stop(extra); + auto right_stop = right_->stop(extra); + + // Return first of any non-ok error code received from motors. + if (left_stop != grpc::StatusCode::OK) { + return left_stop; + } + if (right_stop != grpc::StatusCode::OK) { + return right_stop; + } + return grpc::StatusCode::OK; +} + +void MyBase::set_power(const Vector3& linear, const Vector3& angular, const AttributeMap& extra) { + // Stop the base if absolute value of linear and angular velocity is less + // than 0.01. + if (abs(linear.y()) < 0.01 && abs(angular.z()) < 0.01) { + stop(extra); // ignore returned status code from stop + return; + } + + // Use linear and angular velocity to calculate percentage of max power to + // pass to set_power for left & right motors + auto sum = abs(linear.y()) + abs(angular.z()); + left_->set_power(((linear.y() - angular.z()) / sum), extra); + right_->set_power(((linear.y() + angular.z()) / sum), extra); +} + +AttributeMap MyBase::do_command(const AttributeMap& command) { + std::cout << "Received DoCommand request for MyBase " << Resource::name() << std::endl; + return command; +} + +std::vector MyBase::get_geometries(const AttributeMap& extra) { + auto left_geometries = left_->get_geometries(extra); + auto right_geometries = right_->get_geometries(extra); + std::vector geometries(left_geometries); + geometries.insert(geometries.end(), right_geometries.begin(), right_geometries.end()); + return geometries; +} + +Base::properties MyBase::get_properties(const AttributeMap& extra) { + // Return fake properties. + return {2, 4, 8}; +} diff --git a/src/viam/examples/modules/complex/base/impl.hpp b/src/viam/examples/modules/complex/base/impl.hpp new file mode 100644 index 000000000..f6e933566 --- /dev/null +++ b/src/viam/examples/modules/complex/base/impl.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +using namespace viam::sdk; + +// `MyBase` inherits from the `Base` class defined in the viam C++ SDK and +// implements some of the relevant methods along with `reconfigure`. It also +// specifies a static `validate` method that checks config validity. +class MyBase : public Base { + public: + MyBase(Dependencies deps, ResourceConfig cfg) : Base(cfg.name()) { + this->reconfigure(deps, cfg); + }; + void reconfigure(Dependencies deps, ResourceConfig cfg) override; + static std::vector validate(ResourceConfig cfg); + + bool is_moving() override; + grpc::StatusCode stop(const AttributeMap& extra) override; + void set_power(const Vector3& linear, + const Vector3& angular, + const AttributeMap& extra) override; + + AttributeMap do_command(const AttributeMap& command) override; + std::vector get_geometries(const AttributeMap& extra) override; + Base::properties get_properties(const AttributeMap& extra) override; + + void move_straight(int64_t distance_mm, double mm_per_sec, const AttributeMap& extra) override { + throw std::runtime_error("move_straight unimplemented"); + } + void spin(double angle_deg, double degs_per_sec, const AttributeMap& extra) override { + throw std::runtime_error("spin unimplemented"); + } + void set_velocity(const Vector3& linear, + const Vector3& angular, + const AttributeMap& extra) override { + throw std::runtime_error("set_velocity unimplemented"); + } + + private: + std::shared_ptr left_; + std::shared_ptr right_; +}; diff --git a/src/viam/examples/modules/complex/client.cpp b/src/viam/examples/modules/complex/client.cpp new file mode 100644 index 000000000..2a33ee291 --- /dev/null +++ b/src/viam/examples/modules/complex/client.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "gizmo/api.hpp" +#include "summation/api.hpp" + +using viam::robot::v1::Status; +using namespace viam::sdk; + +int main() { + const char* uri = ""; + DialOptions dial_options; + std::string type = ""; + std::string payload = ""; + Credentials credentials(type, payload); + dial_options.set_credentials(credentials); + boost::optional opts(dial_options); + std::string address(uri); + Options options(1, opts); + + // Connect to robot. + std::shared_ptr robot = RobotClient::at_address(address, options); + // Print resources. + std::cout << "Resources" << std::endl; + std::vector* resource_names = robot->resource_names(); + for (ResourceName resource : *resource_names) { + std::cout << "\tResource name: " << resource.name() << resource.type() << resource.subtype() + << std::endl; + } + + // Exercise Gizmo methods. + auto gc = robot->resource_by_name("gizmo1"); + if (!gc) { + std::cerr << "could not get 'gizmo1' resource from robot" << std::endl; + return EXIT_FAILURE; + } + bool do_one_ret = gc->do_one("arg1"); + std::cout << "gizmo1 do_one returned: " << do_one_ret; + bool do_one_client_stream_ret = gc->do_one_client_stream({"arg1", "arg2", "arg3"}); + std::cout << "gizmo1 do_one_client_stream returned: " << do_one_client_stream_ret; + std::string do_two_ret = gc->do_two(false); + std::cout << "gizmo1 do_two returned: " << do_two_ret; + std::vector do_one_server_stream_ret = gc->do_one_server_stream("arg1"); + std::cout << "gizmo1 do_one_server_stream returned: "; + for (bool ret : do_one_server_stream_ret) { + std::cout << '\t' << ret << std::endl; + } + std::vector do_one_bidi_stream_ret = gc->do_one_bidi_stream({"arg1", "arg2", "arg3"}); + std::cout << "gizmo1 do_one_bidi_stream returned: "; + for (bool ret : do_one_bidi_stream_ret) { + std::cout << '\t' << ret << std::endl; + } + + // Exercise Summation methods. + auto sc = robot->resource_by_name("summation1"); + if (!sc) { + std::cerr << "could not get 'summation1' resource from robot" << std::endl; + return EXIT_FAILURE; + } + double sum = sc->sum({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + std::cout << "summation1 sum of numbers [0, 10) is: " << sum << std::endl; + + // Exercise a Base method. + auto bc = robot->resource_by_name("base1"); + if (!bc) { + std::cerr << "could not get 'base1' resource from robot" << std::endl; + return EXIT_FAILURE; + } + if (bc->is_moving()) { + std::cout << "base1 is moving" << std::endl; + } else { + std::cout << "base1 is not moving" << std::endl; + } + + return EXIT_SUCCESS; +} diff --git a/src/viam/examples/modules/complex/config/buf.gen.local.yaml.in b/src/viam/examples/modules/complex/config/buf.gen.local.yaml.in new file mode 100644 index 000000000..da02c9757 --- /dev/null +++ b/src/viam/examples/modules/complex/config/buf.gen.local.yaml.in @@ -0,0 +1,21 @@ +# Copyright 2023 Viam Inc. +# +# 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. + +version: v1 +plugins: + - name: cpp + out: gen + - name: cpp-grpc + out: gen + path: ${VIAMCPPSDK_GRPC_CPP_PLUGIN} diff --git a/src/viam/examples/modules/complex/config/buf.gen.remote.plugin.yaml.in b/src/viam/examples/modules/complex/config/buf.gen.remote.plugin.yaml.in new file mode 100644 index 000000000..78fcdf91f --- /dev/null +++ b/src/viam/examples/modules/complex/config/buf.gen.remote.plugin.yaml.in @@ -0,0 +1,22 @@ +# Copyright 2023 Viam Inc. +# +# 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. + +version: v1 +managed: + enabled: true +plugins: + - plugin: buf.build/grpc/cpp:v@VIAMCPPSDK_GRPCXX_VERSION@ + out: gen + - plugin: buf.build/protocolbuffers/cpp + out: gen diff --git a/src/viam/examples/modules/complex/gizmo/api.cpp b/src/viam/examples/modules/complex/gizmo/api.cpp new file mode 100644 index 000000000..3a3f74e25 --- /dev/null +++ b/src/viam/examples/modules/complex/gizmo/api.cpp @@ -0,0 +1,304 @@ +#include "api.hpp" + +#include + +#include + +#include +#include +#include + +#include "../gen/gizmo.grpc.pb.h" +#include "../gen/gizmo.pb.h" + +using namespace viam::sdk; +using namespace viam::component::gizmo::v1; + +/* GizmoRegistration methods */ + +GizmoRegistration::GizmoRegistration(const google::protobuf::ServiceDescriptor* service_descriptor) + : ResourceRegistration(service_descriptor){}; + +std::shared_ptr GizmoRegistration::create_resource_server( + std::shared_ptr manager) { + return std::make_shared(manager); +}; + +std::shared_ptr GizmoRegistration::create_rpc_client( + std::string name, std::shared_ptr chan) { + return std::make_shared(std::move(name), std::move(chan)); +}; + +/* Gizmo methods */ + +std::shared_ptr Gizmo::resource_registration() { + const google::protobuf::DescriptorPool* p = google::protobuf::DescriptorPool::generated_pool(); + const google::protobuf::ServiceDescriptor* sd = + p->FindServiceByName(GizmoService::service_full_name()); + if (!sd) { + throw std::runtime_error("Unable to get service descriptor for the gizmo service"); + } + return std::make_shared(sd); +} + +API Gizmo::static_api() { + return {"viam", "component", "gizmo"}; +} + +API Gizmo::dynamic_api() const { + return static_api(); +} + +Gizmo::Gizmo(std::string name) : Component(std::move(name)){}; + +/* Gizmo server methods */ + +GizmoServer::GizmoServer() : ResourceServer(std::make_shared()){}; +GizmoServer::GizmoServer(std::shared_ptr manager) : ResourceServer(manager){}; + +grpc::Status GizmoServer::DoOne(grpc::ServerContext* context, + const DoOneRequest* request, + DoOneResponse* response) { + if (!request) { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "Called [Gizmo::DoOne] without a request"); + }; + + auto rg = ResourceServer::resource_manager()->resource(request->name()); + if (!rg) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + request->name()); + } + + // Downcast resource to Gizmo. + const std::shared_ptr gizmo = std::dynamic_pointer_cast(rg); + + response->set_ret1(gizmo->do_one(request->arg1())); + + return grpc::Status(); +} + +grpc::Status GizmoServer::DoOneClientStream(grpc::ServerContext* context, + grpc::ServerReader* reader, + DoOneClientStreamResponse* response) { + if (!reader) { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "Called [Gizmo::DoOneClientStream] without a reader"); + }; + + std::vector args = {}; + std::string gizmo_name; + DoOneClientStreamRequest curr_req; + while (reader->Read(&curr_req)) { + args.push_back(curr_req.arg1()); + if (!gizmo_name.empty() && curr_req.name() != gizmo_name) { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, + "[Gizmo::DoOneClientStream] cannot reference multiple Gizmos"); + } + gizmo_name = curr_req.name(); + } + + auto rg = ResourceServer::resource_manager()->resource(gizmo_name); + if (!rg) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + gizmo_name); + } + + // Downcast resource to Gizmo. + const std::shared_ptr gizmo = std::dynamic_pointer_cast(rg); + + response->set_ret1(gizmo->do_one_client_stream(args)); + + return grpc::Status(); +} + +grpc::Status GizmoServer::DoOneServerStream(grpc::ServerContext* context, + const DoOneServerStreamRequest* request, + grpc::ServerWriter* writer) { + if (!request) { + return grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, + "Called [Gizmo::DoOneServerStream] without a request"); + }; + + auto rg = ResourceServer::resource_manager()->resource(request->name()); + if (!rg) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + request->name()); + } + + // Downcast resource to Gizmo. + const std::shared_ptr gizmo = std::dynamic_pointer_cast(rg); + + for (bool ret1 : gizmo->do_one_server_stream(request->arg1())) { + DoOneServerStreamResponse curr_resp = {}; + curr_resp.set_ret1(ret1); + writer->Write(curr_resp); + } + + return grpc::Status(); +} + +grpc::Status GizmoServer::DoOneBiDiStream( + grpc::ServerContext* context, + grpc::ServerReaderWriter* stream) { + if (!stream) { + return grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, + "Called [Gizmo::DoOneBiDiStream] without a stream"); + }; + + std::vector args = {}; + std::string gizmo_name; + DoOneBiDiStreamRequest curr_req; + while (stream->Read(&curr_req)) { + args.push_back(curr_req.arg1()); + if (!gizmo_name.empty() && curr_req.name() != gizmo_name) { + return grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, + "[Gizmo::DoOneBiDiStream] cannot reference multiple Gizmos"); + } + gizmo_name = curr_req.name(); + } + + auto rg = ResourceServer::resource_manager()->resource(gizmo_name); + if (!rg) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + gizmo_name); + } + + // Downcast resource to Gizmo. + const std::shared_ptr gizmo = std::dynamic_pointer_cast(rg); + + for (bool ret1 : gizmo->do_one_bidi_stream(args)) { + DoOneBiDiStreamResponse curr_resp = {}; + curr_resp.set_ret1(ret1); + stream->Write(curr_resp); + } + return grpc::Status(); +} + +grpc::Status GizmoServer::DoTwo(::grpc::ServerContext* context, + const DoTwoRequest* request, + DoTwoResponse* response) { + if (!request) { + return grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, + "Called [Gizmo::DoTwo] without a request"); + }; + + auto rg = ResourceServer::resource_manager()->resource(request->name()); + if (!rg) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + request->name()); + } + + // Downcast resource to Gizmo. + const std::shared_ptr gizmo = std::dynamic_pointer_cast(rg); + + response->set_ret1(gizmo->do_two(request->arg1())); + + return grpc::Status(); +} + +void GizmoServer::register_server(std::shared_ptr server) { + server->register_service(this); +} + +/* Gizmo client methods */ + +GizmoClient::GizmoClient(std::string name, std::shared_ptr channel) + : Gizmo(std::move(name)), stub_(GizmoService::NewStub(channel)), channel_(std::move(channel)){}; + +bool GizmoClient::do_one(std::string arg1) { + DoOneRequest request; + DoOneResponse response; + + grpc::ClientContext ctx; + + *request.mutable_name() = this->name(); + request.set_arg1(arg1); + + const grpc::Status status = stub_->DoOne(&ctx, request, &response); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return response.ret1(); +} + +bool GizmoClient::do_one_client_stream(std::vector arg1) { + DoOneClientStreamResponse response; + grpc::ClientContext ctx; + + auto writer(stub_->DoOneClientStream(&ctx, &response)); + for (std::string arg : arg1) { + DoOneClientStreamRequest curr_req = {}; + curr_req.set_arg1(arg); + if (!writer->Write(curr_req)) { + // Stream is broken; stop writing. + break; + } + } + writer->WritesDone(); + grpc::Status status = writer->Finish(); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return response.ret1(); +} + +std::vector GizmoClient::do_one_server_stream(std::string arg1) { + DoOneServerStreamRequest request; + grpc::ClientContext ctx; + + auto reader(stub_->DoOneServerStream(&ctx, request)); + DoOneServerStreamResponse curr_resp = {}; + std::vector rets = {}; + while (reader->Read(&curr_resp)) { + rets.push_back(curr_resp.ret1()); + } + grpc::Status status = reader->Finish(); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return rets; +} + +std::vector GizmoClient::do_one_bidi_stream(std::vector arg1) { + grpc::ClientContext ctx; + + auto stream(stub_->DoOneBiDiStream(&ctx)); + for (std::string arg : arg1) { + DoOneBiDiStreamRequest curr_req = {}; + curr_req.set_arg1(arg); + if (!stream->Write(curr_req)) { + // Stream is broken; stop writing. + break; + } + } + stream->WritesDone(); + + DoOneBiDiStreamResponse curr_resp = {}; + std::vector rets = {}; + while (stream->Read(&curr_resp)) { + rets.push_back(curr_resp.ret1()); + } + + grpc::Status status = stream->Finish(); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return rets; +} + +std::string GizmoClient::do_two(bool arg1) { + DoTwoRequest request; + DoTwoResponse response; + + grpc::ClientContext ctx; + + *request.mutable_name() = this->name(); + request.set_arg1(arg1); + + const grpc::Status status = stub_->DoTwo(&ctx, request, &response); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return response.ret1(); +} diff --git a/src/viam/examples/modules/complex/gizmo/api.hpp b/src/viam/examples/modules/complex/gizmo/api.hpp new file mode 100644 index 000000000..dc44ac730 --- /dev/null +++ b/src/viam/examples/modules/complex/gizmo/api.hpp @@ -0,0 +1,90 @@ +// Note that a `Gizmo` is implemented with `MyGizmo` in impl.hpp and impl.cpp. +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "gizmo.grpc.pb.h" +#include "gizmo.pb.h" + +using namespace viam::sdk; +using namespace viam::component::gizmo::v1; + +// `GizmoRegistration` Defines a `ResourceRegistration` for the `Gizmo` +// component. +class GizmoRegistration : public ResourceRegistration { + public: + explicit GizmoRegistration(const google::protobuf::ServiceDescriptor* service_descriptor); + std::shared_ptr create_resource_server( + std::shared_ptr manager) override; + std::shared_ptr create_rpc_client(std::string name, + std::shared_ptr chan) override; +}; + +// `Gizmo` is a custom modular component. +class Gizmo : public Component { + public: + // methods shared across all components + static std::shared_ptr resource_registration(); + static API static_api(); + API dynamic_api() const override; + + virtual bool do_one(std::string arg1) = 0; + virtual bool do_one_client_stream(std::vector arg1) = 0; + virtual std::vector do_one_server_stream(std::string arg1) = 0; + virtual std::vector do_one_bidi_stream(std::vector arg1) = 0; + virtual std::string do_two(bool arg1) = 0; + + protected: + explicit Gizmo(std::string name); +}; + +// `GizmoClient` is the gRPC client implementation of a `Gizmo` component. +class GizmoClient : public Gizmo { + public: + GizmoClient(std::string name, std::shared_ptr channel); + + bool do_one(std::string arg1) override; + bool do_one_client_stream(std::vector arg1) override; + std::vector do_one_server_stream(std::string arg1) override; + std::vector do_one_bidi_stream(std::vector arg1) override; + std::string do_two(bool arg1) override; + + private: + std::unique_ptr stub_; + std::shared_ptr channel_; +}; + +// `GizmoServer` is the gRPC server implementation of a `Gizmo` component. +class GizmoServer : public ResourceServer, public GizmoService::Service { + public: + GizmoServer(); + explicit GizmoServer(std::shared_ptr manager); + + grpc::Status DoOne(grpc::ServerContext* context, + const DoOneRequest* request, + DoOneResponse* response) override; + + grpc::Status DoOneClientStream(grpc::ServerContext* context, + grpc::ServerReader* reader, + DoOneClientStreamResponse* response) override; + + grpc::Status DoOneServerStream(grpc::ServerContext* context, + const DoOneServerStreamRequest* request, + grpc::ServerWriter* writer) override; + + grpc::Status DoOneBiDiStream( + grpc::ServerContext* context, + grpc::ServerReaderWriter* stream) override; + + grpc::Status DoTwo(grpc::ServerContext* context, + const DoTwoRequest* request, + DoTwoResponse* response) override; + + void register_server(std::shared_ptr server) override; +}; diff --git a/src/viam/examples/modules/complex/gizmo/impl.cpp b/src/viam/examples/modules/complex/gizmo/impl.cpp new file mode 100644 index 000000000..da0128f50 --- /dev/null +++ b/src/viam/examples/modules/complex/gizmo/impl.cpp @@ -0,0 +1,82 @@ +#include "impl.hpp" + +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace viam::sdk; + +std::string find_arg1(ResourceConfig cfg) { + auto gizmo_name = cfg.name(); + auto arg1 = cfg.attributes()->find("arg1"); + if (arg1 == cfg.attributes()->end()) { + std::ostringstream buffer; + buffer << gizmo_name << ": Required parameter `arg1` not found in configuration"; + throw std::invalid_argument(buffer.str()); + } + const auto* const arg1_string = arg1->second->get(); + if (!arg1_string || arg1_string->empty()) { + std::ostringstream buffer; + buffer << gizmo_name << ": Required non-empty string parameter `arg1`" + << "` is either not a string or is an empty string"; + throw std::invalid_argument(buffer.str()); + } + return *arg1_string; +} + +void MyGizmo::reconfigure(Dependencies deps, ResourceConfig cfg) { + arg1_ = find_arg1(cfg); +} + +std::vector MyGizmo::validate(ResourceConfig cfg) { + // Custom validation can be done by specifying a validate function at the + // time of resource registration (see complex/main.cpp) like this one. + // Validate functions can `throw` exceptions that will be returned to the + // parent through gRPC. Validate functions can also return a vector of + // strings representing the implicit dependencies of the resource. + // + // Here, we return no implicit dependencies ({}). find_arg1 will `throw` an + // exception if the `arg1` attribute is missing, is not a string or is an + // empty string. + find_arg1(cfg); + return {}; +} + +bool MyGizmo::do_one(std::string arg1) { + return arg1_ == arg1; +} + +bool MyGizmo::do_one_client_stream(std::vector arg1) { + if (arg1.empty()) { + return false; + } + bool resp = true; + for (const std::string& arg : arg1) { + resp = resp && arg1_ == arg; + } + return resp; +} + +std::vector MyGizmo::do_one_server_stream(std::string arg1) { + return {arg1_ == arg1, false, true, false}; +} + +std::vector MyGizmo::do_one_bidi_stream(std::vector arg1) { + std::vector resp = {}; + for (const std::string& arg : arg1) { + resp.push_back(arg1_ == arg); + } + return resp; +} + +std::string MyGizmo::do_two(bool arg1) { + std::string arg1_string = arg1 ? "true" : "false"; + return "arg1=" + arg1_string; +} diff --git a/src/viam/examples/modules/complex/gizmo/impl.hpp b/src/viam/examples/modules/complex/gizmo/impl.hpp new file mode 100644 index 000000000..582fe75ca --- /dev/null +++ b/src/viam/examples/modules/complex/gizmo/impl.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "api.hpp" + +using namespace viam::sdk; + +// MyGizmo inherits from the `Gizmo` class defined in `api.hpp` and implements +// all relevant methods along with `reconfigure`. It also specifies a static +// `validate` method that checks config validity. +class MyGizmo : public Gizmo { + public: + MyGizmo(Dependencies deps, ResourceConfig cfg) : Gizmo(cfg.name()) { + this->reconfigure(deps, cfg); + }; + void reconfigure(Dependencies deps, ResourceConfig cfg) override; + static std::vector validate(ResourceConfig cfg); + + bool do_one(std::string arg1) override; + bool do_one_client_stream(std::vector arg1) override; + std::vector do_one_server_stream(std::string arg1) override; + std::vector do_one_bidi_stream(std::vector arg1) override; + std::string do_two(bool arg1) override; + + private: + std::string arg1_; +}; diff --git a/src/viam/examples/modules/complex/main.cpp b/src/viam/examples/modules/complex/main.cpp new file mode 100644 index 000000000..d6ced3cbb --- /dev/null +++ b/src/viam/examples/modules/complex/main.cpp @@ -0,0 +1,107 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/impl.hpp" +#include "gizmo/api.hpp" +#include "gizmo/impl.hpp" +#include "summation/api.hpp" +#include "summation/impl.hpp" + +using namespace viam::sdk; + +int main(int argc, char** argv) { + if (argc < 2) { + std::cerr << "Need socket path as command line argument" << std::endl; + return EXIT_FAILURE; + } + std::string socket_addr = argv[1]; + + // Use set_logger_severity_from_args to set the boost trivial logger's + // severity depending on commandline arguments. + set_logger_severity_from_args(argc, argv); + BOOST_LOG_TRIVIAL(debug) << "Starting module with debug level logging"; + + // C++ modules must handle SIGINT and SIGTERM. You can use the SignalManager + // class and its wait method to handle the correct signals. + SignalManager signals; + + API base_api = Base::static_api(); + Model mybase_model("viam", "base", "mybase"); + + std::shared_ptr mybase_mr = std::make_shared( + ResourceType("Base"), + base_api, + mybase_model, + [](Dependencies deps, ResourceConfig cfg) { return std::make_unique(deps, cfg); }, + MyBase::validate); + + Registry::register_model(mybase_mr); + + API gizmo_api = Gizmo::static_api(); + Model mygizmo_model("viam", "gizmo", "mygizmo"); + // Make sure to explicity register resources with custom APIs. Note that + // this must be done in `main` and not in resource implementation files due + // to order of static initialization. + Registry::register_resource(gizmo_api, Gizmo::resource_registration()); + + std::shared_ptr mygizmo_mr = std::make_shared( + ResourceType("Gizmo"), + gizmo_api, + mygizmo_model, + [](Dependencies deps, ResourceConfig cfg) { return std::make_unique(deps, cfg); }, + MyGizmo::validate); + + Registry::register_model(mygizmo_mr); + + API summation_api = Summation::static_api(); + Model mysummation_model("viam", "summation", "mysummation"); + // Make sure to explicity register resources with custom APIs. Note that + // this must be done in `main` and not in resource implementation files due + // to order of static initialization. + Registry::register_resource(summation_api, Summation::resource_registration()); + + std::shared_ptr mysummation_mr = + std::make_shared(ResourceType("Summation"), + summation_api, + mysummation_model, + [](Dependencies deps, ResourceConfig cfg) { + return std::make_unique(deps, cfg); + }); + + Registry::register_model(mysummation_mr); + + // The `ModuleService_` must outlive the Server, so the declaration order + // here matters. + auto my_mod = std::make_shared(socket_addr); + auto server = std::make_shared(); + + my_mod->add_model_from_registry(server, base_api, mybase_model); + my_mod->add_model_from_registry(server, gizmo_api, mygizmo_model); + my_mod->add_model_from_registry(server, summation_api, mysummation_model); + my_mod->start(server); + BOOST_LOG_TRIVIAL(info) << "Complex example module listening on " << socket_addr; + + server->start(); + int sig = 0; + auto result = signals.wait(&sig); + server->shutdown(); + return EXIT_SUCCESS; +}; diff --git a/src/viam/examples/modules/complex/proto/buf.lock b/src/viam/examples/modules/complex/proto/buf.lock new file mode 100644 index 000000000..6c4355d41 --- /dev/null +++ b/src/viam/examples/modules/complex/proto/buf.lock @@ -0,0 +1,8 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: googleapis + repository: googleapis + commit: 28151c0d0a1641bf938a7672c500e01d + digest: shake256:49215edf8ef57f7863004539deff8834cfb2195113f0b890dd1f67815d9353e28e668019165b9d872395871eeafcbab3ccfdb2b5f11734d3cca95be9e8d139de diff --git a/src/viam/examples/modules/complex/proto/buf.yaml b/src/viam/examples/modules/complex/proto/buf.yaml new file mode 100644 index 000000000..fa715f545 --- /dev/null +++ b/src/viam/examples/modules/complex/proto/buf.yaml @@ -0,0 +1,24 @@ +# Copyright 2023 Viam Inc. +# +# 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. + + +version: v1 +deps: + - buf.build/googleapis/googleapis +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/src/viam/examples/modules/complex/proto/gizmo.proto b/src/viam/examples/modules/complex/proto/gizmo.proto new file mode 100644 index 000000000..be9c0112d --- /dev/null +++ b/src/viam/examples/modules/complex/proto/gizmo.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package viam.component.gizmo.v1; + +import "google/api/annotations.proto"; + +service GizmoService { + rpc DoOne(DoOneRequest) returns (DoOneResponse) { + option (google.api.http) = { + post: "/viam/api/v1/component/gizmo/{name}/do_one" + }; + } + + rpc DoOneClientStream(stream DoOneClientStreamRequest) returns (DoOneClientStreamResponse); + + rpc DoOneServerStream(DoOneServerStreamRequest) returns (stream DoOneServerStreamResponse); + + rpc DoOneBiDiStream(stream DoOneBiDiStreamRequest) returns (stream DoOneBiDiStreamResponse); + + rpc DoTwo(DoTwoRequest) returns (DoTwoResponse) { + option (google.api.http) = { + post: "/viam/api/v1/component/gizmo/{name}/do_two" + }; + } +} + +message DoOneRequest { + string name = 1; + string arg1 = 2; +} + +message DoOneResponse { + bool ret1 = 1; +} + +message DoOneServerStreamRequest { + string name = 1; + string arg1 = 2; +} + +message DoOneServerStreamResponse { + bool ret1 = 1; +} + +message DoOneClientStreamRequest { + string name = 1; + string arg1 = 2; +} + +message DoOneClientStreamResponse { + bool ret1 = 1; +} + +message DoOneBiDiStreamRequest { + string name = 1; + string arg1 = 2; +} + +message DoOneBiDiStreamResponse { + bool ret1 = 1; +} + +message DoTwoRequest { + string name = 1; + bool arg1 = 2; +} + +message DoTwoResponse { + string ret1 = 1; +} diff --git a/src/viam/examples/modules/complex/proto/summation.proto b/src/viam/examples/modules/complex/proto/summation.proto new file mode 100644 index 000000000..329b98ea4 --- /dev/null +++ b/src/viam/examples/modules/complex/proto/summation.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package viam.service.summation.v1; + +import "google/api/annotations.proto"; + +service SummationService { + rpc Sum(SumRequest) returns (SumResponse) { + option (google.api.http) = { + post: "/viam/api/v1/service/summation/{name}/sum" + }; + } +} + +message SumRequest { + string name = 1; + repeated double numbers = 2; +} + +message SumResponse { + double sum = 1; +} diff --git a/src/viam/examples/modules/complex/summation/api.cpp b/src/viam/examples/modules/complex/summation/api.cpp new file mode 100644 index 000000000..1c0d37ac5 --- /dev/null +++ b/src/viam/examples/modules/complex/summation/api.cpp @@ -0,0 +1,111 @@ +#include "api.hpp" + +#include + +#include + +#include +#include +#include + +#include "../gen/summation.grpc.pb.h" +#include "../gen/summation.pb.h" + +using namespace viam::sdk; +using namespace viam::service::summation::v1; + +/* SummationRegistration methods */ + +SummationRegistration::SummationRegistration( + const google::protobuf::ServiceDescriptor* service_descriptor) + : ResourceRegistration(service_descriptor){}; + +std::shared_ptr SummationRegistration::create_resource_server( + std::shared_ptr manager) { + return std::make_shared(manager); +}; + +std::shared_ptr SummationRegistration::create_rpc_client( + std::string name, std::shared_ptr chan) { + return std::make_shared(std::move(name), std::move(chan)); +}; + +/* Summation methods */ + +std::shared_ptr Summation::resource_registration() { + const google::protobuf::DescriptorPool* p = google::protobuf::DescriptorPool::generated_pool(); + const google::protobuf::ServiceDescriptor* sd = + p->FindServiceByName(SummationService::service_full_name()); + if (!sd) { + throw std::runtime_error("Unable to get service descriptor for the summation service"); + } + return std::make_shared(sd); +} + +API Summation::static_api() { + return {"viam", "service", "summation"}; +} + +API Summation::dynamic_api() const { + return static_api(); +} + +Summation::Summation(std::string name) : Service(std::move(name)){}; + +/* Summation server methods */ + +SummationServer::SummationServer() : ResourceServer(std::make_shared()){}; +SummationServer::SummationServer(std::shared_ptr manager) + : ResourceServer(manager){}; + +grpc::Status SummationServer::Sum(grpc::ServerContext* context, + const SumRequest* request, + SumResponse* response) { + if (!request) { + return ::grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, + "Called [Summation::Sum] without a request"); + }; + + auto rs = ResourceServer::resource_manager()->resource(request->name()); + if (!rs) { + return grpc::Status(grpc::UNKNOWN, "resource not found: " + request->name()); + } + + // Downcast resource to Summation. + const std::shared_ptr summation = std::dynamic_pointer_cast(rs); + + std::vector numbers_vec = {request->numbers().begin(), request->numbers().end()}; + response->set_sum(summation->sum(numbers_vec)); + + return ::grpc::Status(); +} + +void SummationServer::register_server(std::shared_ptr server) { + server->register_service(this); +} + +/* Summation client methods */ + +SummationClient::SummationClient(std::string name, std::shared_ptr channel) + : Summation(std::move(name)), + stub_(SummationService::NewStub(channel)), + channel_(std::move(channel)){}; + +double SummationClient::sum(std::vector numbers) { + SumRequest request; + SumResponse response; + + grpc::ClientContext ctx; + + *request.mutable_name() = this->name(); + for (double number : numbers) { + request.add_numbers(number); + } + + const grpc::Status status = stub_->Sum(&ctx, request, &response); + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + + return response.sum(); +} diff --git a/src/viam/examples/modules/complex/summation/api.hpp b/src/viam/examples/modules/complex/summation/api.hpp new file mode 100644 index 000000000..30d9a6c4d --- /dev/null +++ b/src/viam/examples/modules/complex/summation/api.hpp @@ -0,0 +1,70 @@ +// Note that `Summation` is implemented with `MySummation` in impl.hpp and impl.cpp. +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +#include "summation.grpc.pb.h" +#include "summation.pb.h" + +using namespace viam::sdk; +using namespace viam::service::summation::v1; + +// `SummationRegistration` defines a `ResourceRegistration` for the `Summation` +// service. +class SummationRegistration : public ResourceRegistration { + public: + explicit SummationRegistration(const google::protobuf::ServiceDescriptor* service_descriptor); + std::shared_ptr create_resource_server( + std::shared_ptr manager) override; + std::shared_ptr create_rpc_client(std::string name, + std::shared_ptr chan) override; +}; + +// A `Summation` is a custom modular service. +class Summation : public Service { + public: + // methods shared across all services + static std::shared_ptr resource_registration(); + static API static_api(); + API dynamic_api() const override; + + virtual double sum(std::vector numbers) = 0; + + protected: + explicit Summation(std::string name); +}; + +// `SummationClient` is the gRPC client implementation of a `Summation` +// service. +class SummationClient : public Summation { + public: + SummationClient(std::string name, std::shared_ptr channel); + + double sum(std::vector numbers) override; + + private: + std::unique_ptr stub_; + std::shared_ptr channel_; +}; + +// `SummationServer` is the gRPC server implementation of a `Summation` +// service. +class SummationServer : public ResourceServer, public SummationService::Service { + public: + SummationServer(); + explicit SummationServer(std::shared_ptr manager); + + grpc::Status Sum(grpc::ServerContext* context, + const SumRequest* request, + SumResponse* response) override; + + void register_server(std::shared_ptr server) override; +}; diff --git a/src/viam/examples/modules/complex/summation/impl.cpp b/src/viam/examples/modules/complex/summation/impl.cpp new file mode 100644 index 000000000..bd9cab7bc --- /dev/null +++ b/src/viam/examples/modules/complex/summation/impl.cpp @@ -0,0 +1,47 @@ +#include "impl.hpp" + +#include + +#include + +#include +#include +#include + +using namespace viam::sdk; + +// Returns value of "subtract" in cfg.attributes() or `false` if subtract is not +// in attributes or is not a boolean value. +bool find_subtract(ResourceConfig cfg) { + auto summation_name = cfg.name(); + auto subtract = cfg.attributes()->find("subtract"); + if (subtract == cfg.attributes()->end()) { + return false; + } + const bool* const subtract_bool = subtract->second->get(); + if (!subtract_bool) { + return false; + } + return *subtract_bool; +} + +void MySummation::reconfigure(Dependencies deps, ResourceConfig cfg) { + subtract_ = find_subtract(cfg); +} + +double MySummation::sum(std::vector numbers) { + if (numbers.empty()) { + throw std::runtime_error("MySummation requires at least one number to sum"); + } + + double result = 0.0; + for (double number : numbers) { + if (subtract_) { + result -= number; + } else { + result += number; + } + } + + return result; +} diff --git a/src/viam/examples/modules/complex/summation/impl.hpp b/src/viam/examples/modules/complex/summation/impl.hpp new file mode 100644 index 000000000..8e74f1ac9 --- /dev/null +++ b/src/viam/examples/modules/complex/summation/impl.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "api.hpp" + +using namespace viam::sdk; + +// MySummation inherits from the `Summation` class defined in `api.hpp` and +// implements all relevant methods along with `reconfigure`. +class MySummation : public Summation { + public: + MySummation(Dependencies deps, ResourceConfig cfg) : Summation(cfg.name()) { + this->reconfigure(deps, cfg); + }; + void reconfigure(Dependencies deps, ResourceConfig cfg) override; + static std::vector validate(ResourceConfig cfg); + + double sum(std::vector numbers) override; + + private: + bool subtract_; +}; diff --git a/src/viam/examples/modules/simple/README.md b/src/viam/examples/modules/simple/README.md index c26390b79..551c20a83 100644 --- a/src/viam/examples/modules/simple/README.md +++ b/src/viam/examples/modules/simple/README.md @@ -30,7 +30,7 @@ An example configuration for a printer could look like this: "modules": [ { "name": "MyModule", - "executable_path": "/home/viam-cpp-sdk/build/install/bin/example_module" + "executable_path": "/home/viam-cpp-sdk/build/install/bin/simple_module" } ], "components": [ diff --git a/src/viam/examples/modules/simple/main.cpp b/src/viam/examples/modules/simple/main.cpp index b0a7757cc..3b8caa6b2 100644 --- a/src/viam/examples/modules/simple/main.cpp +++ b/src/viam/examples/modules/simple/main.cpp @@ -79,7 +79,8 @@ class Printer : public Generic { int main(int argc, char** argv) { if (argc < 2) { - throw std::invalid_argument("Need socket path as command line argument"); + std::cerr << "Need socket path as command line argument" << std::endl; + return EXIT_FAILURE; } std::string socket_addr = argv[1]; @@ -127,5 +128,5 @@ int main(int argc, char** argv) { int sig = 0; auto result = signals.wait(&sig); server->shutdown(); - return 0; + return EXIT_SUCCESS; }; diff --git a/src/viam/examples/modules/tflite/main.cpp b/src/viam/examples/modules/tflite/main.cpp index 967da5585..bb672333a 100644 --- a/src/viam/examples/modules/tflite/main.cpp +++ b/src/viam/examples/modules/tflite/main.cpp @@ -284,27 +284,7 @@ class MLModelServiceTFLite : public vsdk::MLModelService { auto state = std::make_shared(std::move(dependencies), std::move(configuration)); - // Validate that our dependencies (if any - we don't actually - // expect any for this service) exist. If we did have - // Dependencies this is where we would have an opportunity to - // downcast them to the right thing and store them in our - // state so we could use them as needed. - // - // TODO(RSDK-3601): Validating that dependencies are present - // should be handled by the ModuleService automatically, - // rather than requiring each component to validate the - // presence of dependencies. - for (const auto& kv : state->dependencies) { - if (!kv.second) { - std::ostringstream buffer; - buffer << service_name << ": Dependency " - << "`" << kv.first.to_string() << "` was not found during (re)configuration"; - throw std::invalid_argument(buffer.str()); - } - } - // Now we can begin parsing and validating the provided `configuration`. - // Pull the model path out of the configuration. const auto& attributes = state->configuration.attributes(); auto model_path = attributes->find("model_path"); diff --git a/src/viam/sdk/components/camera/camera.cpp b/src/viam/sdk/components/camera/camera.cpp index 23e6d5744..6b2effab9 100644 --- a/src/viam/sdk/components/camera/camera.cpp +++ b/src/viam/sdk/components/camera/camera.cpp @@ -93,16 +93,6 @@ ::viam::component::camera::v1::Format Camera::MIME_string_to_format(std::string return viam::component::camera::v1::FORMAT_UNSPECIFIED; } -std::vector repeated_field_to_vector(const google::protobuf::RepeatedField& f) { - std::vector v(f.begin(), f.end()); - return v; -} - -google::protobuf::RepeatedField vector_to_repeated_field(const std::vector& v) { - google::protobuf::RepeatedField rf = {v.begin(), v.end()}; - return rf; -} - Camera::raw_image Camera::from_proto(viam::component::camera::v1::GetImageResponse proto) { Camera::raw_image raw_image; std::string img_string = proto.image(); @@ -157,7 +147,7 @@ Camera::distortion_parameters Camera::from_proto( viam::component::camera::v1::DistortionParameters proto) { Camera::distortion_parameters params; params.model = proto.model(); - params.parameters = repeated_field_to_vector(proto.parameters()); + params.parameters = {proto.parameters().begin(), proto.parameters().end()}; return params; } @@ -196,7 +186,7 @@ viam::component::camera::v1::DistortionParameters Camera::to_proto( Camera::distortion_parameters params) { viam::component::camera::v1::DistortionParameters proto; *proto.mutable_model() = params.model; - *proto.mutable_parameters() = vector_to_repeated_field(params.parameters); + *proto.mutable_parameters() = {params.parameters.begin(), params.parameters.end()}; return proto; } diff --git a/src/viam/sdk/module/service.cpp b/src/viam/sdk/module/service.cpp index 0facac22b..ca26a6b49 100644 --- a/src/viam/sdk/module/service.cpp +++ b/src/viam/sdk/module/service.cpp @@ -2,9 +2,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -44,12 +46,19 @@ namespace sdk { namespace { Dependencies get_dependencies(ModuleService_* m, - google::protobuf::RepeatedPtrField proto) { + google::protobuf::RepeatedPtrField const& proto, + std::string const& resource_name) { Dependencies deps; - for (auto& dep : proto) { - auto name = Name::from_string(dep); - const std::shared_ptr resource = m->get_parent_resource(name); - deps.emplace(name, resource); + for (const auto& dep : proto) { + auto dep_name = Name::from_string(dep); + const std::shared_ptr dep_resource = m->get_parent_resource(dep_name); + if (!dep_resource) { + std::ostringstream buffer; + buffer << resource_name << ": Dependency " + << "`" << dep_name << "` was not found during (re)configuration"; + throw std::invalid_argument(buffer.str()); + } + deps.emplace(dep_name, dep_resource); } return deps; } @@ -72,7 +81,7 @@ ::grpc::Status ModuleService_::AddResource(::grpc::ServerContext* context, const std::lock_guard lock(lock_); std::shared_ptr res; - const Dependencies deps = get_dependencies(this, request->dependencies()); + const Dependencies deps = get_dependencies(this, request->dependencies(), cfg.name()); const std::shared_ptr reg = Registry::lookup_model(cfg.api(), cfg.model()); if (reg) { try { @@ -100,7 +109,7 @@ ::grpc::Status ModuleService_::ReconfigureResource( ResourceConfig cfg = ResourceConfig::from_proto(proto); const std::shared_ptr module = this->module_; - const Dependencies deps = get_dependencies(this, request->dependencies()); + const Dependencies deps = get_dependencies(this, request->dependencies(), cfg.name()); const std::unordered_map>& services = module->services(); if (services.find(cfg.api()) == services.end()) {