Skip to content

Commit

Permalink
RSDK-4522 RSDK-3601 Add complex module example (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjirewis authored Oct 3, 2023
1 parent f6d3718 commit 10f6b74
Show file tree
Hide file tree
Showing 26 changed files with 1,545 additions and 42 deletions.
1 change: 1 addition & 0 deletions src/viam/examples/modules/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@

add_subdirectory(tflite)
add_subdirectory(simple)
add_subdirectory(complex)
150 changes: 150 additions & 0 deletions src/viam/examples/modules/complex/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
89 changes: 89 additions & 0 deletions src/viam/examples/modules/complex/README.md
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
```
111 changes: 111 additions & 0 deletions src/viam/examples/modules/complex/base/impl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#include "impl.hpp"

#include <fstream>
#include <iostream>
#include <sstream>

#include <grpcpp/support/status.h>

#include <viam/sdk/components/base/base.hpp>
#include <viam/sdk/components/component.hpp>
#include <viam/sdk/config/resource.hpp>
#include <viam/sdk/resource/resource.hpp>

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<std::string>();
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<Motor>(kv.second);
}
if (kv.first.short_name() == right) {
right_ = std::dynamic_pointer_cast<Motor>(kv.second);
}
}
}

std::vector<std::string> 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<GeometryConfig> MyBase::get_geometries(const AttributeMap& extra) {
auto left_geometries = left_->get_geometries(extra);
auto right_geometries = right_->get_geometries(extra);
std::vector<GeometryConfig> 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};
}
Loading

0 comments on commit 10f6b74

Please sign in to comment.