Start RPC example Change-Id: Ie39a707573c615ceeb3650c036331683757cbc50 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/quickstart/cmake/+/422273
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore
@@ -0,0 +1 @@ +build
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f7242af --- /dev/null +++ b/CMakeLists.txt
@@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.20) +project(rpc-demo C CXX) + +add_compile_definitions(PW_SYSTEM_ENABLE_TRACE_SERVICE=0) + +# 1. Fetch Nanopb +include(FetchContent) +FetchContent_Declare( + nanopb + GIT_REPOSITORY https://github.com/nanopb/nanopb.git + GIT_TAG nanopb-0.4.9.1 +) +FetchContent_MakeAvailable(nanopb) + +set(dir_pw_third_party_nanopb "${nanopb_SOURCE_DIR}" CACHE PATH "" FORCE) + +FetchContent_Declare( + pigweed + GIT_REPOSITORY https://pigweed.googlesource.com/pigweed/pigweed + GIT_TAG 05195263e8e0b43c692152538f0554c16874012e +) + +# 3. Configure Pigweed backends. +# We must set these backend variables in the CMake CACHE before calling +# FetchContent_MakeAvailable(pigweed). This is because MakeAvailable automatically +# calls add_subdirectory(), which defines Pigweed targets that depend on these +# backends. Setting them here avoids the need to use the deprecated +# FetchContent_Populate() pattern to retrieve the source path before adding the +# subdirectory. +set(pw_log_BACKEND "pw_log_basic" CACHE STRING "" FORCE) +set(pw_assert.check_BACKEND "pw_assert_basic.check_backend" CACHE STRING "" FORCE) +set(pw_assert.assert_BACKEND "pw_assert.print_and_abort_assert_backend" CACHE STRING "" FORCE) +set(pw_assert_basic.handler_BACKEND "pw_assert_basic.basic_handler" CACHE STRING "" FORCE) +set(pw_sys_io_BACKEND "pw_sys_io_stdio" CACHE STRING "" FORCE) +set(pw_sync.mutex_BACKEND "pw_sync_stl.mutex_backend" CACHE STRING "" FORCE) +set(pw_sync.interrupt_spin_lock_BACKEND "pw_sync_stl.interrupt_spin_lock" CACHE STRING "" FORCE) +set(pw_chrono.system_clock_BACKEND "pw_chrono_stl.system_clock" CACHE STRING "" FORCE) +set(pw_chrono.system_timer_BACKEND "pw_chrono_stl.system_timer" CACHE STRING "" FORCE) +set(pw_thread.sleep_BACKEND "pw_thread_stl.sleep" CACHE STRING "" FORCE) +set(pw_thread.id_BACKEND "pw_thread_stl.id" CACHE STRING "" FORCE) +set(pw_thread.thread_BACKEND "pw_thread_stl.thread" CACHE STRING "" FORCE) +set(pw_thread.yield_BACKEND "pw_thread_stl.yield" CACHE STRING "" FORCE) +set(pw_thread.thread_iteration_BACKEND "pw_thread_stl.thread_iteration" CACHE STRING "" FORCE) + +# pw_system backends +set(pw_system.rpc_server_BACKEND "pw_system.hdlc_rpc_server" CACHE STRING "" FORCE) +set(pw_system.io_BACKEND "pw_system.socket_target_io" CACHE STRING "" FORCE) +set(pw_system.target_hooks_BACKEND "pw_system.stl_target_hooks" CACHE STRING "" FORCE) + +# pw_sync backends needed by pw_system +set(pw_sync.binary_semaphore_BACKEND "pw_sync_stl.binary_semaphore_backend" CACHE STRING "" FORCE) +set(pw_sync.thread_notification_BACKEND "pw_sync.binary_semaphore_thread_notification_backend" CACHE STRING "" FORCE) +set(pw_sync.timed_thread_notification_BACKEND "pw_sync.binary_semaphore_timed_thread_notification_backend" CACHE STRING "" FORCE) + +# Disable experimental editions globally for Pigweed proto compiler +set(pw_protobuf_compiler_GENERATE_PROTOS_ARGS "--no-experimental-editions" CACHE STRING "" FORCE) + +set(pw_protobuf_compiler_PYTHON_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated_python" CACHE PATH "" FORCE) + +# 4. Make Pigweed available +FetchContent_MakeAvailable(pigweed) + +# 5. Set PIGWEED_DIR for our own use +set(PIGWEED_DIR "${pigweed_SOURCE_DIR}") +set(ENV{PW_ROOT} "${PIGWEED_DIR}") + +# Include Pigweed CMake helper +include(${PIGWEED_DIR}/pw_build/pigweed.cmake) +include(${PIGWEED_DIR}/pw_protobuf_compiler/proto.cmake) + +# Define a target to group all python protos needed for the console. +add_custom_target(generate_python_protos + DEPENDS + math_service.python + pw_protobuf.codegen_protos.python + pw_protobuf.common_proto.python + pw_protobuf.status_proto.python + pw_protobuf.field_options_proto.python + pw_rpc.protos.python +) + +# 6. Declare the proto library. +pw_proto_library(math_service + SOURCES + math_service.proto +) + +# 8. Define Demo Executable +add_executable(rpc_demo main.cc) +target_link_libraries(rpc_demo + PRIVATE + math_service.nanopb_rpc + pw_rpc.nanopb.echo_service + pw_system.init + pw_system.io + pw_system.log + pw_system.rpc_server + pw_system.work_queue + pw_system.target_hooks + pw_log +) +add_dependencies(rpc_demo generate_python_protos)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 677be08..80e3307 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md
@@ -17,13 +17,23 @@ ## Contributing changes and submitting code reviews -All changes require review, including changes by project members. +1. Clone this repo: -For detailed instructions on how to contribute changes, -see [the Gerrit docs](https://gerrit-review.googlesource.com/Documentation/intro-user.html). + ``` + git clone https://pigweed.googlesource.com/pigweed/quickstart/cmake + ``` + +2. Complete the [Gerrit setup](https://pigweed.dev/contributing/#gerrit-setup) + part of Pigweed's upstream contributor process. + +3. [Create a change](https://pigweed.dev/contributing/#create-your-change) + and get it [reviewed](https://pigweed.dev/contributing/#code-review). + +If you're unfamiliar with Gerrit (our code review tool) see [User +Guide](https://gerrit-review.googlesource.com/Documentation/intro-user.html). ## Community guidelines This project observes the following community guidelines: - * [Google's Open Source Community Guidelines](https://opensource.google/conduct/) +* [Google's Open Source Community Guidelines](https://opensource.google/conduct/)
diff --git a/OWNERS b/OWNERS index 528c611..7a3a52d 100644 --- a/OWNERS +++ b/OWNERS
@@ -4,4 +4,5 @@ # # Syntax for OWNERS files can be found at: # https://pigweed-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html#syntax -* +kayce@google.com +frolv@google.com
diff --git a/README.md b/README.md index c5ae3d3..8cc190c 100644 --- a/README.md +++ b/README.md
@@ -1,5 +1,164 @@ -Pigweed Open Source Template Repository -======================================= +# Pigweed: minimal CMake example -This repository is a template that we will use when creating new open source -repositories for Pigweed. +A minimal, complete example of integrating [Pigweed](https://pigweed.dev) +into a CMake project. + +## Overview + +`main.cc` creates an RPC server that listens on a local TCP socket for RPC +requests. Using [`pw_console`](https://pigweed.dev/pw_console) as an RPC client +you will send an RPC request to this server. Communication over the socket uses +HDLC framing. All RPC requests and responses are structured as protobufs. +All protobuf generation is configured inside of `CMakeLists.txt`. + +## Limitations + +* This example has only been verified to work on Debian. + +* The example is only set up to build a host-side simulation of the firmware. + It does not set up a cross-compilation toolchain or provide flashing tools. + +* The firmware build intentionally avoids a + [bootstrap](https://pigweed.dev/pw_env_setup) to demonstrate manual setup + of Pigweed modules in a CMake project. Most of the toolchain must be + installed globally and available on the system path. We use a bootstrap + to speed up the manual testing (i.e. bootstrapping in order to spin up + the `pw_console` RPC client quickly) but it's not required on the + client-side, either. + +## Quickstart + +### 1. Install dependencies + +Linux: + +``` +sudo apt-get install -y build-essential cmake ninja-build \ + protobuf-compiler python3 python3-protobuf python3-serial +``` + +Note: Pigweed and Nanopb (on-device protobuf codegen library) are managed in +`CMakeLists.txt` via +[`FetchContent`](https://cmake.org/cmake/help/latest/module/FetchContent.html). + +### 2. Build + +Configure the build: + +``` +cmake -B build -S . -G Ninja +``` + +Run the build: + +``` +cmake --build build --target rpc_demo +``` + +Note: The Python protobuf codegen also runs as part of this build. +The `pw_console` client will use this generated Python to communicate +with the simulated firmware device. + +### 3. Run + +1. Open a console tab and run `./build/rpc_demo`. + + The simulated firmware device boots up and starts listening on + `localhost:33000`. + +2. Open another console tab and `cd` into the root of the Pigweed repo. + + ``` + cd third_party/pigweed + ``` + +3. Bootstrap an environment for `pw_console`. + + ``` + . bootstrap.sh + ``` + + Note: Bootstrap is only used here to simplify and speed up the + manual testing part of the example. It's possible to use `pw_console` + without a bootstrap. + +4. Start a `pw_console` session. + + ``` + python3 ../../../run_console.py --socket-addr localhost:33000 + ``` + +5. In the Python REPL of `pw_console` invoke the `Echo` RPC. + You should see the server respond with a tuple containing `Status.OK` + followed by a response containing the same message that you sent. + + ``` + >>> device.rpcs.pw.rpc.EchoService.Echo(msg="hello") + (Status.OK, pw.rpc.EchoMessage(msg="hello")) + ``` + +6. Now try invoking the `Add` RPC. You should see the server respond + with a tuple containing `Status.OK` again followed by the correct sum. + + ``` + >>> device.rpcs.rpc.math.MathService.Add(a=3, b=5) + (Status.OK, rpc.math.AddResponse(result=8)) + ``` + +7. Go back to the tab running the simulated firmware device. You + should see the server logging messages. + + ``` + Awaiting connection on port 33000 + Client connected + INF pw_system initialized, main thread sleeping... + INF System init + INF Registering RPC services + INF Starting threads + INF Running RPC server + INF Registering custom MathService + INF Server received Add: 5 + 3 + INF Simulated device is still alive + ``` + +## Next steps + +* Talk to us in the `#cmake-build` channel of our + [Discord](https://pigweed.dev/community/). + +* Learn more about [Pigweed's CMake + support](https://pigweed.dev/build/cmake/). + +## Appendix: Files + +Source files: + +* `//CMakeLists.txt`: Defines the CMake build system. It fetches dependencies + (Nanopb, Pigweed), configures Pigweed backends, sets up C++ and Python + protobuf codegen, and defines the `rpc_demo` executable target. + +* `//main.cc`: The entry point for the C++ application. It initializes + `pw_system` and implements `UserAppInit` to register the custom `MathService`. + +* `//math_service.proto`: Defines the protobuf service `MathService` with an + `Add` method. + +* `//math_service.h`: Implements the `MathService` defined in `math_service.proto`. + +* `//run_console.py`: Python script to launch `pw_console` configured to talk + to the running application. It sets up the Python path to include generated + protos. + +Generated files: + +* `//build/math_service/nanopb/math_service.pb.h` and `//build/math_service/nanopb/math_service.pb.c`: Nanopb + generated C code for the `math_service.proto` messages. + +* `//build/math_service/nanopb_rpc/math_service.rpc.pb.h`: Pigweed RPC generated C++ header for the + `math_service.proto` service. + +* `//build/generated_python/python/math_service_pb2.py`: Python protobuf module generated + from `math_service.proto`, used by `run_console.py`. + +* `//build/python_packages/`: Contains Python protobuf modules generated from + Pigweed internal protos, required by Pigweed RPC plugins.
diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..d23237d --- /dev/null +++ b/main.cc
@@ -0,0 +1,34 @@ +#include "pw_system/init.h" +#include "pw_system/rpc_server.h" +#include "math_service.h" +#include "pw_rpc/echo_service_nanopb.h" +#include "pw_log/log.h" +#include "pw_thread/sleep.h" +#include <chrono> + +MathService math_service; +pw::rpc::EchoService echo_service; + +namespace pw::system { + +// This will run once after pw::system::Init() completes. This callback must +// return or it will block the work queue. +void UserAppInit() { + PW_LOG_INFO("Registering custom MathService"); + GetRpcServer().RegisterService(math_service); + PW_LOG_INFO("Registering EchoService"); + GetRpcServer().RegisterService(echo_service); +} + +} // namespace pw::system + +int main() { + pw::system::Init(); + + PW_LOG_INFO("pw_system initialized, main thread sleeping..."); + while (true) { + pw::this_thread::sleep_for(std::chrono::seconds(10)); + PW_LOG_INFO("Simulated device is still alive"); + } + return 0; +}
diff --git a/math_service.h b/math_service.h new file mode 100644 index 0000000..d16b2ae --- /dev/null +++ b/math_service.h
@@ -0,0 +1,16 @@ +#pragma once + +#include "math_service.rpc.pb.h" +#include "pw_log/log.h" +#include "pw_status/status.h" + +class MathService final + : public rpc::math::pw_rpc::nanopb::MathService::Service<MathService> { + public: + pw::Status Add(const rpc_math_AddRequest& request, + rpc_math_AddResponse& response) { + PW_LOG_INFO("Server received Add: %d + %d", static_cast<int>(request.a), static_cast<int>(request.b)); + response.result = request.a + request.b; + return pw::OkStatus(); + } +};
diff --git a/math_service.proto b/math_service.proto new file mode 100644 index 0000000..ab031e5 --- /dev/null +++ b/math_service.proto
@@ -0,0 +1,16 @@ +syntax = "proto3"; + +package rpc.math; + +message AddRequest { + int32 a = 1; + int32 b = 2; +} + +message AddResponse { + int32 result = 1; +} + +service MathService { + rpc Add(AddRequest) returns (AddResponse); +}
diff --git a/run_console.py b/run_console.py new file mode 100755 index 0000000..c42d144 --- /dev/null +++ b/run_console.py
@@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +# Calculate paths relative to this script +SCRIPT_DIR = Path(__file__).resolve().parent +GENERATED_PYTHON_DIR = SCRIPT_DIR / 'build' / 'generated_python' / 'python' + +# Ensure the generated python protos are importable +if GENERATED_PYTHON_DIR.exists(): + sys.path.append(str(GENERATED_PYTHON_DIR)) +else: + print(f"Error: Generated python directory not found at {GENERATED_PYTHON_DIR}") + print("Please build the project first (./build.sh)") + sys.exit(1) + +try: + import math_service_pb2 +except ImportError as e: + print(f"Error: Failed to import math_service_pb2: {e}") + print("Ensure you have built the project.") + sys.exit(1) + +try: + import pw_system.console +except ImportError: + print("Error: Failed to import pw_system.console.") + print("Please run this script from a Pigweed bootstrapped environment:") + print(" cd path/to/pigweed") + print(" . ./bootstrap.sh") + print(" python path/to/this/project/run_console.py --socket-addr default") + sys.exit(1) + +def main(): + # If no connection arguments are provided, default to socket + if not any(arg in sys.argv for arg in ['-s', '--socket-addr', '-d', '--device']): + sys.argv.extend(['--socket-addr', 'default']) + print("Defaulting to --socket-addr default") + + # Force --no-rpc-logging since the demo server doesn't support RPC logging + if '--rpc-logging' in sys.argv: + print("Warning: RPC logging is not supported by this demo server. Forcing --no-rpc-logging.") + sys.argv.remove('--rpc-logging') + if '--no-rpc-logging' not in sys.argv: + sys.argv.append('--no-rpc-logging') + + # Pass our compiled proto to the console + sys.exit(pw_system.console.main(compiled_protos=[math_service_pb2])) + + + +if __name__ == '__main__': + main()