Snap for 10103804 from 281241091fb911ccac12963fe02d4d59afa91803 to mainline-tzdata5-release
Change-Id: I5189a0b3b98643a541e421aa929fc6fabf3da079
diff --git a/.bazelignore b/.bazelignore
index 280ad54..5b3b096 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -4,3 +4,8 @@
environment
node_modules
out
+bazel-bin
+bazel-out
+bazel-pigweed
+bazel-testlogs
+outbazel
diff --git a/.gitignore b/.gitignore
index f06c43c..ed3ae36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
compile_commands.json
out/
bazel-*
+outbazel/
.presubmit/
docs/_build
diff --git a/.pylintrc b/.pylintrc
index 1d46aee..60a4f91 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,10 +1,5 @@
[MASTER] # inclusive-language: ignore
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code.
-extension-pkg-allowlist=mypy
-
# Add files or directories to the blocklist. They should be base names, not
# paths.
ignore=CVS
@@ -28,7 +23,7 @@
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
-load-plugins=
+load-plugins=pylint.extensions.no_self_use
# Pickle collected data for later comparisons.
persistent=yes
@@ -60,11 +55,23 @@
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
-disable=bad-continuation, # Rely on yapf for formatting
+disable=broad-exception-raised,
+ consider-iterating-dictionary,
+ consider-using-f-string,
+ consider-using-generator,
+ consider-using-in,
consider-using-with,
fixme,
- subprocess-run-check,
+ implicit-str-concat,
raise-missing-from,
+ redundant-u-string-prefix,
+ subprocess-run-check,
+ superfluous-parens,
+ unnecessary-lambda-assignment,
+ unspecified-encoding,
+ use-dict-literal,
+ use-list-literal,
+
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -131,13 +138,6 @@
# Maximum number of lines in a module.
max-module-lines=9999
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
- dict-separator
-
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
@@ -161,7 +161,7 @@
callbacks=cb_,
_cb
-# A regular expression matching the name of placeholder variables (i.e.
+# A regular expression matching the name of placeholder variables (i.e.
# expected to not be used). # inclusive-language: ignore
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
@@ -309,7 +309,7 @@
_
# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
+include-naming-hint=yes
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
@@ -511,5 +511,5 @@
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
- Exception
+overgeneral-exceptions=builtins.BaseException,
+ builtins.Exception
diff --git a/BUILD.gn b/BUILD.gn
index a2b7532..aa97568 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -86,7 +86,6 @@
# exclusively to facilitate easy upstream development and testing.
group("default") {
deps = [
- ":check_modules",
":docs",
":host",
":pi_pico",
@@ -94,6 +93,7 @@
":python.tests",
":static_analysis",
":stm32f429i",
+ ":warn_if_modules_out_of_date",
]
}
@@ -126,11 +126,22 @@
depth = 1
}
-# Check that PIGWEED_MODULES is up-to-date and sorted.
-action("check_modules") {
+# Warns if PIGWEED_MODULES is not up-to-date and sorted.
+action("warn_if_modules_out_of_date") {
forward_variables_from(_update_or_check_modules_lists, "*")
outputs = [ "$target_gen_dir/$target_name.passed" ]
- args += [ "--warn-only" ] + rebase_path(outputs, root_build_dir)
+ args += [
+ "--mode=WARN",
+ "--stamp",
+ ] + rebase_path(outputs, root_build_dir)
+ pool = ":module_check_pool"
+}
+
+# Fails if PIGWEED_MODULES is not up-to-date and sorted.
+action("check_modules") {
+ forward_variables_from(_update_or_check_modules_lists, "*")
+ outputs = [ "$target_gen_dir/$target_name.ALWAYS_RERUN" ] # Never created
+ args += [ "--mode=CHECK" ]
pool = ":module_check_pool"
}
@@ -139,6 +150,7 @@
action("update_modules") {
forward_variables_from(_update_or_check_modules_lists, "*")
outputs = [ "$target_gen_dir/$target_name.ALWAYS_RERUN" ] # Never created
+ args += [ "--mode=UPDATE" ]
pool = ":module_check_pool"
}
@@ -266,17 +278,33 @@
group("integration_tests") {
_default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
deps = [
+ "$dir_pw_cli/py:process_integration_test.action($_default_tc)",
"$dir_pw_rpc:cpp_client_server_integration_test($_default_tc)",
"$dir_pw_rpc/py:python_client_cpp_server_test.action($_default_tc)",
"$dir_pw_unit_test/py:rpc_service_test.action($_default_tc)",
]
}
-# OSS-Fuzz uses this target to build fuzzers alone.
+# Build-only target for fuzzers.
group("fuzzers") {
- # Fuzzing is only supported on Linux and MacOS using clang.
+ deps = []
+
+ # TODO(b/274437709): The client_fuzzer encounters build errors on macos. Limit
+ # it to Linux hosts for now.
+ if (host_os == "linux") {
+ _default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
+ deps += [ "$dir_pw_rpc/fuzz:client_fuzzer($_default_tc)" ]
+ }
+
if (host_os != "win") {
- deps = [ ":pw_module_tests($dir_pigweed/targets/host:host_clang_fuzz)" ]
+ # Coverage-guided fuzzing is only supported on Linux and MacOS using clang.
+ deps += [
+ "$dir_pw_bluetooth_hci:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_fuzzer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_protobuf:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_random:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_tokenizer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ ]
}
}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6c5266a..4e463d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,8 +29,14 @@
pw_chrono.system_clock pw_chrono_zephyr.system_clock pw_chrono/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_INTERRUPT_CONTEXT
pw_interrupt.context pw_interrupt_zephyr.context pw_interrupt/backend.cmake)
- pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_ZEPHYR
pw_log pw_log_zephyr pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
+ pw_log_tokenized.handler pw_log_zephyr.tokenized_handler pw_log_tokenized/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
+ pw_log pw_log_tokenized pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD_SLEEP
+ pw_thread.sleep pw_thread_zephyr.sleep pw_thread/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_MUTEX
pw_sync.mutex pw_sync_zephyr.mutex_backend pw_sync/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE
@@ -110,6 +116,7 @@
add_subdirectory(pw_thread EXCLUDE_FROM_ALL)
add_subdirectory(pw_thread_freertos EXCLUDE_FROM_ALL)
add_subdirectory(pw_thread_stl EXCLUDE_FROM_ALL)
+add_subdirectory(pw_thread_zephyr EXCLUDE_FROM_ALL)
add_subdirectory(pw_tokenizer EXCLUDE_FROM_ALL)
add_subdirectory(pw_toolchain EXCLUDE_FROM_ALL)
add_subdirectory(pw_trace EXCLUDE_FROM_ALL)
diff --git a/Kconfig.zephyr b/Kconfig.zephyr
index afb0c68..9e07bf0 100644
--- a/Kconfig.zephyr
+++ b/Kconfig.zephyr
@@ -13,7 +13,7 @@
# the License.
config ZEPHYR_PIGWEED_MODULE
- select LIB_CPLUSPLUS
+ select REQUIRES_FULL_LIBCPP
depends on STD_CPP17 || STD_CPP2A || STD_CPP20 || STD_CPP2B
if ZEPHYR_PIGWEED_MODULE
@@ -38,6 +38,7 @@
rsource "pw_string/Kconfig"
rsource "pw_sync_zephyr/Kconfig"
rsource "pw_sys_io_zephyr/Kconfig"
+rsource "pw_thread_zephyr/Kconfig"
rsource "pw_tokenizer/Kconfig"
rsource "pw_varint/Kconfig"
diff --git a/OWNERS b/OWNERS
index c0c23f8..cc577ae 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,4 @@
# CHRE team maintainers in AOSP
bduddie@google.com
-karthikmb@google.com
+berchet@google.com
stange@google.com
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index a75a246..ef740fc 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -1,4 +1,5 @@
docker
+pw_alignment
pw_allocator
pw_analog
pw_android_toolchain
@@ -115,6 +116,7 @@
pw_thread_freertos
pw_thread_stl
pw_thread_threadx
+pw_thread_zephyr
pw_tls_client
pw_tls_client_boringssl
pw_tls_client_mbedtls
diff --git a/WORKSPACE b/WORKSPACE
index 2012cc1..8dcbd47 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -180,59 +180,12 @@
nanopb_workspace()
-# Set up NodeJs rules.
-# Required by: pigweed.
-# Used in modules: //pw_web.
-http_archive(
- name = "build_bazel_rules_nodejs",
- sha256 = "b32a4713b45095e9e1921a7fcb1adf584bc05959f3336e7351bcf77f015a2d7c",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.1.0/rules_nodejs-4.1.0.tar.gz"],
-)
-
-# Get the latest LTS version of Node.
-load(
- "@build_bazel_rules_nodejs//:index.bzl",
- "node_repositories",
- "yarn_install",
-)
-
-node_repositories(package_json = ["//:package.json"])
-
-yarn_install(
- name = "npm",
- package_json = "//:package.json",
- yarn_lock = "//:yarn.lock",
-)
-
-# Set up web-testing rules.
-# Required by: pigweed.
-# Used in modules: //pw_web.
-http_archive(
- name = "io_bazel_rules_webtesting",
- sha256 = "9bb461d5ef08e850025480bab185fd269242d4e533bca75bfb748001ceb343c3",
- urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.3/rules_webtesting.tar.gz"],
-)
-
-load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
-
-web_test_repositories()
-
-load(
- "@io_bazel_rules_webtesting//web/versioned:browsers-0.3.2.bzl",
- "browser_repositories",
-)
-
-browser_repositories(
- chromium = True,
- firefox = True,
-)
-
# Set up embedded C/C++ toolchains.
# Required by: pigweed.
# Used in modules: //pw_polyfill, //pw_build (all pw_cc* targets).
git_repository(
name = "bazel_embedded",
- commit = "17c93d5fa52c4c78860b8bbd327325fba6c85555",
+ commit = "91dcc13ebe5df755ca2fe896ff6f7884a971d05b",
remote = "https://github.com/bazelembedded/bazel-embedded.git",
shallow_since = "1631751909 +0800",
)
@@ -375,11 +328,6 @@
rules_fuzzing_init()
-# esbuild setup
-load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories")
-
-esbuild_repositories(npm_repository = "npm") # Note, npm is the default value for npm_repository
-
RULES_JVM_EXTERNAL_TAG = "2.8"
RULES_JVM_EXTERNAL_SHA = "79c9850690d7614ecdb72d68394f994fef7534b292c4867ce5e7dec0aa7bdfad"
@@ -426,3 +374,11 @@
remote = "https://boringssl.googlesource.com/boringssl",
shallow_since = "1637714942 +0000",
)
+
+http_archive(
+ name = "freertos",
+ build_file = "//:third_party/freertos/BUILD.bazel",
+ sha256 = "89af32b7568c504624f712c21fe97f7311c55fccb7ae6163cda7adde1cde7f62",
+ strip_prefix = "FreeRTOS-Kernel-10.5.1",
+ urls = ["https://github.com/FreeRTOS/FreeRTOS-Kernel/archive/refs/tags/V10.5.1.tar.gz"],
+)
diff --git a/bootstrap.sh b/bootstrap.sh
index 429c3ac..a615fc5 100644
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -63,9 +63,13 @@
. "$PW_ROOT/pw_env_setup/util.sh"
+# Check environment properties
pw_deactivate
pw_eval_sourced "$_pw_sourced" "$_PW_BOOTSTRAP_PATH"
-pw_check_root "$PW_ROOT"
+if ! pw_check_root "$PW_ROOT"; then
+ return
+fi
+
_PW_ACTUAL_ENVIRONMENT_ROOT="$(pw_get_env_root)"
export _PW_ACTUAL_ENVIRONMENT_ROOT
SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index a45ec50..bbfc362 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -99,6 +99,7 @@
group("third_party_docs") {
deps = [
"$dir_pigweed/third_party/boringssl:docs",
+ "$dir_pigweed/third_party/emboss:docs",
"$dir_pigweed/third_party/freertos:docs",
"$dir_pigweed/third_party/fuchsia:docs",
"$dir_pigweed/third_party/googletest:docs",
@@ -110,10 +111,27 @@
_doxygen_input_files = [
# All sources with doxygen comment blocks.
"$dir_pw_async/public/pw_async/dispatcher.h",
+ "$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h",
"$dir_pw_async/public/pw_async/task.h",
+ "$dir_pw_async_basic/public/pw_async_basic/dispatcher.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/gatt/client.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/gatt/server.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/host.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/central.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/connection.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h",
"$dir_pw_chrono/public/pw_chrono/system_clock.h",
"$dir_pw_chrono/public/pw_chrono/system_timer.h",
+ "$dir_pw_function/public/pw_function/scope_guard.h",
+ "$dir_pw_log_tokenized/public/pw_log_tokenized/handler.h",
+ "$dir_pw_log_tokenized/public/pw_log_tokenized/metadata.h",
+ "$dir_pw_rpc/public/pw_rpc/internal/config.h",
+ "$dir_pw_string/public/pw_string/format.h",
+ "$dir_pw_string/public/pw_string/string.h",
+ "$dir_pw_function/public/pw_function/function.h",
+ "$dir_pw_function/public/pw_function/pointer.h",
"$dir_pw_string/public/pw_string/string_builder.h",
+ "$dir_pw_string/public/pw_string/util.h",
"$dir_pw_sync/public/pw_sync/binary_semaphore.h",
"$dir_pw_sync/public/pw_sync/borrow.h",
"$dir_pw_sync/public/pw_sync/counting_semaphore.h",
@@ -125,8 +143,8 @@
"$dir_pw_sync/public/pw_sync/timed_mutex.h",
"$dir_pw_sync/public/pw_sync/timed_thread_notification.h",
"$dir_pw_sync/public/pw_sync/virtual_basic_lockable.h",
- "$dir_pw_rpc/public/pw_rpc/internal/config.h",
"$dir_pw_tokenizer/public/pw_tokenizer/encode_args.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/tokenize.h",
]
pw_python_action("generate_doxygen") {
diff --git a/docs/Doxyfile b/docs/Doxyfile
index 177de67..28f2b8f 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -2387,7 +2387,10 @@
PW_UNLOCK_FUNCTION(...)= \
PW_NO_LOCK_SAFETY_ANALYSIS= \
PW_CXX_STANDARD_IS_SUPPORTED(...)=1 \
- PW_EXTERN_C_START=
+ PW_EXTERN_C_START= \
+ PW_LOCKS_EXCLUDED(...)= \
+ PW_EXCLUSIVE_LOCKS_REQUIRED(...)= \
+ PW_GUARDED_BY(...)= \
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
diff --git a/docs/_static/css/pigweed.css b/docs/_static/css/pigweed.css
index 3721f7f..bea0fee 100644
--- a/docs/_static/css/pigweed.css
+++ b/docs/_static/css/pigweed.css
@@ -132,3 +132,47 @@
font-weight: bold;
font-size: var(--font-size--normal);
}
+
+/* Support taglines inline with page titles */
+section.with-subtitle > h1 {
+ display: inline;
+}
+
+/* Restore the padding to the top of the page that was removed by making the
+ h1 element inline */
+section.with-subtitle {
+ padding-top: 1.5em;
+}
+
+.section-subtitle {
+ display: inline;
+ font-size: larger;
+ font-weight: bold;
+}
+
+/* Styling for module doc section buttons */
+ul.pw-module-section-buttons {
+ display: flex;
+ justify-content: center;
+ padding: 0;
+}
+
+li.pw-module-section-button {
+ display: inline;
+ list-style-type: none;
+ padding: 0 4px;
+}
+
+li.pw-module-section-button p {
+ display: inline;
+}
+
+li.pw-module-section-button p a {
+ background-color: var(--color-section-button) !important;
+ border-color: var(--color-section-button) !important;
+}
+
+li.pw-module-section-button p a:hover {
+ background-color: var(--color-section-button-hover) !important;
+ border-color: var(--color-section-button-hover) !important;
+}
diff --git a/docs/build_system.rst b/docs/build_system.rst
index 62f9df4..a7a5c8e 100644
--- a/docs/build_system.rst
+++ b/docs/build_system.rst
@@ -766,9 +766,10 @@
.. _Bazel config reference: https://docs.bazel.build/versions/main/skylark/config.html
+.. _docs-build_system-bazel_configuration:
-Pigweeds configuration
-^^^^^^^^^^^^^^^^^^^^^^
+Pigweed's Bazel configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pigweeds Bazel configuration API is designed to be distributed across the
Pigweed repository and/or your downstream repository. If you are coming from
GN's centralized configuration API it might be useful to think about
diff --git a/docs/conf.py b/docs/conf.py
index 39d681c..f99f920 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -41,6 +41,7 @@
extensions = [
'pw_docgen.sphinx.google_analytics', # Enables optional Google Analytics
+ 'pw_docgen.sphinx.module_metadata',
'sphinx.ext.autodoc', # Automatic documentation for Python code
'sphinx.ext.napoleon', # Parses Google-style docstrings
'sphinxarg.ext', # Automatic documentation of Python argparse
@@ -151,6 +152,8 @@
'color-highlight-on-target': '#ffffcc',
# Background color emphasized code lines.
'color-code-hll-background': '#ffffcc',
+ 'color-section-button': '#b529aa',
+ 'color-section-button-hover': '#fb71fe',
},
'dark_css_variables': {
'color-sidebar-brand-text': '#fb71fe',
@@ -178,6 +181,8 @@
'color-highlight-on-target': '#ffc55140',
# Background color emphasized code lines.
'color-code-hll-background': '#ffc55140',
+ 'color-section-button': '#fb71fe',
+ 'color-section-button-hover': '#b529aa',
},
}
diff --git a/docs/style_guide.rst b/docs/style_guide.rst
index 6f7ce67..956ac27 100644
--- a/docs/style_guide.rst
+++ b/docs/style_guide.rst
@@ -1248,7 +1248,7 @@
.. admonition:: See also
- `Breathe directives to use in rst files <https://breathe.readthedocs.io/en/latest/directives.html>`_
+ `Breathe directives to use in RST files <https://breathe.readthedocs.io/en/latest/directives.html>`_
Example Doxygen Comment Block
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1304,28 +1304,39 @@
Doxygen Syntax
^^^^^^^^^^^^^^
-Pigweed prefers to use rst wherever possible but there are a few Doxygen
+Pigweed prefers to use RST wherever possible, but there are a few Doxygen
syntatic elements that may be needed.
-`Structural commands
-<https://doxygen.nl/manual/docblocks.html#structuralcommands>`_ for the first
-line of a Doxygen comment block.
+Common Doxygen commands for use within a comment block:
-To group multiple things into a single comment block put them both at the
-start on their own lines. For example:
+- ``@rst`` To start a reStructuredText block. This is a custom alias for
+ ``\verbatim embed:rst:leading-asterisk``.
+- `@code <https://www.doxygen.nl/manual/commands.html#cmdcode>`_ Start a code
+ block.
+- `@param <https://www.doxygen.nl/manual/commands.html#cmdparam>`_ Document
+ parameters, this may be repeated.
+- `@pre <https://www.doxygen.nl/manual/commands.html#cmdpre>`_ Starts a
+ paragraph where the precondition of an entity can be described.
+- `@post <https://www.doxygen.nl/manual/commands.html#cmdpost>`_ Starts a
+ paragraph where the postcondition of an entity can be described.
+- `@return <https://www.doxygen.nl/manual/commands.html#cmdreturn>`_ Single
+ paragraph to describe return value(s).
+- `@retval <https://www.doxygen.nl/manual/commands.html#cmdretval>`_ Document
+ return values by name. This is rendered as a definition list.
+- `@note <https://www.doxygen.nl/manual/commands.html#cmdnote>`_ Add a note
+ admonition to the end of documentation.
+- `@b <https://www.doxygen.nl/manual/commands.html#cmdb>`_ To bold one word.
-- ``@class`` to document a Class.
-- ``@struct`` to document a C-struct.
-- ``@union`` to document a union.
-- ``@enum`` to document an enumeration type.
-- ``@fn`` to document a function.
-- ``@var`` to document a variable or typedef or enum value.
-- ``@def`` to document a #define.
-- ``@typedef`` to document a type definition.
-- ``@file`` to document a file.
-- ``@namespace`` to document a namespace.
-- ``@package`` to document a Java package.
-- ``@interface`` to document an IDL interface.
+Doxygen provides `structural commands
+<https://doxygen.nl/manual/docblocks.html#structuralcommands>`_ that associate a
+comment block with a particular symbol. Example of these include ``@class``,
+``@struct``, ``@def``, ``@fn``, and ``@file``. Do not use these unless
+necessary, since they are redundant with the declarations themselves.
+
+One case where structural commands are necessary is when a single comment block
+describes multiple symbols. To group multiple symbols into a single comment
+block, include a structural commands for each symbol on its own line. For
+example, the following comment documents two macros:
.. code-block:: cpp
@@ -1335,23 +1346,6 @@
/// Documents functions that dynamically check to see if a lock is held, and
/// fail if it is not held.
-Common Doxygen commands for use within a comment block.
-
-- ``@rst`` To start a reStructuredText block. This is an alias for
- ``\verbatim embed:rst:leading-asterisk``.
-- `@code <https://www.doxygen.nl/manual/commands.html#cmdcode>`_ Start a code block.
-- `@param <https://www.doxygen.nl/manual/commands.html#cmdparam>`_ Document
- parameters, this may be repeated.
-- `@pre <https://www.doxygen.nl/manual/commands.html#cmdpre>`_ Starts a
- paragraph where the precondition of an entity can be described.
-- `@return <https://www.doxygen.nl/manual/commands.html#cmdreturn>`_ Single
- paragraph to describe return value(s).
-- `@retval <https://www.doxygen.nl/manual/commands.html#cmdretval>`_ Document
- return values by name. This is rendered as a definition list.
-- `@note <https://www.doxygen.nl/manual/commands.html#cmdnote>`_ Add a note
- admonition to the end of documentation.
-- `@b <https://www.doxygen.nl/manual/commands.html#cmdb>`_ To bold one word.
-
.. seealso:: `All Doxygen commands <https://www.doxygen.nl/manual/commands.html>`_
Cross-references
diff --git a/package-lock.json b/package-lock.json
index fbc143a..d00b93c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
"name": "pigweedjs",
- "version": "0.0.5",
+ "version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pigweedjs",
- "version": "0.0.5",
+ "version": "0.0.7",
"license": "Apache-2.0",
"dependencies": {
"@protobuf-ts/protoc": "^2.7.0",
"google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
"object-path": "^0.11.8",
"ts-protoc-gen": "^0.15.0"
},
@@ -784,6 +785,12 @@
"node": ">=6"
}
},
+ "node_modules/@grpc/proto-loader/node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "dev": true
+ },
"node_modules/@grpc/proto-loader/node_modules/protobufjs": {
"version": "6.11.2",
"dev": true,
@@ -7032,9 +7039,9 @@
"license": "MIT"
},
"node_modules/long": {
- "version": "4.0.0",
- "dev": true,
- "license": "Apache-2.0"
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+ "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
@@ -10604,6 +10611,12 @@
"yargs": "^16.1.1"
},
"dependencies": {
+ "long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "dev": true
+ },
"protobufjs": {
"version": "6.11.2",
"dev": true,
@@ -15068,8 +15081,9 @@
"dev": true
},
"long": {
- "version": "4.0.0",
- "dev": true
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+ "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"loose-envify": {
"version": "1.4.0",
diff --git a/package.json b/package.json
index 837da7f..b3eebb5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pigweedjs",
- "version": "0.0.7",
+ "version": "0.0.8",
"description": "An open source collection of embedded-targeted libraries",
"author": "The Pigweed Authors",
"license": "Apache-2.0",
@@ -75,6 +75,7 @@
"dependencies": {
"@protobuf-ts/protoc": "^2.7.0",
"google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
"object-path": "^0.11.8",
"ts-protoc-gen": "^0.15.0"
},
diff --git a/pigweed.json b/pigweed.json
new file mode 100644
index 0000000..7a4623e
--- /dev/null
+++ b/pigweed.json
@@ -0,0 +1,9 @@
+{
+ "pw": {
+ "pw_presubmit": {
+ "format": {
+ "python_formatter": "black"
+ }
+ }
+ }
+}
diff --git a/pw_alignment/BUILD.bazel b/pw_alignment/BUILD.bazel
new file mode 100644
index 0000000..298605f
--- /dev/null
+++ b/pw_alignment/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+pw_cc_library(
+ name = "pw_alignment",
+ hdrs = ["public/pw_alignment/alignment.h"],
+ includes = ["public"],
+)
diff --git a/pw_alignment/BUILD.gn b/pw_alignment/BUILD.gn
new file mode 100644
index 0000000..3ea208f
--- /dev/null
+++ b/pw_alignment/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_alignment") {
+ public = [ "public/pw_alignment/alignment.h" ]
+ public_configs = [ ":public_include_path" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_alignment/docs.rst b/pw_alignment/docs.rst
new file mode 100644
index 0000000..8d4958d
--- /dev/null
+++ b/pw_alignment/docs.rst
@@ -0,0 +1,42 @@
+.. _module-pw_alignment:
+
+============
+pw_alignment
+============
+This module defines an interface for ensuring natural alignment of objects.
+
+Avoiding Atomic Libcalls
+========================
+The main motivation is to provide a portable way for
+preventing the compiler from emitting libcalls to builtin atomics
+functions and instead use native atomic instructions. This is especially
+useful for any pigweed user that uses ``std::atomic``.
+
+Take for example `std::atomic<std::optional<bool>>`. Accessing the underlying object
+would normally involve a call to a builtin `__atomic_*` function provided by a builtins
+library. However, if the compiler can determine that the size of the object is the same
+as its alignment, then it can replace a libcall to `__atomic_*` with native instructions.
+
+There can be certain situations where a compiler might not be able to assert this.
+Depending on the implementation of `std::optional<bool>`, this object could
+have a size of 2 bytes but an alignment of 1 byte which wouldn't satisfy the constraint.
+
+Basic usage
+-----------
+`pw_alignment` provides a wrapper class `pw::NaturallyAligned` for enforcing natural alignment without any
+changes to how the object is used. Since this is commonly used with atomics, an
+aditional class `pw::AlignedAtomic` is provided for simplifying things.
+
+.. code-block:: c++
+
+ // Passing a `std::optional<bool>` to `__atomic_exchange` might not replace the call
+ // with native instructions if the compiler cannot determine all instances of this object
+ // will happen to be aligned.
+ std::atomic<std::optional<bool>> maybe_nat_aligned_obj;
+
+ // `pw::NaturallyAligned<...>` forces the object to be aligned to its size, so passing
+ // it to `__atomic_exchange` will result in a replacement with native instructions.
+ std::atomic<pw::NaturallyAligned<std::optional<bool>>> nat_aligned_obj;
+
+ // Shorter spelling for the same as above.
+ std::AlignedAtomic<std::optional<bool>> also_nat_aligned_obj;
diff --git a/pw_alignment/public/pw_alignment/alignment.h b/pw_alignment/public/pw_alignment/alignment.h
new file mode 100644
index 0000000..e295c5d
--- /dev/null
+++ b/pw_alignment/public/pw_alignment/alignment.h
@@ -0,0 +1,84 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// todo-check: ignore
+// TODO(fxbug.dev/120998): Once this bug is addressed, this module can likely
+// be removed and we could just inline the using statements.
+
+#include <atomic>
+#include <limits>
+#include <type_traits>
+
+namespace pw {
+
+#if __cplusplus >= 202002L
+using bit_ceil = std::bit_ceil;
+#else
+constexpr size_t countl_zero(size_t x) noexcept {
+ size_t size_digits = std::numeric_limits<size_t>::digits;
+
+ if (sizeof(x) <= sizeof(unsigned int))
+ return __builtin_clz(static_cast<unsigned int>(x)) -
+ (std::numeric_limits<unsigned int>::digits - size_digits);
+
+ if (sizeof(x) <= sizeof(unsigned long))
+ return __builtin_clzl(static_cast<unsigned long>(x)) -
+ (std::numeric_limits<unsigned long>::digits - size_digits);
+
+ static_assert(sizeof(x) <= sizeof(unsigned long long));
+ return __builtin_clzll(static_cast<unsigned long long>(x)) -
+ (std::numeric_limits<unsigned long long>::digits - size_digits);
+}
+
+constexpr size_t bit_width(size_t x) noexcept {
+ return std::numeric_limits<size_t>::digits - countl_zero(x);
+}
+
+constexpr size_t bit_ceil(size_t x) noexcept {
+ if (x == 0)
+ return 1;
+ return size_t{1} << bit_width(size_t{x - 1});
+}
+#endif
+
+// The NaturallyAligned class is a wrapper class for ensuring the object is
+// aligned to a power of 2 bytes greater than or equal to its size.
+template <typename T>
+struct [[gnu::aligned(bit_ceil(sizeof(T)))]] NaturallyAligned
+ : public T{NaturallyAligned() : T(){} NaturallyAligned(const T& t) :
+ T(t){} template <class U>
+ NaturallyAligned(const U& u) : T(u){} NaturallyAligned
+ operator=(T other){return T::operator=(other);
+} // namespace pw
+}
+;
+
+// This is a convenience wrapper for ensuring the object held by std::atomic is
+// naturally aligned. Ensuring the underlying objects's alignment is natural
+// allows clang to replace libcalls to atomic functions
+// (__atomic_load/store/exchange/etc) with native instructions when appropriate.
+//
+// Example usage:
+//
+// // Here std::optional<bool> has a size of 2 but alignment of 1, which would
+// // normally lower to an __atomic_* libcall, but pw::NaturallyAligned in
+// // std::atomic tells the compiler to align the object to 2 bytes, which
+// // satisfies the requirements for replacing __atomic_* with instructions.
+// pw::AlignedAtomic<std::optional<bool>> mute_enable{};
+//
+template <typename T>
+using AlignedAtomic = std::atomic<NaturallyAligned<T>>;
+
+} // namespace pw
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
index abc4359..05678f7 100755
--- a/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
@@ -25,8 +25,8 @@
from pathlib import Path
from typing import List
-import serial # type: ignore
-import serial.tools.list_ports # type: ignore
+import serial
+import serial.tools.list_ports
import pw_arduino_build.log
from pw_arduino_build import teensy_detector
from pw_arduino_build.file_operations import decode_file_json
diff --git a/pw_arduino_build/py/setup.cfg b/pw_arduino_build/py/setup.cfg
index 05f6d2a..f59cba7 100644
--- a/pw_arduino_build/py/setup.cfg
+++ b/pw_arduino_build/py/setup.cfg
@@ -23,6 +23,7 @@
zip_safe = False
install_requires =
pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
coloredlogs
parameterized
diff --git a/pw_assert/BUILD.bazel b/pw_assert/BUILD.bazel
index f45ba62..f6d1f86 100644
--- a/pw_assert/BUILD.bazel
+++ b/pw_assert/BUILD.bazel
@@ -57,6 +57,23 @@
)
pw_cc_library(
+ name = "libc_assert",
+ hdrs = [
+ "libc_assert_public_overrides/assert.h",
+ "libc_assert_public_overrides/cassert",
+ "public/pw_assert/internal/libc_assert.h",
+ ],
+ includes = [
+ "libc_assert_public_overrides",
+ "public",
+ ],
+ deps = [
+ "//pw_assert",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
name = "print_and_abort",
hdrs = ["public/pw_assert/internal/print_and_abort.h"],
includes = ["public"],
diff --git a/pw_assert/BUILD.gn b/pw_assert/BUILD.gn
index 0502d2d..28552dc 100644
--- a/pw_assert/BUILD.gn
+++ b/pw_assert/BUILD.gn
@@ -37,6 +37,11 @@
visibility = [ ":*" ]
}
+config("libc_assert_overrides") {
+ include_dirs = [ "libc_assert_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
config("print_and_abort_check_backend_overrides") {
include_dirs = [ "print_and_abort_check_public_overrides" ]
visibility = [ ":*" ]
@@ -117,6 +122,19 @@
group("assert_compatibility_backend.impl") {
}
+pw_source_set("libc_assert") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "libc_assert_public_overrides/assert.h",
+ "libc_assert_public_overrides/cassert",
+ "public/pw_assert/internal/libc_assert.h",
+ ]
+ public_deps = [
+ ":assert",
+ dir_pw_preprocessor,
+ ]
+}
+
pw_source_set("print_and_abort") {
public_configs = [ ":public_include_path" ]
public_deps = [ ":config" ]
diff --git a/pw_assert/CMakeLists.txt b/pw_assert/CMakeLists.txt
index 64fb40d..623e3ef 100644
--- a/pw_assert/CMakeLists.txt
+++ b/pw_assert/CMakeLists.txt
@@ -69,6 +69,19 @@
pw_preprocessor
)
+pw_add_library(pw_assert.libc_assert INTERFACE
+ HEADERS
+ libc_assert_public_overrides/assert.h
+ libc_assert_public_overrides/cassert
+ public/pw_assert/internal/libc_assert.h
+ PUBLIC_INCLUDES
+ public
+ libc_assert_public_overrides
+ PUBLIC_DEPS
+ pw_assert.assert
+ pw_preprocessor
+)
+
pw_add_library(pw_assert.print_and_abort INTERFACE
HEADERS
public/pw_assert/internal/print_and_abort.h
diff --git a/pw_assert/docs.rst b/pw_assert/docs.rst
index 6feb29b..f411e81 100644
--- a/pw_assert/docs.rst
+++ b/pw_assert/docs.rst
@@ -769,6 +769,15 @@
-------------
The facade is compatible with both C and C++.
+---------------------------------------
+C Standard Library `assert` Replacement
+---------------------------------------
+An optional replacement of the C standard Library's `assert` macro is provided
+through the `libc_assert` target which fully implements replacement `assert.h`
+and `cassert` headers using `PW_ASSERT`. While this is effective for porting
+external code to microcontrollers, we do not advise embedded projects use the
+`assert` macro unless absolutely necessary.
+
----------------
Roadmap & Status
----------------
diff --git a/pw_assert/libc_assert_public_overrides/assert.h b/pw_assert/libc_assert_public_overrides/assert.h
new file mode 100644
index 0000000..3d574a1
--- /dev/null
+++ b/pw_assert/libc_assert_public_overrides/assert.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/internal/libc_assert.h"
diff --git a/pw_assert/libc_assert_public_overrides/cassert b/pw_assert/libc_assert_public_overrides/cassert
new file mode 100644
index 0000000..3d574a1
--- /dev/null
+++ b/pw_assert/libc_assert_public_overrides/cassert
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/internal/libc_assert.h"
diff --git a/pw_assert/public/pw_assert/internal/libc_assert.h b/pw_assert/public/pw_assert/internal/libc_assert.h
new file mode 100644
index 0000000..865650e
--- /dev/null
+++ b/pw_assert/public/pw_assert/internal/libc_assert.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// Include these headers as C++ code, in case assert.h is included within an
+// extern "C" block.
+#ifdef __cplusplus
+extern "C++" {
+#endif // __cplusplus
+
+#include "pw_assert/assert.h"
+#include "pw_preprocessor/util.h"
+
+#ifdef __cplusplus
+} // extern "C++"
+#endif // __cplusplus
+
+// Provide static_assert() on >=C11
+#if (defined(__USE_ISOC11) || \
+ defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) && \
+ !defined(__cplusplus)
+#define static_assert _Static_assert
+#endif // C11 or newer
+
+// Provide assert()
+#undef assert
+#if defined(NDEBUG) // Required by ANSI C standard.
+#define assert(condition) ((void)0)
+#else
+#define assert(condition) PW_ASSERT(condition)
+#endif // defined(NDEBUG)
diff --git a/pw_async/BUILD.bazel b/pw_async/BUILD.bazel
index 7ec7863..2ae446d 100644
--- a/pw_async/BUILD.bazel
+++ b/pw_async/BUILD.bazel
@@ -18,6 +18,7 @@
"fake_dispatcher_test.cc",
"public/pw_async/dispatcher.h",
"public/pw_async/fake_dispatcher.h",
+ "public/pw_async/fake_dispatcher_fixture.h",
"public/pw_async/internal/types.h",
"public/pw_async/task.h",
],
diff --git a/pw_async/BUILD.gn b/pw_async/BUILD.gn
index c1e2879..483b9b5 100644
--- a/pw_async/BUILD.gn
+++ b/pw_async/BUILD.gn
@@ -16,6 +16,7 @@
import("$dir_pw_async/async.gni")
import("$dir_pw_async/backend.gni")
+import("$dir_pw_async/fake_dispatcher_fixture.gni")
import("$dir_pw_async/fake_dispatcher_test.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
@@ -45,6 +46,7 @@
public_deps = [
"$dir_pw_chrono:system_clock",
dir_pw_function,
+ dir_pw_status,
]
public = [
"public/pw_async/internal/types.h",
@@ -67,6 +69,14 @@
] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
}
+fake_dispatcher_fixture("fake_dispatcher_fixture") {
+ backend = ":fake_dispatcher"
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
pw_test_group("tests") {
}
@@ -76,6 +86,9 @@
# Satisfy source_is_in_build_files presubmit step
pw_source_set("fake_dispatcher_test") {
- sources = [ "fake_dispatcher_test.cc" ]
+ sources = [
+ "fake_dispatcher_test.cc",
+ "public/pw_async/fake_dispatcher_fixture.h",
+ ]
visibility = []
}
diff --git a/pw_async/docs.rst b/pw_async/docs.rst
index 178066b..e1a34e0 100644
--- a/pw_async/docs.rst
+++ b/pw_async/docs.rst
@@ -119,7 +119,7 @@
});
// Execute `task` in 5 seconds.
- dispatcher.PostDelayedTask(task, 5s);
+ dispatcher.PostAfter(task, 5s);
// Blocks until `task` runs.
work_thread.join();
@@ -133,21 +133,15 @@
#include "pw_async_basic/dispatcher.h"
- BasicDispatcher dispatcher;
-
- void interrupt_handler() {
- dispatcher.PostTask([](pw::async::Context& ctx){
- // Handle interrupt
- });
- }
-
int main() {
+ BasicDispatcher dispatcher;
+
Task task([](pw::async::Context& ctx){
printf("hello world\n");
});
// Execute `task` in 5 seconds.
- dispatcher.PostDelayedTask(task, 5s);
+ dispatcher.PostAfter(task, 5s);
dispatcher.Run();
return 0;
@@ -156,23 +150,12 @@
Fake Dispatcher
===============
To test async code, FakeDispatcher should be dependency injected in place of
-Dispatcher. Then, time should be driven in unit tests.
+Dispatcher. Then, time should be driven in unit tests using the ``Run*()``
+methods. For convenience, you can use the test fixture
+FakeDispatcherFixture.
-.. code-block:: cpp
-
- TEST(Example) {
- FakeDispatcher dispatcher;
-
- MyClass obj(&dispatcher);
-
- obj.ScheduleSomeTasks();
- dispatcher.RunUntilIdle();
- EXPECT_TRUE(some condition);
-
- obj.ScheduleTaskToRunIn30Seconds();
- dispatcher.RunFor(30s);
- EXPECT_TRUE(task ran);
- }
+.. doxygenclass:: pw::async::test::FakeDispatcherFixture
+ :members:
.. attention::
@@ -185,7 +168,6 @@
Roadmap
-------
- Stabilize Task cancellation API
-- Create test fixture for FakeDispatcher
- Utility for dynamically allocated Tasks
- Bazel support
- CMake support
diff --git a/pw_async/fake_dispatcher_fixture.gni b/pw_async/fake_dispatcher_fixture.gni
new file mode 100644
index 0000000..a58be16
--- /dev/null
+++ b/pw_async/fake_dispatcher_fixture.gni
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+# Creates a pw_source_set that provides a concrete FakeDispatcherFixture.
+#
+# Parameters
+#
+# backend (required)
+# [target] The FakeDispatcher backend to use.
+template("fake_dispatcher_fixture") {
+ assert(defined(invoker.backend))
+
+ pw_source_set(target_name) {
+ public = [ "$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h" ]
+ public_deps = [
+ "$dir_pw_unit_test",
+ invoker.backend,
+ ]
+ forward_variables_from(invoker, [ "visibility" ])
+ }
+}
diff --git a/pw_async/fake_dispatcher_test.cc b/pw_async/fake_dispatcher_test.cc
index 73dc090..3674f4c 100644
--- a/pw_async/fake_dispatcher_test.cc
+++ b/pw_async/fake_dispatcher_test.cc
@@ -14,145 +14,203 @@
#include "pw_async/fake_dispatcher.h"
#include "gtest/gtest.h"
-#include "pw_sync/thread_notification.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
+#define ASSERT_OK(status) ASSERT_EQ(OkStatus(), status)
+#define ASSERT_CANCELLED(status) ASSERT_EQ(Status::Cancelled(), status)
+
using namespace std::chrono_literals;
namespace pw::async::test {
-// Lambdas can only capture one ptr worth of memory without allocating, so we
-// group the data we want to share between tasks and their containing tests
-// inside one struct.
-struct TestPrimitives {
- int count = 0;
- sync::ThreadNotification notification;
-};
-
TEST(FakeDispatcher, PostTasks) {
FakeDispatcher dispatcher;
- TestPrimitives tp;
- auto inc_count = [&tp]([[maybe_unused]] Context& c) { ++tp.count; };
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ };
Task task(inc_count);
- dispatcher.PostTask(task);
+ dispatcher.Post(task);
Task task2(inc_count);
- dispatcher.PostTask(task2);
+ dispatcher.Post(task2);
- Task task3([&tp]([[maybe_unused]] Context& c) { ++tp.count; });
- dispatcher.PostTask(task3);
+ Task task3(inc_count);
+ dispatcher.Post(task3);
+
+ // Should not run; RunUntilIdle() does not advance time.
+ Task task4([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ });
+ dispatcher.PostAfter(task4, 1ms);
dispatcher.RunUntilIdle();
dispatcher.RequestStop();
-
- ASSERT_TRUE(tp.count == 3);
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 4);
}
+// Lambdas can only capture one ptr worth of memory without allocating, so we
+// group the data we want to share between tasks and their containing tests
+// inside one struct.
struct TaskPair {
Task task_a;
Task task_b;
int count = 0;
- sync::ThreadNotification notification;
};
TEST(FakeDispatcher, DelayedTasks) {
FakeDispatcher dispatcher;
TaskPair tp;
- Task task0(
- [&tp]([[maybe_unused]] Context& c) { tp.count = tp.count * 10 + 4; });
+ Task task0([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 4;
+ });
+ dispatcher.PostAfter(task0, 200ms);
- dispatcher.PostDelayedTask(task0, 200ms);
-
- Task task1([&tp]([[maybe_unused]] Context& c) {
+ Task task1([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
tp.count = tp.count * 10 + 1;
- c.dispatcher->PostDelayedTask(tp.task_a, 50ms);
- c.dispatcher->PostDelayedTask(tp.task_b, 25ms);
+ c.dispatcher->PostAfter(tp.task_a, 50ms);
+ c.dispatcher->PostAfter(tp.task_b, 25ms);
+ });
+ dispatcher.PostAfter(task1, 100ms);
+
+ tp.task_a.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 3;
+ });
+ tp.task_b.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 2;
});
- dispatcher.PostDelayedTask(task1, 100ms);
-
- tp.task_a.set_function(
- [&tp]([[maybe_unused]] Context& c) { tp.count = tp.count * 10 + 3; });
-
- tp.task_b.set_function(
- [&tp]([[maybe_unused]] Context& c) { tp.count = tp.count * 10 + 2; });
-
- dispatcher.RunUntilIdle();
+ dispatcher.RunFor(200ms);
dispatcher.RequestStop();
-
- ASSERT_TRUE(tp.count == 1234);
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(tp.count, 1234);
}
TEST(FakeDispatcher, CancelTasks) {
FakeDispatcher dispatcher;
- TestPrimitives tp;
- auto inc_count = [&tp]([[maybe_unused]] Context& c) { ++tp.count; };
+ auto shouldnt_run = []([[maybe_unused]] Context& c,
+ [[maybe_unused]] Status status) { FAIL(); };
- // This task gets canceled in the last task.
- Task task0(inc_count);
- dispatcher.PostDelayedTask(task0, 40ms);
+ TaskPair tp;
+ // This task gets canceled in cancel_task.
+ tp.task_a.set_function(shouldnt_run);
+ dispatcher.PostAfter(tp.task_a, 40ms);
// This task gets canceled immediately.
- Task task1(inc_count);
- dispatcher.PostDelayedTask(task1, 10ms);
+ Task task1(shouldnt_run);
+ dispatcher.PostAfter(task1, 10ms);
ASSERT_TRUE(dispatcher.Cancel(task1));
// This task cancels the first task.
- Task cancel_task(
- [&task0](Context& c) { ASSERT_TRUE(c.dispatcher->Cancel(task0)); });
- dispatcher.PostDelayedTask(cancel_task, 20ms);
+ Task cancel_task([&tp](Context& c, Status status) {
+ ASSERT_OK(status);
+ ASSERT_TRUE(c.dispatcher->Cancel(tp.task_a));
+ ++tp.count;
+ });
+ dispatcher.PostAfter(cancel_task, 20ms);
- dispatcher.RunUntilIdle();
+ dispatcher.RunFor(50ms);
dispatcher.RequestStop();
-
- ASSERT_TRUE(tp.count == 0);
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(tp.count, 1);
}
// Test RequestStop() from inside task.
TEST(FakeDispatcher, RequestStopInsideTask) {
FakeDispatcher dispatcher;
- TestPrimitives tp;
- auto inc_count = [&tp]([[maybe_unused]] Context& c) { ++tp.count; };
+ int count = 0;
+ auto cancelled_cb = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
// These tasks are never executed and cleaned up in RequestStop().
- Task task0(inc_count), task1(inc_count);
- dispatcher.PostDelayedTask(task0, 20ms);
- dispatcher.PostDelayedTask(task1, 21ms);
+ Task task0(cancelled_cb), task1(cancelled_cb);
+ dispatcher.PostAfter(task0, 20ms);
+ dispatcher.PostAfter(task1, 21ms);
- Task stop_task([&tp]([[maybe_unused]] Context& c) {
- ++tp.count;
- c.dispatcher->RequestStop();
+ Task stop_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ static_cast<FakeDispatcher*>(c.dispatcher)->RequestStop();
+ static_cast<FakeDispatcher*>(c.dispatcher)->RunUntilIdle();
});
- dispatcher.PostTask(stop_task);
+ dispatcher.Post(stop_task);
dispatcher.RunUntilIdle();
-
- ASSERT_TRUE(tp.count == 1);
+ ASSERT_EQ(count, 3);
}
TEST(FakeDispatcher, PeriodicTasks) {
FakeDispatcher dispatcher;
- TestPrimitives tp;
-
- Task periodic_task([&tp]([[maybe_unused]] Context& c) { ++tp.count; });
- dispatcher.SchedulePeriodicTask(periodic_task, 20ms, dispatcher.now() + 50ms);
+ int count = 0;
+ Task periodic_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ });
+ dispatcher.PostPeriodicAt(periodic_task, 20ms, dispatcher.now() + 50ms);
// Cancel periodic task after it has run thrice, at +50ms, +70ms, and +90ms.
- Task cancel_task(
- [&periodic_task](Context& c) { c.dispatcher->Cancel(periodic_task); });
- dispatcher.PostDelayedTask(cancel_task, 100ms);
+ Task cancel_task([&periodic_task](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Cancel(periodic_task);
+ });
+ dispatcher.PostAfter(cancel_task, 100ms);
- dispatcher.RunUntilIdle();
+ dispatcher.RunFor(300ms);
dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 3);
+}
- ASSERT_TRUE(tp.count == 3);
+TEST(FakeDispatcher, TasksCancelledByDispatcherDestructor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ {
+ FakeDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+ }
+
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunFor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ FakeDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunFor(5s);
+ ASSERT_EQ(count, 3);
}
} // namespace pw::async::test
diff --git a/pw_async/fake_dispatcher_test.gni b/pw_async/fake_dispatcher_test.gni
index c55e849..33a32ee 100644
--- a/pw_async/fake_dispatcher_test.gni
+++ b/pw_async/fake_dispatcher_test.gni
@@ -26,6 +26,8 @@
# backend (required)
# [target] The FakeDispatcher backend to test.
template("fake_dispatcher_test") {
+ assert(defined(invoker.backend))
+
pw_test(target_name) {
enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
diff --git a/pw_async/public/pw_async/dispatcher.h b/pw_async/public/pw_async/dispatcher.h
index 98185d9..8faaccd 100644
--- a/pw_async/public/pw_async/dispatcher.h
+++ b/pw_async/public/pw_async/dispatcher.h
@@ -20,7 +20,7 @@
class Task;
/// Asynchronous Dispatcher abstract class. A default implementation is provided
-/// in dispatcher_basic.h.
+/// in pw_async_basic.
///
/// Dispatcher implements VirtualSystemClock so the Dispatcher's time can be
/// injected into other modules under test. This is useful for consistently
@@ -30,46 +30,29 @@
public:
~Dispatcher() override = default;
- /// Stop processing tasks and break out of the task loop.
- virtual void RequestStop() = 0;
-
/// Post caller owned |task|.
- virtual void PostTask(Task& task) = 0;
+ virtual void Post(Task& task) = 0;
/// Post caller owned |task| to be run after |delay|.
- virtual void PostDelayedTask(Task& task,
- chrono::SystemClock::duration delay) = 0;
+ virtual void PostAfter(Task& task, chrono::SystemClock::duration delay) = 0;
/// Post caller owned |task| to be run at |time|.
- virtual void PostTaskForTime(Task& task,
- chrono::SystemClock::time_point time) = 0;
+ virtual void PostAt(Task& task, chrono::SystemClock::time_point time) = 0;
/// Post caller owned |task| to be run immediately then rerun at a regular
/// |interval|.
- virtual void SchedulePeriodicTask(Task& task,
- chrono::SystemClock::duration interval) = 0;
- /// Post caller owned |task| to be run at |start_time| then rerun at a regular
+ virtual void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) = 0;
+ /// Post caller owned |task| to be run at |time| then rerun at a regular
/// |interval|. |interval| must not be zero.
- virtual void SchedulePeriodicTask(
- Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) = 0;
+ virtual void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point time) = 0;
/// Returns true if |task| is succesfully canceled.
/// If cancelation fails, the task may be running or completed.
/// Periodic tasks may be posted once more after they are canceled.
virtual bool Cancel(Task& task) = 0;
-
- /// Execute tasks until the Dispatcher enters a state where none are queued.
- virtual void RunUntilIdle() = 0;
-
- /// Run the Dispatcher until Now() has reached `end_time`, executing all tasks
- /// that come due before then.
- virtual void RunUntil(chrono::SystemClock::time_point end_time) = 0;
-
- /// Run the Dispatcher until `duration` has elapsed, executing all tasks that
- /// come due in that period.
- virtual void RunFor(chrono::SystemClock::duration duration) = 0;
};
} // namespace pw::async
diff --git a/pw_async/public/pw_async/fake_dispatcher.h b/pw_async/public/pw_async/fake_dispatcher.h
index 0497637..43931d0 100644
--- a/pw_async/public/pw_async/fake_dispatcher.h
+++ b/pw_async/public/pw_async/fake_dispatcher.h
@@ -18,71 +18,57 @@
namespace pw::async::test {
-// FakeDispatcher is a facade for an implementation of Dispatcher that is used
-// in unit tests. FakeDispatcher uses simulated time. RunUntil() and RunFor()
-// advance time immediately, and now() returns the current simulated time.
-//
-// To support various Task backends, FakeDispatcher wraps a
-// backend::NativeFakeDispatcher that implements standard FakeDispatcher
-// behavior using backend::NativeTask objects.
+/// FakeDispatcher is a facade for an implementation of Dispatcher that is used
+/// in unit tests. FakeDispatcher uses simulated time. RunUntil() and RunFor()
+/// advance time immediately, and now() returns the current simulated time.
+///
+/// To support various Task backends, FakeDispatcher wraps a
+/// backend::NativeFakeDispatcher that implements standard FakeDispatcher
+/// behavior using backend::NativeTask objects.
class FakeDispatcher final : public Dispatcher {
public:
FakeDispatcher() : native_dispatcher_(*this) {}
- void RequestStop() override { native_dispatcher_.RequestStop(); }
+ /// Execute all runnable tasks and return without advancing simulated time.
+ void RunUntilIdle() { native_dispatcher_.RunUntilIdle(); }
- // Post caller owned |task|.
- void PostTask(Task& task) override { native_dispatcher_.PostTask(task); }
-
- // Post caller owned |task| to be run after |delay|.
- void PostDelayedTask(Task& task,
- chrono::SystemClock::duration delay) override {
- native_dispatcher_.PostDelayedTask(task, delay);
- }
-
- // Post caller owned |task| to be run at |time|.
- void PostTaskForTime(Task& task,
- chrono::SystemClock::time_point time) override {
- native_dispatcher_.PostTaskForTime(task, time);
- }
-
- // Post caller owned |task| to be run immediately then rerun at a regular
- // |interval|.
- void SchedulePeriodicTask(Task& task,
- chrono::SystemClock::duration interval) override {
- native_dispatcher_.SchedulePeriodicTask(task, interval);
- }
- // Post caller owned |task| to be run at |start_time| then rerun at a regular
- // |interval|.
- void SchedulePeriodicTask(
- Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) override {
- native_dispatcher_.SchedulePeriodicTask(task, interval, start_time);
- }
-
- // Returns true if |task| is succesfully canceled.
- // If cancelation fails, the task may be running or completed.
- // Periodic tasks may run once more after they are canceled.
- bool Cancel(Task& task) override { return native_dispatcher_.Cancel(task); }
-
- // Execute tasks until the Dispatcher enters a state where none are queued.
- void RunUntilIdle() override { native_dispatcher_.RunUntilIdle(); }
-
- // Run the Dispatcher until Now() has reached `end_time`, executing all tasks
- // that come due before then.
- void RunUntil(chrono::SystemClock::time_point end_time) override {
+ /// Run the dispatcher until Now() has reached `end_time`, executing all tasks
+ /// that come due before then.
+ void RunUntil(chrono::SystemClock::time_point end_time) {
native_dispatcher_.RunUntil(end_time);
}
- // Run the Dispatcher until `duration` has elapsed, executing all tasks that
- // come due in that period.
- void RunFor(chrono::SystemClock::duration duration) override {
+ /// Run the Dispatcher until `duration` has elapsed, executing all tasks that
+ /// come due in that period.
+ void RunFor(chrono::SystemClock::duration duration) {
native_dispatcher_.RunFor(duration);
}
- // VirtualSystemClock overrides:
+ /// Stop processing tasks. After calling RequestStop(), the next time the
+ /// Dispatcher is run, all waiting Tasks will be dequeued and their
+ /// TaskFunctions called with a PW_STATUS_CANCELLED status.
+ void RequestStop() { native_dispatcher_.RequestStop(); }
+ // Dispatcher overrides:
+ void Post(Task& task) override { native_dispatcher_.Post(task); }
+ void PostAfter(Task& task, chrono::SystemClock::duration delay) override {
+ native_dispatcher_.PostAfter(task, delay);
+ }
+ void PostAt(Task& task, chrono::SystemClock::time_point time) override {
+ native_dispatcher_.PostAt(task, time);
+ }
+ void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) override {
+ native_dispatcher_.PostPeriodic(task, interval);
+ }
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) override {
+ native_dispatcher_.PostPeriodicAt(task, interval, start_time);
+ }
+ bool Cancel(Task& task) override { return native_dispatcher_.Cancel(task); }
+
+ // VirtualSystemClock overrides:
chrono::SystemClock::time_point now() override {
return native_dispatcher_.now();
}
diff --git a/pw_async/public/pw_async/fake_dispatcher_fixture.h b/pw_async/public/pw_async/fake_dispatcher_fixture.h
new file mode 100644
index 0000000..b32efeb
--- /dev/null
+++ b/pw_async/public/pw_async/fake_dispatcher_fixture.h
@@ -0,0 +1,66 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "gtest/gtest.h"
+#include "pw_async/fake_dispatcher.h"
+
+namespace pw::async::test {
+
+/// Test fixture that is a simple wrapper around a FakeDispatcher.
+///
+/// Example:
+/// @code{.cpp}
+/// using ExampleTest = pw::async::test::FakeDispatcherFixture;
+///
+/// TEST_F(ExampleTest, Example) {
+/// MyClass obj(dispatcher());
+///
+/// obj.ScheduleSomeTasks();
+/// RunUntilIdle();
+/// EXPECT_TRUE(some condition);
+///
+/// obj.ScheduleTaskToRunIn30Seconds();
+/// RunFor(30s);
+/// EXPECT_TRUE(task ran);
+/// }
+/// @endcode
+class FakeDispatcherFixture : public ::testing::Test {
+ public:
+ /// Returns the FakeDispatcher that should be used for dependency injection.
+ FakeDispatcher& dispatcher() { return dispatcher_; }
+
+ /// Returns the current fake time.
+ chrono::SystemClock::time_point now() { return dispatcher_.now(); }
+
+ /// Dispatches all tasks with due times up until `now()`.
+ void RunUntilIdle() { dispatcher_.RunUntilIdle(); }
+
+ /// Dispatches all tasks with due times up to `end_time`, progressively
+ /// advancing the fake clock.
+ void RunUntil(chrono::SystemClock::time_point end_time) {
+ dispatcher_.RunUntil(end_time);
+ }
+
+ /// Dispatches all tasks with due times up to `now() + duration`,
+ /// progressively advancing the fake clock.
+ void RunFor(chrono::SystemClock::duration duration) {
+ dispatcher_.RunFor(duration);
+ }
+
+ private:
+ FakeDispatcher dispatcher_;
+};
+
+} // namespace pw::async::test
diff --git a/pw_async/public/pw_async/internal/types.h b/pw_async/public/pw_async/internal/types.h
index 67b5a88..c8b7ea2 100644
--- a/pw_async/public/pw_async/internal/types.h
+++ b/pw_async/public/pw_async/internal/types.h
@@ -14,19 +14,32 @@
#pragma once
#include "pw_function/function.h"
+#include "pw_status/status.h"
namespace pw::async {
class Dispatcher;
class Task;
-// Task functions take a `Context` as their argument. Before executing a Task,
-// the Dispatcher sets the pointer to itself and to the Task in `Context`.
struct Context {
Dispatcher* dispatcher;
Task* task;
};
-using TaskFunction = Function<void(Context&)>;
+// A TaskFunction is a unit of work that is wrapped by a Task and executed on a
+// Dispatcher.
+//
+// TaskFunctions take a `Context` as their first argument. Before executing a
+// Task, the Dispatcher sets the pointer to itself and to the Task in `Context`.
+//
+// TaskFunctions take a `Status` as their second argument. When a Task is
+// running as normal, |status| == PW_STATUS_OK. If a Task will not be able to
+// run as scheduled, the Dispatcher will still invoke the TaskFunction with
+// |status| == PW_STATUS_CANCELLED. This provides an opportunity to reclaim
+// resources held by the Task.
+//
+// A Task will not run as scheduled if, for example, it is still waiting when
+// the Dispatcher shuts down.
+using TaskFunction = Function<void(Context&, Status)>;
} // namespace pw::async
diff --git a/pw_async/public/pw_async/task.h b/pw_async/public/pw_async/task.h
index 6579139..c1fa8e6 100644
--- a/pw_async/public/pw_async/task.h
+++ b/pw_async/public/pw_async/task.h
@@ -48,7 +48,7 @@
}
/// Executes this task.
- void operator()(Context& ctx) { native_type_(ctx); }
+ void operator()(Context& ctx, Status status) { native_type_(ctx, status); }
/// Returns the inner NativeTask containing backend-specific state. Only
/// Dispatcher backends or non-portable code should call these methods!
diff --git a/pw_async_basic/BUILD.bazel b/pw_async_basic/BUILD.bazel
index 214299f..8a1fb3a 100644
--- a/pw_async_basic/BUILD.bazel
+++ b/pw_async_basic/BUILD.bazel
@@ -18,6 +18,7 @@
"dispatcher.cc",
"dispatcher_test.cc",
"fake_dispatcher.cc",
+ "fake_dispatcher_fixture_test.cc",
"public/pw_async_basic/dispatcher.h",
"public/pw_async_basic/fake_dispatcher.h",
"public/pw_async_basic/task.h",
diff --git a/pw_async_basic/BUILD.gn b/pw_async_basic/BUILD.gn
index e228edd..9ccce5e 100644
--- a/pw_async_basic/BUILD.gn
+++ b/pw_async_basic/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_async/async.gni")
+import("$dir_pw_async/fake_dispatcher_fixture.gni")
import("$dir_pw_async/fake_dispatcher_test.gni")
import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/target_types.gni")
@@ -81,6 +82,16 @@
backend = ":fake_dispatcher"
}
+fake_dispatcher_fixture("fake_dispatcher_fixture") {
+ backend = ":fake_dispatcher"
+}
+
+pw_test("fake_dispatcher_fixture_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [ "fake_dispatcher_fixture_test.cc" ]
+ deps = [ ":fake_dispatcher_fixture" ]
+}
+
pw_source_set("dispatcher") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_async_basic/dispatcher.h" ]
@@ -115,6 +126,7 @@
tests = [
":dispatcher_test",
":fake_dispatcher_test",
+ ":fake_dispatcher_fixture_test",
]
}
diff --git a/pw_async_basic/dispatcher.cc b/pw_async_basic/dispatcher.cc
index 3ab4813..682ff4f 100644
--- a/pw_async_basic/dispatcher.cc
+++ b/pw_async_basic/dispatcher.cc
@@ -23,26 +23,40 @@
const chrono::SystemClock::duration SLEEP_DURATION = 5s;
+BasicDispatcher::~BasicDispatcher() {
+ RequestStop();
+ lock_.lock();
+ DrainTaskQueue();
+ lock_.unlock();
+}
+
void BasicDispatcher::Run() {
lock_.lock();
while (!stop_requested_) {
- RunLoopOnce();
+ MaybeSleep();
+ ExecuteDueTasks();
}
+ DrainTaskQueue();
lock_.unlock();
}
void BasicDispatcher::RunUntilIdle() {
lock_.lock();
- while (!task_queue_.empty()) {
- RunLoopOnce();
+ ExecuteDueTasks();
+ if (stop_requested_) {
+ DrainTaskQueue();
}
lock_.unlock();
}
void BasicDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
lock_.lock();
- while (end_time < now()) {
- RunLoopOnce();
+ while (end_time < now() && !stop_requested_) {
+ MaybeSleep();
+ ExecuteDueTasks();
+ }
+ if (stop_requested_) {
+ DrainTaskQueue();
}
lock_.unlock();
}
@@ -51,7 +65,7 @@
RunUntil(now() + duration);
}
-void BasicDispatcher::RunLoopOnce() {
+void BasicDispatcher::MaybeSleep() {
if (task_queue_.empty() || task_queue_.front().due_time_ > now()) {
// Sleep until a notification is received or until the due time of the
// next task. Notifications are sent when tasks are posted or 'stop' is
@@ -64,11 +78,12 @@
PW_LOG_DEBUG("no task due; waiting for signal");
timed_notification_.try_acquire_until(wake_time);
lock_.lock();
-
- return;
}
+}
- while (!task_queue_.empty() && task_queue_.front().due_time_ <= now()) {
+void BasicDispatcher::ExecuteDueTasks() {
+ while (!task_queue_.empty() && task_queue_.front().due_time_ <= now() &&
+ !stop_requested_) {
backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
@@ -79,7 +94,7 @@
lock_.unlock();
PW_LOG_DEBUG("running task");
Context ctx{this, &task.task_};
- task(ctx);
+ task(ctx, OkStatus());
lock_.lock();
}
}
@@ -88,37 +103,49 @@
std::lock_guard lock(lock_);
PW_LOG_DEBUG("stop requested");
stop_requested_ = true;
- task_queue_.clear();
timed_notification_.release();
}
-void BasicDispatcher::PostTask(Task& task) { PostTaskForTime(task, now()); }
+void BasicDispatcher::DrainTaskQueue() {
+ PW_LOG_DEBUG("draining task queue");
+ while (!task_queue_.empty()) {
+ backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
-void BasicDispatcher::PostDelayedTask(Task& task,
- chrono::SystemClock::duration delay) {
- PostTaskForTime(task, now() + delay);
+ lock_.unlock();
+ PW_LOG_DEBUG("running cancelled task");
+ Context ctx{this, &task.task_};
+ task(ctx, Status::Cancelled());
+ lock_.lock();
+ }
}
-void BasicDispatcher::PostTaskForTime(Task& task,
- chrono::SystemClock::time_point time) {
+void BasicDispatcher::Post(Task& task) { PostAt(task, now()); }
+
+void BasicDispatcher::PostAfter(Task& task,
+ chrono::SystemClock::duration delay) {
+ PostAt(task, now() + delay);
+}
+
+void BasicDispatcher::PostAt(Task& task, chrono::SystemClock::time_point time) {
lock_.lock();
PW_LOG_DEBUG("posting task");
PostTaskInternal(task.native_type(), time);
lock_.unlock();
}
-void BasicDispatcher::SchedulePeriodicTask(
- Task& task, chrono::SystemClock::duration interval) {
- SchedulePeriodicTask(task, interval, now());
+void BasicDispatcher::PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) {
+ PostPeriodicAt(task, interval, now());
}
-void BasicDispatcher::SchedulePeriodicTask(
+void BasicDispatcher::PostPeriodicAt(
Task& task,
chrono::SystemClock::duration interval,
chrono::SystemClock::time_point start_time) {
PW_DCHECK(interval != chrono::SystemClock::duration::zero());
task.native_type().set_interval(interval);
- PostTaskForTime(task, start_time);
+ PostAt(task, start_time);
}
bool BasicDispatcher::Cancel(Task& task) {
@@ -126,7 +153,6 @@
return task_queue_.remove(task.native_type());
}
-// Ensure lock_ is held when invoking this function.
void BasicDispatcher::PostTaskInternal(
backend::NativeTask& task, chrono::SystemClock::time_point time_due) {
task.due_time_ = time_due;
diff --git a/pw_async_basic/dispatcher_test.cc b/pw_async_basic/dispatcher_test.cc
index 2c47d4e..3f72bbb 100644
--- a/pw_async_basic/dispatcher_test.cc
+++ b/pw_async_basic/dispatcher_test.cc
@@ -19,6 +19,9 @@
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
+#define ASSERT_OK(status) ASSERT_EQ(OkStatus(), status)
+#define ASSERT_CANCELLED(status) ASSERT_EQ(Status::Cancelled(), status)
+
using namespace std::chrono_literals;
namespace pw::async {
@@ -36,25 +39,28 @@
thread::Thread work_thread(thread::stl::Options(), dispatcher);
TestPrimitives tp;
- auto inc_count = [&tp]([[maybe_unused]] Context& c) { ++tp.count; };
+ auto inc_count = [&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++tp.count;
+ };
Task task(inc_count);
- dispatcher.PostTask(task);
+ dispatcher.Post(task);
Task task2(inc_count);
- dispatcher.PostTask(task2);
+ dispatcher.Post(task2);
- Task task3([&tp]([[maybe_unused]] Context& c) {
+ Task task3([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
++tp.count;
tp.notification.release();
});
- dispatcher.PostTask(task3);
+ dispatcher.Post(task3);
tp.notification.acquire();
dispatcher.RequestStop();
work_thread.join();
-
- ASSERT_TRUE(tp.count == 3);
+ ASSERT_EQ(tp.count, 3);
}
struct TaskPair {
@@ -68,32 +74,26 @@
BasicDispatcher dispatcher;
thread::Thread work_thread(thread::stl::Options(), dispatcher);
- TaskPair tp;
-
- Task task0([&tp](Context& c) {
- ++tp.count;
-
- c.dispatcher->PostTask(tp.task_a);
+ sync::ThreadNotification notification;
+ Task task1([¬ification]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ notification.release();
});
- tp.task_a.set_function([&tp](Context& c) {
- ++tp.count;
-
- c.dispatcher->PostTask(tp.task_b);
+ Task task2([&task1](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Post(task1);
});
- tp.task_b.set_function([&tp]([[maybe_unused]] Context& c) {
- ++tp.count;
- tp.notification.release();
+ Task task3([&task2](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Post(task2);
});
+ dispatcher.Post(task3);
- dispatcher.PostTask(task0);
-
- tp.notification.acquire();
+ notification.acquire();
dispatcher.RequestStop();
work_thread.join();
-
- ASSERT_TRUE(tp.count == 3);
}
// Test RequestStop() from inside task.
@@ -101,25 +101,100 @@
BasicDispatcher dispatcher;
thread::Thread work_thread(thread::stl::Options(), dispatcher);
- TestPrimitives tp;
- auto inc_count = [&tp]([[maybe_unused]] Context& c) { ++tp.count; };
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
// These tasks are never executed and cleaned up in RequestStop().
Task task0(inc_count), task1(inc_count);
- dispatcher.PostDelayedTask(task0, 20ms);
- dispatcher.PostDelayedTask(task1, 21ms);
+ dispatcher.PostAfter(task0, 20ms);
+ dispatcher.PostAfter(task1, 21ms);
- Task stop_task([&tp]([[maybe_unused]] Context& c) {
- ++tp.count;
- c.dispatcher->RequestStop();
- tp.notification.release();
+ Task stop_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ static_cast<BasicDispatcher*>(c.dispatcher)->RequestStop();
});
- dispatcher.PostTask(stop_task);
+ dispatcher.Post(stop_task);
- tp.notification.acquire();
work_thread.join();
+ ASSERT_EQ(count, 3);
+}
- ASSERT_TRUE(tp.count == 1);
+TEST(DispatcherBasic, TasksCancelledByRequestStopInDifferentThread) {
+ BasicDispatcher dispatcher;
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ work_thread.join();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByDispatcherDestructor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ {
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+ }
+
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunUntilIdle) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunFor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunFor(5s);
+ ASSERT_EQ(count, 3);
}
} // namespace pw::async
diff --git a/pw_async_basic/docs.rst b/pw_async_basic/docs.rst
index fcf26a9..0363a26 100644
--- a/pw_async_basic/docs.rst
+++ b/pw_async_basic/docs.rst
@@ -7,9 +7,15 @@
This module includes basic implementations of pw_async's Dispatcher and
FakeDispatcher.
-Usage
-=====
+---
+API
+---
+.. doxygenclass:: pw::async::BasicDispatcher
+ :members:
+-----
+Usage
+-----
First, set the following GN variables:
.. code-block::
@@ -36,7 +42,7 @@
#include "pw_async_basic/dispatcher.h"
void DelayedPrint(pw::async::Dispatcher& dispatcher) {
- dispatcher.PostDelayedTask([](auto&){
+ dispatcher.PostAfter([](auto&){
printf("hello world\n");
}, 5s);
}
@@ -48,8 +54,7 @@
return 0;
}
-
+-----------
Size Report
-===========
-
+-----------
.. include:: docs_size_report
diff --git a/pw_async_basic/fake_dispatcher.cc b/pw_async_basic/fake_dispatcher.cc
index ac999fd..cbba89e 100644
--- a/pw_async_basic/fake_dispatcher.cc
+++ b/pw_async_basic/fake_dispatcher.cc
@@ -24,21 +24,28 @@
NativeFakeDispatcher::NativeFakeDispatcher(Dispatcher& dispatcher)
: dispatcher_(dispatcher) {}
-NativeFakeDispatcher::~NativeFakeDispatcher() { RequestStop(); }
+NativeFakeDispatcher::~NativeFakeDispatcher() {
+ RequestStop();
+ DrainTaskQueue();
+}
void NativeFakeDispatcher::RunUntilIdle() {
- while (!task_queue_.empty()) {
- // Only advance to the due time of the next task because new tasks can be
- // scheduled in the next task.
- now_ = task_queue_.front().due_time();
- RunLoopOnce();
+ ExecuteDueTasks();
+ if (stop_requested_) {
+ DrainTaskQueue();
}
}
void NativeFakeDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
- while (!task_queue_.empty() && task_queue_.front().due_time() <= end_time) {
+ while (!task_queue_.empty() && task_queue_.front().due_time() <= end_time &&
+ !stop_requested_) {
now_ = task_queue_.front().due_time();
- RunLoopOnce();
+ ExecuteDueTasks();
+ }
+
+ if (stop_requested_) {
+ DrainTaskQueue();
+ return;
}
if (now_ < end_time) {
@@ -50,8 +57,9 @@
RunUntil(now() + duration);
}
-void NativeFakeDispatcher::RunLoopOnce() {
- while (!task_queue_.empty() && task_queue_.front().due_time() <= now()) {
+void NativeFakeDispatcher::ExecuteDueTasks() {
+ while (!task_queue_.empty() && task_queue_.front().due_time() <= now() &&
+ !stop_requested_) {
::pw::async::backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
@@ -60,41 +68,50 @@
}
Context ctx{&dispatcher_, &task.task_};
- task(ctx);
+ task(ctx, OkStatus());
}
}
void NativeFakeDispatcher::RequestStop() {
PW_LOG_DEBUG("stop requested");
- task_queue_.clear();
+ stop_requested_ = true;
}
-void NativeFakeDispatcher::PostTask(Task& task) {
- PostTaskForTime(task, now());
+void NativeFakeDispatcher::DrainTaskQueue() {
+ while (!task_queue_.empty()) {
+ ::pw::async::backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
+
+ PW_LOG_DEBUG("running cancelled task");
+ Context ctx{&dispatcher_, &task.task_};
+ task(ctx, Status::Cancelled());
+ }
}
-void NativeFakeDispatcher::PostDelayedTask(
- Task& task, chrono::SystemClock::duration delay) {
- PostTaskForTime(task, now() + delay);
+void NativeFakeDispatcher::Post(Task& task) { PostAt(task, now()); }
+
+void NativeFakeDispatcher::PostAfter(Task& task,
+ chrono::SystemClock::duration delay) {
+ PostAt(task, now() + delay);
}
-void NativeFakeDispatcher::PostTaskForTime(
- Task& task, chrono::SystemClock::time_point time) {
+void NativeFakeDispatcher::PostAt(Task& task,
+ chrono::SystemClock::time_point time) {
PW_LOG_DEBUG("posting task");
PostTaskInternal(task.native_type(), time);
}
-void NativeFakeDispatcher::SchedulePeriodicTask(
+void NativeFakeDispatcher::PostPeriodic(
Task& task, chrono::SystemClock::duration interval) {
- SchedulePeriodicTask(task, interval, now());
+ PostPeriodicAt(task, interval, now());
}
-void NativeFakeDispatcher::SchedulePeriodicTask(
+void NativeFakeDispatcher::PostPeriodicAt(
Task& task,
chrono::SystemClock::duration interval,
chrono::SystemClock::time_point start_time) {
task.native_type().set_interval(interval);
- PostTaskForTime(task, start_time);
+ PostAt(task, start_time);
}
bool NativeFakeDispatcher::Cancel(Task& task) {
diff --git a/pw_async_basic/fake_dispatcher_fixture_test.cc b/pw_async_basic/fake_dispatcher_fixture_test.cc
new file mode 100644
index 0000000..5262c34
--- /dev/null
+++ b/pw_async_basic/fake_dispatcher_fixture_test.cc
@@ -0,0 +1,36 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#include "pw_async/fake_dispatcher_fixture.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::async {
+namespace {
+
+using FakeDispatcherFixture = test::FakeDispatcherFixture;
+
+TEST_F(FakeDispatcherFixture, PostTasks) {
+ int count = 0;
+ auto inc_count = [&count](Context& /*c*/, Status /*status*/) { ++count; };
+
+ Task task(inc_count);
+ dispatcher().Post(task);
+
+ ASSERT_EQ(count, 0);
+ RunUntilIdle();
+ ASSERT_EQ(count, 1);
+}
+
+} // namespace
+} // namespace pw::async
diff --git a/pw_async_basic/public/pw_async_basic/dispatcher.h b/pw_async_basic/public/pw_async_basic/dispatcher.h
index 2e31cde..7050d66 100644
--- a/pw_async_basic/public/pw_async_basic/dispatcher.h
+++ b/pw_async_basic/public/pw_async_basic/dispatcher.h
@@ -22,55 +22,73 @@
namespace pw::async {
+/// BasicDispatcher is a generic implementation of Dispatcher.
class BasicDispatcher final : public Dispatcher, public thread::ThreadCore {
public:
- explicit BasicDispatcher() : stop_requested_(false) {}
- ~BasicDispatcher() override { RequestStop(); }
+ explicit BasicDispatcher() = default;
+ ~BasicDispatcher() override;
+
+ /// Execute all runnable tasks and return without waiting.
+ void RunUntilIdle();
+
+ /// Run the dispatcher until Now() has reached `end_time`, executing all tasks
+ /// that come due before then.
+ void RunUntil(chrono::SystemClock::time_point end_time);
+
+ /// Run the dispatcher until `duration` has elapsed, executing all tasks that
+ /// come due in that period.
+ void RunFor(chrono::SystemClock::duration duration);
+
+ /// Stop processing tasks. If the dispatcher is serving a task loop, break out
+ /// of the loop, dequeue all waiting tasks, and call their TaskFunctions with
+ /// a PW_STATUS_CANCELLED status. If no task loop is being served, execute the
+ /// dequeueing procedure the next time the Dispatcher is run.
+ void RequestStop() PW_LOCKS_EXCLUDED(lock_);
+
+ // ThreadCore overrides:
+
+ /// Run the dispatcher until RequestStop() is called. Overrides
+ /// ThreadCore::Run() so that BasicDispatcher is compatible with
+ /// pw::thread::Thread.
+ void Run() override PW_LOCKS_EXCLUDED(lock_);
// Dispatcher overrides:
- void RequestStop() override PW_LOCKS_EXCLUDED(lock_);
- void PostTask(Task& task) override;
- void PostDelayedTask(Task& task,
- chrono::SystemClock::duration delay) override;
- void PostTaskForTime(Task& task,
- chrono::SystemClock::time_point time) override;
- void SchedulePeriodicTask(Task& task,
- chrono::SystemClock::duration interval) override;
- void SchedulePeriodicTask(
- Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) override;
+ void Post(Task& task) override;
+ void PostAfter(Task& task, chrono::SystemClock::duration delay) override;
+ void PostAt(Task& task, chrono::SystemClock::time_point time) override;
+ void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) override;
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) override;
bool Cancel(Task& task) override PW_LOCKS_EXCLUDED(lock_);
- void RunUntilIdle() override;
- void RunUntil(chrono::SystemClock::time_point end_time) override;
- void RunFor(chrono::SystemClock::duration duration) override;
// VirtualSystemClock overrides:
chrono::SystemClock::time_point now() override {
return chrono::SystemClock::now();
}
- // ThreadCore overrides:
- void Run() override PW_LOCKS_EXCLUDED(lock_);
-
private:
// Insert |task| into task_queue_ maintaining its min-heap property, keyed by
- // |time_due|. Must be holding lock_ when calling this function.
+ // |time_due|.
void PostTaskInternal(backend::NativeTask& task,
chrono::SystemClock::time_point time_due)
PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
- // If no tasks are due, sleeps until a notification is received or until the
- // due time of the next task.
- //
- // If at least one task is due, dequeues and runs each task that is due.
- //
- // Must be holding lock_ when calling this function.
- void RunLoopOnce() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+ // If no tasks are due, sleep until a notification is received, the next task
+ // comes due, or a timeout elapses; whichever occurs first.
+ void MaybeSleep() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ // Dequeue and run each task that is due.
+ void ExecuteDueTasks() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ // Dequeue each task and call each TaskFunction with a PW_STATUS_CANCELLED
+ // status.
+ void DrainTaskQueue() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
sync::InterruptSpinLock lock_;
sync::TimedThreadNotification timed_notification_;
- bool stop_requested_ PW_GUARDED_BY(lock_);
+ bool stop_requested_ PW_GUARDED_BY(lock_) = false;
// A priority queue of scheduled Tasks sorted by earliest due times first.
IntrusiveList<backend::NativeTask> task_queue_ PW_GUARDED_BY(lock_);
};
diff --git a/pw_async_basic/public/pw_async_basic/fake_dispatcher.h b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
index 49938fb..96fd7aa 100644
--- a/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
+++ b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
@@ -26,16 +26,16 @@
void RequestStop();
- void PostTask(Task& task);
+ void Post(Task& task);
- void PostDelayedTask(Task& task, chrono::SystemClock::duration delay);
+ void PostAfter(Task& task, chrono::SystemClock::duration delay);
- void PostTaskForTime(Task& task, chrono::SystemClock::time_point time);
+ void PostAt(Task& task, chrono::SystemClock::time_point time);
- void SchedulePeriodicTask(Task& task, chrono::SystemClock::duration interval);
- void SchedulePeriodicTask(Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time);
+ void PostPeriodic(Task& task, chrono::SystemClock::duration interval);
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time);
bool Cancel(Task& task);
@@ -53,10 +53,15 @@
void PostTaskInternal(::pw::async::backend::NativeTask& task,
chrono::SystemClock::time_point time_due);
- // Executes all pending tasks with a due time <= now().
- void RunLoopOnce();
+ // Dequeue and run each task that is due.
+ void ExecuteDueTasks();
+
+ // Dequeue each task and run each TaskFunction with a PW_STATUS_CANCELLED
+ // status.
+ void DrainTaskQueue();
Dispatcher& dispatcher_;
+ bool stop_requested_ = false;
// A priority queue of scheduled tasks sorted by earliest due times first.
IntrusiveList<::pw::async::backend::NativeTask> task_queue_;
diff --git a/pw_async_basic/public/pw_async_basic/task.h b/pw_async_basic/public/pw_async_basic/task.h
index 1d7247f..d83ea34 100644
--- a/pw_async_basic/public/pw_async_basic/task.h
+++ b/pw_async_basic/public/pw_async_basic/task.h
@@ -36,7 +36,7 @@
NativeTask(::pw::async::Task& task) : task_(task) {}
explicit NativeTask(::pw::async::Task& task, TaskFunction&& f)
: func_(std::move(f)), task_(task) {}
- void operator()(Context& ctx) { func_(ctx); }
+ void operator()(Context& ctx, Status status) { func_(ctx, status); }
void set_function(TaskFunction&& f) { func_ = std::move(f); }
pw::chrono::SystemClock::time_point due_time() const { return due_time_; }
diff --git a/pw_async_basic/size_report/post_1_task.cc b/pw_async_basic/size_report/post_1_task.cc
index f2e4da4..ef6065b 100644
--- a/pw_async_basic/size_report/post_1_task.cc
+++ b/pw_async_basic/size_report/post_1_task.cc
@@ -21,7 +21,7 @@
pw::async::BasicDispatcher dispatcher;
pw::async::Task task(
[](pw::async::Context& /*ctx*/) { printf("hello world\n"); });
- dispatcher.PostTask(task);
+ dispatcher.Post(task);
dispatcher.Run();
return 0;
}
diff --git a/pw_bloat/py/pw_bloat/label_output.py b/pw_bloat/py/pw_bloat/label_output.py
index cd3ad32..938cc90 100644
--- a/pw_bloat/py/pw_bloat/label_output.py
+++ b/pw_bloat/py/pw_bloat/label_output.py
@@ -173,7 +173,7 @@
if old_labels is None:
return new_labels
diff_list = []
- for (new_lb, old_lb) in zip(new_labels, old_labels):
+ for new_lb, old_lb in zip(new_labels, old_labels):
if (new_lb.name == old_lb.name) and (new_lb.size == old_lb.size):
diff_list.append(self._LabelContent('', 0, ''))
else:
@@ -248,7 +248,6 @@
if (len(self._ascii_table_rows) > 5) and (
self._ascii_table_rows[-1][0] != '+'
):
-
self._ascii_table_rows.append(
self._row_divider(
len(self._col_names), self._cs.H.value
diff --git a/pw_bluetooth/BUILD.gn b/pw_bluetooth/BUILD.gn
index 8e50943..7f7ea2c 100644
--- a/pw_bluetooth/BUILD.gn
+++ b/pw_bluetooth/BUILD.gn
@@ -91,6 +91,11 @@
imports = [ "public/pw_bluetooth/hci.emb" ]
deps = [ ":emboss_hci" ]
}
+} else {
+ group("emboss_hci") {
+ }
+ group("emboss_vendor") {
+ }
}
pw_test_group("tests") {
diff --git a/pw_bluetooth/docs.rst b/pw_bluetooth/docs.rst
index 62bad33..428f625 100644
--- a/pw_bluetooth/docs.rst
+++ b/pw_bluetooth/docs.rst
@@ -17,6 +17,76 @@
is the entry point from which all other APIs are exposed. Currently, only Low
Energy APIs exist.
+Host
+====
+.. doxygenclass:: pw::bluetooth::Host
+ :members:
+
+low_energy::Central
+===================
+.. doxygenclass:: pw::bluetooth::low_energy::Central
+ :members:
+
+low_energy::Peripheral
+======================
+.. doxygenclass:: pw::bluetooth::low_energy::Peripheral
+ :members:
+
+low_energy::AdvertisedPeripheral
+================================
+.. doxygenclass:: pw::bluetooth::low_energy::AdvertisedPeripheral
+ :members:
+
+low_energy::Connection
+======================
+.. doxygenclass:: pw::bluetooth::low_energy::Connection
+ :members:
+
+low_energy::ConnectionOptions
+=============================
+.. doxygenstruct:: pw::bluetooth::low_energy::ConnectionOptions
+ :members:
+
+low_energy::RequestedConnectionParameters
+=========================================
+.. doxygenstruct:: pw::bluetooth::low_energy::RequestedConnectionParameters
+ :members:
+
+low_energy::ConnectionParameters
+================================
+.. doxygenstruct:: pw::bluetooth::low_energy::ConnectionParameters
+ :members:
+
+gatt::Server
+============
+.. doxygenclass:: pw::bluetooth::gatt::Server
+ :members:
+
+gatt::LocalServiceInfo
+======================
+.. doxygenstruct:: pw::bluetooth::gatt::LocalServiceInfo
+ :members:
+
+gatt::LocalService
+==================
+.. doxygenclass:: pw::bluetooth::gatt::LocalService
+ :members:
+
+gatt::LocalServiceDelegate
+==========================
+.. doxygenclass:: pw::bluetooth::gatt::LocalServiceDelegate
+ :members:
+
+gatt::Client
+============
+.. doxygenclass:: pw::bluetooth::gatt::Client
+ :members:
+
+gatt::RemoteService
+===================
+.. doxygenclass:: pw::bluetooth::gatt::RemoteService
+ :members:
+
Callbacks
=========
This module contains callback-heavy APIs. Callbacks must not call back into the
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/client.h b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
index a8346f8..e12de22 100644
--- a/pw_bluetooth/public/pw_bluetooth/gatt/client.h
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
@@ -27,86 +27,86 @@
namespace pw::bluetooth::gatt {
-// Represents a GATT service on a remote GATT server.
-// Clients should call `SetErrorCallback` before using in order to handle fatal
-// errors.
+/// Represents a GATT service on a remote GATT server.
+/// Clients should call `SetErrorCallback` before using in order to handle fatal
+/// errors.
class RemoteService {
public:
enum class RemoteServiceError {
- // The service has been modified or removed.
+ /// The service has been modified or removed.
kServiceRemoved = 0,
- // The peer serving this service has disconnected.
+ /// The peer serving this service has disconnected.
kPeerDisconnected = 1,
};
- // Wrapper around a possible truncated value received from the server.
+ /// Wrapper around a possible truncated value received from the server.
struct ReadValue {
- // Characteristic or descriptor handle.
+ /// Characteristic or descriptor handle.
Handle handle;
- // The value of the characteristic or descriptor.
+ /// The value of the characteristic or descriptor.
Vector<std::byte> value;
- // True if `value` might be truncated (the buffer was completely filled by
- // the server and the read was a short read). `ReadCharacteristic` or
- // `ReadDescriptor` should be used to read the complete value.
+ /// True if `value` might be truncated (the buffer was completely filled by
+ /// the server and the read was a short read). `ReadCharacteristic` or
+ /// `ReadDescriptor` should be used to read the complete value.
bool maybe_truncated;
};
- // A result returned by `ReadByType`.
+ /// A result returned by `ReadByType`.
struct ReadByTypeResult {
- // Characteristic or descriptor handle.
+ /// Characteristic or descriptor handle.
Handle handle;
- // The value of the characteristic or descriptor, if it was read
- // successfully, or an error explaining why the value could not be read.
+ /// The value of the characteristic or descriptor, if it was read
+ /// successfully, or an error explaining why the value could not be read.
Result<Error, ReadValue> result;
};
- // Represents the supported options to read a long characteristic or
- // descriptor value from a server. Long values are those that may not fit in a
- // single message (longer than 22 bytes).
+ /// Represents the supported options to read a long characteristic or
+ /// descriptor value from a server. Long values are those that may not fit in
+ /// a single message (longer than 22 bytes).
struct LongReadOptions {
- // The byte to start the read at. Must be less than the length of the
- // value.
+ /// The byte to start the read at. Must be less than the length of the
+ /// value.
uint16_t offset = 0;
- // The maximum number of bytes to read.
+ /// The maximum number of bytes to read.
uint16_t max_bytes = kMaxValueLength;
};
- // Represents the supported write modes for writing characteristics &
- // descriptors to the server.
+ /// Represents the supported write modes for writing characteristics &
+ /// descriptors to the server.
enum class WriteMode : uint8_t {
- // Wait for a response from the server before returning but do not verify
- // the echo response. Supported for both characteristics and descriptors.
+ /// Wait for a response from the server before returning but do not verify
+ /// the echo response. Supported for both characteristics and descriptors.
kDefault = 0,
- // Every value blob is verified against an echo response from the server.
- // The procedure is aborted if a value blob has not been reliably delivered
- // to the peer. Only supported for characteristics.
+ /// Every value blob is verified against an echo response from the server.
+ /// The procedure is aborted if a value blob has not been reliably delivered
+ /// to the peer. Only supported for characteristics.
kReliable = 1,
- // Delivery will not be confirmed before returning. Writing without a
- // response is only supported for short characteristics with the
- // `WRITE_WITHOUT_RESPONSE` property. The value must fit into a single
- // message. It is guaranteed that at least 20 bytes will fit into a single
- // message. If the value does not fit, a `kFailure` error will be produced.
- // The value will be written at offset 0. Only supported for
- // characteristics.
+ /// Delivery will not be confirmed before returning. Writing without a
+ /// response is only supported for short characteristics with the
+ /// `WRITE_WITHOUT_RESPONSE` property. The value must fit into a single
+ /// message. It is guaranteed that at least 20 bytes will fit into a single
+ /// message. If the value does not fit, a `kFailure` error will be produced.
+ /// The value will be written at offset 0. Only supported for
+ /// characteristics.
kWithoutResponse = 2,
};
- // Represents the supported options to write a characteristic/descriptor value
- // to a server.
+ /// Represents the supported options to write a characteristic/descriptor
+ /// value to a server.
struct WriteOptions {
- // The mode of the write operation. For descriptors, only
- // `WriteMode::kDefault` is supported
+ /// The mode of the write operation. For descriptors, only
+ /// `WriteMode::kDefault` is supported
WriteMode mode = WriteMode::kDefault;
- // Request a write starting at the byte indicated.
- // Must be 0 if `mode` is `WriteMode.kWithoutResponse`.
+ /// Request a write starting at the byte indicated.
+ /// Must be 0 if `mode` is `WriteMode.kWithoutResponse`.
uint16_t offset = 0;
};
@@ -114,181 +114,193 @@
using ReadCallback = Function<void(Result<ReadValue>)>;
using NotificationCallback = Function<void(ReadValue)>;
- // Set a callback that will be called when there is an error with this
- // RemoteService, after which this RemoteService will be invalid.
+ /// Set a callback that will be called when there is an error with this
+ /// RemoteService, after which this RemoteService will be invalid.
void SetErrorCallback(Function<void(RemoteServiceError)>&& error_callback);
- // Calls `characteristic_callback` with the characteristics and descriptors in
- // this service.
+ /// Calls `characteristic_callback` with the characteristics and descriptors
+ /// in this service.
void DiscoverCharacteristics(
Function<void(Characteristic)>&& characteristic_callback);
- // Reads characteristics and descriptors with the specified type. This method
- // is useful for reading values before discovery has completed, thereby
- // reducing latency.
- // `uuid` - The UUID of the characteristics/descriptors to read.
- // `result_callback` - Results are returned via this callback. Results may be
- // empty if no matching values are read. If reading a value results in a
- // permission error, the handle and error will be included.
- //
- // This may fail with the following errors:
- // kInvalidParameters: if `uuid` refers to an internally reserved descriptor
- // type (e.g. the Client Characteristic Configuration descriptor).
- // kTooManyResults: More results were read than can fit
- // in a Vector. Consider reading characteristics/descriptors individually
- // after performing discovery.
- // kFailure: The server returned an error not specific to a single result.
+ /// Reads characteristics and descriptors with the specified type. This method
+ /// is useful for reading values before discovery has completed, thereby
+ /// reducing latency.
+ /// @param uuid The UUID of the characteristics/descriptors to read.
+ /// @param result_callback Results are returned via this callback. Results may
+ /// be empty if no matching values are read. If reading a value results in a
+ /// permission error, the handle and error will be included.
+ ///
+ /// This may fail with the following errors:
+ /// - kInvalidParameters: if `uuid` refers to an internally reserved
+ /// descriptor type (e.g. the Client Characteristic Configuration descriptor).
+ /// - kTooManyResults: More results were read than can fit in a Vector.
+ /// Consider reading characteristics/descriptors individually after performing
+ /// discovery.
+ /// - kFailure: The server returned an error not specific to a single result.
void ReadByType(Uuid uuid, ReadByTypeCallback&& result_callback);
- // Reads the value of a characteristic.
- // `handle` - The handle of the characteristic to be read.
- // `options` - If null, a short read will be performed, which may be truncated
- // to what fits in a single message (at least 22 bytes). If long read
- // options are present, performs a long read with the indicated options.
- // `result_callback` - called with the result of the read and the value of the
- // characteristic if successful.
- //
- // This may fail with the following errors:
- // kInvalidHandle - if `handle` is invalid
- // kInvalidParameters - if `options is invalid
- // kReadNotPermitted or kInsufficient* if the server rejects the request.
- // kFailure if the server returns an error not covered by the above errors.
+ /// Reads the value of a characteristic.
+ /// @param handle The handle of the characteristic to be read.
+ /// @param options If null, a short read will be performed, which may be
+ /// truncated to what fits in a single message (at least 22 bytes). If long
+ /// read options are present, performs a long read with the indicated options.
+ /// @param result_callback called with the result of the read and the value of
+ /// the characteristic if successful.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters `options` is invalid.
+ /// @retval kReadNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above.
void ReadCharacteristic(Handle handle,
std::optional<LongReadOptions> options,
ReadCallback&& result_callback);
- // Writes `value` to the characteristic with `handle` using the provided
- // `options`. It is not recommended to send additional writes while a write
- // is already in progress (the server may receive simultaneous writes in any
- // order).
- //
- // Parameters:
- // `handle` - Handle of the characteristic to be written to
- // `value` - The value to be written.
- // `options` - Options that apply to the write.
- //
- // This may fail with the following errors:
- // kInvalidHandle - if `handle` is invalid
- // kInvalidParameters - if `options is invalid
- // kWriteNotPermitted or kInsufficient* if the server rejects the request.
- // kFailure if the server returns an error not covered by the above errors.
+ /// Writes `value` to the characteristic with `handle` using the provided
+ /// `options`. It is not recommended to send additional writes while a write
+ /// is already in progress (the server may receive simultaneous writes in any
+ /// order).
+ ///
+ /// @param handle Handle of the characteristic to be written to
+ /// @param value The value to be written.
+ /// @param options Options that apply to the write.
+ /// @param result_callback Returns a result upon completion of the write.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters`options is invalid.
+ /// @retval kWriteNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
void WriteCharacteristic(Handle handle,
span<const std::byte> value,
WriteOptions options,
Function<void(Result<Error>)>&& result_callback);
- // Reads the value of the characteristic descriptor with `handle` and
- // returns it in the reply.
- // `handle` - The descriptor handle to read.
- // `options` - Options that apply to the read.
- // `result_callback` - Returns a result containing the value of the descriptor
- // on success.
- //
- // This may fail with the following errors:
- // `kInvalidHandle` - if `handle` is invalid.
- // `kInvalidParameters` - if `options` is invalid.
- // `kReadNotPermitted` or `INSUFFICIENT_*` - if the server rejects the read
- // request. `kFailure` - if the server returns an error.
+ /// Reads the value of the characteristic descriptor with `handle` and
+ /// returns it in the reply.
+ /// @param handle The descriptor handle to read.
+ /// @param options Options that apply to the read.
+ /// @param result_callback Returns a result containing the value of the
+ /// descriptor on success.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters`options` is invalid.
+ /// @retval kReadNotPermitted
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
void ReadDescriptor(Handle handle,
std::optional<LongReadOptions> options,
ReadCallback&& result_callback);
+ /// Writes `value` to the descriptor with `handle` using the provided
+ /// `options`. It is not recommended to send additional writes while a write
+ /// is already in progress (the server may receive simultaneous writes in any
+ /// order).
+ ///
+ /// @param handle Handle of the descriptor to be written to
+ /// @param value The value to be written.
+ /// @param options Options that apply to the write.
+ /// @param result_callback Returns a result upon completion of the write.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters `options is invalid
+ /// @retval kWriteNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
void WriteDescriptor(Handle handle,
span<const std::byte> value,
WriteOptions options,
Function<void(Result<Error>)>&& result_callback);
- // Subscribe to notifications & indications from the characteristic with
- // the given `handle`.
- //
- // Either notifications or indications will be enabled depending on
- // characteristic properties. Indications will be preferred if they are
- // supported. This operation fails if the characteristic does not have the
- // "notify" or "indicate" property.
- //
- // A write request will be issued to configure the characteristic for
- // notifications/indications if it contains a Client Characteristic
- // Configuration descriptor. This method fails if an error occurs while
- // writing to the descriptor.
- //
- // On success, `notification_callback` will be called when
- // the peer sends a notification or indication. Indications are
- // automatically confirmed.
- //
- // Subscriptions can be canceled with `StopNotifications`.
- //
- // Parameters:
- // `handle` - the handle of the characteristic to subscribe to.
- // `notification_callback` - will be called with the values of
- // notifications/indications when received.
- // `result_callback` - called with the result of enabling
- // notifications/indications.
- //
- // This may fail with the following errors:
- // `kFailure` - the characteristic does not support notifications or
- // indications.
- // `kInvalidHandle` - `handle` is invalid.
- // `kWriteNotPermitted`or `kInsufficient*` - descriptor write error.
+ /// Subscribe to notifications & indications from the characteristic with
+ /// the given `handle`.
+ ///
+ /// Either notifications or indications will be enabled depending on
+ /// characteristic properties. Indications will be preferred if they are
+ /// supported. This operation fails if the characteristic does not have the
+ /// "notify" or "indicate" property.
+ ///
+ /// A write request will be issued to configure the characteristic for
+ /// notifications/indications if it contains a Client Characteristic
+ /// Configuration descriptor. This method fails if an error occurs while
+ /// writing to the descriptor.
+ ///
+ /// On success, `notification_callback` will be called when
+ /// the peer sends a notification or indication. Indications are
+ /// automatically confirmed.
+ ///
+ /// Subscriptions can be canceled with `StopNotifications`.
+ ///
+ /// @param handle the handle of the characteristic to subscribe to.
+ /// @param notification_callback will be called with the values of
+ /// notifications/indications when received.
+ /// @param result_callback called with the result of enabling
+ /// notifications/indications.
+ /// @retval kFailure The characteristic does not support notifications or
+ /// indications.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kWriteNotPermitted CCC descriptor write error.
+ /// @retval Insufficient* CCC descriptor write error.
void RegisterNotificationCallback(
Handle handle,
NotificationCallback&& notification_callback,
Function<void(Result<Error>)>&& result_callback);
- // Stops notifications for the characteristic with the given `handle`.
+ /// Stops notifications for the characteristic with the given `handle`.
void StopNotifications(Handle handle);
private:
- // Disconnect from the remote service. This method is called by the
- // ~RemoteService::Ptr() when it goes out of scope, the API client should
- // never call this method.
+ /// Disconnect from the remote service. This method is called by the
+ /// ~RemoteService::Ptr() when it goes out of scope, the API client should
+ /// never call this method.
void Disconnect();
public:
- // Movable RemoteService smart pointer. The remote server will remain
- // connected until the returned RemoteService::Ptr is destroyed.
+ /// Movable RemoteService smart pointer. The remote server will remain
+ /// connected until the returned RemoteService::Ptr is destroyed.
using Ptr = internal::RaiiPtr<RemoteService, &RemoteService::Disconnect>;
};
-// Represents a GATT client that interacts with services on a GATT server.
+/// Represents a GATT client that interacts with services on a GATT server.
class Client {
public:
- // Represents a remote GATT service.
+ /// Represents a remote GATT service.
struct RemoteServiceInfo {
- // Uniquely identifies this GATT service.
+ /// Uniquely identifies this GATT service.
Handle handle;
- // Indicates whether this is a primary or secondary service.
+ /// Indicates whether this is a primary or secondary service.
bool primary;
- // The UUID that identifies the type of this service.
- // There may be multiple services with the same UUID.
+ /// The UUID that identifies the type of this service.
+ /// There may be multiple services with the same UUID.
Uuid type;
};
virtual ~Client() = default;
- // Enumerates existing services found on the peer that this Client represents,
- // and provides a stream of updates thereafter. Results can be filtered by
- // specifying a list of UUIDs in `uuids`. To further interact with services,
- // clients must obtain a RemoteService protocol by calling ConnectToService().
- // `uuid_allowlist` - The allowlist of UUIDs to filter services with.
- // `updated_callback` - Will be called with services that are
- // updated/modified.
- // `removed_callback` - Called with the handles of services
- // that have been removed. Note that handles may be reused.
+ /// Enumerates existing services found on the peer that this Client
+ /// represents, and provides a stream of updates thereafter. Results can be
+ /// filtered by specifying a list of UUIDs in `uuids`. To further interact
+ /// with services, clients must obtain a RemoteService protocol by calling
+ /// ConnectToService(). `uuid_allowlist` - The allowlist of UUIDs to filter
+ /// services with. `updated_callback` - Will be called with services that are
+ /// updated/modified.
+ /// `removed_callback` - Called with the handles of services
+ /// that have been removed. Note that handles may be reused.
virtual void WatchServices(
Vector<Uuid> uuid_allowlist,
Function<void(RemoteServiceInfo)>&& updated_callback,
Function<void(Handle)>&& removed_callback) = 0;
- // Stops service watching if started by `WatchServices`.
+ /// Stops service watching if started by `WatchServices`.
virtual void StopWatchingServices();
- // Connects to a RemoteService. Only 1 connection per service is allowed.
- // `handle` - the handle of the service to connect to.
- //
- // This may fail with the following errors:
- // kInvalidParameters - `handle` does not correspond to a known service.
+ /// Connects to a RemoteService. Only 1 connection per service is allowed.
+ /// `handle` - the handle of the service to connect to.
+ ///
+ /// This may fail with the following errors:
+ /// kInvalidParameters - `handle` does not correspond to a known service.
virtual Result<Error, RemoteService::Ptr> ConnectToService(Handle handle) = 0;
};
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/server.h b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
index 3cf6fc0..34e7291 100644
--- a/pw_bluetooth/public/pw_bluetooth/gatt/server.h
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
@@ -28,208 +28,236 @@
namespace pw::bluetooth::gatt {
-// Parameters for registering a local GATT service.
+/// Parameters for registering a local GATT service.
struct LocalServiceInfo {
- // A unique (within a Server) handle identifying this service.
+ /// A unique (within a Server) handle identifying this service.
Handle handle;
- // Indicates whether this is a primary or secondary service.
+ /// Indicates whether this is a primary or secondary service.
bool primary;
- // The UUID that identifies the type of this service.
- // There may be multiple services with the same UUID.
+ /// The UUID that identifies the type of this service.
+ /// There may be multiple services with the same UUID.
Uuid type;
- // The characteristics of this service.
+ /// The characteristics of this service.
span<const Characteristic> characteristics;
- // Handles of other services that are included by this service.
+ /// Handles of other services that are included by this service.
span<const Handle> includes;
};
-// Interface for serving a local GATT service. This is implemented by the API
-// client.
+/// Interface for serving a local GATT service. This is implemented by the API
+/// client.
class LocalServiceDelegate {
public:
virtual ~LocalServiceDelegate() = default;
- // Called when there is a fatal error related to this service that forces the
- // service to close. LocalServiceDelegate methods will no longer be called.
- // This invalidates the associated LocalService. It is OK to destroy both
- // `LocalServiceDelegate` and the associated `LocalService::Ptr` from within
- // this method.
+ /// Called when there is a fatal error related to this service that forces the
+ /// service to close. LocalServiceDelegate methods will no longer be called.
+ /// This invalidates the associated LocalService. It is OK to destroy both
+ /// `LocalServiceDelegate` and the associated `LocalService::Ptr` from within
+ /// this method.
virtual void OnError(Error error) = 0;
- // This notifies the current configuration of a particular
- // characteristic/descriptor for a particular peer. It will be called when the
- // peer GATT client changes the configuration.
- //
- // The Bluetooth stack maintains the state of each peer's configuration across
- // reconnections. As such, this method will also be called when a peer
- // connects for each characteristic with the initial, persisted state of the
- // newly-connected peer's configuration. However, clients should not rely on
- // this state being persisted indefinitely by the Bluetooth stack.
- //
- // Parameters:
- // `peer_id` - The PeerId of the GATT client associated with this particular
- // CCC.
- // `handle` - The handle of the characteristic associated with the `notify`
- // and `indicate` parameters.
- // `notify` - True if the client has enabled notifications, false otherwise.
- // `indicate` - True if the client has enabled indications, false otherwise.
+ /// This notifies the current configuration of a particular
+ /// characteristic/descriptor for a particular peer. It will be called when
+ /// the peer GATT client changes the configuration.
+ ///
+ /// The Bluetooth stack maintains the state of each peer's configuration
+ /// across reconnections. As such, this method will be called with both
+ /// `notify` and `indicate` set to false for each characteristic when a peer
+ /// disconnects. Also, when a peer reconnects this method will be called again
+ /// with the initial, persisted state of the newly-connected peer's
+ /// configuration. However, clients should not rely on this state being
+ /// persisted indefinitely by the Bluetooth stack.
+ ///
+ /// @param peer_id The PeerId of the GATT client associated with this
+ /// particular CCC.
+ /// @param handle The handle of the characteristic associated with the
+ /// `notify` and `indicate` parameters.
+ /// @param notify True if the client has enabled notifications, false
+ /// otherwise.
+ /// @param indicate True if the client has enabled indications, false
+ /// otherwise.
virtual void CharacteristicConfiguration(PeerId peer_id,
Handle handle,
bool notify,
bool indicate) = 0;
- // Called when a peer requests to read the value of a characteristic or
- // descriptor. It is guaranteed that the peer satisfies the permissions
- // associated with this attribute.
- //
- // Parameters:
- // `peer_id` - The PeerId of the GATT client making the read request.
- // `handle` - The handle of the requested descriptor/characteristic.
- // `offset` - The offset at which to start reading the requested value.
- // `result_callback` - Called with the value of the characteristic on success,
- // or an Error on failure. The value will be truncated to fit in the MTU
- // if necessary. It is OK to call `result_callback` in `ReadValue`.
+ /// Called when a peer requests to read the value of a characteristic or
+ /// descriptor. It is guaranteed that the peer satisfies the permissions
+ /// associated with this attribute.
+ ///
+ /// @param peer_id The PeerId of the GATT client making the read request.
+ /// @param handle The handle of the requested descriptor/characteristic.
+ /// @param offset The offset at which to start reading the requested value.
+ /// @param result_callback Called with the value of the characteristic on
+ /// success, or an Error on failure. The value will be truncated to fit in the
+ /// MTU if necessary. It is OK to call `result_callback` in `ReadValue`.
virtual void ReadValue(PeerId peer_id,
Handle handle,
uint32_t offset,
Function<void(Result<Error, span<const std::byte>>)>&&
result_callback) = 0;
- // Called when a peer issues a request to write the value of a characteristic
- // or descriptor. It is guaranteed that the peer satisfies the permissions
- // associated with this attribute.
- //
- // Parameters:
- // `peer_id` - The PeerId of the GATT client making the write request.
- // `handle` - The handle of the requested descriptor/characteristic.
- // `offset` - The offset at which to start writing the requested value. If the
- // offset is 0, any existing value should be overwritten by the new value.
- // Otherwise, the existing value between offset:(offset + len(value))
- // should be changed to `value`.
- // `value` - The new value for the descriptor/characteristic.
- // `status_callback` - Called with the result of the write.
+ /// Called when a peer issues a request to write the value of a characteristic
+ /// or descriptor. It is guaranteed that the peer satisfies the permissions
+ /// associated with this attribute.
+ ///
+ /// @param peer_id The PeerId of the GATT client making the write request.
+ /// @param handle The handle of the requested descriptor/characteristic.
+ /// @param offset The offset at which to start writing the requested value. If
+ /// the offset is 0, any existing value should be overwritten by the new
+ /// value. Otherwise, the existing value between `offset:(offset +
+ /// len(value))` should be changed to `value`.
+ /// @param value The new value for the descriptor/characteristic.
+ /// @param status_callback Called with the result of the write.
virtual void WriteValue(PeerId peer_id,
Handle handle,
uint32_t offset,
span<const std::byte> value,
Function<void(Result<Error>)>&& status_callback) = 0;
- // Called when the MTU of a peer is updated. Also called for peers that are
- // already connected when the server is published. This method is safe to
- // ignore if you do not care about the MTU. It is intended for use cases where
- // throughput needs to be optimized.
+ /// Called when the MTU of a peer is updated. Also called for peers that are
+ /// already connected when the server is published.
+ ///
+ /// Notifications and indications must fit in a single packet including both
+ /// the 3-byte notification/indication header and the user-provided payload.
+ /// If these are not used, the MTU can be safely ignored as it is intended for
+ /// use cases where the throughput needs to be optimized.
virtual void MtuUpdate(PeerId peer_id, uint16_t mtu) = 0;
};
-// Interface provided by the backend to interact with a published service.
-// LocalService is valid for the lifetime of a published GATT service. It is
-// used to control the service and send notifications/indications.
+/// Interface provided by the backend to interact with a published service.
+/// LocalService is valid for the lifetime of a published GATT service. It is
+/// used to control the service and send notifications/indications.
class LocalService {
public:
- // The parameters used to signal a characteristic value change from a
- // LocalService to a peer.
+ /// The parameters used to signal a characteristic value change from a
+ /// LocalService to a peer.
struct ValueChangedParameters {
- // The PeerIds of the peers to signal. The LocalService should respect the
- // Characteristic Configuration associated with a peer+handle when deciding
- // whether to signal it. If empty, all peers are signalled.
+ /// The PeerIds of the peers to signal. The LocalService should respect the
+ /// Characteristic Configuration associated with a peer+handle when deciding
+ /// whether to signal it. If empty, all peers are signalled.
span<const PeerId> peer_ids;
- // The handle of the characteristic value being signaled.
+ /// The handle of the characteristic value being signaled.
Handle handle;
- // The new value for the descriptor/characteristic.
+ /// The new value for the descriptor/characteristic.
span<const std::byte> value;
};
+ /// The Result type for a ValueChanged indication or notification message. The
+ /// error can be locally generated for notifications and either locally or
+ /// remotely generated for indications.
+ using ValueChangedResult = Result<Error>;
+
+ /// The callback type for a ValueChanged indication or notification
+ /// completion.
+ using ValueChangedCallback = Function<void(ValueChangedResult)>;
+
virtual ~LocalService() = default;
- // Sends a notification to peers. Notifications should be used instead of
- // indications when the service does *not* require peer confirmation of the
- // update.
- //
- // Notifications should not be sent to peers which have not enabled
- // notifications on a particular characteristic - if they are sent, they will
- // not be propagated. The Bluetooth stack will track this configuration for
- // the lifetime of the service.
- //
- // Parameters:
- // `parameters` - The parameters associated with the changed characteristic.
- // `completion_callback` - Called when the notification has been sent.
- // Additional values should not be notified until this callback is called.
+ /// Sends a notification to peers. Notifications should be used instead of
+ /// indications when the service does *not* require peer confirmation of the
+ /// update.
+ ///
+ /// Notifications should not be sent to peers which have not enabled
+ /// notifications on a particular characteristic or that have disconnected
+ /// since - if they are sent, they will not be propagated and the
+ /// `completion_callback` will be called with an error condition. The
+ /// Bluetooth stack will track this configuration for the lifetime of the
+ /// service.
+ ///
+ /// The maximum size of the `parameters.value` field depends on the MTU
+ /// negotiated with the peer. A 3-byte header plus the value contents must fit
+ /// in a packet of MTU bytes.
+ ///
+ /// @param parameters The parameters associated with the changed
+ /// characteristic.
+ /// @param completion_callback Called when the notification has been sent to
+ /// all peers or an error is produced when trying to send the notification to
+ /// any of the peers. This function is called only once when all associated
+ /// work is done, if the implementation wishes to receive a call on a
+ /// per-peer basis, they should send this event with a single PeerId in
+ /// `parameters.peer_ids`. Additional values should not be notified until
+ /// this callback is called.
virtual void NotifyValue(const ValueChangedParameters& parameters,
- Closure&& completion_callback) = 0;
+ ValueChangedCallback&& completion_callback) = 0;
- // Sends an indication to peers. Indications should be used instead of
- // notifications when the service *does* require peer confirmation of the
- // update.
- //
- // Indications should not be sent to peers which have not enabled indications
- // on a particular characteristic - if they are sent, they will not be
- // propagated. The Bluetooth stack will track this configuration for the
- // lifetime of the service.
- //
- // If any of the peers in `update.peer_ids` fails to confirm the indication
- // within the ATT transaction timeout (30 seconds per Bluetooth 5.2 Vol. 4
- // Part G 3.3.3), the link between the peer and the local adapter will be
- // closed.
- //
- // Parameters:
- // `parameters` - The parameters associated with the changed characteristic.
- // `confirmation` - When all the peers listed in `parameters.peer_ids` have
- // confirmed the indication, `confirmation` is called. If the
- // implementation wishes to receive indication confirmations on a per-peer
- // basis, they should send this event with a single PeerId in
- // `parameters.peer_ids`. Additional values should not be indicated until
- // this callback is called.
+ /// Sends an indication to peers. Indications should be used instead of
+ /// notifications when the service *does* require peer confirmation of the
+ /// update.
+ ///
+ /// Indications should not be sent to peers which have not enabled indications
+ /// on a particular characteristic - if they are sent, they will not be
+ /// propagated. The Bluetooth stack will track this configuration for the
+ /// lifetime of the service.
+ ///
+ /// If any of the peers in `parameters.peer_ids` fails to confirm the
+ /// indication within the ATT transaction timeout (30 seconds per
+ /// Bluetooth 5.2 Vol. 4 Part G 3.3.3), the link between the peer and the
+ /// local adapter will be closed.
+ ///
+ /// The maximum size of the `parameters.value` field depends on the MTU
+ /// negotiated with the peer. A 3-byte header plus the value contents must fit
+ /// in a packet of MTU bytes.
+ ///
+ /// @param parameters The parameters associated with the changed
+ /// characteristic.
+ /// @param confirmation When all the peers listed in `parameters.peer_ids`
+ /// have confirmed the indication, `confirmation` is called. If the
+ /// implementation wishes to receive indication confirmations on a per-peer
+ /// basis, they should send this event with a single PeerId in
+ /// `parameters.peer_ids`. Additional values should not be indicated until
+ /// this callback is called.
virtual void IndicateValue(const ValueChangedParameters& parameters,
- Function<void(Result<Error>)>&& confirmation) = 0;
+ ValueChangedCallback&& confirmation) = 0;
private:
- // Unpublish the local service. This method is called by the
- // ~LocalService::Ptr() when it goes out of scope, the API client should never
- // call this method.
+ /// Unpublish the local service. This method is called by the
+ /// ~LocalService::Ptr() when it goes out of scope, the API client should
+ /// never call this method.
virtual void UnpublishService() = 0;
public:
- // Movable LocalService smart pointer. When the LocalService::Ptr object is
- // destroyed the service will be unpublished.
+ /// Movable LocalService smart pointer. When the LocalService::Ptr object is
+ /// destroyed the service will be unpublished.
using Ptr = internal::RaiiPtr<LocalService, &LocalService::UnpublishService>;
};
-// Interface for a GATT server that serves many GATT services.
+/// Interface for a GATT server that serves many GATT services.
class Server {
public:
enum class PublishServiceError {
kInternalError = 0,
- // The service handle provided was not unique.
+ /// The service handle provided was not unique.
kInvalidHandle = 1,
- // Invalid service UUID provided.
+ /// Invalid service UUID provided.
kInvalidUuid = 2,
- // Invalid service characteristics provided.
+ /// Invalid service characteristics provided.
kInvalidCharacteristics = 3,
- // Invalid service includes provided.
+ /// Invalid service includes provided.
kInvalidIncludes = 4,
};
- // The Result passed by PublishService.
+ /// The Result passed by PublishService.
using PublishServiceResult = Result<PublishServiceError, LocalService::Ptr>;
virtual ~Server() = default;
- // Publishes the service defined by `info` and implemented by `delegate` so
- // that it is available to all remote peers.
- //
- // The caller must assign distinct handles to the characteristics and
- // descriptors listed in `info`. These identifiers will be used in requests
- // sent to `delegate`. On success, a `LocalService::Ptr` is returned. When the
- // `LocalService::Ptr` is destroyed or an error occurs
- // (LocalServiceDelegate.OnError), the service will be unpublished.
+ /// Publishes the service defined by `info` and implemented by `delegate` so
+ /// that it is available to all remote peers.
+ ///
+ /// The caller must assign distinct handles to the characteristics and
+ /// descriptors listed in `info`. These identifiers will be used in requests
+ /// sent to `delegate`. On success, a `LocalService::Ptr` is returned. When
+ /// the `LocalService::Ptr` is destroyed or an error occurs
+ /// (LocalServiceDelegate.OnError), the service will be unpublished.
virtual void PublishService(
const LocalServiceInfo& info,
LocalServiceDelegate* delegate,
diff --git a/pw_bluetooth/public/pw_bluetooth/hci.emb b/pw_bluetooth/public/pw_bluetooth/hci.emb
index ed22679..d781b3f 100644
--- a/pw_bluetooth/public/pw_bluetooth/hci.emb
+++ b/pw_bluetooth/public/pw_bluetooth/hci.emb
@@ -520,23 +520,29 @@
enum LEPeriodicAdvertisingCreateSyncUseParams:
[maximum_bits: 1]
+
USE_PARAMS = 0x00
-- Use the Advertising_SID, Advertiser_Address_Type, and Adertiser_Address parameters to
-- determine which advertiser to listen to.
+
USE_PERIODIC_ADVERTISER_LIST = 0x01
-- Use the Periodic Advertiser List to determine which advertiser to listen to.
bits LEPeriodicAdvertisingCreateSyncOptions:
-- First parameter to the LE Periodic Advertising Create Sync command
- 0 [+1] LEPeriodicAdvertisingCreateSyncUseParams advertiser_source
- $next [+1] Flag enable_reporting
+
+ 0 [+1] LEPeriodicAdvertisingCreateSyncUseParams advertiser_source
+
+ $next [+1] Flag enable_reporting
-- 0: Reporting initially enabled
-- 1: Reporting initially disabled
- $next [+1] Flag enable_duplicate_filtering
+
+ $next [+1] Flag enable_duplicate_filtering
-- 0: Duplicate filtering initially disabled
-- 1: Duplicate filtering initially enabled
- $next [+5] UInt padding
+
+ $next [+5] UInt padding
-- Reserved for future use
@@ -546,23 +552,30 @@
[maximum_bits: 8]
PUBLIC = 0x00
-- Public Device Address or Public Identity Address
+
RANDOM = 0x01
-- Random Device Address or Random (static) Identity Address
bits LEPeriodicAdvertisingSyncCTEType:
-- Bit definitions for a |sync_cte_type| field in an LE Periodic Advertising Create Sync command
+
0 [+1] Flag dont_sync_aoa
-- Do not sync to packets with an AoA Constant Tone Extension
+
$next [+1] Flag dont_sync_aod_1us
-- Do not sync to packets with an AoD Constant Tone Extension with 1 microsecond slots
+
$next [+1] Flag dont_sync_aod_2us
-- Do not sync to packets with an AoD Constant Tone Extension with 2 microsecond slots
+
$next [+1] Flag dont_sync_type_3
-- Do not sync to packets with a typoe 3 Constant Tone Extension (currently reserved for future
-- use)
+
$next [+1] Flag dont_sync_without_cte
-- Do not sync to packets without a Constant Tone Extension
+
$next [+3] UInt padding
-- Reserved for future use
@@ -597,14 +610,19 @@
enum LEOwnAddressType:
-- Possible values that can be used for the |own_address_type| parameter in various HCI commands
+
[maximum_bits: 8]
+
PUBLIC = 0x00
-- Public Device Address
+
RANDOM = 0x01
-- Random Device Address
+
PRIVATE_DEFAULT_TO_PUBLIC = 0x02
-- Controller generates the Resolvable Private Address based on the local IRK from the resolving
-- list. If the resolving list contains no matching entry, then use the public address.
+
PRIVATE_DEFAULT_TO_RANDOM = 0x03
-- Controller generates the Resolvable Private Address based on the local IRK from the resolving
-- list. If the resolving list contains no matching entry, then use the random address from
@@ -625,6 +643,7 @@
[maximum_bits: 8]
PASSIVE = 0x00
-- Passive Scanning. No scanning PDUs shall be sent (default)
+
ACTIVE = 0x01
-- Active scanning. Scanning PDUs may be sent.
@@ -639,15 +658,19 @@
bits LEPHYBits:
- 0 [+1] Flag le_1m
+ 0 [+1] Flag le_1m
-- Scan advertisements on the LE 1M PHY
- $next [+1] Flag padding1
+
+ $next [+1] Flag padding1
-- Reserved for future use
- $next [+1] Flag le_coded
+
+ $next [+1] Flag le_coded
-- Scan advertisements on the LE Coded PHY
- $next [+5] UInt padding2
+
+ $next [+5] UInt padding2
-- Reserved for future use
+
enum LEPrivacyMode:
-- Possible values for the |privacy_mode| parameter in an LE Set Privacy Mode
-- command
@@ -679,6 +702,251 @@
INTERLACED_SCAN = 0x01
-- Interlaced scan (optional)
+
+bits LEEventMask:
+ 0 [+1] Flag le_connection_complete
+ $next [+1] Flag le_advertising_report
+ $next [+1] Flag le_connection_update_complete
+ $next [+1] Flag le_read_remote_features_complete
+ $next [+1] Flag le_long_term_key_request
+ $next [+1] Flag le_remote_connection_parameter_request
+ $next [+1] Flag le_data_length_change
+ $next [+1] Flag le_read_local_p256_public_key_complete
+ $next [+1] Flag le_generate_dhkey_complete
+ $next [+1] Flag le_enhanced_connection_complete
+ $next [+1] Flag le_directed_advertising_report
+ $next [+1] Flag le_phy_update_complete
+ $next [+1] Flag le_extended_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_established
+ $next [+1] Flag le_periodic_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_lost
+ $next [+1] Flag le_extended_scan_timeout
+ $next [+1] Flag le_extended_advertising_set_terminated
+ $next [+1] Flag le_scan_request_received
+ $next [+1] Flag le_channel_selection_algorithm
+ $next [+1] Flag le_connectionless_iq_report
+ $next [+1] Flag le_connection_iq_report
+ $next [+1] Flag le_cte_request_failed
+ $next [+1] Flag le_periodic_advertising_sync_transfer_received_event
+ $next [+1] Flag le_cis_established_event
+ $next [+1] Flag le_cis_request_event
+ $next [+1] Flag le_create_big_complete_event
+ $next [+1] Flag le_terminate_big_complete_event
+ $next [+1] Flag le_big_sync_established_event
+ $next [+1] Flag le_big_sync_lost_event
+ $next [+1] Flag le_request_peer_sca_complete_event
+ $next [+1] Flag le_path_loss_threshold_event
+ $next [+1] Flag le_transmit_power_reporting_event
+ $next [+1] Flag le_biginfo_advertising_report_event
+ $next [+1] Flag le_subrate_change_event
+
+
+enum LEAdvertisingType:
+ [maximum_bits: 8]
+ CONNECTABLE_AND_SCANNABLE_UNDIRECTED = 0x00
+ -- ADV_IND
+
+ CONNECTABLE_HIGH_DUTY_CYCLE_DIRECTED = 0x01
+ -- ADV_DIRECT_IND
+
+ SCANNABLE_UNDIRECTED = 0x02
+ -- ADV_SCAN_IND
+
+ NOT_CONNECTABLE_UNDIRECTED = 0x03
+ -- ADV_NONCONN_IND
+
+ CONNECTABLE_LOW_DUTY_CYCLE_DIRECTED = 0x04
+ -- ADV_DIRECT_IND
+
+
+bits LEAdvertisingChannels:
+ 0 [+1] Flag channel_37
+ $next [+1] Flag channel_38
+ $next [+1] Flag channel_39
+
+
+enum LEAdvertisingFilterPolicy:
+ [maximum_bits: 8]
+
+ ALLOW_ALL = 0x00
+ -- Process scan and connection requests from all devices (i.e., the Filter
+ -- Accept List is not in use) (default).
+
+ ALLOW_ALL_CONNECTIONS_AND_USE_FILTER_ACCEPT_LIST_FOR_SCANS = 0x01
+ -- Process connection requests from all devices and scan requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_ALL_SCANS_AND_USE_FILTER_ACCEPT_LIST_FOR_CONNECTIONS = 0x02
+ -- Process scan requests from all devices and connection requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_FILTER_ACCEPT_LIST_ONLY = 0x03
+ -- Process scan and connection requests only from devices in the Filter
+ -- Accept List.
+
+
+enum LESetExtendedAdvDataOp:
+ -- Potential values for the Operation parameter in a HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ INTERMEDIATE_FRAGMENT = 0x00
+ -- Intermediate fragment of fragmented extended advertising data.
+
+ FIRST_FRAGMENT = 0x01
+ -- First fragment of fragmented extended advertising data.
+
+ LAST_FRAGMENT = 0x02
+ -- Last fragment of fragmented extended advertising data.
+
+ COMPLETE = 0x03
+ -- Complete extended advertising data.
+
+ UNCHANGED_DATA = 0x04
+ -- Unchanged data (just update the Advertising DID)
+
+
+enum LEExtendedAdvFragmentPreference:
+ -- Potential values for the Fragment_Preference parameter in a
+ -- HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ MAY_FRAGMENT = 0x00
+ -- The Controller may fragment all Host advertising data
+
+ SHOULD_NOT_FRAGMENT = 0x01
+ -- The Controller should not fragment or should minimize fragmentation of Host advertising data
+
+
+enum FlowControlMode:
+ [maximum_bits: 8]
+ PACKET_BASED = 0x00
+ DATA_BLOCK_BASED = 0x01
+
+
+bits EventMaskPage2:
+ 8 [+1] Flag number_of_completed_data_blocks_event
+ 14 [+1] Flag triggered_clock_capture_event
+ 15 [+1] Flag synchronization_train_complete_event
+ 16 [+1] Flag synchronization_train_received_event
+ 17 [+1] Flag connectionless_peripheral_broadcast_receive_event
+ 18 [+1] Flag connectionless_peripheral_broadcast_timeout_event
+ 19 [+1] Flag truncated_page_complete_event
+ 20 [+1] Flag peripheral_page_response_timeout_event
+ 21 [+1] Flag connectionless_peripheral_broadcast_channel_map_event
+ 22 [+1] Flag inquiry_response_notification_event
+ 23 [+1] Flag authenticated_payload_timeout_expired_event
+ 24 [+1] Flag sam_status_change_event
+ 25 [+1] Flag encryption_change_event_v2
+
+
+enum LinkType:
+ [maximum_bits: 8]
+ SCO = 0x00
+ ACL = 0x01
+ ESCO = 0x02
+
+
+enum EncryptionStatus:
+ OFF = 0x00
+ ON_WITH_E0_FOR_BREDR_OR_AES_FOR_LE = 0x01
+ ON_WITH_AES_FOR_BREDR = 0x03
+
+
+bits LmpFeatures(page: UInt:8):
+ -- Bit mask of Link Manager Protocol features.
+ if page == 0:
+ 0 [+1] Flag three_slot_packets
+ 1 [+1] Flag five_slot_packets
+ 2 [+1] Flag encryption
+ 3 [+1] Flag slot_offset
+ 4 [+1] Flag timing_accuracy
+ 5 [+1] Flag role_switch
+ 6 [+1] Flag hold_mode
+ 7 [+1] Flag sniff_mode
+ # 8: previously used
+ 9 [+1] Flag power_control_requests
+ 10 [+1] Flag channel_quality_driven_data_rate
+ 11 [+1] Flag sco_link
+ 12 [+1] Flag hv2_packets
+ 13 [+1] Flag hv3_packets
+ 14 [+1] Flag mu_law_log_synchronous_data
+ 15 [+1] Flag a_law_log_synchronous_data
+ 16 [+1] Flag cvsd_synchronous_data
+ 17 [+1] Flag paging_parameter_negotiation
+ 18 [+1] Flag power_control
+ 19 [+1] Flag transparent_synchronous_data
+ 20 [+3] UInt flow_control_lag
+ 23 [+1] Flag broadcast_encryption
+ # 24: reserved for future use
+ 25 [+1] Flag enhanced_data_rate_acl_2_mbs_mode
+ 26 [+1] Flag enhanced_data_rate_acl_3_mbs_mode
+ 27 [+1] Flag enhanced_inquiry_scan
+ 28 [+1] Flag interlaced_inquiry_scan
+ 29 [+1] Flag interlaced_page_scan
+ 30 [+1] Flag rssi_with_inquiry_results
+ 31 [+1] Flag extended_sco_link_ev3_packets
+ 32 [+1] Flag ev4_packets
+ 33 [+1] Flag ev5_packets
+ # 34: reserved for future use
+ 35 [+1] Flag afh_capable_peripheral
+ 36 [+1] Flag afh_classification_peripheral
+ 37 [+1] Flag bredr_not_supported
+ 38 [+1] Flag le_supported_controller
+ 39 [+1] Flag three_slot_enhanced_data_rate_acl_packets
+ 40 [+1] Flag five_slot_enhanced_data_rate_acl_packets
+ 41 [+1] Flag sniff_subrating
+ 42 [+1] Flag pause_encryption
+ 43 [+1] Flag afh_capable_central
+ 44 [+1] Flag afh_classification_central
+ 45 [+1] Flag enhanced_data_rate_esco_2_mbs_mode
+ 46 [+1] Flag enhanced_data_rate_esco_3_mbs_mode
+ 47 [+1] Flag three_slot_enhanced_data_rate_esco_packets
+ 48 [+1] Flag extended_inquiry_response
+ 49 [+1] Flag simultaneous_le_and_bredr_to_same_device_capable_controller
+ # 50: reserved for future use
+ 51 [+1] Flag secure_simple_pairing_controller_support
+ 52 [+1] Flag encapsulated_pdu
+ 53 [+1] Flag erroneous_data_reporting
+ 54 [+1] Flag non_flushable_packet_boundary_flag
+ # 55: reserved for future use
+ 56 [+1] Flag hci_link_supervision_timeout_changed_event
+ 57 [+1] Flag variable_inquiry_tx_power_level
+ 58 [+1] Flag enhanced_power_control
+ # 59-62: reserved for future use
+ 63 [+1] Flag extended_features
+
+ if page == 1:
+ 0 [+1] Flag secure_simple_pairing_host_support
+ 1 [+1] Flag le_supported_host
+ # 2: previously used
+ 3 [+1] Flag secure_connection_host_support
+
+ if page == 2:
+ 0 [+1] Flag connectionless_peripheral_broadcast_transmitter_operation
+ 1 [+1] Flag connectionless_peripheral_broadcast_receiver_operation
+ 2 [+1] Flag synchronization_train
+ 3 [+1] Flag synchronization_scan
+ 4 [+1] Flag hci_inquiry_response_notification_event
+ 5 [+1] Flag generalized_interlaced_scan
+ 6 [+1] Flag coarse_clock_adjustment
+ # 7: reserved for future use
+ 8 [+1] Flag secure_connections_controller_support
+ 9 [+1] Flag ping
+ 10 [+1] Flag slot_availability_mask
+ 11 [+1] Flag train_nudging
+
+
+enum LEClockAccuracy:
+ -- Possible values that can be reported for the |central_clock_accuracy| and
+ -- |advertiser_clock_accuracy| parameters.
+ [maximum_bits: 8]
+ PPM_500 = 0x00
+ PPM_250 = 0x01
+ PPM_150 = 0x02
+ PPM_100 = 0x03
+ PPM_75 = 0x04
+ PPM_50 = 0x05
+ PPM_30 = 0x06
+ PPM_20 = 0x07
+
# ========================= HCI packet headers ==========================
@@ -1263,6 +1531,52 @@
$next [+1] UInt max_extended_advertising_events
+struct LESetExtendedAdvertisingDataCommand:
+ -- LE Set Extended Advertising Data Command (v5.0) (LE)
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+1] LESetExtendedAdvDataOp operation
+
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the Controller as to whether advertising data should be fragmented.
+
+ $next [+1] UInt advertising_data_length (sz)
+ -- Length of the advertising data included in this command packet, up to
+ -- kMaxLEExtendedAdvertisingDataLength bytes. If the advertising set uses legacy advertising
+ -- PDUs that support advertising data then this shall not exceed kMaxLEAdvertisingDataLength
+ -- bytes.
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] advertising_data
+ -- Variable length advertising data.
+
+
+struct LESetExtendedScanResponseDataCommand:
+ -- LE Set Extended Scan Response Data Command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Used to identify an advertising set
+ [requires: 0x00 <= this <= 0xEF]
+
+ $next [+1] LESetExtendedAdvDataOp operation
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the controller as to whether advertising data should be fragmented
+
+ $next [+1] UInt scan_response_data_length (sz)
+ -- The number of octets in the scan_response_data parameter
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] scan_response_data
+ -- Scan response data formatted as defined in Core Spec v5.4, Vol 3, Part C, Section 11
+
+
struct LESetExtendedAdvertisingEnableCommand:
-- LE Set Extended Advertising Enable command (v5.0) (LE)
let hdr_size = CommandHeader.$size_in_bytes
@@ -1273,6 +1587,20 @@
$next [+single_data_size*num_sets] LESetExtendedAdvertisingEnableData[] data
+struct LEReadMaxAdvertisingDataLengthCommand:
+ -- LE Read Maximum Advertising Data Length Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadNumSupportedAdvertisingSetsCommand:
+ -- LE Read Number of Supported Advertising Sets Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
struct LERemoveAdvertisingSetCommand:
-- LE Remove Advertising Set command (v5.0) (LE)
let hdr_size = CommandHeader.$size_in_bytes
@@ -1280,16 +1608,26 @@
$next [+1] UInt advertising_handle
+struct LEClearAdvertisingSetsCommand:
+ -- LE Clear Advertising Sets Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
struct LESetExtendedScanParametersData:
-- Data fields for variable-length portion of an LE Set Extneded Scan Parameters command
- 0 [+1] LEScanType scan_type
- $next [+2] UInt scan_interval
+
+ 0 [+1] LEScanType scan_type
+
+ $next [+2] UInt scan_interval
-- Time interval from when the Controller started its last scan until it begins the subsequent
-- scan on the primary advertising physical channel.
-- Time = N × 0.625 ms
-- Time Range: 2.5 ms to 40.959375 s
[requires: 0x0004 <= this]
- $next [+2] UInt scan_window
+
+ $next [+2] UInt scan_window
-- Duration of the scan on the primary advertising physical channel.
-- Time = N × 0.625 ms
-- Time Range: 2.5 ms to 40.959375 s
@@ -1300,13 +1638,13 @@
-- LE Set Extended Scan Parameters Command (v5.0) (LE)
-- num_entries corresponds to the number of bits set in the |scanning_phys| field
let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEOwnAddressType own_address_type
- $next [+1] LEScanFilterPolicy scanning_filter_policy
- $next [+1] LEPHYBits scanning_phys
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEOwnAddressType own_address_type
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+ $next [+1] LEPHYBits scanning_phys
let single_entry_size = LESetExtendedScanParametersData.$size_in_bytes
- let total_entries_size = num_entries * single_entry_size
- $next [+total_entries_size] LESetExtendedScanParametersData[num_entries] data
+ let total_entries_size = num_entries*single_entry_size
+ $next [+total_entries_size] LESetExtendedScanParametersData[num_entries] data
-- Indicates the type of address being used in the scan request packets (for active scanning).
@@ -1402,26 +1740,35 @@
struct LEPeriodicAdvertisingCreateSyncCommand:
-- LE Periodic Advertising Create Sync Command (v5.0) (LE)
+
let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEPeriodicAdvertisingCreateSyncOptions options
- $next [+1] UInt advertising_sid
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] LEPeriodicAdvertisingCreateSyncOptions options
+
+ $next [+1] UInt advertising_sid
-- Advertising SID subfield in the ADI field used to identify the Periodic Advertising
[requires: 0x00 <= this <= 0x0F]
- $next [+1] LEPeriodicAdvertisingAddressType advertiser_address_type
- $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
+
+ $next [+1] LEPeriodicAdvertisingAddressType advertiser_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
-- Public Device Address, Random Device Address, Public Identity Address, or Random (static)
-- Identity Address of the advertiser
- $next [+2] UInt skip
+
+ $next [+2] UInt skip
-- The maximum number of periodic advertising events that can be skipped after a successful
-- receive
[requires: 0x0000 <= this <= 0x01F3]
- $next [+2] UInt sync_timeout
+
+ $next [+2] UInt sync_timeout
-- Synchronization timeout for the periodic advertising.
-- Time = N * 10 ms
-- Time Range: 100 ms to 163.84 s
[requires: 0x000A <= this <= 0x4000]
- $next [+1] LEPeriodicAdvertisingSyncCTEType sync_cte_type
+
+ $next [+1] LEPeriodicAdvertisingSyncCTEType sync_cte_type
-- Constant Tone Extension sync options
@@ -1429,14 +1776,14 @@
-- LE Periodic Advertising Create Sync Cancel Command (v5.0) (LE)
-- Note that this command has no arguments
let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
+ 0 [+hdr_size] CommandHeader header
struct LEPeriodicAdvertisingTerminateSyncCommand:
-- LE Periodic Advertising Terminate Sync Command (v5.0) (LE)
let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt sync_handle
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt sync_handle
-- Identifies the periodic advertising train
[requires: 0x0000 <= this <= 0x0EFF]
@@ -1564,6 +1911,464 @@
let hdr_size = CommandHeader.$size_in_bytes
0 [+hdr_size] CommandHeader header
+
+struct WriteLEHostSupportCommand:
+ -- Write LE Host Support Command (v4.0) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam le_supported_host
+ -- Sets the LE Supported (Host) Link Manager Protocol feature bit.
+
+ $next [+1] UInt unused
+ -- Core Spec v5.0, Vol 2, Part E, Section 6.35: This parameter was named
+ -- "Simultaneous_LE_Host" and the value is set to "disabled(0x00)" and
+ -- "shall be ignored".
+ -- Core Spec v5.3, Vol 4, Part E, Section 7.3.79: This parameter was renamed
+ -- to "Unused" and "shall be ignored by the controller".
+
+
+struct ReadLocalVersionInformationCommand:
+ -- Read Local Version Information Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalSupportedCommandsCommand:
+ -- Read Local Supported Commands Command (v1.2)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadBufferSizeCommand:
+ -- Read Buffer Size Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadBdAddrCommand:
+ -- Read BD_ADDR Command (v1.1) (BR/EDR, LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalSupportedFeaturesCommand:
+ -- Read Local Supported Features Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalExtendedFeaturesCommand:
+ -- Read Local Extended Features Command (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt page_number
+ -- 0x00: Requests the normal LMP features as returned by
+ -- Read_Local_Supported_Features.
+ -- 0x01-0xFF: Return the corresponding page of features.
+
+
+struct ReadEncryptionKeySizeCommand:
+ -- Read Encryption Key Size (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Identifies an active ACL link (only the lower 12 bits are meaningful).
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LESetEventMaskCommand:
+ -- LE Set Event Mask Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+8] bits:
+ 0 [+35] LEEventMask le_event_mask
+ -- Bitmask that indicates which LE events are generated by the HCI for the Host.
+
+
+struct LEReadBufferSizeCommandV1:
+ -- LE Read Buffer Size Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadBufferSizeCommandV2:
+ -- LE Read Buffer Size Command (v5.2) (LE)
+ -- Version 2 of this command changed the opcode and added ISO return
+ -- parameters.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadLocalSupportedFeaturesCommand:
+ -- LE Read Local Supported Features Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetRandomAddressCommand:
+ -- LE Set Random Address Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr random_address
+
+
+struct LESetAdvertisingParametersCommand:
+ -- LE Set Advertising Parameters Command (v4.0) (LE)
+
+ [requires: advertising_interval_min <= advertising_interval_max]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt advertising_interval_min
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+2] UInt advertising_interval_max
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+1] LEAdvertisingType adv_type
+ -- Used to determine the packet type that is used for advertising when
+ -- advertising is enabled.
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+1] LEPeerAddressType peer_address_type
+ -- ANONYMOUS address type not allowed.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the device to be connected.
+
+ $next [+1] bits:
+
+ 0 [+3] LEAdvertisingChannels advertising_channel_map
+ -- Indicates the advertising channels that shall be used when transmitting
+ -- advertising packets. At least 1 channel must be enabled.
+ -- Default: all channels enabled
+
+ $next [+1] LEAdvertisingFilterPolicy advertising_filter_policy
+ -- This parameter shall be ignored when directed advertising is enabled.
+
+
+struct LEReadAdvertisingChannelTxPowerCommand:
+ -- LE Read Advertising Channel Tx Power Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetAdvertisingDataCommand:
+ -- LE Set Advertising Data Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_data_length
+ -- The number of significant octets in `advertising_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] advertising_data
+ -- 31 octets of advertising data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetScanResponseDataCommand:
+ -- LE Set Scan Response Data Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt scan_response_data_length
+ -- The number of significant octets in `scan_response_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] scan_response_data
+ -- 31 octets of scan response data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetScanParametersCommand:
+ -- LE Set Scan Parameters Command (v4.0) (LE)
+
+ [requires: le_scan_window <= le_scan_interval]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] LEScanType le_scan_type
+ -- Controls the type of scan to perform.
+
+ $next [+2] UInt le_scan_interval
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] LEOwnAddressType own_address_type
+ -- The type of address being used in the scan request packets.
+
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+
+
+struct LESetScanEnableCommand:
+ -- LE Set Scan Enable Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam le_scan_enable
+ $next [+1] GenericEnableParam filter_duplicates
+ -- Controls whether the Link Layer should filter out duplicate advertising
+ -- reports to the Host, or if the Link Layer should generate advertising
+ -- reports for each packet received. Ignored if le_scan_enable is set to
+ -- disabled.
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.4.3.5
+
+
+struct LECreateConnectionCommand:
+ -- LE Create Connection Command (v4.0) (LE)
+
+ [requires: le_scan_window <= le_scan_interval && connection_interval_min <= connection_interval_max]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt le_scan_interval
+ -- The time interval from when the Controller started the last LE scan until
+ -- it begins the subsequent LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Amount of time for the duration of the LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] GenericEnableParam initiator_filter_policy
+
+ $next [+1] LEAddressType peer_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of connection
+ -- events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+
+struct LECreateConnectionCancelCommand:
+ -- LE Create Connection Cancel Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEClearFilterAcceptListCommand:
+ -- LE Clear Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEAddDeviceToFilterAcceptListCommand:
+ -- LE Add Device To Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LERemoveDeviceFromFilterAcceptListCommand:
+ -- LE Remove Device From Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LEConnectionUpdateCommand:
+ -- LE Connection Update Command (v4.0) (LE)
+
+ [requires: connection_interval_min <= connection_interval_max && min_connection_event_length <= max_connection_event_length]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of subrated
+ -- connection events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+
+struct LEReadRemoteFeaturesCommand:
+ -- LE Read Remote Features Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEEnableEncryptionCommand:
+ -- LE Enable Encryption Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] UInt random_number
+ $next [+2] UInt encrypted_diversifier
+ $next [+LinkKey.$size_in_bytes] LinkKey long_term_key
+
+
+struct LELongTermKeyRequestNegativeReplyCommand:
+ -- LE Long Term Key Request Negative Reply Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEReadSupportedStatesCommand:
+ -- LE Read Supported States Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEClearResolvingListCommand:
+ -- LE Clear Resolving List Command (v4.2) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetAddressResolutionEnableCommand:
+ -- LE Set Address Resolution Enable Command (v4.2) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam address_resolution_enable
+
+
+struct LESetAdvertisingSetRandomAddressCommand:
+ -- LE Set Advertising Set Random Address Command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr random_address
+ -- The random address to use in the advertising PDUs.
+
+
+struct WriteAuthenticatedPayloadTimeoutCommand:
+ -- Write Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt authenticated_payload_timeout
+ -- Default = 0x0BB8 (30 s)
+ -- Time = N * 10 ms
+ -- Time Range: 10 ms to 655,350 ms
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+
+struct ReadAuthenticatedPayloadTimeoutCommand:
+ -- Read Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct ReadLEHostSupportCommand:
+ -- Read LE Host Support Command (v4.0) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+
+
+struct ReadFlowControlModeCommand:
+ -- Read Flow Control Mode Command (v3.0 + HS) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+
+
+struct WriteFlowControlModeCommand:
+ -- Write Flow Control Mode Command (v3.0 + HS) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+1] FlowControlMode flow_control_mode
+
+
+struct SetEventMaskPage2Command:
+ -- Set Event Mask Page 2 Command (v3.0 + HS)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+8] bits:
+ 0 [+26] EventMaskPage2 event_mask_page_2
+ -- Bit mask used to control which HCI events are generated by the HCI for the Host.
+
# ========================= HCI Event packets ===========================
# Core Spec v5.3 Vol 4, Part E, Section 7.7
@@ -1594,6 +2399,160 @@
let event_fixed_size = $size_in_bytes-hdr_size
let return_parameters_size = header.parameter_total_size-event_fixed_size
+
+struct ConnectionCompleteEvent:
+ -- Connection Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The address of the connected device
+
+ $next [+1] LinkType link_type
+ $next [+1] GenericEnableParam encryption_enabled
+
+
+struct ConnectionRequestEvent:
+ -- Connection Request Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The address of the device that's requesting the connection.
+
+ $next [+3] ClassOfDevice class_of_device
+ -- The Class of Device of the device which requests the connection.
+
+ $next [+1] LinkType link_type
+
+
+struct DisconnectionCompleteEvent:
+ -- Disconnection Complete Event (v1.1) (BR/EDR & LE)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] StatusCode reason
+
+
+struct AuthenticationCompleteEvent:
+ -- Authentication Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct RemoteNameRequestCompleteEvent:
+ -- Remote Name Request Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ $next [+248] UInt:8[248] remote_name
+ -- UTF-8 encoded friendly name. If the name is less than 248 characters, it
+ -- is null terminated and the remaining bytes are not valid.
+
+
+struct EncryptionChangeEventV1:
+ -- Encryption Change Event (v1.1) (BR/EDR & LE)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] EncryptionStatus encryption_enabled
+
+
+struct ChangeConnectionLinkKeyCompleteEvent:
+ -- Change Connection Link Key Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct ReadRemoteSupportedFeaturesCompleteEvent:
+ -- Read Remote Supported Features Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] LmpFeatures(0) lmp_features
+ -- Page 0 of the LMP features.
+
+
+struct LEMetaEvent:
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt subevent_code
+ -- The event code for the LE subevent.
+
+
+struct LEConnectionCompleteSubevent:
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] ConnectionRole role
+
+ $next [+1] LEPeerAddressType peer_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+ -- Public Device Address or Random Device Address of the peer device.
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+1] LEClockAccuracy central_clock_accuracy
+ -- Only valid for a peripheral. On a central, this parameter shall be set to 0x00.
+
+
+struct LEConnectionUpdateCompleteSubevent:
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
# ============================ Test packets =============================
diff --git a/pw_bluetooth/public/pw_bluetooth/host.h b/pw_bluetooth/public/pw_bluetooth/host.h
index 6fa0ad2..d5f3f30 100644
--- a/pw_bluetooth/public/pw_bluetooth/host.h
+++ b/pw_bluetooth/public/pw_bluetooth/host.h
@@ -32,162 +32,162 @@
namespace pw::bluetooth {
-// Host is the entrypoint API for interacting with a Bluetooth host stack. Host
-// is an abstract class that is implemented by a host stack implementation.
+/// Host is the entrypoint API for interacting with a Bluetooth host stack. Host
+/// is an abstract class that is implemented by a host stack implementation.
class Host {
public:
- // Represents the persistent configuration of a single Host instance. This is
- // used for identity representation in advertisements & bonding secrets
- // recall.
+ /// Represents the persistent configuration of a single Host instance. This is
+ /// used for identity representation in advertisements & bonding secrets
+ /// recall.
struct PersistentData {
- // The local Identity Resolving Key used by a Host to generate Resolvable
- // Private Addresses when privacy is enabled. May be absent for hosts that
- // do not use LE privacy, or that only use Non-Resolvable Private Addresses.
- //
- // NOTE: This key is distributed to LE peers during pairing procedures. The
- // client must take care to assign an IRK that consistent with the local
- // Host identity.
+ /// The local Identity Resolving Key used by a Host to generate Resolvable
+ /// Private Addresses when privacy is enabled. May be absent for hosts that
+ /// do not use LE privacy, or that only use Non-Resolvable Private
+ /// Addresses.
+ ///
+ /// NOTE: This key is distributed to LE peers during pairing procedures. The
+ /// client must take care to assign an IRK that consistent with the local
+ /// Host identity.
std::optional<Key> identity_resolving_key;
- // All bonds that use a public identity address must contain the same local
- // address.
+ /// All bonds that use a public identity address must contain the same local
+ /// address.
span<const low_energy::BondData> bonds;
};
- // The security level required for this pairing. This corresponds to the
- // security levels defined in the Security Manager Protocol in Core spec v5.3,
- // Vol 3, Part H, Section 2.3.1
+ /// The security level required for this pairing. This corresponds to the
+ /// security levels defined in the Security Manager Protocol in Core spec
+ /// v5.3, Vol 3, Part H, Section 2.3.1
enum class PairingSecurityLevel : uint8_t {
- // Encrypted without person-in-the-middle protection (unauthenticated)
+ /// Encrypted without person-in-the-middle protection (unauthenticated)
kEncrypted,
- // Encrypted with person-in-the-middle protection (authenticated), although
- // this level of security does not fully protect against passive
- // eavesdroppers
+ /// Encrypted with person-in-the-middle protection (authenticated), although
+ /// this level of security does not fully protect against passive
+ /// eavesdroppers
kAuthenticated,
- // Encrypted with person-in-the-middle protection (authenticated).
- // This level of security fully protects against eavesdroppers.
+ /// Encrypted with person-in-the-middle protection (authenticated).
+ /// This level of security fully protects against eavesdroppers.
kLeSecureConnections,
};
- // Whether or not the device should form a bluetooth bond during the pairing
- // prodecure. As described in Core Spec v5.2, Vol 3, Part C, Sec 4.3
+ /// Whether or not the device should form a bluetooth bond during the pairing
+ /// prodecure. As described in Core Spec v5.2, Vol 3, Part C, Sec 4.3
enum class BondableMode : uint8_t {
- // The device will form a bond during pairing with peers
+ /// The device will form a bond during pairing with peers
kBondable,
- // The device will not form a bond during pairing with peers
+ /// The device will not form a bond during pairing with peers
kNonBondable,
};
- // Parameters that give a caller more fine-grained control over the pairing
- // process.
+ /// Parameters that give a caller more fine-grained control over the pairing
+ /// process.
struct PairingOptions {
- // Determines the Security Manager security level to pair with.
+ /// Determines the Security Manager security level to pair with.
PairingSecurityLevel security_level = PairingSecurityLevel::kAuthenticated;
- // Indicated whether the device should form a bond or not during pairing. If
- // not present, interpreted as bondable mode.
+ /// Indicated whether the device should form a bond or not during pairing.
+ /// If not present, interpreted as bondable mode.
BondableMode bondable_mode = BondableMode::kBondable;
};
- // `Close` should complete before `Host` is destroyed.
+ /// `Close()` should complete before `Host` is destroyed.
virtual ~Host() = default;
- // Initializes the host stack. Vendor specific controller initialization (e.g.
- // loading firmware) must be done before initializing `Host`.
- //
- // Parameters:
- // `controller` - Pointer to a concrete `Controller` that the host stack
- // should use to communicate with the controller.
- // `data` - Data to persist from a previous instance of `Host`.
- // `on_initialization_complete` - Called when initialization is complete.
- // Other methods should not be called until initialization completes.
+ /// Initializes the host stack. Vendor specific controller initialization
+ /// (e.g. loading firmware) must be done before initializing `Host`.
+ ///
+ /// @param controller Pointer to a concrete `Controller` that the host stack
+ /// should use to communicate with the controller.
+ /// @param data Data to persist from a previous instance of `Host`.
+ /// @param on_initialization_complete Called when initialization is complete.
+ /// Other methods should not be called until initialization completes.
virtual void Initialize(
Controller* controller,
PersistentData data,
Function<void(Status)>&& on_initialization_complete) = 0;
- // Safely shuts down the host, ending all active Bluetooth procedures:
- // - All objects/pointers associated with this host are destroyed/invalidated
- // and all connections disconnected.
- // - All scanning and advertising procedures are stopped.
- //
- // The Host may send events or call callbacks as procedures get terminated.
- // `callback` will be called once all procedures have terminated.
+ /// Safely shuts down the host, ending all active Bluetooth procedures:
+ /// - All objects/pointers associated with this host are destroyed/invalidated
+ /// and all connections disconnected.
+ /// - All scanning and advertising procedures are stopped.
+ ///
+ /// The Host may send events or call callbacks as procedures get terminated.
+ /// @param callback Will be called once all procedures have terminated.
virtual void Close(Closure callback) = 0;
- // Returns a pointer to the Central API, which is used to scan and connect to
- // peers.
+ /// Returns a pointer to the Central API, which is used to scan and connect to
+ /// peers.
virtual low_energy::Central* Central() = 0;
- // Returns a pointer to the Peripheral API, which is used to advertise and
- // accept connections from peers.
+ /// Returns a pointer to the Peripheral API, which is used to advertise and
+ /// accept connections from peers.
virtual low_energy::Peripheral* Peripheral() = 0;
- // Returns a pointer to the GATT Server API, which is used to publish GATT
- // services.
+ /// Returns a pointer to the GATT Server API, which is used to publish GATT
+ /// services.
virtual gatt::Server* GattServer() = 0;
- // Deletes a peer from the Bluetooth host. If the peer is connected, it will
- // be disconnected. `peer_id` will no longer refer to any peer.
- //
- // Returns `OK` after no peer exists that's identified by `peer_id` (even
- // if it didn't exist), `ABORTED` if the peer could not be disconnected or
- // deleted and still exists.
+ /// Deletes a peer from the Bluetooth host. If the peer is connected, it will
+ /// be disconnected. `peer_id` will no longer refer to any peer.
+ ///
+ /// Returns `OK` after no peer exists that's identified by `peer_id` (even
+ /// if it didn't exist), `ABORTED` if the peer could not be disconnected or
+ /// deleted and still exists.
virtual Status ForgetPeer(PeerId peer_id) = 0;
- // Enable or disable the LE privacy feature. When enabled, the host will use a
- // private device address in all LE procedures. When disabled, the public
- // identity address will be used instead (which is the default).
+ /// Enable or disable the LE privacy feature. When enabled, the host will use
+ /// a private device address in all LE procedures. When disabled, the public
+ /// identity address will be used instead (which is the default).
virtual void EnablePrivacy(bool enabled) = 0;
- // Set the GAP LE Security Mode of the host. Only encrypted,
- // connection-based security modes are supported, i.e. Mode 1 and Secure
- // Connections Only mode. If the security mode is set to Secure Connections
- // Only, any existing encrypted connections which do not meet the security
- // requirements of Secure Connections Only mode will be disconnected.
+ /// Set the GAP LE Security Mode of the host. Only encrypted,
+ /// connection-based security modes are supported, i.e. Mode 1 and Secure
+ /// Connections Only mode. If the security mode is set to Secure Connections
+ /// Only, any existing encrypted connections which do not meet the security
+ /// requirements of Secure Connections Only mode will be disconnected.
virtual void SetSecurityMode(low_energy::SecurityMode security_mode) = 0;
- // Assigns the pairing delegate that will respond to authentication challenges
- // using the given I/O capabilities. Calling this method cancels any on-going
- // pairing procedure started using a previous delegate. Pairing requests will
- // be rejected if no PairingDelegate has been assigned.
+ /// Assigns the pairing delegate that will respond to authentication
+ /// challenges using the given I/O capabilities. Calling this method cancels
+ /// any on-going pairing procedure started using a previous delegate. Pairing
+ /// requests will be rejected if no PairingDelegate has been assigned.
virtual void SetPairingDelegate(InputCapability input,
OutputCapability output,
PairingDelegate* pairing_delegate) = 0;
- // NOTE: This is intended to satisfy test scenarios that require pairing
- // procedures to be initiated without relying on service access. In normal
- // operation, Bluetooth security is enforced during service access.
- //
- // Initiates pairing to the peer with the supplied `peer_id` and `options`.
- // Returns an error if no connected peer with `peer_id` is found or the
- // pairing procedure fails.
- //
- // If `options` specifies a higher security level than the current pairing,
- // this method attempts to raise the security level. Otherwise this method has
- // no effect and returns success.
- //
- // Returns the following errors via `callback`:
- // `NOT_FOUND` - The peer `peer_id` was not found.
- // `ABORTED` - The pairing procedure failed.
+ /// NOTE: This is intended to satisfy test scenarios that require pairing
+ /// procedures to be initiated without relying on service access. In normal
+ /// operation, Bluetooth security is enforced during service access.
+ ///
+ /// Initiates pairing to the peer with the supplied `peer_id` and `options`.
+ /// Returns an error if no connected peer with `peer_id` is found or the
+ /// pairing procedure fails.
+ ///
+ /// If `options` specifies a higher security level than the current pairing,
+ /// this method attempts to raise the security level. Otherwise this method
+ /// has no effect and returns success.
+ ///
+ /// Returns the following errors via `callback`:
+ /// `NOT_FOUND` - The peer `peer_id` was not found.
+ /// `ABORTED` - The pairing procedure failed.
virtual void Pair(PeerId peer_id,
PairingOptions options,
Function<void(Status)>&& callback) = 0;
- // Configures a callback to be called when new bond data for a peer has been
- // created. This data should be persisted and used to initialize Host in the
- // future. New bond data may be received for an already bonded peer, in which
- // case the new data should overwrite the old data.
+ /// Configures a callback to be called when new bond data for a peer has been
+ /// created. This data should be persisted and used to initialize Host in the
+ /// future. New bond data may be received for an already bonded peer, in which
+ /// case the new data should overwrite the old data.
virtual void SetBondDataCallback(
Function<void(low_energy::BondData)>&& callback) = 0;
- // Looks up the `PeerId` corresponding to `address`. If `address` does not
- // correspond to a known peer, a new `PeerId` will be generated for the
- // address. If a `PeerId` cannot be generated, std::nullopt will be returned.
+ /// Looks up the `PeerId` corresponding to `address`. If `address` does not
+ /// correspond to a known peer, a new `PeerId` will be generated for the
+ /// address. If a `PeerId` cannot be generated, std::nullopt will be returned.
virtual std::optional<PeerId> PeerIdFromAddress(Address address) = 0;
- // Looks up the Address corresponding to `peer_id`. Returns null if `peer_id`
- // does not correspond to a known peer.
+ /// Looks up the Address corresponding to `peer_id`. Returns null if `peer_id`
+ /// does not correspond to a known peer.
virtual std::optional<Address> DeviceAddressFromPeerId(PeerId peer_id) = 0;
};
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/central.h b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
index 2753feb..391e691 100644
--- a/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
@@ -27,104 +27,105 @@
namespace pw::bluetooth::low_energy {
-// Represents the LE central role. Used to scan and connect to peripherals.
+/// Represents the LE central role. Used to scan and connect to peripherals.
class Central {
public:
- // Represents an ongoing LE scan.
- class Scan {
+ /// Represents an ongoing LE scan.
+ class ScanHandle {
public:
+ /// Possible errors that can cause a scan to stop prematurely.
enum class ScanError : uint8_t { kCanceled = 0 };
- virtual ~Scan() = 0;
+ virtual ~ScanHandle() = 0;
- // Set a callback that will be called if the scan is stopped due to an error
- // in the BLE stack.
+ /// Set a callback that will be called if the scan is stopped due to an
+ /// error in the BLE stack.
virtual void SetErrorCallback(Function<void(ScanError)>&& callback) = 0;
private:
- // Stop the current scan. This method is called by the ~Scan::Ptr() when it
- // goes out of scope, the API client should never call this method.
+ /// Stop the current scan. This method is called by the ~ScanHandle::Ptr()
+ /// when it goes out of scope, the API client should never call this method.
virtual void StopScan() = 0;
public:
- // Movable Scan smart pointer. The controller will continue scanning until
- // the returned Scan::Ptr is destroyed.
- using Ptr = internal::RaiiPtr<Scan, &Scan::StopScan>;
+ /// Movable ScanHandle smart pointer. The controller will continue scanning
+ /// until the ScanHandle::Ptr is destroyed.
+ using Ptr = internal::RaiiPtr<ScanHandle, &ScanHandle::StopScan>;
};
- // Filter parameters for use during a scan. A discovered peer only matches the
- // filter if it satisfies all of the present filter parameters.
+ /// Filter parameters for use during a scan. A discovered peer only matches
+ /// the filter if it satisfies all of the present filter parameters.
struct ScanFilter {
- // Filter based on advertised service UUID.
+ /// Filter based on advertised service UUID.
std::optional<Uuid> service_uuid;
- // Filter based on service data containing the given UUID.
+ /// Filter based on service data containing the given UUID.
std::optional<Uuid> service_data_uuid;
- // Filter based on a manufacturer identifier present in the manufacturer
- // data. If this filter parameter is set, then the advertising payload must
- // contain manufacturer specific data with the provided company identifier
- // to satisfy this filter. Manufacturer identifiers can be found at
- // https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
+ /// Filter based on a manufacturer identifier present in the manufacturer
+ /// data. If this filter parameter is set, then the advertising payload must
+ /// contain manufacturer specific data with the provided company identifier
+ /// to satisfy this filter. Manufacturer identifiers can be found at
+ /// https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
std::optional<uint16_t> manufacturer_id;
- // Filter based on whether or not a device is connectable. For example, a
- // client that is only interested in peripherals that it can connect to can
- // set this to true. Similarly a client can scan only for broadcasters by
- // setting this to false.
+ /// Filter based on whether or not a device is connectable. For example, a
+ /// client that is only interested in peripherals that it can connect to can
+ /// set this to true. Similarly a client can scan only for broadcasters by
+ /// setting this to false.
std::optional<bool> connectable;
- // Filter results based on a portion of the advertised device name.
- // Substring matches are allowed.
- // The name length must be at most pw::bluetooth::kMaxDeviceNameLength.
+ /// Filter results based on a portion of the advertised device name.
+ /// Substring matches are allowed.
+ /// The name length must be at most pw::bluetooth::kMaxDeviceNameLength.
std::optional<std::string_view> name;
- // Filter results based on the path loss of the radio wave. A device that
- // matches this filter must satisfy the following:
- // 1. Radio transmission power level and received signal strength must be
- // available for the path loss calculation.
- // 2. The calculated path loss value must be less than, or equal to,
- // `max_path_loss`.
- //
- // NOTE: This field is calculated using the RSSI and TX Power information
- // obtained from advertising and scan response data during a scan procedure.
- // It should NOT be confused with information for an active connection
- // obtained using the "Path Loss Reporting" feature.
+ /// Filter results based on the path loss of the radio wave. A device that
+ /// matches this filter must satisfy the following:
+ /// 1. Radio transmission power level and received signal strength must be
+ /// available for the path loss calculation.
+ /// 2. The calculated path loss value must be less than, or equal to,
+ /// `max_path_loss`.
+ ///
+ /// @note This field is calculated using the RSSI and TX Power information
+ /// obtained from advertising and scan response data during a scan
+ /// procedure. It should NOT be confused with information for an active
+ /// connection obtained using the "Path Loss Reporting" feature.
std::optional<uint8_t> max_path_loss;
};
- // Parameters used during a scan.
+ /// Parameters used during a scan.
struct ScanOptions {
- // List of filters for use during a scan. A peripheral that satisfies any of
- // these filters will be reported. At least 1 filter must be specified.
- // While not recommended, clients that require that all peripherals be
- // reported can specify an empty filter.
+ /// List of filters for use during a scan. A peripheral that satisfies any
+ /// of these filters will be reported. At least 1 filter must be specified.
+ /// While not recommended, clients that require that all peripherals be
+ /// reported can specify an empty filter.
Vector<ScanFilter> filters;
- // The time interval between scans.
- // Time = N * 0.625ms
- // Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
- // Default: 10ms
+ /// The time interval between scans.
+ /// - Time = N * 0.625ms
+ /// - Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+ /// - Default: 10ms
uint16_t interval = 0x0010;
- // The duration of the scan. The window must be less than or equal to the
- // interval.
- // Time = N * 0.625ms
- // Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
- // Default: 10ms
+ /// The duration of the scan. The window must be less than or equal to the
+ /// interval.
+ /// - Time = N * 0.625ms
+ /// - Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+ /// - Default: 10ms
uint16_t window = 0x0010;
};
- // Information obtained from advertising and scan response data broadcast by a
- // peer.
+ /// Information obtained from advertising and scan response data broadcast by
+ /// a peer.
struct ScanData {
- // The radio transmit power level.
- // NOTE: This field should NOT be confused with the "connection TX Power
- // Level" of a peer that is currently connected to the system obtained via
- // the "Transmit Power reporting" feature.
+ /// The radio transmit power level.
+ /// @note This field should NOT be confused with the "connection TX Power
+ /// Level" of a peer that is currently connected to the system obtained via
+ /// the "Transmit Power reporting" feature.
std::optional<uint8_t> tx_power;
- // The appearance of the device.
+ /// The appearance of the device.
std::optional<Appearance> appearance;
Vector<Uuid> service_uuids;
@@ -133,122 +134,121 @@
Vector<ManufacturerData> manufacturer_data;
- // String representing a URI to be advertised, as defined in IETF STD
- // 66: https://tools.ietf.org/html/std66. Each entry should be a UTF-8
- // string including the scheme. For more information, see
- // https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for
- // allowed schemes; NOTE: Bluetooth advertising compresses schemas over the
- // air to save space. See
- // https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping.
+ /// String representing a URI to be advertised, as defined in IETF STD 66:
+ /// https://tools.ietf.org/html/std66. Each entry should be a UTF-8 string
+ /// including the scheme. For more information, see
+ /// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for
+ /// allowed schemes;
+ /// @note Bluetooth advertising compresses schemas over the air to save
+ /// space. See
+ /// https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping.
Vector<std::string_view> uris;
- // The time when this scan data was received.
+ /// The time when this scan data was received.
chrono::SystemClock::time_point timestamp;
};
struct ScanResult {
- // ScanResult is non-copyable becuase strings are only valid in the
- // result callback.
+ /// ScanResult is non-copyable because strings are only valid in the result
+ /// callback.
ScanResult(const ScanResult&) = delete;
ScanResult& operator=(const ScanResult&) = delete;
- // Uniquely identifies this peer on the current system.
+ /// Uniquely identifies this peer on the current system.
PeerId peer_id;
- // Whether or not this peer is connectable. Non-connectable peers are
- // typically in the LE broadcaster role.
+ /// Whether or not this peer is connectable. Non-connectable peers are
+ /// typically in the LE broadcaster role.
bool connectable;
- // The last observed signal strength of this peer. This field is only
- // present for a peer that is broadcasting. The RSSI can be stale if the
- // peer has not been advertising.
- //
- // NOTE: This field should NOT be confused with the "connection RSSI" of a
- // peer that is currently connected to the system.
+ /// The last observed signal strength of this peer. This field is only
+ /// present for a peer that is broadcasting. The RSSI can be stale if the
+ /// peer has not been advertising.
+ ///
+ /// @note This field should NOT be confused with the "connection RSSI" of a
+ /// peer that is currently connected to the system.
std::optional<uint8_t> rssi;
- // Information from advertising and scan response data broadcast by this
- // peer. This contains the advertising data last received from the peer.
+ /// Information from advertising and scan response data broadcast by this
+ /// peer. This contains the advertising data last received from the peer.
ScanData scan_data;
- // The name of this peer. The name is often obtained during a scan procedure
- // and can get updated during the name discovery procedure following a
- // connection.
- //
- // This field is present if the name is known.
+ /// The name of this peer. The name is often obtained during a scan
+ /// procedure and can get updated during the name discovery procedure
+ /// following a connection.
+ ///
+ /// This field is present if the name is known.
std::optional<std::string_view> name;
- // Timestamp of when the information in this `ScanResult` was last updated.
+ /// Timestamp of when the information in this `ScanResult` was last updated.
chrono::SystemClock::time_point last_updated;
};
- // Possible errors returned by `Connect`.
+ /// Possible errors returned by `Connect`.
enum class ConnectError : uint8_t {
- // The peer ID is unknown.
+ /// The peer ID is unknown.
kUnknownPeer,
- // The `ConnectionOptions` were invalid.
+ /// The `ConnectionOptions` were invalid.
kInvalidOptions,
- // A connection to the peer already exists.
+ /// A connection to the peer already exists.
kAlreadyExists,
- // A connection could not be established.
+ /// A connection could not be established.
kCouldNotBeEstablished,
};
enum class StartScanError : uint8_t {
- // A scan is already in progress. Only 1 scan may be active at a time.
+ /// A scan is already in progress. Only 1 scan may be active at a time.
kScanInProgress,
- // Some of the scan options are invalid.
+ /// Some of the scan options are invalid.
kInvalidParameters,
- // An internal error occurred and a scan could not be started.
+ /// An internal error occurred and a scan could not be started.
kInternal,
};
- // The Result type returned by Connect() via the passed callback.
+ /// The Result type returned by Connect() via the passed callback.
using ConnectResult = Result<ConnectError, Connection::Ptr>;
virtual ~Central() = default;
- // Connect to the peer with the given identifier.
- //
- // The requested `Connection` represents the client's interest in the LE
- // connection to the peer. Destroying the `Connection` will disconnect from
- // the peer. Only 1 connection per peer may exist at a time.
- //
- // The `Connection` will be closed by the system if the connection to the peer
- // is lost or an error occurs, as indicated by `Connection.OnError`.
- //
- // Parameters:
- // `id` - Identifier of the peer to initiate a connection to.
- // `options` - Options used to configure the connection.
- // `callback` - Called when a connection is successfully established, or an
- // error occurs.
- //
- // Possible errors are documented in `ConnectError`.
+ /// Connect to the peer with the given identifier.
+ ///
+ /// The requested `Connection` represents the client's interest in the LE
+ /// connection to the peer. Destroying the `Connection` will disconnect from
+ /// the peer. Only 1 connection per peer may exist at a time.
+ ///
+ /// The `Connection` will be closed by the system if the connection to the
+ /// peer is lost or an error occurs, as indicated by `Connection.OnError`.
+ ///
+ /// @param peer_id Identifier of the peer to initiate a connection to.
+ /// @param options Options used to configure the connection.
+ /// @param callback Called when a connection is successfully established, or
+ /// an error occurs.
+ ///
+ /// Possible errors are documented in `ConnectError`.
virtual void Connect(PeerId peer_id,
ConnectionOptions options,
Function<void(ConnectResult)>&& callback) = 0;
- // Scans for nearby LE peripherals and broadcasters. The lifetime of the scan
- // session is tied to the returned `Scan` object. Once a scan is started,
- // `scan_result_callback` will be called with scan results. Only 1 scan may be
- // active at a time. It is OK to destroy the `Scan::Ptr` object in
- // `scan_result_callback` to stop scanning (no more results will be returned).
- //
- // Parameters:
- // `options` - Options used to configure the scan session.
- // `scan_result_callback` - If scanning starts successfully,called for LE
- // peers that satisfy the filters indicated in `options`. The initial
- // calls may report recently discovered peers. Subsequent calls will
- // be made only when peers have been scanned or updated since the last
- // call.
- // `scan_started_callback` - Called with a `Scan` object if the
- // scan successfully starts, or a `ScanError` otherwise.
+ /// Scans for nearby LE peripherals and broadcasters. The lifetime of the scan
+ /// session is tied to the returned `ScanHandle` object. Once a scan is
+ /// started, `scan_result_callback` will be called with scan results. Only 1
+ /// scan may be active at a time. It is OK to destroy the `ScanHandle::Ptr`
+ /// object in `scan_result_callback` to stop scanning (no more results will be
+ /// returned).
+ ///
+ /// @param options Options used to configure the scan session.
+ /// @param scan_result_callback If scanning starts successfully,called for LE
+ /// peers that satisfy the filters indicated in `options`. The initial calls
+ /// may report recently discovered peers. Subsequent calls will be made only
+ /// when peers have been scanned or updated since the last call.
+ /// @param scan_started_callback Called with a `ScanHandle` object if the scan
+ /// successfully starts, or a `ScanError` otherwise.
virtual void Scan(ScanOptions options,
Function<void(ScanResult)>&& scan_result_callback,
- Function<void(Result<StartScanError, Scan::Ptr>)>&&
+ Function<void(Result<StartScanError, ScanHandle::Ptr>)>&&
scan_started_callback) = 0;
};
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
index e7d823f..d310953 100644
--- a/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
@@ -19,84 +19,84 @@
namespace pw::bluetooth::low_energy {
-// Actual connection parameters returned by the controller.
+/// Actual connection parameters returned by the controller.
struct ConnectionParameters {
- // The connection interval indicates the frequency of link layer connection
- // events over which data channel PDUs can be transmitted. See Core Spec v5.3,
- // Vol 6, Part B, Section 4.5.1 for more information on the link layer
- // connection events.
- // Range: 0x0006 to 0x0C80
- // Time: N * 1.25 ms
- // Time Range: 7.5 ms to 4 s.
+ /// The connection interval indicates the frequency of link layer connection
+ /// events over which data channel PDUs can be transmitted. See Core Spec
+ /// v5.3, Vol 6, Part B, Section 4.5.1 for more information on the link layer
+ /// connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
uint16_t interval;
- // The maximum allowed peripheral connection latency in number of connection
- // events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
- // Range: 0x0000 to 0x01F3
+ /// The maximum allowed peripheral connection latency in number of connection
+ /// events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ /// - Range: 0x0000 to 0x01F3
uint16_t latency;
- // This defines the maximum time between two received data packet PDUs
- // before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
- // B, Section 4.5.2.
- // Range: 0x000A to 0x0C80
- // Time: N * 10 ms
- // Time Range: 100 ms to 32 s
+ /// This defines the maximum time between two received data packet PDUs
+ /// before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+ /// B, Section 4.5.2.
+ /// - Range: 0x000A to 0x0C80
+ /// - Time: N * 10 ms
+ /// - Time Range: 100 ms to 32 s
uint16_t supervision_timeout;
};
-// Connection parameters that either the local device or a peer device are
-// requesting.
+/// Connection parameters that either the local device or a peer device are
+/// requesting.
struct RequestedConnectionParameters {
- // Minimum value for the connection interval. This shall be less than or equal
- // to `max_interval`. The connection interval indicates the frequency of link
- // layer connection events over which data channel PDUs can be transmitted.
- // See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more information on
- // the link layer connection events.
- // Range: 0x0006 to 0x0C80
- // Time: N * 1.25 ms
- // Time Range: 7.5 ms to 4 s.
+ /// Minimum value for the connection interval. This shall be less than or
+ /// equal to `max_interval`. The connection interval indicates the frequency
+ /// of link layer connection events over which data channel PDUs can be
+ /// transmitted. See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
+ /// information on the link layer connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
uint16_t min_interval;
- // Maximum value for the connection interval. This shall be greater than or
- // equal to `min_interval`. The connection interval indicates the frequency
- // of link layer connection events over which data channel PDUs can be
- // transmitted. See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
- // information on the link layer connection events.
- // Range: 0x0006 to 0x0C80
- // Time: N * 1.25 ms
- // Time Range: 7.5 ms to 4 s.
+ /// Maximum value for the connection interval. This shall be greater than or
+ /// equal to `min_interval`. The connection interval indicates the frequency
+ /// of link layer connection events over which data channel PDUs can be
+ /// transmitted. See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
+ /// information on the link layer connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
uint16_t max_interval;
- // Maximum peripheral latency for the connection in number of connection
- // events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
- // Range: 0x0000 to 0x01F3
+ /// Maximum peripheral latency for the connection in number of connection
+ /// events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ /// - Range: 0x0000 to 0x01F3
uint16_t max_latency;
- // This defines the maximum time between two received data packet PDUs
- // before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
- // B, Section 4.5.2.
- // Range: 0x000A to 0x0C80
- // Time: N * 10 ms
- // Time Range: 100 ms to 32 s
+ /// This defines the maximum time between two received data packet PDUs
+ /// before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+ /// B, Section 4.5.2.
+ /// - Range: 0x000A to 0x0C80
+ /// - Time: N * 10 ms
+ /// - Time Range: 100 ms to 32 s
uint16_t supervision_timeout;
};
-// Represents parameters that are set on a per-connection basis.
+/// Represents parameters that are set on a per-connection basis.
struct ConnectionOptions {
- // When true, the connection operates in bondable mode. This means pairing
- // will form a bond, or persist across disconnections, if the peer is also
- // in bondable mode. When false, the connection operates in non-bondable
- // mode, which means the local device only allows pairing that does not form
- // a bond.
+ /// When true, the connection operates in bondable mode. This means pairing
+ /// will form a bond, or persist across disconnections, if the peer is also
+ /// in bondable mode. When false, the connection operates in non-bondable
+ /// mode, which means the local device only allows pairing that does not form
+ /// a bond.
bool bondable_mode = true;
- // When present, service discovery performed following the connection is
- // restricted to primary services that match this field. Otherwise, by
- // default all available services are discovered.
+ /// When present, service discovery performed following the connection is
+ /// restricted to primary services that match this field. Otherwise, by
+ /// default all available services are discovered.
std::optional<Uuid> service_filter;
- // When present, specifies the initial connection parameters. Otherwise, the
- // connection parameters will be selected by the implementation.
+ /// When present, specifies the initial connection parameters. Otherwise, the
+ /// connection parameters will be selected by the implementation.
std::optional<RequestedConnectionParameters> parameters;
};
@@ -107,66 +107,66 @@
/// represents. Destroying the object results in a disconnection.
class Connection {
public:
- // Possible errors when updating the connection parameters.
+ /// Possible errors when updating the connection parameters.
enum class ConnectionParameterUpdateError : uint8_t {
kFailure,
kInvalidParameters,
kRejected,
};
- // Possible reasons a connection was disconnected.
+ /// Possible reasons a connection was disconnected.
enum class DisconnectReason : uint8_t {
kFailure,
kRemoteUserTerminatedConnection,
- // This usually indicates that the link supervision timeout expired.
+ /// This usually indicates that the link supervision timeout expired.
kConnectionTimeout,
};
- // If a disconnection has not occurred, destroying this object will result in
- // disconnection.
+ /// If a disconnection has not occurred, destroying this object will result in
+ /// disconnection.
virtual ~Connection() = default;
- // Sets a callback that will be called when the peer disconnects or there is a
- // connection error that causes a disconnection. This should be configured by
- // the client immediately after establishing the connection. `callback` will
- // not be called for disconnections initiated by the client (e.g. by
- // destroying `Connection`). It is OK to destroy this object from within
- // `callback`.
+ /// Sets a callback that will be called when the peer disconnects or there is
+ /// a connection error that causes a disconnection. This should be configured
+ /// by the client immediately after establishing the connection. `callback`
+ /// will not be called for disconnections initiated by the client (e.g. by
+ /// destroying `Connection`). It is OK to destroy this object from within
+ /// `callback`.
virtual void SetDisconnectCallback(
Function<void(DisconnectReason)>&& callback) = 0;
- // Returns a GATT client to the connected peer that is valid for the lifetime
- // of this connection. The client is valid for the lifetime of this
- // connection.
+ /// Returns a GATT client to the connected peer that is valid for the lifetime
+ /// of this connection. The client is valid for the lifetime of this
+ /// connection.
virtual gatt::Client* GattClient() = 0;
- // Returns the current ATT Maximum Transmission Unit. By subtracting ATT
- // headers from the MTU, the maximum payload size of messages can be
- // calculated.
+ /// Returns the current ATT Maximum Transmission Unit. By subtracting ATT
+ /// headers from the MTU, the maximum payload size of messages can be
+ /// calculated.
virtual uint16_t AttMtu() = 0;
- // Sets a callback that will be called with the new ATT MTU whenever it is
- // updated.
+ /// Sets a callback that will be called with the new ATT MTU whenever it is
+ /// updated.
virtual void SetAttMtuChangeCallback(Function<void(uint16_t)> callback) = 0;
- // Returns the current connection parameters.
+ /// Returns the current connection parameters.
virtual ConnectionParameters Parameters() = 0;
- // Requests an update to the connection parameters. `callback` will be called
- // with the result of the request.
+ /// Requests an update to the connection parameters. `callback` will be called
+ /// with the result of the request.
virtual void RequestConnectionParameterUpdate(
RequestedConnectionParameters parameters,
Function<void(Result<ConnectionParameterUpdateError>)>&& callback) = 0;
private:
- // Request to disconnect this connection. This method is called by the
- // ~Connection::Ptr() when it goes out of scope, the API client should never
- // call this method.
+ /// Request to disconnect this connection. This method is called by the
+ /// ~Connection::Ptr() when it goes out of scope, the API client should never
+ /// call this method.
virtual void Disconnect() = 0;
public:
- // Movable Connection smart pointer. When Connection::Ptr is destroyed the
- // Connection will disconnect automatically.
+ /// Movable Connection smart pointer. When Connection::Ptr is destroyed the
+ /// Connection will disconnect automatically.
using Ptr = internal::RaiiPtr<Connection, &Connection::Disconnect>;
};
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
index 23c56d1..34c88fc 100644
--- a/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
@@ -26,130 +26,133 @@
namespace pw::bluetooth::low_energy {
-// `AdvertisedPeripheral` instances are valid for the duration of advertising.
+/// `AdvertisedPeripheral` instances are valid for the duration of advertising.
class AdvertisedPeripheral {
public:
virtual ~AdvertisedPeripheral() = default;
- // Set a callback that will be called when an error occurs and advertising
- // has been stopped (invalidating this object). It is OK to destroy the
- // `AdvertisedPeripheral::Ptr` object from within `callback`.
+ /// Set a callback that will be called when an error occurs and advertising
+ /// has been stopped (invalidating this object). It is OK to destroy the
+ /// `AdvertisedPeripheral::Ptr` object from within `callback`.
virtual void SetErrorCallback(Closure callback) = 0;
- // For connectable advertisements, this callback will be called when an LE
- // central connects to the advertisement. It is recommended to set this
- // callback immediately after advertising starts to avoid dropping
- // connections.
- //
- // The returned Connection can be used to interact with the peer. It also
- // represents a peripheral's ownership over the connection: the client can
- // drop the object to request a disconnection. Similarly, the Connection
- // error handler is called by the system to indicate that the connection to
- // the peer has been lost. While connections are exclusive among peripherals,
- // they may be shared with centrals.
- //
- // If advertising is not stopped, this callback may be called with multiple
- // connections over the lifetime of an advertisement. It is OK to destroy
- // the `AdvertisedPeripheral::Ptr` object from within `callback` in order to
- // stop advertising.
+ /// For connectable advertisements, this callback will be called when an LE
+ /// central connects to the advertisement. It is recommended to set this
+ /// callback immediately after advertising starts to avoid dropping
+ /// connections.
+ ///
+ /// The returned Connection can be used to interact with the peer. It also
+ /// represents a peripheral's ownership over the connection: the client can
+ /// drop the object to request a disconnection. Similarly, the Connection
+ /// error handler is called by the system to indicate that the connection to
+ /// the peer has been lost. While connections are exclusive among peripherals,
+ /// they may be shared with centrals.
+ ///
+ /// If advertising is not stopped, this callback may be called with multiple
+ /// connections over the lifetime of an advertisement. It is OK to destroy
+ /// the `AdvertisedPeripheral::Ptr` object from within `callback` in order to
+ /// stop advertising.
virtual void SetConnectionCallback(
Function<void(Connection::Ptr)>&& callback) = 0;
private:
- // Stop advertising. This method is called by the ~AdvertisedPeripheral::Ptr()
- // when it goes out of scope, the API client should never call this method.
+ /// Stop advertising. This method is called by the
+ /// ~AdvertisedPeripheral::Ptr() when it goes out of scope, the API client
+ /// should never call this method.
virtual void StopAdvertising() = 0;
public:
- // Movable AdvertisedPeripheral smart pointer. The peripheral will continue
- // advertising until the returned AdvertisedPeripheral::Ptr is destroyed.
+ /// Movable AdvertisedPeripheral smart pointer. The peripheral will continue
+ /// advertising until the returned AdvertisedPeripheral::Ptr is destroyed.
using Ptr = internal::RaiiPtr<AdvertisedPeripheral,
&AdvertisedPeripheral::StopAdvertising>;
};
-// Represents the LE Peripheral role, which advertises and is connected to.
+/// Represents the LE Peripheral role, which advertises and is connected to.
class Peripheral {
public:
- // The range of the time interval between advertisements. Shorter intervals
- // result in faster discovery at the cost of higher power consumption. The
- // exact interval used is determined by the Bluetooth controller.
- // Time = N * 0.625ms.
- // Time range: 0x0020 (20ms) - 0x4000 (10.24s)
+ /// The range of the time interval between advertisements. Shorter intervals
+ /// result in faster discovery at the cost of higher power consumption. The
+ /// exact interval used is determined by the Bluetooth controller.
+ /// - Time = N * 0.625ms.
+ /// - Time range: 0x0020 (20ms) - 0x4000 (10.24s)
struct AdvertisingIntervalRange {
- uint16_t min = 0x0800; // 1.28s
- uint16_t max = 0x0800; // 1.28s
+ /// Default: 1.28s
+ uint16_t min = 0x0800;
+ /// Default: 1.28s
+ uint16_t max = 0x0800;
};
- // Represents the parameters for configuring advertisements.
+ /// Represents the parameters for configuring advertisements.
struct AdvertisingParameters {
- // The fields that will be encoded in the data section of advertising
- // packets.
+ /// The fields that will be encoded in the data section of advertising
+ /// packets.
AdvertisingData data;
- // The fields that are to be sent in a scan response packet. Clients may use
- // this to send additional data that does not fit inside an advertising
- // packet on platforms that do not support the advertising data length
- // extensions.
- //
- // If present advertisements will be configured to be scannable.
+ /// The fields that are to be sent in a scan response packet. Clients may
+ /// use this to send additional data that does not fit inside an advertising
+ /// packet on platforms that do not support the advertising data length
+ /// extensions.
+ ///
+ /// If present advertisements will be configured to be scannable.
std::optional<AdvertisingData> scan_response;
- // See `AdvertisingIntervalRange` documentation.
+ /// See `AdvertisingIntervalRange` documentation.
AdvertisingIntervalRange interval_range;
- // If present, the controller will broadcast connectable advertisements
- // which allow peers to initiate connections to the Peripheral. The fields
- // of `ConnectionOptions` will configure any connections set up from
- // advertising.
+ /// If present, the controller will broadcast connectable advertisements
+ /// which allow peers to initiate connections to the Peripheral. The fields
+ /// of `ConnectionOptions` will configure any connections set up from
+ /// advertising.
std::optional<ConnectionOptions> connection_options;
};
- // Errors returned by `Advertise`.
+ /// Errors returned by `Advertise`.
enum class AdvertiseError {
- // The operation or parameters requested are not supported on the current
- // hardware.
+ /// The operation or parameters requested are not supported on the current
+ /// hardware.
kNotSupported = 1,
- // The provided advertising data exceeds the maximum allowed length when
- // encoded.
+ /// The provided advertising data exceeds the maximum allowed length when
+ /// encoded.
kAdvertisingDataTooLong = 2,
- // The provided scan response data exceeds the maximum allowed length when
- // encoded.
+ /// The provided scan response data exceeds the maximum allowed length when
+ /// encoded.
kScanResponseDataTooLong = 3,
- // The requested parameters are invalid.
+ /// The requested parameters are invalid.
kInvalidParameters = 4,
- // This may be called if the maximum number of simultaneous advertisements
- // has already been reached.
+ /// This may be called if the maximum number of simultaneous advertisements
+ /// has already been reached.
kNotEnoughAdvertisingSlots = 5,
- // Advertising could not be initiated due to a hardware or system error.
+ /// Advertising could not be initiated due to a hardware or system error.
kFailed = 6,
};
+ /// Callback for `Advertise()` method.
using AdvertiseCallback =
Function<void(Result<AdvertiseError, AdvertisedPeripheral::Ptr>)>;
virtual ~Peripheral() = default;
- // Start advertising continuously as a LE peripheral. If advertising cannot
- // be initiated then `result_callback` will be called with an error. Once
- // started, advertising can be stopped by destroying the returned
- // `AdvertisedPeripheral::Ptr`.
- //
- // If the system supports multiple advertising, this may be called as many
- // times as there are advertising slots. To reconfigure an advertisement,
- // first close the original advertisement and then initiate a new
- // advertisement.
- //
- // Parameters:
- // `parameters` - Parameters used while configuring the advertising
- // instance.
- // `result_callback` - Called once advertising has started or failed. On
- // success, called with an `AdvertisedPeripheral` that models the lifetime
- // of the advertisement. Destroying it will stop advertising.
+ /// Start advertising continuously as a LE peripheral. If advertising cannot
+ /// be initiated then `result_callback` will be called with an error. Once
+ /// started, advertising can be stopped by destroying the returned
+ /// `AdvertisedPeripheral::Ptr`.
+ ///
+ /// If the system supports multiple advertising, this may be called as many
+ /// times as there are advertising slots. To reconfigure an advertisement,
+ /// first close the original advertisement and then initiate a new
+ /// advertisement.
+ ///
+ /// @param parameters Parameters used while configuring the advertising
+ /// instance.
+ /// @param result_callback Called once advertising has started or failed. On
+ /// success, called with an `AdvertisedPeripheral` that models the lifetime of
+ /// the advertisement. Destroying it will stop advertising.
virtual void Advertise(const AdvertisingParameters& parameters,
AdvertiseCallback&& result_callback) = 0;
};
diff --git a/pw_bluetooth/public/pw_bluetooth/types.h b/pw_bluetooth/public/pw_bluetooth/types.h
index 3639bbf..d969b62 100644
--- a/pw_bluetooth/public/pw_bluetooth/types.h
+++ b/pw_bluetooth/public/pw_bluetooth/types.h
@@ -98,4 +98,4 @@
kSportsActivityLocationAndNavPod = 5188,
};
-} // namespace pw::bluetooth
\ No newline at end of file
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/vendor.emb b/pw_bluetooth/public/pw_bluetooth/vendor.emb
index 2c5681b..9ee7734 100644
--- a/pw_bluetooth/public/pw_bluetooth/vendor.emb
+++ b/pw_bluetooth/public/pw_bluetooth/vendor.emb
@@ -27,6 +27,17 @@
# ======================= Android HCI extensions ========================
# Documentation: https://source.android.com/devices/bluetooth/hci_requirements
+enum Capability:
+ [maximum_bits: 8]
+ NOT_CAPABLE = 0x00
+ CAPABLE = 0x01
+
+bits AudioCodecSupportMask:
+ 0 [+1] Flag sbc
+ 1 [+1] Flag aac
+ 2 [+1] Flag aptx
+ 3 [+1] Flag aptx_hd
+ 4 [+1] Flag ldac
# ============ Commands ============
@@ -39,6 +50,10 @@
$next [+1] hci.GenericEnableParam enable
$next [+1] UInt advertising_handle
+struct LEGetVendorCapabilitiesCommand:
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
# ============ Events ============
@@ -56,3 +71,42 @@
$next [+2] UInt connection_handle
-- Handle used to identify the connection that caused the state change (i.e.
-- advertising instance to be disabled). Value will be 0xFFFF if invalid.
+
+struct LEGetVendorCapabilitiesCommandCompleteEvent:
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+1] UInt max_advt_instances
+ -- Number of advertisement instances supported
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+1] Capability offloaded_resolution_of_private_address
+ -- BT chip capability of RPA
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+2] UInt total_scan_results_storage
+ -- Storage for scan results in bytes
+ $next [+1] UInt max_irk_list_sz
+ -- Number of IRK entries supported in the firmware
+ $next [+1] Capability filtering_support
+ -- Support for filtering in the controller
+ $next [+1] UInt max_filter
+ -- Number of filters supported
+ $next [+1] Capability activity_energy_info_support
+ -- Supports reporting of activity and energy information
+ $next [+2] bits version_supported:
+ -- Specifies the version of the Google feature spec supported
+ 0 [+8] UInt major_number
+ $next [+8] UInt minor_number
+ $next [+2] UInt total_num_of_advt_tracked
+ -- Total number of advertisers tracked for OnLost/OnFound purposes
+ $next [+1] Capability extended_scan_support
+ -- Supports extended scan window and interval
+ $next [+1] Capability debug_logging_supported
+ -- Supports logging of binary debug information from controller
+ $next [+1] Capability le_address_generation_offloading_support
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask a2dp_source_offload_capability_mask
+ $next [+1] Capability bluetooth_quality_report_support
+ -- Supports reporting of Bluetooth Quality events
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask dynamic_audio_buffer_support
diff --git a/pw_bluetooth_hci/BUILD.gn b/pw_bluetooth_hci/BUILD.gn
index e4082fe..0f51071 100644
--- a/pw_bluetooth_hci/BUILD.gn
+++ b/pw_bluetooth_hci/BUILD.gn
@@ -63,10 +63,14 @@
tests = [
":packet_test",
":uart_transport_test",
- ":uart_transport_fuzzer",
+ ":uart_transport_fuzzer_test",
]
}
+group("fuzzers") {
+ deps = [ ":uart_transport_fuzzer" ]
+}
+
pw_test("packet_test") {
sources = [ "packet_test.cc" ]
deps = [
diff --git a/pw_bluetooth_hci/uart_transport_fuzzer.cc b/pw_bluetooth_hci/uart_transport_fuzzer.cc
index a2f1249..4324c4f 100644
--- a/pw_bluetooth_hci/uart_transport_fuzzer.cc
+++ b/pw_bluetooth_hci/uart_transport_fuzzer.cc
@@ -36,16 +36,18 @@
const CommandPacket& command_packet = packet.command_packet();
const uint16_t opcode = command_packet.opcode();
- stream.Write(as_bytes(span<const uint16_t>(&opcode, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&opcode, 1))).IgnoreError();
const uint16_t opcode_command_field =
command_packet.opcode_command_field();
- stream.Write(as_bytes(span<const uint16_t>(&opcode_command_field, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&opcode_command_field, 1)))
+ .IgnoreError();
const uint8_t opcode_group_field = command_packet.opcode_group_field();
- stream.Write(as_bytes(span<const uint8_t>(&opcode_group_field, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&opcode_group_field, 1)))
+ .IgnoreError();
- stream.Write(command_packet.parameters());
+ stream.Write(command_packet.parameters()).IgnoreError();
return;
}
@@ -54,19 +56,21 @@
const uint16_t handle_and_fragmentation_bits =
async_data_packet.handle_and_fragmentation_bits();
- stream.Write(
- as_bytes(span<const uint16_t>(&handle_and_fragmentation_bits, 1)));
+ stream
+ .Write(as_bytes(
+ span<const uint16_t>(&handle_and_fragmentation_bits, 1)))
+ .IgnoreError();
const uint16_t handle = async_data_packet.handle();
- stream.Write(as_bytes(span<const uint16_t>(&handle, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle, 1))).IgnoreError();
const uint8_t pb_flag = async_data_packet.pb_flag();
- stream.Write(as_bytes(span<const uint8_t>(&pb_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&pb_flag, 1))).IgnoreError();
const uint8_t bc_flag = async_data_packet.bc_flag();
- stream.Write(as_bytes(span<const uint8_t>(&bc_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&bc_flag, 1))).IgnoreError();
- stream.Write(async_data_packet.data());
+ stream.Write(async_data_packet.data()).IgnoreError();
return;
}
@@ -75,17 +79,18 @@
const uint16_t handle_and_status_bits =
sync_data_packet.handle_and_status_bits();
- stream.Write(
- as_bytes(span<const uint16_t>(&handle_and_status_bits, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle_and_status_bits, 1)))
+ .IgnoreError();
const uint16_t handle = sync_data_packet.handle();
- stream.Write(as_bytes(span<const uint16_t>(&handle, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle, 1))).IgnoreError();
const uint8_t packet_status_flag =
sync_data_packet.packet_status_flag();
- stream.Write(as_bytes(span<const uint8_t>(&packet_status_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&packet_status_flag, 1)))
+ .IgnoreError();
- stream.Write(sync_data_packet.data());
+ stream.Write(sync_data_packet.data()).IgnoreError();
return;
}
@@ -93,9 +98,10 @@
const EventPacket& event_packet = packet.event_packet();
const uint8_t event_code = event_packet.event_code();
- stream.Write(as_bytes(span<const uint8_t>(&event_code, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&event_code, 1)))
+ .IgnoreError();
- stream.Write(event_packet.parameters());
+ stream.Write(event_packet.parameters()).IgnoreError();
return;
}
diff --git a/pw_bluetooth_profiles/device_info_service_test.cc b/pw_bluetooth_profiles/device_info_service_test.cc
index 153027e..5782ef4 100644
--- a/pw_bluetooth_profiles/device_info_service_test.cc
+++ b/pw_bluetooth_profiles/device_info_service_test.cc
@@ -53,8 +53,9 @@
: fake_server_(fake_server) {}
// LocalService overrides:
- void NotifyValue(const ValueChangedParameters& /* parameters */,
- Closure&& /* completion_callback */) override {
+ void NotifyValue(
+ const ValueChangedParameters& /* parameters */,
+ ValueChangedCallback&& /* completion_callback */) override {
FAIL(); // Unimplemented
}
void IndicateValue(
diff --git a/pw_build/BUILD.gn b/pw_build/BUILD.gn
index 51e0fcc..25f5343 100644
--- a/pw_build/BUILD.gn
+++ b/pw_build/BUILD.gn
@@ -88,6 +88,18 @@
}
}
+config("rust_edition_2015") {
+ rustflags = [ "--edition=2015" ]
+}
+
+config("rust_edition_2018") {
+ rustflags = [ "--edition=2018" ]
+}
+
+config("rust_edition_2021") {
+ rustflags = [ "--edition=2021" ]
+}
+
config("strict_warnings") {
cflags = [
"-Wall",
diff --git a/pw_build/bazel_internal/pigweed_internal.bzl b/pw_build/bazel_internal/pigweed_internal.bzl
index ab69f46..ed452ff 100644
--- a/pw_build/bazel_internal/pigweed_internal.bzl
+++ b/pw_build/bazel_internal/pigweed_internal.bzl
@@ -15,8 +15,8 @@
# the License.
""" An internal set of tools for creating embedded CC targets. """
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain")
load("@rules_cc//cc:action_names.bzl", "C_COMPILE_ACTION_NAME")
-load("@rules_cc//cc:toolchain_utils.bzl", "find_cpp_toolchain")
DEBUGGING = [
"-g",
@@ -139,6 +139,6 @@
),
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
},
- toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
+ toolchains = use_cpp_toolchain(),
fragments = ["cpp"],
)
diff --git a/pw_build/cc_blob_library_test.cc b/pw_build/cc_blob_library_test.cc
index 91db07a..faa4f6a 100644
--- a/pw_build/cc_blob_library_test.cc
+++ b/pw_build/cc_blob_library_test.cc
@@ -21,10 +21,7 @@
namespace {
static_assert(test::ns::kFirstBlob0123.size() == 4);
-static_assert(test::ns::kFirstBlob0123.data() != nullptr);
-
static_assert(test::ns::kSecondBlob0123.size() == 4);
-static_assert(test::ns::kSecondBlob0123.data() != nullptr);
TEST(CcBlobLibraryTest, FirstBlobContentsMatch) {
EXPECT_EQ(test::ns::kFirstBlob0123[0], std::byte{0});
diff --git a/pw_build/cc_executable.gni b/pw_build/cc_executable.gni
index 230b127..fe10da4 100644
--- a/pw_build/cc_executable.gni
+++ b/pw_build/cc_executable.gni
@@ -24,21 +24,6 @@
# templates may need to create pw_source_set targets internally, and can't
# import target_types.gni because it creates a circular import path.
-declare_args() {
- # The name of the GN target type used to build Pigweed executables.
- #
- # If this is a custom template, the .gni file containing the template must
- # be imported at the top of the target configuration file to make it globally
- # available.
- pw_build_EXECUTABLE_TARGET_TYPE = "executable"
-
- # The path to the .gni file that defines pw_build_EXECUTABLE_TARGET_TYPE.
- #
- # If pw_build_EXECUTABLE_TARGET_TYPE is not the default of `executable`, this
- # .gni file is imported to provide the template definition.
- pw_build_EXECUTABLE_TARGET_TYPE_FILE = ""
-}
-
# This template wraps a configurable target type specified by the current
# toolchain to be used for all pw_executable targets. This allows projects to
# stamp out unique build logic for each pw_executable target, such as generating
diff --git a/pw_build/cc_library.gni b/pw_build/cc_library.gni
index 81465c5..02fe625 100644
--- a/pw_build/cc_library.gni
+++ b/pw_build/cc_library.gni
@@ -23,16 +23,6 @@
# templates may need to create pw_source_set targets internally, and can't
# import target_types.gni because it creates a circular import path.
-declare_args() {
- # Additional build targets to add as dependencies for pw_executable,
- # pw_static_library, and pw_shared_library targets. The
- # $dir_pw_build:link_deps target pulls in these libraries.
- #
- # pw_build_LINK_DEPS can be used to break circular dependencies for low-level
- # libraries such as pw_assert.
- pw_build_LINK_DEPS = []
-}
-
# These templates are wrappers for GN's built-in source_set, static_library,
# and shared_library targets.
#
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index db1964f..a1df9d9 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -358,6 +358,9 @@
* ``working_directory``: Optional file path. When provided the current working
directory will be set to this location before the Python module or script is
run.
+* ``command_launcher``: Optional string. Arguments to prepend to the Python
+ command, e.g. ``'/usr/bin/fakeroot --'`` will run the Python script within a
+ fakeroot environment.
* ``venv``: Optional gn target of the pw_python_venv that should be used to run
this action.
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index 2653247..77cec91 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -28,6 +28,7 @@
# Declare a build arg for each module.
declare_args() {
dir_docker = get_path_info("../docker", "abspath")
+ dir_pw_alignment = get_path_info("../pw_alignment", "abspath")
dir_pw_allocator = get_path_info("../pw_allocator", "abspath")
dir_pw_analog = get_path_info("../pw_analog", "abspath")
dir_pw_android_toolchain = get_path_info("../pw_android_toolchain", "abspath")
@@ -152,6 +153,7 @@
dir_pw_thread_freertos = get_path_info("../pw_thread_freertos", "abspath")
dir_pw_thread_stl = get_path_info("../pw_thread_stl", "abspath")
dir_pw_thread_threadx = get_path_info("../pw_thread_threadx", "abspath")
+ dir_pw_thread_zephyr = get_path_info("../pw_thread_zephyr", "abspath")
dir_pw_tls_client = get_path_info("../pw_tls_client", "abspath")
dir_pw_tls_client_boringssl =
get_path_info("../pw_tls_client_boringssl", "abspath")
@@ -176,6 +178,7 @@
# A list with paths to all Pigweed module. DO NOT SET THIS BUILD ARGUMENT!
pw_modules = [
dir_docker,
+ dir_pw_alignment,
dir_pw_allocator,
dir_pw_analog,
dir_pw_android_toolchain,
@@ -292,6 +295,7 @@
dir_pw_thread_freertos,
dir_pw_thread_stl,
dir_pw_thread_threadx,
+ dir_pw_thread_zephyr,
dir_pw_tls_client,
dir_pw_tls_client_boringssl,
dir_pw_tls_client_mbedtls,
@@ -311,6 +315,7 @@
# A list with all Pigweed module test groups. DO NOT SET THIS BUILD ARGUMENT!
pw_module_tests = [
"$dir_docker:tests",
+ "$dir_pw_alignment:tests",
"$dir_pw_allocator:tests",
"$dir_pw_analog:tests",
"$dir_pw_android_toolchain:tests",
@@ -427,6 +432,7 @@
"$dir_pw_thread_freertos:tests",
"$dir_pw_thread_stl:tests",
"$dir_pw_thread_threadx:tests",
+ "$dir_pw_thread_zephyr:tests",
"$dir_pw_tls_client:tests",
"$dir_pw_tls_client_boringssl:tests",
"$dir_pw_tls_client_mbedtls:tests",
@@ -446,6 +452,7 @@
# A list with all Pigweed modules docs groups. DO NOT SET THIS BUILD ARGUMENT!
pw_module_docs = [
"$dir_docker:docs",
+ "$dir_pw_alignment:docs",
"$dir_pw_allocator:docs",
"$dir_pw_analog:docs",
"$dir_pw_android_toolchain:docs",
@@ -562,6 +569,7 @@
"$dir_pw_thread_freertos:docs",
"$dir_pw_thread_stl:docs",
"$dir_pw_thread_threadx:docs",
+ "$dir_pw_thread_zephyr:docs",
"$dir_pw_tls_client:docs",
"$dir_pw_tls_client_boringssl:docs",
"$dir_pw_tls_client_mbedtls:docs",
diff --git a/pw_build/gn_internal/build_target.gni b/pw_build/gn_internal/build_target.gni
index dbdd149..149c87e 100644
--- a/pw_build/gn_internal/build_target.gni
+++ b/pw_build/gn_internal/build_target.gni
@@ -14,6 +14,29 @@
import("//build_overrides/pigweed.gni")
+declare_args() {
+ # Additional build targets to add as dependencies for pw_executable,
+ # pw_static_library, and pw_shared_library targets. The
+ # $dir_pw_build:link_deps target pulls in these libraries.
+ #
+ # pw_build_LINK_DEPS can be used to break circular dependencies for low-level
+ # libraries such as pw_assert.
+ pw_build_LINK_DEPS = []
+
+ # The name of the GN target type used to build Pigweed executables.
+ #
+ # If this is a custom template, the .gni file containing the template must
+ # be imported at the top of the target configuration file to make it globally
+ # available.
+ pw_build_EXECUTABLE_TARGET_TYPE = "executable"
+
+ # The path to the .gni file that defines pw_build_EXECUTABLE_TARGET_TYPE.
+ #
+ # If pw_build_EXECUTABLE_TARGET_TYPE is not the default of `executable`, this
+ # .gni file is imported to provide the template definition.
+ pw_build_EXECUTABLE_TARGET_TYPE_FILE = ""
+}
+
# This template is the underlying implementation that defines what makes
# pw_source_set, pw_executable, pw_shared_library, and pw_static_library unique.
# For more information, see the documentation at
@@ -61,6 +84,7 @@
_builtin_target_types = [
"executable",
+ "rust_library",
"shared_library",
"source_set",
"static_library",
diff --git a/pw_build/platforms/BUILD.bazel b/pw_build/platforms/BUILD.bazel
index bd87ad1..5549577 100644
--- a/pw_build/platforms/BUILD.bazel
+++ b/pw_build/platforms/BUILD.bazel
@@ -71,7 +71,7 @@
platform(
name = "cortex_m4_fpu",
constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m4"],
+ parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
)
platform(
@@ -117,3 +117,28 @@
constraint_values = ["//pw_build/constraints/board:microbit"],
parents = [":nrf52833"],
)
+
+# --- Test platforms ---
+
+# This is a platform for compilation testing of freertos backends. This is not
+# a complete specification of any real target platform.
+platform(
+ name = "testonly_freertos",
+ constraint_values = [
+ # Use FreeRTOS backends.
+ "//pw_build/constraints/rtos:freertos",
+ # Use the FreeRTOS config file for stm32f429i_disc1_stm32cube.
+ "//targets/stm32f429i_disc1_stm32cube:freertos_config_cv",
+ # Use the ARM_CM4F port of FreeRTOS.
+ "@freertos//:port_ARM_CM4F",
+ # Specify this chipset to use the baremetal pw_sys_io backend (because
+ # the default pw_sys_io_stdio backend is not compatible with FreeRTOS).
+ "//pw_build/constraints/chipset:stm32f429",
+ # os:none means, we're not building for any host platform (Windows,
+ # Linux, or Mac). The pw_sys_io_baremetal_stm32f429 backend is only
+ # compatible with os:none.
+ "@platforms//os:none",
+ ],
+ # Inherit from cortex_m4_fpu to use the appropriate Arm toolchain.
+ parents = [":cortex_m4_fpu"],
+)
diff --git a/pw_build/py/project_builder_prefs_test.py b/pw_build/py/project_builder_prefs_test.py
index 9ede88f..f128bee 100644
--- a/pw_build/py/project_builder_prefs_test.py
+++ b/pw_build/py/project_builder_prefs_test.py
@@ -46,7 +46,10 @@
def test_load_no_existing_files(self) -> None:
# Create a prefs instance with no loaded config.
prefs = ProjectBuilderPrefs(
- load_argparse_arguments=add_project_builder_arguments
+ load_argparse_arguments=add_project_builder_arguments,
+ project_file=False,
+ project_user_file=False,
+ user_file=False,
)
# Construct an expected result config.
expected_config: Dict[Any, Any] = {}
@@ -68,7 +71,10 @@
# Create a prefs instance with the test config file.
prefs = ProjectBuilderPrefs(
- load_argparse_arguments=add_project_builder_arguments
+ load_argparse_arguments=add_project_builder_arguments,
+ project_file=False,
+ project_user_file=False,
+ user_file=False,
)
# Construct an expected result config.
diff --git a/pw_build/py/pw_build/build_recipe.py b/pw_build/py/pw_build/build_recipe.py
index 0a777f8..ebd4c1b 100644
--- a/pw_build/py/pw_build/build_recipe.py
+++ b/pw_build/py/pw_build/build_recipe.py
@@ -315,8 +315,12 @@
_LOG.error(" ╚════════════════════════════════════")
_LOG.error('')
- def status_slug(self) -> OneStyleAndTextTuple:
+ def status_slug(self, restarting: bool = False) -> OneStyleAndTextTuple:
status = ('', '')
+ if not self.recipe.enabled:
+ return ('fg:ansidarkgray', 'Disabled')
+
+ waiting = False
if self.done:
if self.passed():
status = ('fg:ansigreen', 'OK ')
@@ -325,8 +329,12 @@
elif self.started:
status = ('fg:ansiyellow', 'Building')
else:
- status = ('fg:ansigray', 'Waiting ')
+ waiting = True
+ status = ('default', 'Waiting ')
+ # Only show Aborting if the process is building (or has failures).
+ if restarting and not waiting and not self.passed():
+ status = ('fg:ansiyellow', 'Aborting')
return status
def current_step_formatted(self) -> StyleAndTextTuples:
@@ -405,6 +413,7 @@
build_dir: Path
steps: List[BuildCommand] = field(default_factory=list)
title: Optional[str] = None
+ enabled: bool = True
def __hash__(self):
return hash((self.build_dir, self.title, len(self.steps)))
@@ -422,6 +431,9 @@
self._status: BuildRecipeStatus = BuildRecipeStatus(self)
self.project_builder: Optional['ProjectBuilder'] = None
+ def toggle_enabled(self) -> None:
+ self.enabled = not self.enabled
+
def set_project_builder(self, project_builder) -> None:
self.project_builder = project_builder
diff --git a/pw_build/py/pw_build/create_python_tree.py b/pw_build/py/pw_build/create_python_tree.py
index 9771ff3..981f12d 100644
--- a/pw_build/py/pw_build/create_python_tree.py
+++ b/pw_build/py/pw_build/create_python_tree.py
@@ -396,7 +396,6 @@
if args.setupcfg_common_file or (
args.setupcfg_override_name and args.setupcfg_override_version
):
-
config = load_common_config(
common_config=args.setupcfg_common_file,
package_name_override=args.setupcfg_override_name,
diff --git a/pw_build/py/pw_build/generate_modules_lists.py b/pw_build/py/pw_build/generate_modules_lists.py
index fbcdbe3..ae9eb95 100644
--- a/pw_build/py/pw_build/generate_modules_lists.py
+++ b/pw_build/py/pw_build/generate_modules_lists.py
@@ -23,6 +23,7 @@
import argparse
import difflib
+import enum
import io
import os
from pathlib import Path
@@ -181,6 +182,12 @@
)
+class Mode(enum.Enum):
+ WARN = 0 # Warn if anything is out of date
+ CHECK = 1 # Fail if anything is out of date
+ UPDATE = 2 # Update the generated modules lists
+
+
def _parse_args() -> dict:
parser = argparse.ArgumentParser(
description=__doc__,
@@ -190,11 +197,13 @@
parser.add_argument('modules_list', type=Path, help='Input modules list')
parser.add_argument('modules_gni_file', type=Path, help='Output .gni file')
parser.add_argument(
- '--warn-only',
- type=Path,
- help='Only check PIGWEED_MODULES; takes a path to a stamp file to use',
+ '--mode', type=Mode.__getitem__, choices=Mode, required=True
)
-
+ parser.add_argument(
+ '--stamp',
+ type=Path,
+ help='Stamp file for operations that should only run once (warn)',
+ )
return vars(parser.parse_args())
@@ -202,7 +211,8 @@
root: Path,
modules_list: Path,
modules_gni_file: Path,
- warn_only: Optional[Path],
+ mode: Mode,
+ stamp: Optional[Path] = None,
) -> int:
"""Manages the list of Pigweed modules."""
prefix = Path(os.path.relpath(root, modules_gni_file.parent))
@@ -215,7 +225,7 @@
modules.sort() # Sort in case the modules list in case it wasn't sorted.
# Check if the contents of the .gni file are out of date.
- if warn_only:
+ if mode in (Mode.WARN, Mode.CHECK):
text = io.StringIO()
for line in _generate_modules_gni(prefix, modules):
print(line, file=text)
@@ -252,7 +262,7 @@
errors.append('\n'.join(diff))
errors.append('\n')
- elif not warnings: # Update the modules .gni file.
+ elif mode is Mode.UPDATE: # Update the modules .gni file
with modules_gni_file.open('w', encoding='utf-8') as file:
for line in _generate_modules_gni(prefix, modules):
print(line, file=file)
@@ -270,14 +280,19 @@
# Delete the stamp so this always reruns. Deleting is necessary since
# some of the checks do not depend on input files.
- if warn_only and warn_only.exists():
- warn_only.unlink()
+ if stamp and stamp.exists():
+ stamp.unlink()
- # Warnings are non-fatal if warn_only is True.
- return 1 if errors or not warn_only else 0
+ if mode is Mode.WARN:
+ return 0
- if warn_only:
- warn_only.touch()
+ if mode is Mode.CHECK:
+ return 1
+
+ return 1 if errors else 0 # Allow warnings but not errors when updating
+
+ if stamp:
+ stamp.touch()
return 0
diff --git a/pw_build/py/pw_build/project_builder.py b/pw_build/py/pw_build/project_builder.py
index b8b0e2f..cb8ab59 100644
--- a/pw_build/py/pw_build/project_builder.py
+++ b/pw_build/py/pw_build/project_builder.py
@@ -730,7 +730,10 @@
return
# If restarting or interrupted.
if BUILDER_CONTEXT.interrupted():
- _LOG.info(self.color.yellow('Exited due to keyboard interrupt.'))
+ if BUILDER_CONTEXT.ctrl_c_pressed:
+ _LOG.info(
+ self.color.yellow('Exited due to keyboard interrupt.')
+ )
return
# If any build is still pending.
if any(recipe.status.pending() for recipe in self):
@@ -770,7 +773,7 @@
logger.info(' ╔════════════════════════════════════')
logger.info(' ║')
- for (slug, cmd) in zip(build_status, build_descriptions):
+ for slug, cmd in zip(build_status, build_descriptions):
logger.info(' ║ %s %s', slug, cmd)
logger.info(' ║')
@@ -788,6 +791,11 @@
def run_recipe(
index: int, project_builder: ProjectBuilder, cfg: BuildRecipe, env
) -> bool:
+ if BUILDER_CONTEXT.interrupted():
+ return False
+ if not cfg.enabled:
+ return False
+
num_builds = len(project_builder)
index_message = f'[{index}/{num_builds}]'
diff --git a/pw_build/py/pw_build/project_builder_context.py b/pw_build/py/pw_build/project_builder_context.py
index 1a2adc1..f459423 100644
--- a/pw_build/py/pw_build/project_builder_context.py
+++ b/pw_build/py/pw_build/project_builder_context.py
@@ -92,7 +92,9 @@
continue
build_status: StyleAndTextTuples = []
- build_status.append(cfg.status.status_slug())
+ build_status.append(
+ cfg.status.status_slug(restarting=self.ctx.restart_flag)
+ )
build_status.append(('', ' '))
build_status.extend(cfg.status.current_step_formatted())
@@ -203,7 +205,7 @@
bottom_toolbar=self.bottom_toolbar,
cancel_callback=self.ctrl_c_interrupt,
)
- self.progress_bar.__enter__()
+ self.progress_bar.__enter__() # pylint: disable=unnecessary-dunder-call
self.create_title_bar_container()
self.progress_bar.app.layout.container.children[ # type: ignore
@@ -214,7 +216,7 @@
def exit_progress(self) -> None:
if not self.progress_bar:
return
- self.progress_bar.__exit__()
+ self.progress_bar.__exit__() # pylint: disable=unnecessary-dunder-call
def clear_progress_scrollback(self) -> None:
if not self.progress_bar:
@@ -230,17 +232,15 @@
self.progress_bar.invalidate()
def get_title_style(self) -> str:
+ if self.restart_flag:
+ return 'fg:ansiyellow'
+
+ # Assume passing
style = 'fg:ansigreen'
if self.current_state == ProjectBuilderState.BUILDING:
style = 'fg:ansiyellow'
- if (
- self.current_state != ProjectBuilderState.IDLE
- and self.interrupted()
- ):
- return 'fg:ansiyellow'
-
for cfg in self.recipes:
if cfg.status.failed():
style = 'fg:ansired'
@@ -267,10 +267,7 @@
if cfg.status.done:
done_count += 1
- if (
- self.current_state != ProjectBuilderState.IDLE
- and self.interrupted()
- ):
+ if self.restart_flag:
title = 'INTERRUPT'
elif fail_count > 0:
title = f'FAILED ({fail_count})'
@@ -438,13 +435,21 @@
) -> None:
"""Exit function called when the user presses ctrl-c."""
+ # Note: The correct way to exit Python is via sys.exit() however this
+ # takes a number of seconds when running pw_watch with multiple parallel
+ # builds. Instead, this function calls os._exit() to shutdown
+ # immediately. This is similar to `pw_watch.watch._exit`:
+ # https://cs.opensource.google/pigweed/pigweed/+/main:pw_watch/py/pw_watch/watch.py?q=_exit.code
+
if not self.progress_bar:
self.restore_logging_and_shutdown(log_after_shutdown)
+ logging.shutdown()
os._exit(exit_code) # pylint: disable=protected-access
# Shut everything down after the progress_bar exits.
def _really_exit(future: asyncio.Future) -> NoReturn:
self.restore_logging_and_shutdown(log_after_shutdown)
+ logging.shutdown()
os._exit(future.result()) # pylint: disable=protected-access
if self.progress_bar.app.future:
diff --git a/pw_build/py/pw_build/project_builder_prefs.py b/pw_build/py/pw_build/project_builder_prefs.py
index a6c3515..a13534d 100644
--- a/pw_build/py/pw_build/project_builder_prefs.py
+++ b/pw_build/py/pw_build/project_builder_prefs.py
@@ -16,9 +16,10 @@
import argparse
import copy
import shlex
+from pathlib import Path
from typing import Any, Callable, Dict, List, Tuple, Union
-from pw_cli.toml_config_loader_mixin import TomlConfigLoaderMixin
+from pw_cli.toml_config_loader_mixin import YamlConfigLoaderMixin
_DEFAULT_CONFIG: Dict[Any, Any] = {
# Config settings not available as a command line options go here.
@@ -34,6 +35,10 @@
},
}
+_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_build.yaml')
+_DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_build.user.yaml')
+_DEFAULT_USER_FILE = Path('$HOME/.pw_build.yaml')
+
def load_defaults_from_argparse(
add_parser_arguments: Callable[
@@ -51,7 +56,7 @@
return defaults_flags
-class ProjectBuilderPrefs(TomlConfigLoaderMixin):
+class ProjectBuilderPrefs(YamlConfigLoaderMixin):
"""Pigweed Watch preferences storage class."""
def __init__(
@@ -59,15 +64,17 @@
load_argparse_arguments: Callable[
[argparse.ArgumentParser], argparse.ArgumentParser
],
+ project_file: Union[Path, bool] = _DEFAULT_PROJECT_FILE,
+ project_user_file: Union[Path, bool] = _DEFAULT_PROJECT_USER_FILE,
+ user_file: Union[Path, bool] = _DEFAULT_USER_FILE,
) -> None:
self.load_argparse_arguments = load_argparse_arguments
self.config_init(
config_section_title='pw_build',
- # Don't load any config files
- project_file=False,
- project_user_file=False,
- user_file=False,
+ project_file=project_file,
+ project_user_file=project_user_file,
+ user_file=user_file,
default_config=_DEFAULT_CONFIG,
environment_var='PW_BUILD_CONFIG_FILE',
)
diff --git a/pw_build/py/pw_build/python_runner.py b/pw_build/py/pw_build/python_runner.py
index efaa778..2bd8ac5 100755
--- a/pw_build/py/pw_build/python_runner.py
+++ b/pw_build/py/pw_build/python_runner.py
@@ -110,6 +110,9 @@
help='Path to a virtualenv json config to use for this action.',
)
parser.add_argument(
+ '--command-launcher', help='Arguments to prepend to Python command'
+ )
+ parser.add_argument(
'original_cmd',
nargs=argparse.REMAINDER,
help='Python script with arguments to run',
@@ -200,6 +203,7 @@
capture_output: bool,
touch: Optional[Path],
working_directory: Optional[Path],
+ command_launcher: Optional[str],
lockfile: Optional[Path],
) -> int:
"""Script entry point."""
@@ -258,6 +262,9 @@
if python_interpreter is not None:
command = [str(root_build_dir / python_interpreter)]
+ if command_launcher is not None:
+ command = shlex.split(command_launcher) + command
+
if module is not None:
command += ['-m', module]
diff --git a/pw_build/py/setup.cfg b/pw_build/py/setup.cfg
index 56d0edd..37b8b73 100644
--- a/pw_build/py/setup.cfg
+++ b/pw_build/py/setup.cfg
@@ -22,15 +22,14 @@
packages = find:
zip_safe = False
install_requires =
- build==0.8.0
+ # NOTE: These requirements should stay as >= the lowest version we support.
+ build>=0.8.0
wheel
coverage
setuptools
types-setuptools
- # NOTE: mypy needs to stay in sync with mypy-protobuf
- # Currently using mypy 0.991 and mypy-protobuf 3.3.0 (see constraint.list)
mypy>=0.971
- pylint==2.9.3
+ pylint>=2.9.3
[options.entry_points]
console_scripts =
diff --git a/pw_build/python_action.gni b/pw_build/python_action.gni
index 11028af..0c76627 100644
--- a/pw_build/python_action.gni
+++ b/pw_build/python_action.gni
@@ -53,6 +53,10 @@
# working_directory Switch to the provided working directory before running
# the Python script or action.
#
+# command_launcher Arguments to prepend to the Python command, e.g.
+# '/usr/bin/fakeroot --' to run the Python script within a
+# fakeroot environment.
+#
# venv Optional gn target of the pw_python_venv that should be used
# to run this action.
#
@@ -145,6 +149,13 @@
]
}
+ if (defined(invoker.command_launcher)) {
+ _script_args += [
+ "--command-launcher",
+ invoker.command_launcher,
+ ]
+ }
+
if (defined(invoker._pw_action_type)) {
_action_type = invoker._pw_action_type
} else {
diff --git a/pw_build/rust_executable.gni b/pw_build/rust_executable.gni
new file mode 100644
index 0000000..cc564b6
--- /dev/null
+++ b/pw_build/rust_executable.gni
@@ -0,0 +1,56 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/gn_internal/build_target.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# This template wraps a configurable target type specified by the current
+# toolchain to be used for all pw_rust_executable targets. This allows projects
+# to stamp out unique build logic for each pw_rust_executable target.
+# This wrapper is analogous to pw_executable with additions to support rust
+# specific parameters such as rust edition and cargo config features.
+#
+# Default configs, default visibility, and link deps are applied to the target
+# before forwarding to the underlying type as specified by
+# pw_build_EXECUTABLE_TARGET_TYPE.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_executable.
+template("pw_rust_executable") {
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+
+ _edition = "2021"
+ if (defined(invoker.edition)) {
+ _edition = invoker.edition
+ }
+ assert(_edition == "2015" || _edition == "2018" || _edition == "2021",
+ "edition ${_edition} is not supported")
+
+ if (defined(invoker.configs)) {
+ configs = invoker.configs
+ } else {
+ configs = []
+ }
+ configs += [ "$dir_pw_build:rust_edition_${_edition}" ]
+
+ underlying_target_type = pw_build_EXECUTABLE_TARGET_TYPE
+ target_type_file = pw_build_EXECUTABLE_TARGET_TYPE_FILE
+ output_dir = "${target_out_dir}/bin"
+ add_global_link_deps = true
+ }
+}
diff --git a/pw_build/rust_library.gni b/pw_build/rust_library.gni
new file mode 100644
index 0000000..370f47d
--- /dev/null
+++ b/pw_build/rust_library.gni
@@ -0,0 +1,65 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/gn_internal/build_target.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# This template wraps a configurable target type specified by the current
+# toolchain to be used for all pw_rust_library targets. A wrapper for GN's
+# built-in rust_library target, it is analogous to pw_static_library with
+# additions to support rust specific parameters such as rust edition and cargo
+# config features.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_library
+template("pw_rust_library") {
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+
+ crate_name = target_name
+ if (defined(name)) {
+ crate_name = name
+ }
+
+ _edition = "2021"
+ if (defined(edition)) {
+ _edition = edition
+ }
+ assert(_edition == "2015" || _edition == "2018" || _edition == "2021",
+ "edition ${_edition} is not supported")
+
+ if (!defined(configs)) {
+ configs = []
+ }
+ configs += [ "$dir_pw_build:rust_edition_${_edition}" ]
+
+ if (!defined(rustflags)) {
+ rustflags = []
+ }
+ if (defined(features)) {
+ foreach(i, features) {
+ rustflags += [ "--cfg=feature=\"${i}\"" ]
+ }
+ }
+
+ underlying_target_type = "rust_library"
+ crate_name = string_replace(crate_name, "-", "_")
+ output_name = crate_name
+ output_dir = "${target_out_dir}/lib"
+ add_global_link_deps = true
+ }
+}
diff --git a/pw_build/target_types.gni b/pw_build/target_types.gni
index afb0380..3803c71 100644
--- a/pw_build/target_types.gni
+++ b/pw_build/target_types.gni
@@ -16,3 +16,5 @@
import("$dir_pw_build/cc_executable.gni")
import("$dir_pw_build/cc_library.gni")
+import("$dir_pw_build/rust_executable.gni")
+import("$dir_pw_build/rust_library.gni")
diff --git a/pw_chrono/public/pw_chrono/system_clock.h b/pw_chrono/public/pw_chrono/system_clock.h
index 55801ce..72035fb 100644
--- a/pw_chrono/public/pw_chrono/system_clock.h
+++ b/pw_chrono/public/pw_chrono/system_clock.h
@@ -36,8 +36,6 @@
namespace pw::chrono {
namespace backend {
-/// @var GetSystemClockTickCount
-///
/// The ARM AEBI does not permit the opaque 'time_point' to be passed via
/// registers, ergo the underlying fundamental type is forward declared.
/// A SystemCLock tick has the units of one SystemClock::period duration.
@@ -47,8 +45,6 @@
} // namespace backend
-/// @struct SystemClock
-///
/// The `SystemClock` represents an unsteady, monotonic clock.
///
/// The epoch of this clock is unspecified and may not be related to wall time
@@ -136,8 +132,6 @@
}
};
-/// @class VirtualSystemCLock
-///
/// An abstract interface representing a SystemClock.
///
/// This interface allows decoupling code that uses time from the code that
@@ -173,6 +167,8 @@
static VirtualSystemClock& RealClock();
virtual ~VirtualSystemClock() = default;
+
+ /// Returns the current time.
virtual SystemClock::time_point now() = 0;
};
diff --git a/pw_chrono/public/pw_chrono/system_timer.h b/pw_chrono/public/pw_chrono/system_timer.h
index cf4c510..0ec9d5e 100644
--- a/pw_chrono/public/pw_chrono/system_timer.h
+++ b/pw_chrono/public/pw_chrono/system_timer.h
@@ -19,14 +19,12 @@
namespace pw::chrono {
-/// @class SystemTimer
+/// The `SystemTimer` allows an `ExpiryCallback` be executed at a set time in
+/// the future.
///
-/// The SystemTimer allows an ExpiryCallback be executed at a set time in the
-/// future.
-///
-/// The base SystemTimer only supports a one-shot style timer with a callback.
+/// The base `SystemTimer` only supports a one-shot style timer with a callback.
/// A periodic timer can be implemented by rescheduling the timer in the
-/// callback through InvokeAt(kDesiredPeriod + expired_deadline).
+/// callback through `InvokeAt(kDesiredPeriod + expired_deadline)`.
///
/// When implementing a periodic layer on top, the user should be mindful of
/// handling missed periodic callbacks. They could opt to invoke the callback
@@ -38,12 +36,12 @@
public:
using native_handle_type = backend::NativeSystemTimerHandle;
- /// The ExpiryCallback is either invoked from a high priority thread or an
+ /// The `ExpiryCallback` is either invoked from a high priority thread or an
/// interrupt.
///
- /// For a given timer instance, its ExpiryCallback will not preempt itself.
+ /// For a given timer instance, its `ExpiryCallback` will not preempt itself.
/// This makes it appear like there is a single executor of a timer instance's
- /// ExpiryCallback.
+ /// `ExpiryCallback`.
///
/// Ergo ExpiryCallbacks should be treated as if they are executed by an
/// interrupt, meaning:
@@ -56,7 +54,7 @@
/// Constructs the SystemTimer based on the user provided
/// `pw::Function<void(SystemClock::time_point expired_deadline)>`. Note that
- /// The ExpiryCallback is either invoked from a high priority thread or an
+ /// The `ExpiryCallback` is either invoked from a high priority thread or an
/// interrupt.
SystemTimer(ExpiryCallback&& callback);
diff --git a/pw_chrono_freertos/BUILD.bazel b/pw_chrono_freertos/BUILD.bazel
index dc16bf6..60c9eee 100644
--- a/pw_chrono_freertos/BUILD.bazel
+++ b/pw_chrono_freertos/BUILD.bazel
@@ -38,6 +38,7 @@
],
deps = [
"//pw_chrono:epoch",
+ "@freertos",
],
)
@@ -52,8 +53,9 @@
deps = [
":system_clock_headers",
"//pw_chrono:system_clock_facade",
- # TODO(ewout): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ "//pw_interrupt:context",
+ "//pw_sync:interrupt_spin_lock",
+ "@freertos",
],
)
diff --git a/pw_chrono_zephyr/CMakeLists.txt b/pw_chrono_zephyr/CMakeLists.txt
index 4ed4d33..bea4f6d 100644
--- a/pw_chrono_zephyr/CMakeLists.txt
+++ b/pw_chrono_zephyr/CMakeLists.txt
@@ -14,22 +14,23 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+pw_add_library(pw_chrono_zephyr.system_clock INTERFACE
+ HEADERS
+ public/pw_chrono_zephyr/system_clock_constants.h
+ public/pw_chrono_zephyr/system_clock_config.h
+ public/pw_chrono_zephyr/system_clock_inline.h
+ public_overrides/pw_chrono_backend/system_clock_config.h
+ public_overrides/pw_chrono_backend/system_clock_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_function
+ pw_chrono.epoch
+ pw_chrono.system_clock.facade
+)
+
if(CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK)
- pw_add_library(pw_chrono_zephyr.system_clock INTERFACE
- HEADERS
- public/pw_chrono_zephyr/system_clock_constants.h
- public/pw_chrono_zephyr/system_clock_config.h
- public/pw_chrono_zephyr/system_clock_inline.h
- public_overrides/pw_chrono_backend/system_clock_config.h
- public_overrides/pw_chrono_backend/system_clock_inline.h
- PUBLIC_INCLUDES
- public
- public_overrides
- PUBLIC_DEPS
- pw_function
- pw_chrono.epoch
- pw_chrono.system_clock.facade
- )
zephyr_link_interface(pw_chrono_zephyr.system_clock)
zephyr_link_libraries(pw_chrono_zephyr.system_clock)
endif()
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
index 4013b5c..33641d8 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_chrono/system_clock.h"
diff --git a/pw_cli/py/BUILD.bazel b/pw_cli/py/BUILD.bazel
index e957422..c77ce2c 100644
--- a/pw_cli/py/BUILD.bazel
+++ b/pw_cli/py/BUILD.bazel
@@ -51,10 +51,10 @@
)
py_test(
- name = "plugins_test",
+ name = "envparse_test",
size = "small",
srcs = [
- "plugins_test.py",
+ "envparse_test.py",
],
deps = [
":pw_cli",
@@ -62,10 +62,10 @@
)
py_test(
- name = "envparse_test",
+ name = "plugins_test",
size = "small",
srcs = [
- "envparse_test.py",
+ "plugins_test.py",
],
deps = [
":pw_cli",
diff --git a/pw_cli/py/BUILD.gn b/pw_cli/py/BUILD.gn
index 052ffc6..4bb4cd2 100644
--- a/pw_cli/py/BUILD.gn
+++ b/pw_cli/py/BUILD.gn
@@ -46,3 +46,15 @@
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
}
+
+pw_python_script("process_integration_test") {
+ sources = [ "process_integration_test.py" ]
+ python_deps = [ ":py" ]
+
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+
+ action = {
+ stamp = true
+ }
+}
diff --git a/pw_cli/py/plugins_test.py b/pw_cli/py/plugins_test.py
index 3bafcac..a3f3513 100644
--- a/pw_cli/py/plugins_test.py
+++ b/pw_cli/py/plugins_test.py
@@ -188,7 +188,6 @@
sys.modules[fake_module_name] = fake_module
try:
-
function = lambda: None
function.__module__ = fake_module_name
self.assertIsNotNone(self._registry.register('a', function))
diff --git a/pw_cli/py/process_integration_test.py b/pw_cli/py/process_integration_test.py
new file mode 100644
index 0000000..3d05b1a
--- /dev/null
+++ b/pw_cli/py/process_integration_test.py
@@ -0,0 +1,73 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_cli.process."""
+
+import unittest
+import sys
+import textwrap
+
+from pw_cli import process
+
+import psutil # type: ignore
+
+
+FAST_TIMEOUT_SECONDS = 0.1
+KILL_SIGNALS = set({-9, 137})
+PYTHON = sys.executable
+
+
+class RunTest(unittest.TestCase):
+ """Tests for process.run."""
+
+ def test_returns_output(self) -> None:
+ echo_str = 'foobar'
+ print_str = f'print("{echo_str}")'
+ result = process.run(PYTHON, '-c', print_str)
+ self.assertEqual(result.output, b'foobar\n')
+
+ def test_timeout_kills_process(self) -> None:
+ sleep_100_seconds = 'import time; time.sleep(100)'
+ result = process.run(
+ PYTHON, '-c', sleep_100_seconds, timeout=FAST_TIMEOUT_SECONDS
+ )
+ self.assertIn(result.returncode, KILL_SIGNALS)
+
+ def test_timeout_kills_subprocess(self) -> None:
+ # Spawn a subprocess which waits for 100 seconds, print its pid,
+ # then wait for 100 seconds.
+ sleep_in_subprocess = textwrap.dedent(
+ f"""
+ import subprocess
+ import time
+
+ child = subprocess.Popen(
+ ['{PYTHON}', '-c', 'import time; print("booh"); time.sleep(100)']
+ )
+ print(child.pid, flush=True)
+ time.sleep(100)
+ """
+ )
+ result = process.run(
+ PYTHON, '-c', sleep_in_subprocess, timeout=FAST_TIMEOUT_SECONDS
+ )
+ self.assertIn(result.returncode, KILL_SIGNALS)
+ # THe first line of the output is the PID of the child sleep process.
+ child_pid_str, sep, remainder = result.output.partition(b'\n')
+ del sep, remainder
+ child_pid = int(child_pid_str)
+ self.assertFalse(psutil.pid_exists(child_pid))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_cli/py/pw_cli/process.py b/pw_cli/py/pw_cli/process.py
index e683db8..98852e3 100644
--- a/pw_cli/py/pw_cli/process.py
+++ b/pw_cli/py/pw_cli/process.py
@@ -23,6 +23,9 @@
import pw_cli.color
import pw_cli.log
+import psutil # type: ignore
+
+
_COLOR = pw_cli.color.colors()
_LOG = logging.getLogger(__name__)
@@ -32,7 +35,12 @@
class CompletedProcess:
- """Information about a process executed in run_async."""
+ """Information about a process executed in run_async.
+
+ Attributes:
+ pid: The process identifier.
+ returncode: The return code of the process.
+ """
def __init__(
self,
@@ -84,6 +92,56 @@
return process, bytes(output)
+async def _kill_process_and_children(
+ process: 'asyncio.subprocess.Process',
+) -> None:
+ """Kills child processes of a process with PID `pid`."""
+ # Look up child processes before sending the kill signal to the parent,
+ # as otherwise the child lookup would fail.
+ pid = process.pid
+ try:
+ process_util = psutil.Process(pid=pid)
+ kill_list = list(process_util.children(recursive=True))
+ except psutil.NoSuchProcess:
+ # Creating the kill list raced with parent process completion.
+ #
+ # Don't bother cleaning up child processes of parent processes that
+ # exited on their own.
+ kill_list = []
+
+ for proc in kill_list:
+ try:
+ proc.kill()
+ except psutil.NoSuchProcess:
+ pass
+
+ def wait_for_all() -> None:
+ for proc in kill_list:
+ try:
+ proc.wait()
+ except psutil.NoSuchProcess:
+ pass
+
+ # Wait for process completion on the executor to avoid blocking the
+ # event loop.
+ loop = asyncio.get_event_loop()
+ wait_for_children = loop.run_in_executor(None, wait_for_all)
+
+ # Send a kill signal to the main process before waiting for the children
+ # to complete.
+ try:
+ process.kill()
+ await process.wait()
+ except ProcessLookupError:
+ _LOG.debug(
+ 'Process completed before it could be killed. '
+ 'This may have been caused by the killing one of its '
+ 'child subprocesses.',
+ )
+
+ await wait_for_children
+
+
async def run_async(
program: str,
*args: str,
@@ -93,6 +151,20 @@
) -> CompletedProcess:
"""Runs a command, capturing and optionally logging its output.
+ Args:
+ program: The program to run in a new process.
+ args: The arguments to pass to the program.
+ env: An optional mapping of environment variables within which to run
+ the process.
+ log_output: Whether to log stdout and stderr of the process to this
+ process's stdout (prefixed with the PID of the subprocess from which
+ the output originated). If unspecified, the child process's stdout
+ and stderr will be captured, and both will be stored in the returned
+ `CompletedProcess`'s output`.
+ timeout: An optional floating point number of seconds to allow the
+ subprocess to run before killing it and its children. If unspecified,
+ the subprocess will be allowed to continue exiting until it completes.
+
Returns a CompletedProcess with details from the process.
"""
@@ -123,13 +195,12 @@
await asyncio.wait_for(process.wait(), timeout)
except asyncio.TimeoutError:
_LOG.error('%s timed out after %d seconds', program, timeout)
- process.kill()
- await process.wait()
+ await _kill_process_and_children(process)
if process.returncode:
_LOG.error('%s exited with status %d', program, process.returncode)
else:
- _LOG.debug('%s exited successfully', program)
+ _LOG.error('%s exited successfully', program)
return CompletedProcess(process, output)
diff --git a/pw_cli/py/setup.cfg b/pw_cli/py/setup.cfg
index be83438..20af8de 100644
--- a/pw_cli/py/setup.cfg
+++ b/pw_cli/py/setup.cfg
@@ -21,6 +21,10 @@
[options]
packages = find:
zip_safe = False
+install_requires =
+ psutil
+ pyyaml
+ toml
[options.entry_points]
console_scripts = pw = pw_cli.__main__:main
diff --git a/pw_console/py/pw_console/log_filter.py b/pw_console/py/pw_console/log_filter.py
index a2c4def..60411ef 100644
--- a/pw_console/py/pw_console/log_filter.py
+++ b/pw_console/py/pw_console/log_filter.py
@@ -92,7 +92,7 @@
field: Optional[str] = None
def pattern(self):
- return self.regex.pattern
+ return self.regex.pattern # pylint: disable=no-member
def matches(self, log: LogLine):
field = log.ansi_stripped_log
@@ -110,7 +110,7 @@
elif self.field == 'time':
field = log.record.asctime
- match = self.regex.search(field)
+ match = self.regex.search(field) # pylint: disable=no-member
if self.invert:
return not match
@@ -143,7 +143,9 @@
apply_highlighting(exploded_fragments, i)
else:
# Highlight each non-overlapping search match.
- for match in self.regex.finditer(line_text):
+ for match in self.regex.finditer( # pylint: disable=no-member
+ line_text
+ ): # pylint: disable=no-member
for fragment_i in range(match.start(), match.end()):
apply_highlighting(exploded_fragments, fragment_i)
diff --git a/pw_console/py/pw_console/log_line.py b/pw_console/py/pw_console/log_line.py
index 6543e30..0277166 100644
--- a/pw_console/py/pw_console/log_line.py
+++ b/pw_console/py/pw_console/log_line.py
@@ -43,7 +43,9 @@
"""Parse log metadata fields from various sources."""
# 1. Parse any metadata from the message itself.
- self.metadata = FormatStringWithMetadata(str(self.record.message))
+ self.metadata = FormatStringWithMetadata(
+ str(self.record.message) # pylint: disable=no-member
+ ) # pylint: disable=no-member
self.formatted_log = self.formatted_log.replace(
self.metadata.raw_string, self.metadata.message
)
@@ -65,9 +67,9 @@
# See:
# https://docs.python.org/3/library/logging.html#logging.debug
if hasattr(self.record, 'extra_metadata_fields') and (
- self.record.extra_metadata_fields # type: ignore
+ self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member
):
- fields = self.record.extra_metadata_fields # type: ignore
+ fields = self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member
for key, value in fields.items():
self.metadata.fields[key] = value
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index 36c0463..c9bfa8f 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -290,7 +290,6 @@
mouse_event.event_type == MouseEventType.MOUSE_UP
and mouse_event.button == MouseButton.LEFT
):
-
# If a drag was in progress and this is the first mouse release
# press, set the stop flag.
if (
diff --git a/pw_console/py/pw_console/log_screen.py b/pw_console/py/pw_console/log_screen.py
index ebf5300..2e0d5f2 100644
--- a/pw_console/py/pw_console/log_screen.py
+++ b/pw_console/py/pw_console/log_screen.py
@@ -228,7 +228,6 @@
# Loop through a copy of the line_buffer in case it is mutated before
# this function is complete.
for i, line in enumerate(list(self.line_buffer)):
-
# Is this line the cursor_position? Apply line highlighting
if (
i == self.cursor_position
diff --git a/pw_console/py/pw_console/pigweed_code_style.py b/pw_console/py/pw_console/pigweed_code_style.py
index 689ee67..a21cd68 100644
--- a/pw_console/py/pw_console/pigweed_code_style.py
+++ b/pw_console/py/pw_console/pigweed_code_style.py
@@ -119,7 +119,6 @@
class PigweedCodeStyle(Style):
-
background_color = '#2e2e2e'
default_style = ''
@@ -127,7 +126,6 @@
class PigweedCodeLightStyle(Style):
-
background_color = '#f8f8f8'
default_style = ''
diff --git a/pw_console/py/pw_console/plugins/twenty48_pane.py b/pw_console/py/pw_console/plugins/twenty48_pane.py
index 104e921..891b248 100644
--- a/pw_console/py/pw_console/plugins/twenty48_pane.py
+++ b/pw_console/py/pw_console/plugins/twenty48_pane.py
@@ -374,7 +374,6 @@
"""
def __init__(self, include_resize_handle: bool = True, **kwargs):
-
super().__init__(
pane_title='2048',
height=Dimension(preferred=17),
diff --git a/pw_console/py/pw_console/progress_bar/progress_bar_impl.py b/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
index d68d825..08dd62d 100644
--- a/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
+++ b/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
@@ -59,7 +59,6 @@
) -> AnyFormattedText:
formatted_text = super().format(progress_bar, progress, width)
if hasattr(progress, 'hide_eta') and progress.hide_eta: # type: ignore
-
formatted_text = [('', ' ' * width)]
return formatted_text
@@ -99,7 +98,6 @@
formatters: Optional[Sequence[Formatter]] = None,
style: Optional[BaseStyle] = None,
) -> None:
-
self.title = title
self.formatters = formatters or create_default_formatters()
self.counters: List[ProgressBarCounter[object]] = []
diff --git a/pw_console/py/pw_console/progress_bar/progress_bar_state.py b/pw_console/py/pw_console/progress_bar/progress_bar_state.py
index 37bcc06..159cc31 100644
--- a/pw_console/py/pw_console/progress_bar/progress_bar_state.py
+++ b/pw_console/py/pw_console/progress_bar/progress_bar_state.py
@@ -76,7 +76,7 @@
# Shut down the ProgressBar prompt_toolkit application
prog_bar = self.instance
if prog_bar is not None and hasattr(prog_bar, '__exit__'):
- prog_bar.__exit__()
+ prog_bar.__exit__() # pylint: disable=unnecessary-dunder-call
raise KeyboardInterrupt
signal.signal(signal.SIGINT, handle_sigint)
@@ -95,7 +95,7 @@
)
# Start the ProgressBar prompt_toolkit application in a separate
# thread.
- prog_bar.__enter__()
+ prog_bar.__enter__() # pylint: disable=unnecessary-dunder-call
self.instance = prog_bar
return self.instance
diff --git a/pw_console/py/pw_console/pw_ptpython_repl.py b/pw_console/py/pw_console/pw_ptpython_repl.py
index c9b2f4a..03f4054 100644
--- a/pw_console/py/pw_console/pw_ptpython_repl.py
+++ b/pw_console/py/pw_console/pw_ptpython_repl.py
@@ -95,7 +95,6 @@
extra_completers: Optional[Iterable] = None,
**ptpython_kwargs,
):
-
completer = None
if extra_completers:
# Create the default python completer used by
@@ -279,7 +278,7 @@
# Trigger a prompt_toolkit application redraw.
self.repl_pane.application.application.invalidate()
- async def _run_system_command(
+ async def _run_system_command( # pylint: disable=no-self-use
self, text, stdout_proxy, _stdin_proxy
) -> int:
"""Run a shell command and print results to the repl."""
diff --git a/pw_console/py/pw_console/pyserial_wrapper.py b/pw_console/py/pw_console/pyserial_wrapper.py
index fd74679..57c297a 100644
--- a/pw_console/py/pw_console/pyserial_wrapper.py
+++ b/pw_console/py/pw_console/pyserial_wrapper.py
@@ -12,15 +12,20 @@
# License for the specific language governing permissions and limitations under
# the License.
"""Wrapers for pyserial classes to log read and write data."""
+from __future__ import annotations
from contextvars import ContextVar
import logging
import textwrap
+from typing import TYPE_CHECKING
-import serial # type: ignore
+import serial
from pw_console.widgets.event_count_history import EventCountHistory
+if TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
_LOG = logging.getLogger('pw_console.serial_debug_logger')
@@ -87,8 +92,8 @@
super().__init__(*args, **kwargs)
self.pw_bps_history = BANDWIDTH_HISTORY_CONTEXTVAR.get()
- def read(self, *args, **kwargs):
- data = super().read(*args, **kwargs)
+ def read(self, size: int = 1) -> bytes:
+ data = super().read(size)
self.pw_bps_history['read'].log(len(data))
self.pw_bps_history['total'].log(len(data))
@@ -126,11 +131,11 @@
return data
- def write(self, data: bytes, *args, **kwargs):
- self.pw_bps_history['write'].log(len(data))
- self.pw_bps_history['total'].log(len(data))
+ def write(self, data: ReadableBuffer) -> None:
+ if isinstance(data, bytes) and len(data) > 0:
+ self.pw_bps_history['write'].log(len(data))
+ self.pw_bps_history['total'].log(len(data))
- if len(data) > 0:
prefix = 'Write %2d B: ' % len(data)
_LOG.debug(
'%s%s',
@@ -162,4 +167,4 @@
),
)
- super().write(data, *args, **kwargs)
+ super().write(data)
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index 9547215..20a3b95 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -21,6 +21,7 @@
from dataclasses import dataclass
from typing import (
Any,
+ Awaitable,
Callable,
Dict,
List,
@@ -82,8 +83,9 @@
output: str
stdout: str
stderr: str
- stdout_check_task: Optional[concurrent.futures.Future] = None
+ stdout_check_task: Optional[Awaitable] = None
result_object: Optional[Any] = None
+ result_str: Optional[str] = None
exception_text: Optional[str] = None
@property
@@ -112,7 +114,7 @@
) -> None:
super().__init__(application, pane_title)
- self.executed_code: List = []
+ self.executed_code: List[UserCodeExecution] = []
self.application = application
self.pw_ptpython_repl = python_repl
@@ -481,7 +483,6 @@
exception_text='',
result_object=None,
):
-
code = self._get_executed_code(future)
if code:
code.output = result_text
@@ -489,10 +490,14 @@
code.stderr = stderr_text
code.exception_text = exception_text
code.result_object = result_object
+ if result_object is not None:
+ code.result_str = self._format_result_object(result_object)
+
self._log_executed_code(code, prefix='FINISH')
self.update_output_buffer('repl_pane.append_result_to_executed_code')
- def get_output_buffer_text(self, code_items=None, show_index=True):
+ def _format_result_object(self, result_object: Any) -> str:
+ """Pretty print format a Python object respecting the window width."""
content_width = (
self.current_pane_width if self.current_pane_width else 80
)
@@ -500,12 +505,19 @@
indent=2, width=content_width
).pformat
+ return pprint_respecting_width(result_object)
+
+ def get_output_buffer_text(
+ self,
+ code_items: Optional[List[UserCodeExecution]] = None,
+ show_index: bool = True,
+ ):
executed_code = code_items or self.executed_code
template = self.application.get_template('repl_output.jinja')
+
return template.render(
code_items=executed_code,
- result_format=pprint_respecting_width,
show_index=show_index,
)
diff --git a/pw_console/py/pw_console/templates/repl_output.jinja b/pw_console/py/pw_console/templates/repl_output.jinja
index 4c482c1..976cb8b 100644
--- a/pw_console/py/pw_console/templates/repl_output.jinja
+++ b/pw_console/py/pw_console/templates/repl_output.jinja
@@ -30,8 +30,8 @@
{% endif %}
{% if code.exception_text %}
{{ code.exception_text }}
-{% elif code.result_object %}
-{{ result_format(code.result_object) }}
+{% elif code.result_str %}
+{{ code.result_str }}
{% elif code.output %}
{{ code.output }}
{% endif %}
diff --git a/pw_console/py/pw_console/widgets/window_pane_toolbar.py b/pw_console/py/pw_console/widgets/window_pane_toolbar.py
index 354d3d5..18ec4cd 100644
--- a/pw_console/py/pw_console/widgets/window_pane_toolbar.py
+++ b/pw_console/py/pw_console/widgets/window_pane_toolbar.py
@@ -184,7 +184,6 @@
include_resize_handle: bool = True,
click_to_focus_text: str = 'click to focus',
):
-
self.parent_window_pane = parent_window_pane
self.title = title
self.subtitle = subtitle
diff --git a/pw_console/py/pw_console/window_manager.py b/pw_console/py/pw_console/window_manager.py
index 5a1d241..817e1d7 100644
--- a/pw_console/py/pw_console/window_manager.py
+++ b/pw_console/py/pw_console/window_manager.py
@@ -953,7 +953,6 @@
for logger_name, logger_options in window_options.get(
'loggers', {}
).items():
-
log_level_name = logger_options.get('level', None)
new_pane.add_log_handler(logger_name, level_name=log_level_name)
return new_pane
diff --git a/pw_console/py/setup.cfg b/pw_console/py/setup.cfg
index 97b921a..60021a4 100644
--- a/pw_console/py/setup.cfg
+++ b/pw_console/py/setup.cfg
@@ -28,9 +28,10 @@
ptpython>=3.0.20
pygments
pyperclip
- pyserial
+ pyserial>=3.5,<4.0
pyyaml
types-pygments
+ types-pyserial>=3.5,<4.0
types-pyyaml
websockets
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index 38fb9f4..af91318 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -731,23 +731,36 @@
* - 5
- | Enter the following text and press :kbd:`Enter` to run
- | ``globals()``
+ | ``locals()``
- | The results should appear pretty printed
- |checkbox|
* - 6
+ - | Enter the following text and press :kbd:`Enter` to run
+ | ``zzzz = 'test'``
+ - | No new results are shown
+ | The previous ``locals()`` output does not show ``'zzzz': 'test'``
+ - |checkbox|
+
+ * - 7
+ - | Enter the following text and press :kbd:`Enter` to run
+ | ``locals()``
+ - | The output ends with ``'zzzz': 'test'}``
+ - |checkbox|
+
+ * - 8
- | With the cursor over the Python Results,
| use the mouse wheel to scroll up and down.
- | The output window should be able to scroll all
| the way to the beginning and end of the buffer.
- |checkbox|
- * - 7
+ * - 9
- Click empty whitespace in the ``Python Repl`` window
- Python Repl pane is focused
- |checkbox|
- * - 8
+ * - 10
- | Enter the following text and press :kbd:`Enter` to run
| ``!ls``
- | 1. Shell output of running the ``ls`` command should appear in the
diff --git a/pw_crypto/BUILD.gn b/pw_crypto/BUILD.gn
index 9787100..919ab7b 100644
--- a/pw_crypto/BUILD.gn
+++ b/pw_crypto/BUILD.gn
@@ -19,6 +19,7 @@
import("$dir_pw_build/target_types.gni")
import("$dir_pw_crypto/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/micro_ecc/micro_ecc.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -83,6 +84,7 @@
":sha256_test",
":sha256_mock_test",
":ecdsa_test",
+ ":ecdsa_uecc_little_endian_test",
]
}
@@ -193,8 +195,28 @@
public_deps = [ ":ecdsa.facade" ]
}
+pw_source_set("ecdsa_uecc_little_endian") {
+ sources = [ "ecdsa_uecc.cc" ]
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_third_party/micro_ecc:micro_ecc_little_endian",
+ ]
+ public_deps = [ ":ecdsa.facade" ]
+}
+
+# This test targets the specific backend pointed to by
+# pw_crypto_ECDSA_BACKEND.
pw_test("ecdsa_test") {
enable_if = pw_crypto_ECDSA_BACKEND != ""
deps = [ ":ecdsa" ]
sources = [ "ecdsa_test.cc" ]
}
+
+# This test targets the micro_ecc little endian backend specifically.
+#
+# TODO(b/273819841) deduplicate all backend tests.
+pw_test("ecdsa_uecc_little_endian_test") {
+ enable_if = dir_pw_third_party_micro_ecc != ""
+ sources = [ "ecdsa_test.cc" ]
+ deps = [ ":ecdsa_uecc_little_endian" ]
+}
diff --git a/pw_crypto/docs.rst b/pw_crypto/docs.rst
index 6019528..1230cd3 100644
--- a/pw_crypto/docs.rst
+++ b/pw_crypto/docs.rst
@@ -99,7 +99,9 @@
# Install and configure MbedTLS
pw package install mbedtls
gn gen out --args='
- dir_pw_third_party_mbedtls=pw_env_setup_PACKAGE_ROOT + "/mbedtls"
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_mbedtls=pw_env_setup_PACKAGE_ROOT+"/mbedtls"
pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_mbedtls"
pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_mbedtls"
'
@@ -145,7 +147,9 @@
# Install and configure BoringSSL
pw package install boringssl
gn gen out --args='
- dir_pw_third_party_boringssl=pw_env_setup_PACKAGE_ROOT + "/boringssl"
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_boringssl=pw_env_setup_PACKAGE_ROOT+"/boringssl"
pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_boringssl"
pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_boringssl"
'
@@ -165,10 +169,18 @@
# Install and configure Micro ECC
pw package install micro-ecc
gn gen out --args='
- dir_pw_third_party_micro_ecc=pw_env_setup_PACKAGE_ROOT + "//micro-ecc"
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_micro_ecc=pw_env_setup_PACKAGE_ROOT+"/micro-ecc"
pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc"
'
+The default micro-ecc backend uses big endian as is standard practice. It also
+has a little-endian configuration which can be used to slightly reduce call
+stack frame use and/or when non pw_crypto clients use the same micro-ecc
+with a little-endian configuration. The little-endian version of micro-ecc
+can be selected with ``pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc_little_endian"``
+
Note Micro-ECC does not implement any hashing functions, so you will need to use other backends for SHA256 functionality if needed.
Size Reports
diff --git a/pw_crypto/ecdsa_uecc.cc b/pw_crypto/ecdsa_uecc.cc
index 937d79d..912a28b 100644
--- a/pw_crypto/ecdsa_uecc.cc
+++ b/pw_crypto/ecdsa_uecc.cc
@@ -14,6 +14,8 @@
#define PW_LOG_MODULE_NAME "ECDSA-UECC"
#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
+#include <cstring>
+
#include "pw_crypto/ecdsa.h"
#include "pw_log/log.h"
#include "uECC.h"
@@ -21,33 +23,62 @@
namespace pw::crypto::ecdsa {
constexpr size_t kP256CurveOrderBytes = 32;
+constexpr size_t kP256PublicKeySize = 2 * kP256CurveOrderBytes + 1;
+constexpr size_t kP256SignatureSize = kP256CurveOrderBytes * 2;
Status VerifyP256Signature(ConstByteSpan public_key,
ConstByteSpan digest,
ConstByteSpan signature) {
- const uint8_t* public_key_bytes =
- reinterpret_cast<const uint8_t*>(public_key.data());
- const uint8_t* digest_bytes = reinterpret_cast<const uint8_t*>(digest.data());
- const uint8_t* signature_bytes =
- reinterpret_cast<const uint8_t*>(signature.data());
-
- uECC_Curve curve = uECC_secp256r1();
+ // Signature expected in raw format (r||s)
+ if (signature.size() != kP256SignatureSize) {
+ PW_LOG_DEBUG("Bad signature format");
+ return Status::InvalidArgument();
+ }
// Supports SEC 1 uncompressed form (04||X||Y) only.
- if (public_key.size() != (2 * kP256CurveOrderBytes + 1) ||
- public_key_bytes[0] != 0x04) {
+ if (public_key.size() != kP256PublicKeySize ||
+ std::to_integer<uint8_t>(public_key.data()[0]) != 0x04) {
PW_LOG_DEBUG("Bad public key format");
return Status::InvalidArgument();
}
- // Make sure the public key is on the curve.
- if (!uECC_valid_public_key(public_key_bytes + 1, curve)) {
- return Status::InvalidArgument();
- }
+#if defined(uECC_VLI_NATIVE_LITTLE_ENDIAN) && uECC_VLI_NATIVE_LITTLE_ENDIAN
+ // uECC_VLI_NATIVE_LITTLE_ENDIAN is defined with a non-zero value when
+ // pw_crypto_ECDSA_BACKEND is set to "//pw_crypto:ecdsa_uecc_little_endian".
+ //
+ // Since pw_crypto APIs are big endian only (standard practice), here we
+ // need to convert input parameters to little endian.
+ //
+ // Additionally uECC requires these little endian buffers to be word aligned
+ // in case unaligned accesses are not supported by the hardware. We choose
+ // the maximum 8-byte alignment to avoid referrencing internal uECC headers.
+ alignas(8) uint8_t signature_bytes[kP256SignatureSize];
+ memcpy(signature_bytes, signature.data(), sizeof(signature_bytes));
+ std::reverse(signature_bytes, signature_bytes + kP256CurveOrderBytes); // r
+ std::reverse(signature_bytes + kP256CurveOrderBytes,
+ signature_bytes + sizeof(signature_bytes)); // s
- // Signature expected in raw format (r||s)
- if (signature.size() != kP256CurveOrderBytes * 2) {
- PW_LOG_DEBUG("Bad signature format");
+ alignas(8) uint8_t public_key_bytes[kP256PublicKeySize - 1];
+ memcpy(public_key_bytes, public_key.data() + 1, sizeof(public_key_bytes));
+ std::reverse(public_key_bytes, public_key_bytes + kP256CurveOrderBytes); // X
+ std::reverse(public_key_bytes + kP256CurveOrderBytes,
+ public_key_bytes + sizeof(public_key_bytes)); // Y
+
+ alignas(8) uint8_t digest_bytes[kP256CurveOrderBytes];
+ memcpy(digest_bytes, digest.data(), sizeof(digest_bytes));
+ std::reverse(digest_bytes, digest_bytes + sizeof(digest_bytes));
+#else
+ const uint8_t* public_key_bytes =
+ reinterpret_cast<const uint8_t*>(public_key.data()) + 1;
+ const uint8_t* digest_bytes = reinterpret_cast<const uint8_t*>(digest.data());
+ const uint8_t* signature_bytes =
+ reinterpret_cast<const uint8_t*>(signature.data());
+#endif // uECC_VLI_NATIVE_LITTLE_ENDIAN
+
+ uECC_Curve curve = uECC_secp256r1();
+ // Make sure the public key is on the curve.
+ if (!uECC_valid_public_key(public_key_bytes, curve)) {
+ PW_LOG_DEBUG("Bad public key curve");
return Status::InvalidArgument();
}
@@ -59,7 +90,7 @@
}
// Verify the signature.
- if (!uECC_verify(public_key_bytes + 1,
+ if (!uECC_verify(public_key_bytes,
digest_bytes,
digest.size(),
signature_bytes,
diff --git a/pw_docgen/docs.rst b/pw_docgen/docs.rst
index 5528ea1..6971384 100644
--- a/pw_docgen/docs.rst
+++ b/pw_docgen/docs.rst
@@ -168,6 +168,52 @@
This module houses Pigweed-specific extensions for the Sphinx documentation
generator. Extensions are included and configured in ``docs/conf.py``.
+module_metadata
+---------------
+Per :ref:`SEED-0102 <seed-0102>`, Pigweed module documentation has a standard
+format. The ``pigweed-module`` Sphinx directive provides that format and
+registers module metadata that can be used elsewhere in the Sphinx build.
+
+We need to add the directive after the document title, and add a class *to*
+the document title to achieve the title & subtitle formatting. Here's an
+example:
+
+.. code-block:: rst
+
+ .. rst-class:: with-subtitle
+
+ =========
+ pw_string
+ =========
+
+ .. pigweed-module::
+ :name: pw_string
+ :tagline: Efficient, easy, and safe string manipulation
+ :status: stable
+ :languages: C++14, C++17
+ :code-size-impact: 500 to 1500 bytes
+ :get-started: module-pw_string-get-started
+ :design: module-pw_string-design
+ :guides: module-pw_string-guide
+ :api: module-pw_string-api
+
+ Module sales pitch goes here!
+
+Directive options
+_________________
+- ``name``: The module name (required)
+- ``tagline``: A very short tagline that summarizes the module (required)
+- ``status``: One of ``experimental``, ``unstable``, and ``stable`` (required)
+- ``is-deprecated``: A flag indicating that the module is deprecated
+- ``languages``: A comma-separated list of languages the module supports
+- ``code-size-impact``: A summarize of the average code size impact
+- ``get-started``: A reference to the getting started section (required)
+- ``tutorials``: A reference to the tutorials section
+- ``guides``: A reference to the guides section
+- ``design``: A reference to the design considerations section (required)
+- ``concepts``: A reference to the concepts documentation
+- ``api``: A reference to the API documentation
+
google_analytics
----------------
When this extension is included and a ``google_analytics_id`` is set in the
diff --git a/pw_docgen/py/BUILD.gn b/pw_docgen/py/BUILD.gn
index 3e6e34f..55c48aa 100644
--- a/pw_docgen/py/BUILD.gn
+++ b/pw_docgen/py/BUILD.gn
@@ -27,6 +27,7 @@
"pw_docgen/docgen.py",
"pw_docgen/sphinx/__init__.py",
"pw_docgen/sphinx/google_analytics.py",
+ "pw_docgen/sphinx/module_metadata.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
diff --git a/pw_docgen/py/pw_docgen/sphinx/module_metadata.py b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
new file mode 100644
index 0000000..d6fe054
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
@@ -0,0 +1,226 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Sphinx directives for Pigweed module metadata"""
+
+from typing import List
+
+import docutils
+
+# pylint: disable=consider-using-from-import
+import docutils.parsers.rst.directives as directives # type: ignore
+
+# pylint: enable=consider-using-from-import
+from sphinx.application import Sphinx as SphinxApplication
+from sphinx.util.docutils import SphinxDirective
+from sphinx_design.badges_buttons import ButtonRefDirective # type: ignore
+from sphinx_design.cards import CardDirective # type: ignore
+
+
+def status_choice(arg):
+ return directives.choice(arg, ('experimental', 'unstable', 'stable'))
+
+
+def status_badge(module_status: str) -> str:
+ role = ':bdg-primary:'
+ return role + f'`{module_status.title()}`'
+
+
+def cs_url(module_name: str):
+ return f'https://cs.opensource.google/pigweed/pigweed/+/main:{module_name}/'
+
+
+def concat_tags(*tag_lists: List[str]):
+ all_tags = tag_lists[0]
+
+ for tag_list in tag_lists[1:]:
+ if len(tag_list) > 0:
+ all_tags.append(':octicon:`dot-fill`')
+ all_tags.extend(tag_list)
+
+ return all_tags
+
+
+class PigweedModuleDirective(SphinxDirective):
+ """Directive registering module metadata, rendering title & info card."""
+
+ required_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+ option_spec = {
+ 'name': directives.unchanged_required,
+ 'tagline': directives.unchanged_required,
+ 'status': status_choice,
+ 'is-deprecated': directives.flag,
+ 'languages': directives.unchanged,
+ 'code-size-impact': directives.unchanged,
+ 'facade': directives.unchanged,
+ 'get-started': directives.unchanged_required,
+ 'tutorials': directives.unchanged,
+ 'guides': directives.unchanged,
+ 'concepts': directives.unchanged,
+ 'design': directives.unchanged_required,
+ 'api': directives.unchanged,
+ }
+
+ def try_get_option(self, option: str):
+ try:
+ return self.options[option]
+ except KeyError:
+ raise self.error(f' :{option}: option is required')
+
+ def maybe_get_option(self, option: str):
+ try:
+ return self.options[option]
+ except KeyError:
+ return None
+
+ def create_section_button(self, title: str, ref: str):
+ node = docutils.nodes.list_item(classes=['pw-module-section-button'])
+ node += ButtonRefDirective(
+ name='',
+ arguments=[ref],
+ options={'color': 'primary'},
+ content=[title],
+ lineno=0,
+ content_offset=0,
+ block_text='',
+ state=self.state,
+ state_machine=self.state_machine,
+ ).run()
+
+ return node
+
+ def register_metadata(self):
+ module_name = self.try_get_option('name')
+
+ if 'facade' in self.options:
+ facade = self.options['facade']
+
+ # Initialize the module relationship dict if needed
+ if not hasattr(self.env, 'pw_module_relationships'):
+ self.env.pw_module_relationships = {}
+
+ # Initialize the backend list for this facade if needed
+ if facade not in self.env.pw_module_relationships:
+ self.env.pw_module_relationships[facade] = []
+
+ # Add this module as a backend of the provided facade
+ self.env.pw_module_relationships[facade].append(module_name)
+
+ if 'is-deprecated' in self.options:
+ # Initialize the deprecated modules list if needed
+ if not hasattr(self.env, 'pw_modules_deprecated'):
+ self.env.pw_modules_deprecated = []
+
+ self.env.pw_modules_deprecated.append(module_name)
+
+ def run(self):
+ tagline = docutils.nodes.paragraph(
+ text=self.try_get_option('tagline'),
+ classes=['section-subtitle'],
+ )
+
+ status_tags: List[str] = [
+ status_badge(self.try_get_option('status')),
+ ]
+
+ if 'is-deprecated' in self.options:
+ status_tags.append(':bdg-danger:`Deprecated`')
+
+ language_tags = []
+
+ if 'languages' in self.options:
+ languages = self.options['languages'].split(',')
+
+ if len(languages) > 0:
+ for language in languages:
+ language_tags.append(f':bdg-info:`{language.strip()}`')
+
+ code_size_impact = []
+
+ if code_size_text := self.maybe_get_option('code-size-impact'):
+ code_size_impact.append(f'**Code Size Impact:** {code_size_text}')
+
+ # Move the directive content into a section that we can render wherever
+ # we want.
+ content = docutils.nodes.paragraph()
+ self.state.nested_parse(self.content, 0, content)
+
+ # The card inherits its content from this node's content, which we've
+ # already pulled out. So we can replace this node's content with the
+ # content we need in the card.
+ self.content = docutils.statemachine.StringList(
+ concat_tags(status_tags, language_tags, code_size_impact)
+ )
+
+ card = CardDirective.create_card(
+ inst=self,
+ arguments=[],
+ options={},
+ )
+
+ # Create the top-level section buttons.
+ section_buttons = docutils.nodes.bullet_list(
+ classes=['pw-module-section-buttons']
+ )
+
+ # This is the pattern for required sections.
+ section_buttons += self.create_section_button(
+ 'Get Started', self.try_get_option('get-started')
+ )
+
+ # This is the pattern for optional sections.
+ if (tutorials_ref := self.maybe_get_option('tutorials')) is not None:
+ section_buttons += self.create_section_button(
+ 'Tutorials', tutorials_ref
+ )
+
+ if (guides_ref := self.maybe_get_option('guides')) is not None:
+ section_buttons += self.create_section_button('Guides', guides_ref)
+
+ if (concepts_ref := self.maybe_get_option('concepts')) is not None:
+ section_buttons += self.create_section_button(
+ 'Concepts', concepts_ref
+ )
+
+ section_buttons += self.create_section_button(
+ 'Design', self.try_get_option('design')
+ )
+
+ if (api_ref := self.maybe_get_option('api')) is not None:
+ section_buttons += self.create_section_button(
+ 'API Reference', api_ref
+ )
+
+ return [tagline, section_buttons, content, card]
+
+
+def build_backend_lists(app, _doctree, _fromdocname):
+ env = app.builder.env
+
+ if not hasattr(env, 'pw_module_relationships'):
+ env.pw_module_relationships = {}
+
+
+def setup(app: SphinxApplication):
+ app.add_directive('pigweed-module', PigweedModuleDirective)
+
+ # At this event, the documents and metadata have been generated, and now we
+ # can modify the doctree to reflect the metadata.
+ app.connect('doctree-resolved', build_backend_lists)
+
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/pw_docgen/py/setup.cfg b/pw_docgen/py/setup.cfg
index 3da0a17..41f7505 100644
--- a/pw_docgen/py/setup.cfg
+++ b/pw_docgen/py/setup.cfg
@@ -22,10 +22,12 @@
packages = find:
zip_safe = False
install_requires =
- sphinx>3
+ sphinx>=5.3.0
sphinx-argparse
sphinx-rtd-theme
sphinxcontrib-mermaid>=0.7.1
+ sphinx-design>=0.3.0
+
[options.package_data]
pw_docgen = py.typed
diff --git a/pw_doctor/py/pw_doctor/doctor.py b/pw_doctor/py/pw_doctor/doctor.py
index 427431c..cf3f865 100755
--- a/pw_doctor/py/pw_doctor/doctor.py
+++ b/pw_doctor/py/pw_doctor/doctor.py
@@ -141,14 +141,16 @@
ctx.error('Not all pw plugins loaded successfully')
-def unames_are_equivalent(uname_actual: str, uname_expected: str) -> bool:
+def unames_are_equivalent(
+ uname_actual: str, uname_expected: str, rosetta: bool = False
+) -> bool:
"""Determine if uname values are equivalent for this tool's purposes."""
# Support `mac-arm64` through Rosetta until `mac-arm64` binaries are ready
# Expected and actual unames will not literally match on M1 Macs because
# they pretend to be Intel Macs for the purpose of environment setup. But
# that's intentional and doesn't require any user action.
- if "Darwin" in uname_expected and "arm64" in uname_expected:
+ if rosetta and "Darwin" in uname_expected and "arm64" in uname_expected:
uname_expected = uname_expected.replace("arm64", "x86_64")
return uname_actual == uname_expected
@@ -178,7 +180,9 @@
# redundant because it's contained in release or version, and
# skipping it here simplifies logic.
uname = ' '.join(getattr(os, 'uname', lambda: ())()[2:])
- if not unames_are_equivalent(uname, data['uname']):
+ rosetta_envvar = os.environ.get('_PW_ROSETTA', '0')
+ rosetta = rosetta_envvar.strip().lower() != '0'
+ if not unames_are_equivalent(uname, data['uname'], rosetta):
ctx.warning(
'Current uname (%s) does not match Bootstrap uname (%s), '
'you may need to rerun bootstrap on this system',
diff --git a/pw_env_setup/py/BUILD.gn b/pw_env_setup/py/BUILD.gn
index 6edd6d6..7b68fcb 100644
--- a/pw_env_setup/py/BUILD.gn
+++ b/pw_env_setup/py/BUILD.gn
@@ -30,6 +30,7 @@
"pw_env_setup/cipd_setup/update.py",
"pw_env_setup/cipd_setup/wrapper.py",
"pw_env_setup/colors.py",
+ "pw_env_setup/config_file.py",
"pw_env_setup/env_setup.py",
"pw_env_setup/environment.py",
"pw_env_setup/gni_visitor.py",
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
index b90321b..e200afe 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
@@ -13,7 +13,7 @@
"windows-amd64"
],
"tags": [
- "version:2@3.25.2.chromium.6"
+ "version:2@3.26.0.chromium.7"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/go.json b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
index 8c3ad36..0feb0db 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "version:2@1.20.1"
+ "version:2@1.20.2"
]
},
{
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
index ab218e1..cf0e8f7 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
@@ -8,7 +8,7 @@
"windows-amd64"
],
"tags": [
- "git_revision:c28834ee85ac0752bf4d015797f02d88a5ad2cd9"
+ "git_revision:284a05aeae3cf2e4579e6518ab3a5316058da6d4"
],
"version_file": ".versions/host_tools.cipd_version"
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
index 7d2d6df..117ad97 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "git_revision:fe330c0ae1ec29db30b6f830e50771a335e071fb"
+ "git_revision:41fef642de70ecdcaaa26be96d56a0398f95abd4"
],
"version_file": ".versions/gn.cipd_version"
},
@@ -115,7 +115,7 @@
"mac-amd64"
],
"tags": [
- "git_revision:823a3f11fb8f04c3c3cc0f95f968fef1bfc6534f"
+ "git_revision:823a3f11fb8f04c3c3cc0f95f968fef1bfc6534f,1"
],
"version_file": ".versions/qemu.cipd_version"
},
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
index c8e807d..02dcdfc 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
@@ -1,7 +1,4 @@
{
- "included_files": [
- "black.json"
- ],
"packages": [
{
"path": "infra/3pp/tools/cpython3/${platform}",
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
index 6a98244..deac4b0 100755
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
@@ -133,13 +133,8 @@
return True
-def platform(rosetta=None):
+def platform(rosetta=False):
"""Return the CIPD platform string of the current system."""
- # If running inside a bootstrapped environment we can use the env var.
- # Otherwise, require rosetta be set.
- if rosetta is None:
- rosetta = os.environ['_PW_ROSETTA']
-
osname = {
'darwin': 'mac',
'linux': 'linux',
diff --git a/pw_env_setup/py/pw_env_setup/config_file.py b/pw_env_setup/py/pw_env_setup/config_file.py
new file mode 100644
index 0000000..fc8fa40
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/config_file.py
@@ -0,0 +1,43 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Reads and parses the Pigweed config file.
+
+See also https://pigweed.dev/seed/0101-pigweed.json.html.
+"""
+
+import json
+import os
+
+
+def _get_project_root(env=None):
+ if not env:
+ env = os.environ
+ for var in ('PW_PROJECT_ROOT', 'PW_ROOT'):
+ if var in env:
+ return env[var]
+ raise ValueError('environment variable PW_PROJECT_ROOT not set')
+
+
+def path(env=None):
+ """Return the path where pigweed.json should reside."""
+ return os.path.join(_get_project_root(env=env), 'pigweed.json')
+
+
+def load(env=None):
+ """Load pigweed.json if it exists and return the contents."""
+ config_path = path(env=env)
+ if not os.path.isfile(config_path):
+ return {}
+ with open(config_path, 'r') as ins:
+ return json.load(ins)
diff --git a/pw_env_setup/py/pw_env_setup/environment.py b/pw_env_setup/py/pw_env_setup/environment.py
index ca7f7f4..c87de88 100644
--- a/pw_env_setup/py/pw_env_setup/environment.py
+++ b/pw_env_setup/py/pw_env_setup/environment.py
@@ -483,6 +483,7 @@
Yields the new environment object.
"""
+ orig_env = {}
try:
if export:
orig_env = os.environ.copy()
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
index 10970a0..bee66aa 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
@@ -1,8 +1,9 @@
alabaster==0.7.12
appdirs==1.4.4
-astroid==2.6.6
+astroid==2.14.2
Babel==2.9.1
backcall==0.2.0
+black==23.1.0
build==0.8.0
cachetools==5.0.0
certifi==2021.10.8
@@ -12,6 +13,7 @@
coverage==6.3
cryptography==36.0.1
decorator==5.1.1
+dill==0.3.6
docutils==0.17.1
breathe==4.34.0
google-api-core==2.7.1
@@ -33,17 +35,19 @@
MarkupSafe==2.0.1
matplotlib-inline==0.1.3
mccabe==0.6.1
-mypy==0.991
-mypy-extensions==0.4.3
+mypy==1.0.1
+mypy-extensions==1.0.0
mypy-protobuf==3.3.0
-packaging==21.3
+packaging==23.0
parameterized==0.8.1
parso==0.8.3
pep517==0.12.0
pexpect==4.8.0
+platformdirs==3.0.0
pickleshare==0.7.5
prompt-toolkit==3.0.36
-protobuf==3.20.1
+protobuf==3.20.2
+psutil==5.9.4
ptpython==3.0.22
ptyprocess==0.7.0
pyasn1==0.4.8
@@ -51,7 +55,7 @@
pycparser==2.21
pyelftools==0.27
Pygments==2.14.0
-pylint==2.9.3
+pylint==2.16.2
pyparsing==3.0.6
pyperclip==1.8.2
pyserial==3.5
@@ -67,6 +71,7 @@
sphinx-rtd-theme==1.2.0
sphinx-argparse==0.4.0
sphinx-copybutton==0.5.1
+sphinx-design==0.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
@@ -75,16 +80,17 @@
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
toml==0.10.2
-tomli==2.0.0
+tomli==2.0.1
+tomlkit==0.11.6
traitlets==5.1.1
types-docutils==0.17.4
types-futures==3.3.2
-types-protobuf==3.19.22
+types-protobuf==3.20.4.6
types-Pygments==2.9.13
types-PyYAML==6.0.7
types-setuptools==63.4.1
types-six==1.16.9
-typing-extensions==4.1.1
+typing-extensions==4.4.0
urllib3==1.26.8
watchdog==2.1.6
wcwidth==0.2.5
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
index 72a71b0..fcb9baf 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
@@ -16,9 +16,8 @@
# pigweed.dev Sphinx themes
furo==2022.12.7
sphinx-copybutton==0.5.1
-sphinx-design==0.3.0
myst-parser==0.18.1
breathe==4.34.0
# Renode requirements
-psutil==5.9.1
+psutil==5.9.4
robotframework==5.0.1
diff --git a/pw_env_setup/util.sh b/pw_env_setup/util.sh
index c6e66f1..ffcc0c3 100644
--- a/pw_env_setup/util.sh
+++ b/pw_env_setup/util.sh
@@ -105,7 +105,7 @@
pw_error_info " Pigweed's Python environment currently requires Pigweed to"
pw_error_info " be at a path without spaces. Please checkout Pigweed in a"
pw_error_info " directory without spaces and retry running bootstrap."
- return
+ return -1
fi
}
diff --git a/pw_function/BUILD.bazel b/pw_function/BUILD.bazel
index 6a16156..e130d1c 100644
--- a/pw_function/BUILD.bazel
+++ b/pw_function/BUILD.bazel
@@ -48,3 +48,34 @@
"//pw_compilation_testing:negative_compilation_testing",
],
)
+
+pw_cc_library(
+ name = "pointer",
+ srcs = ["public/pw_function/internal/static_invoker.h"],
+ hdrs = ["public/pw_function/pointer.h"],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "pointer_test",
+ srcs = ["pointer_test.cc"],
+ deps = [
+ ":pointer",
+ ":pw_function",
+ ],
+)
+
+pw_cc_library(
+ name = "scope_guard",
+ hdrs = ["public/pw_function/scope_guard.h"],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "scope_guard_test",
+ srcs = ["scope_guard_test.cc"],
+ deps = [
+ ":pw_function",
+ ":scope_guard",
+ ],
+)
diff --git a/pw_function/BUILD.gn b/pw_function/BUILD.gn
index cceef9b..efea1cc 100644
--- a/pw_function/BUILD.gn
+++ b/pw_function/BUILD.gn
@@ -50,6 +50,17 @@
public = [ "public/pw_function/function.h" ]
}
+pw_source_set("pointer") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_function/pointer.h" ]
+ sources = [ "public/pw_function/internal/static_invoker.h" ]
+}
+
+pw_source_set("scope_guard") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_function/scope_guard.h" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
report_deps = [
@@ -61,6 +72,8 @@
pw_test_group("tests") {
tests = [
":function_test",
+ ":pointer_test",
+ ":scope_guard_test",
"$dir_pw_third_party/fuchsia:function_tests",
]
}
@@ -74,6 +87,22 @@
negative_compilation_tests = true
}
+pw_test("pointer_test") {
+ deps = [
+ ":pointer",
+ ":pw_function",
+ ]
+ sources = [ "pointer_test.cc" ]
+}
+
+pw_test("scope_guard_test") {
+ sources = [ "scope_guard_test.cc" ]
+ deps = [
+ ":pw_function",
+ ":scope_guard",
+ ]
+}
+
pw_size_diff("function_size") {
title = "Pigweed function size report"
diff --git a/pw_function/CMakeLists.txt b/pw_function/CMakeLists.txt
index bdb0a2d..8b32db5 100644
--- a/pw_function/CMakeLists.txt
+++ b/pw_function/CMakeLists.txt
@@ -51,3 +51,40 @@
modules
pw_function
)
+
+pw_add_library(pw_function.pointer INTERFACE
+ HEADERS
+ public/pw_function/pointer.h
+ public/pw_function/internal/static_invoker.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_test(pw_function.pointer_test
+ SOURCES
+ pointer_test.cc
+ PRIVATE_DEPS
+ pw_function
+ pw_function.pointer
+ GROUPS
+ modules
+ pw_function
+)
+
+pw_add_library(pw_function.scope_guard INTERFACE
+ HEADERS
+ public/pw_function/scope_guard.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_test(pw_function.scope_guard_test
+ SOURCES
+ scope_guard_test.cc
+ PRIVATE_DEPS
+ pw_function
+ pw_function.scope_guard
+ GROUPS
+ modules
+ pw_function
+)
diff --git a/pw_function/docs.rst b/pw_function/docs.rst
index 92ec7af..18041ef 100644
--- a/pw_function/docs.rst
+++ b/pw_function/docs.rst
@@ -1,19 +1,20 @@
.. _module-pw_function:
------------
+===========
pw_function
------------
+===========
The ``pw_function`` module provides a standard, general-purpose API for
wrapping callable objects. ``pw_function`` is similar in spirit and API to
``std::function``, but doesn't allocate, and uses several tricks to prevent
code bloat.
+--------
Overview
-========
+--------
Basic usage
------------
-``pw_function`` defines the ``pw::Function`` class. A ``Function`` is a
+===========
+``pw_function`` defines the :cpp:type:`pw::Function` class. A ``Function`` is a
move-only callable wrapper constructable from any callable object. Functions
are templated on the signature of the callable they store.
@@ -53,9 +54,9 @@
function();
}
-``pw::Function``'s default constructor is ``constexpr``, so default-constructed
-functions may be used in classes with ``constexpr`` constructors and in
-``constinit`` expressions.
+:cpp:type:`pw::Function`'s default constructor is ``constexpr``, so
+default-constructed functions may be used in classes with ``constexpr``
+constructors and in ``constinit`` expressions.
.. code-block:: c++
@@ -71,16 +72,16 @@
constinit MyClass instance;
Storage
--------
+=======
By default, a ``Function`` stores its callable inline within the object. The
inline storage size defaults to the size of two pointers, but is configurable
through the build system. The size of a ``Function`` object is equivalent to its
inline storage size.
-The ``pw::InlineFunction`` alias is similar to ``pw::Function``, but is always
-inlined. That is, even if dynamic allocation is enabled for ``pw::Function`` -
-``pw::InlineFunction`` will fail to compile if the callable is larger than the
-inline storage size.
+The :cpp:type:`pw::InlineFunction` alias is similar to :cpp:type:`pw::Function`,
+but is always inlined. That is, even if dynamic allocation is enabled for
+:cpp:type:`pw::Function`, :cpp:type:`pw::InlineFunction` will fail to compile if
+the callable is larger than the inline storage size.
Attempting to construct a function from a callable larger than its inline size
is a compile-time error unless dynamic allocation is enabled.
@@ -117,15 +118,23 @@
``pw::InlineFunction`` can be used.
.. warning::
- If ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled then attempt to cast
- from ``pw::InlineFunction`` to a regular ``pw::Function`` will **ALWAYS**
- allocate memory.
+ If ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled then attempts to cast
+ from `:cpp:type:`pw::InlineFunction` to a regular :cpp:type:`pw::Function`
+ will **ALWAYS** allocate memory.
+---------
API usage
-=========
+---------
-``pw::Function`` function parameters
-------------------------------------
+Reference
+=========
+.. doxygentypedef:: pw::Function
+.. doxygentypedef:: pw::InlineFunction
+.. doxygentypedef:: pw::Callback
+.. doxygentypedef:: pw::InlineCallback
+
+``pw::Function`` as a function parameter
+========================================
When implementing an API which takes a callback, a ``Function`` can be used in
place of a function pointer or equivalent callable.
@@ -138,12 +147,13 @@
// signature template for clarity.
void DoTheThing(int arg, const pw::Function<void(int result)>& callback);
-``pw::Function`` is movable, but not copyable, so APIs must accept
-``pw::Function`` objects either by const reference (``const
+:cpp:type:`pw::Function` is movable, but not copyable, so APIs must accept
+:cpp:type:`pw::Function` objects either by const reference (``const
pw::Function<void()>&``) or rvalue reference (``const pw::Function<void()>&&``).
-If the ``pw::Function`` simply needs to be called, it should be passed by const
-reference. If the ``pw::Function`` needs to be stored, it should be passed as an
-rvalue reference and moved into a ``pw::Function`` variable as appropriate.
+If the :cpp:type:`pw::Function` simply needs to be called, it should be passed
+by const reference. If the :cpp:type:`pw::Function` needs to be stored, it
+should be passed as an rvalue reference and moved into a
+:cpp:type:`pw::Function` variable as appropriate.
.. code-block:: c++
@@ -159,38 +169,40 @@
stored_callback_ = std::move(callback);
}
-.. admonition:: Rules of thumb for passing a ``pw::Function`` to a function
+.. admonition:: Rules of thumb for passing a :cpp:type:`pw::Function` to a function
* **Pass by value**: Never.
- This results in unnecessary ``pw::Function`` instances and move operations.
+ This results in unnecessary :cpp:type:`pw::Function` instances and move
+ operations.
* **Pass by const reference** (``const pw::Function&``): When the
- ``pw::Function`` is only invoked.
+ :cpp:type:`pw::Function` is only invoked.
- When a ``pw::Function`` is called or inspected, but not moved, take a const
- reference to avoid copies and support temporaries.
+ When a :cpp:type:`pw::Function` is called or inspected, but not moved, take
+ a const reference to avoid copies and support temporaries.
* **Pass by rvalue reference** (``pw::Function&&``): When the
- ``pw::Function`` is moved.
+ :cpp:type:`pw::Function` is moved.
- When the function takes ownership of the ``pw::Function`` object, always
- use an rvalue reference (``pw::Function<void()>&&``) instead of a mutable
- lvalue reference (``pw::Function<void()>&``). An rvalue reference forces
- the caller to ``std::move`` when passing a preexisting ``pw::Function``
- variable, which makes the transfer of ownership explicit. It is possible to
- move-assign from an lvalue reference, but this fails to make it obvious to
- the caller that the object is no longer valid.
+ When the function takes ownership of the :cpp:type:`pw::Function` object,
+ always use an rvalue reference (``pw::Function<void()>&&``) instead of a
+ mutable lvalue reference (``pw::Function<void()>&``). An rvalue reference
+ forces the caller to ``std::move`` when passing a preexisting
+ :cpp:type:`pw::Function` variable, which makes the transfer of ownership
+ explicit. It is possible to move-assign from an lvalue reference, but this
+ fails to make it obvious to the caller that the object is no longer valid.
* **Pass by non-const reference** (``pw::Function&``): Rarely, when modifying
a variable.
Non-const references are only necessary when modifying an existing
- ``pw::Function`` variable. Use an rvalue reference instead if the
- ``pw::Function`` is moved into another variable.
+ :cpp:type:`pw::Function` variable. Use an rvalue reference instead if the
+ :cpp:type:`pw::Function` is moved into another variable.
Calling functions that use ``pw::Function``
--------------------------------------------
-A ``pw::Function`` can be implicitly constructed from any callback object. When
-calling an API that takes a ``pw::Function``, simply pass the callable object.
-There is no need to create an intermediate ``pw::Function`` object.
+===========================================
+A :cpp:type:`pw::Function` can be implicitly constructed from any callback
+object. When calling an API that takes a :cpp:type:`pw::Function`, simply pass
+the callable object. There is no need to create an intermediate
+:cpp:type:`pw::Function` object.
.. code-block:: c++
@@ -200,10 +212,10 @@
// Implicitly creates a pw::Function from a capturing lambda and stores it.
StoreTheCallback([this](int result) { result_ = result; });
-When working with an existing ``pw::Function`` variable, the variable can be
-passed directly to functions that take a const reference. If the function takes
-ownership of the ``pw::Function``, move the ``pw::Function`` variable at the
-call site.
+When working with an existing :cpp:type:`pw::Function` variable, the variable
+can be passed directly to functions that take a const reference. If the function
+takes ownership of the :cpp:type:`pw::Function`, move the
+:cpp:type:`pw::Function` variable at the call site.
.. code-block:: c++
@@ -213,42 +225,59 @@
// Takes ownership of the pw::Function.
void StoreTheCallback(std::move(my_function));
-Use ``pw::Callback`` for one-shot functions
--------------------------------------------
-``pw::Callback`` is a specialization of ``pw::Function`` that can only be called
-once. After a ``pw::Callback`` is called, the target function is destroyed. A
-``pw::Callback`` in the "already called" state has the same state as a
-``pw::Callback`` that has been assigned to nullptr.
+``pw::Callback`` for one-shot functions
+=======================================
+:cpp:type:`pw::Callback` is a specialization of :cpp:type:`pw::Function` that
+can only be called once. After a :cpp:type:`pw::Callback` is called, the target
+function is destroyed. A :cpp:type:`pw::Callback` in the "already called" state
+has the same state as a :cpp:type:`pw::Callback` that has been assigned to
+nullptr.
+Invoking ``pw::Function`` from a C-style API
+============================================
+.. doxygenfile:: pw_function/pointer.h
+ :sections: detaileddescription
+
+.. doxygenfunction:: GetFunctionPointer()
+.. doxygenfunction:: GetFunctionPointer(const FunctionType&)
+.. doxygenfunction:: GetFunctionPointerContextFirst()
+.. doxygenfunction:: GetFunctionPointerContextFirst(const FunctionType&)
+
+----------
+ScopeGuard
+----------
+.. doxygenclass:: pw::ScopeGuard
+ :members:
+
+------------
Size reports
-============
+------------
Function class
---------------
-The following size report compares an API using a ``pw::Function`` to a
+==============
+The following size report compares an API using a :cpp:type:`pw::Function` to a
traditional function pointer.
.. include:: function_size
Callable sizes
---------------
+==============
The table below demonstrates typical sizes of various callable types, which can
be used as a reference when sizing external buffers for ``Function`` objects.
.. include:: callable_size
+------
Design
-======
-``pw::Function`` is an alias of
-`fit::function <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_
-.
+------
+:cpp:type:`pw::Function` is an alias of
+`fit::function <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
+:cpp:type:`pw::Callback` is an alias of
+`fit::callback <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
-``pw::Callback`` is an alias of
-`fit::callback <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_
-.
-
+------
Zephyr
-======
+------
To enable ``pw_function` for Zephyr add ``CONFIG_PIGWEED_FUNCTION=y`` to the
project's configuration.
diff --git a/pw_function/pointer_test.cc b/pw_function/pointer_test.cc
new file mode 100644
index 0000000..4951db2
--- /dev/null
+++ b/pw_function/pointer_test.cc
@@ -0,0 +1,126 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_function/pointer.h"
+
+#include "gtest/gtest.h"
+#include "pw_function/function.h"
+
+namespace pw::function {
+namespace {
+
+extern "C" void pw_function_test_InvokeFromCApi(void (*function)(void* context),
+ void* context) {
+ function(context);
+}
+
+extern "C" int pw_function_test_Sum(int (*summer)(int a, int b, void* context),
+ void* context) {
+ return summer(60, 40, context);
+}
+
+TEST(StaticInvoker, Function_NoArguments) {
+ int value = 0;
+ Function<void()> set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+
+ pw_function_test_InvokeFromCApi(
+ GetFunctionPointer<decltype(set_value_to_42)>(), &set_value_to_42);
+
+ EXPECT_EQ(value, 84);
+}
+
+TEST(StaticInvoker, Function_WithArguments) {
+ int sum = 0;
+ Function<int(int, int)> sum_stuff([&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ });
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer(sum_stuff), &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 200);
+}
+
+TEST(StaticInvoker, Callback_NoArguments) {
+ int value = 0;
+ Callback<void()> set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+}
+
+TEST(StaticInvoker, Callback_WithArguments) {
+ int sum = 0;
+ Callback<int(int, int)> sum_stuff([&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ });
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+}
+
+TEST(StaticInvoker, Lambda_NoArguments) {
+ int value = 0;
+ auto set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+
+ pw_function_test_InvokeFromCApi(
+ GetFunctionPointer<decltype(set_value_to_42)>(), &set_value_to_42);
+
+ EXPECT_EQ(value, 84);
+}
+
+TEST(StaticInvoker, Lambda_WithArguments) {
+ int sum = 0;
+ auto sum_stuff = [&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ };
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer(sum_stuff), &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 200);
+}
+
+} // namespace
+} // namespace pw::function
diff --git a/pw_function/public/pw_function/function.h b/pw_function/public/pw_function/function.h
index 736e14c..99842b3 100644
--- a/pw_function/public/pw_function/function.h
+++ b/pw_function/public/pw_function/function.h
@@ -18,33 +18,34 @@
namespace pw {
-// pw::Function is a wrapper for an arbitrary callable object. It can be used by
-// callback-based APIs to allow callers to provide any type of callable.
-//
-// Example:
-//
-// template <typename T>
-// bool All(const pw::Vector<T>& items,
-// pw::Function<bool(const T& item)> predicate) {
-// for (const T& item : items) {
-// if (!predicate(item)) {
-// return false;
-// }
-// }
-// return true;
-// }
-//
-// bool ElementsArePositive(const pw::Vector<int>& items) {
-// return All(items, [](const int& i) { return i > 0; });
-// }
-//
-// bool IsEven(const int& i) { return i % 2 == 0; }
-//
-// bool ElementsAreEven(const pw::Vector<int>& items) {
-// return All(items, IsEven);
-// }
-//
-
+/// `pw::Function` is a wrapper for an arbitrary callable object. It can be used
+/// by callback-based APIs to allow callers to provide any type of callable.
+///
+/// Example:
+/// @code{.cpp}
+///
+/// template <typename T>
+/// bool All(const pw::Vector<T>& items,
+/// pw::Function<bool(const T& item)> predicate) {
+/// for (const T& item : items) {
+/// if (!predicate(item)) {
+/// return false;
+/// }
+/// }
+/// return true;
+/// }
+///
+/// bool ElementsArePositive(const pw::Vector<int>& items) {
+/// return All(items, [](const int& i) { return i > 0; });
+/// }
+///
+/// bool IsEven(const int& i) { return i % 2 == 0; }
+///
+/// bool ElementsAreEven(const pw::Vector<int>& items) {
+/// return All(items, IsEven);
+/// }
+///
+/// @endcode
template <typename Callable,
size_t inline_target_size =
function_internal::config::kInlineCallableSize>
@@ -53,14 +54,14 @@
/*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
Callable>;
-// Always inlined version of pw::Function.
-//
-// IMPORTANT: If pw::Function is configured to allow dynamic allocations then
-// any attempt to convert `pw::InlineFunction` to `pw::Function` will ALWAYS
-// allocate.
-//
+/// Version of `pw::Function` that exclusively uses inline storage.
+///
+/// IMPORTANT: If `pw::Function` is configured to allow dynamic allocations then
+/// any attempt to convert `pw::InlineFunction` to `pw::Function` will ALWAYS
+/// allocate.
+///
// TODO(b/252852651): Remove warning above when conversion from
-// fit::inline_function to fit::function doesn't allocate anymore.
+// `fit::inline_function` to `fit::function` doesn't allocate anymore.
template <typename Callable,
size_t inline_target_size =
function_internal::config::kInlineCallableSize>
@@ -68,16 +69,16 @@
using Closure = Function<void()>;
-// pw::Callback is identical to pw::Function except:
-//
-// 1) On the first call to invoke a `pw::Callback`, the target function held
-// by the `pw::Callback` cannot be called again.
-// 2) When a `pw::Callback` is invoked for the first time, the target function
-// is released and destructed, along with any resources owned by that
-// function (typically the objects captured by a lambda).
-
-// A `pw::Callback` in the "already called" state has the same state as a
-// `pw::Callback` that has been assigned to `nullptr`.
+/// `pw::Callback` is identical to @cpp_type{pw::Function} except:
+///
+/// 1. On the first call to invoke a `pw::Callback`, the target function held
+/// by the `pw::Callback` cannot be called again.
+/// 2. When a `pw::Callback` is invoked for the first time, the target function
+/// is released and destructed, along with any resources owned by that
+/// function (typically the objects captured by a lambda).
+///
+/// A `pw::Callback` in the "already called" state has the same state as a
+/// `pw::Callback` that has been assigned to `nullptr`.
template <typename Callable,
size_t inline_target_size =
function_internal::config::kInlineCallableSize>
@@ -86,6 +87,7 @@
/*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
Callable>;
+/// Version of `pw::Callback` that exclusively uses inline storage.
template <typename Callable,
size_t inline_target_size =
function_internal::config::kInlineCallableSize>
diff --git a/pw_function/public/pw_function/internal/static_invoker.h b/pw_function/public/pw_function/internal/static_invoker.h
new file mode 100644
index 0000000..73605af
--- /dev/null
+++ b/pw_function/public/pw_function/internal/static_invoker.h
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// This code is in its own header since Doxygen / Breathe can't parse it.
+
+namespace pw::function::internal {
+
+template <typename FunctionType, typename CallOperatorType>
+struct StaticInvoker;
+
+template <typename FunctionType, typename Return, typename... Args>
+struct StaticInvoker<FunctionType, Return (FunctionType::*)(Args...)> {
+ // Invoker function with the context argument last to match libc. Could add a
+ // version with the context first if needed.
+ static Return InvokeWithContextLast(Args... args, void* context) {
+ return static_cast<FunctionType*>(context)->operator()(
+ std::forward<Args>(args)...);
+ }
+
+ static Return InvokeWithContextFirst(void* context, Args... args) {
+ return static_cast<FunctionType*>(context)->operator()(
+ std::forward<Args>(args)...);
+ }
+};
+
+// Make the const version identical to the non-const version.
+template <typename FunctionType, typename Return, typename... Args>
+struct StaticInvoker<FunctionType, Return (FunctionType::*)(Args...) const>
+ : StaticInvoker<FunctionType, Return (FunctionType::*)(Args...)> {};
+
+} // namespace pw::function::internal
diff --git a/pw_function/public/pw_function/pointer.h b/pw_function/public/pw_function/pointer.h
new file mode 100644
index 0000000..8915eb0
--- /dev/null
+++ b/pw_function/public/pw_function/pointer.h
@@ -0,0 +1,97 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+/// @file pw_function/pointer.h
+///
+/// Traditional callback APIs often use a function pointer and `void*` context
+/// argument. The context argument makes it possible to use the callback
+/// function with non-global data. For example, the `qsort_s` and `bsearch_s`
+/// functions take a pointer to a comparison function that has `void*` context
+/// as its last parameter. @cpp_type{pw::Function} does not naturally work with
+/// these kinds of APIs.
+///
+/// The functions below make it simple to adapt a @cpp_type{pw::Function} for
+/// use with APIs that accept a function pointer and `void*` context argument.
+
+#include <utility>
+
+#include "pw_function/internal/static_invoker.h"
+
+namespace pw::function {
+
+/// Returns a function pointer that invokes a `pw::Function`, lambda, or other
+/// callable object from a `void*` context argument. This makes it possible to
+/// use C++ callables with C-style APIs that take a function pointer and `void*`
+/// context.
+///
+/// The returned function pointer has the same return type and arguments as the
+/// `pw::Function` or `pw::Callback`, except that the last parameter is a
+/// `void*`. `GetFunctionPointerContextFirst` places the `void*` context
+/// parameter first.
+///
+/// The following example adapts a C++ lambda function for use with C-style API
+/// that takes an `int (*)(int, void*)` function and a `void*` context.
+///
+/// @code{.cpp}
+///
+/// void TakesAFunctionPointer(int (*function)(int, void*), void* context);
+///
+/// void UseFunctionPointerApiWithPwFunction() {
+/// // Declare a callable object so a void* pointer can be obtained for it.
+/// auto my_function = [captures](int value) {
+/// // ...
+/// return value + captures;
+/// };
+///
+/// // Invoke the API with the function pointer and callable pointer.
+/// TakesAFunctionPointer(pw::function::GetFunctionPointer(my_function),
+/// &my_function);
+/// }
+///
+/// @endcode
+///
+/// The function returned from this must ONLY be used with the exact type for
+/// which it was created! Function pointer / context APIs are not type safe.
+template <typename FunctionType>
+constexpr auto GetFunctionPointer() {
+ return internal::StaticInvoker<
+ FunctionType,
+ decltype(&FunctionType::operator())>::InvokeWithContextLast;
+}
+
+/// `GetFunctionPointer` overload that uses the type of the function passed to
+/// this call.
+template <typename FunctionType>
+constexpr auto GetFunctionPointer(const FunctionType&) {
+ return GetFunctionPointer<FunctionType>();
+}
+
+/// Same as `GetFunctionPointer`, but the context argument is passed first.
+/// Returns a `void(void*, int)` function for a `pw::Function<void(int)>`.
+template <typename FunctionType>
+constexpr auto GetFunctionPointerContextFirst() {
+ return internal::StaticInvoker<
+ FunctionType,
+ decltype(&FunctionType::operator())>::InvokeWithContextFirst;
+}
+
+/// `GetFunctionPointerContextFirst` overload that uses the type of the function
+/// passed to this call.
+template <typename FunctionType>
+constexpr auto GetFunctionPointerContextFirst(const FunctionType&) {
+ return GetFunctionPointerContextFirst<FunctionType>();
+}
+
+} // namespace pw::function
diff --git a/pw_function/public/pw_function/scope_guard.h b/pw_function/public/pw_function/scope_guard.h
new file mode 100644
index 0000000..ea790ad
--- /dev/null
+++ b/pw_function/public/pw_function/scope_guard.h
@@ -0,0 +1,87 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <utility>
+
+namespace pw {
+
+/// `ScopeGuard` ensures that the specified functor is executed no matter how
+/// the current scope exits, unless it is dismissed.
+///
+/// Example:
+///
+/// @code
+/// pw::Status SomeFunction() {
+/// PW_TRY(OperationOne());
+/// ScopeGuard undo_operation_one(UndoOperationOne);
+/// PW_TRY(OperationTwo());
+/// ScopeGuard undo_operation_two(UndoOperationTwo);
+/// PW_TRY(OperationThree());
+/// undo_operation_one.Dismiss();
+/// undo_operation_two.Dismiss();
+/// return pw::OkStatus();
+/// }
+/// @endcode
+template <typename Functor>
+class ScopeGuard {
+ public:
+ constexpr ScopeGuard(Functor&& functor)
+ : functor_(std::forward<Functor>(functor)), dismissed_(false) {}
+
+ constexpr ScopeGuard(ScopeGuard&& other) noexcept
+ : functor_(std::move(other.functor_)), dismissed_(other.dismissed_) {
+ other.dismissed_ = true;
+ }
+
+ template <typename OtherFunctor>
+ constexpr ScopeGuard(ScopeGuard<OtherFunctor>&& other)
+ : functor_(std::move(other.functor_)), dismissed_(other.dismissed_) {
+ other.dismissed_ = true;
+ }
+
+ ~ScopeGuard() {
+ if (dismissed_) {
+ return;
+ }
+ functor_();
+ }
+
+ ScopeGuard& operator=(ScopeGuard&& other) noexcept {
+ functor_ = std::move(other.functor_);
+ dismissed_ = std::move(other.dismissed_);
+ other.dismissed_ = true;
+ }
+
+ ScopeGuard() = delete;
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ /// Dismisses the `ScopeGuard`, meaning it will no longer execute the
+ /// `Functor` when it goes out of scope.
+ void Dismiss() { dismissed_ = true; }
+
+ private:
+ template <typename OtherFunctor>
+ friend class ScopeGuard;
+
+ Functor functor_;
+ bool dismissed_;
+};
+
+// Enable type deduction for a compatible function pointer.
+template <typename Function>
+ScopeGuard(Function()) -> ScopeGuard<Function (*)()>;
+
+} // namespace pw
diff --git a/pw_function/scope_guard_test.cc b/pw_function/scope_guard_test.cc
new file mode 100644
index 0000000..404293a
--- /dev/null
+++ b/pw_function/scope_guard_test.cc
@@ -0,0 +1,88 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_function/scope_guard.h"
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "pw_function/function.h"
+
+namespace pw {
+namespace {
+
+TEST(ScopeGuard, ExecutesLambda) {
+ bool executed = false;
+ {
+ ScopeGuard guarded_lambda([&] { executed = true; });
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+static bool static_executed = false;
+void set_static_executed() { static_executed = true; }
+
+TEST(ScopeGuard, ExecutesFunction) {
+ {
+ ScopeGuard guarded_function(set_static_executed);
+
+ EXPECT_FALSE(static_executed);
+ }
+ EXPECT_TRUE(static_executed);
+}
+
+TEST(ScopeGuard, ExecutesPwFunction) {
+ bool executed = false;
+ pw::Function<void()> pw_function([&]() { executed = true; });
+ {
+ ScopeGuard guarded_pw_function(std::move(pw_function));
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+TEST(ScopeGuard, Dismiss) {
+ bool executed = false;
+ {
+ ScopeGuard guard([&] { executed = true; });
+ EXPECT_FALSE(executed);
+ guard.Dismiss();
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_FALSE(executed);
+}
+
+TEST(ScopeGuard, MoveConstructor) {
+ bool executed = false;
+ ScopeGuard first_guard([&] { executed = true; });
+ {
+ ScopeGuard second_guard(std::move(first_guard));
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+TEST(ScopeGuard, MoveOperator) {
+ bool executed = false;
+ ScopeGuard first_guard([&] { executed = true; });
+ {
+ ScopeGuard second_guard = std::move(first_guard);
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_fuzzer/BUILD.gn b/pw_fuzzer/BUILD.gn
index cef173d..142b761 100644
--- a/pw_fuzzer/BUILD.gn
+++ b/pw_fuzzer/BUILD.gn
@@ -17,32 +17,39 @@
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_fuzzer/fuzzer.gni")
-import("$dir_pw_fuzzer/oss_fuzz.gni")
config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
}
-# This is added automatically by the `pw_fuzzer` template.
-config("fuzzing") {
- common_flags = [ "-fsanitize=fuzzer" ]
- cflags = common_flags
- ldflags = common_flags
+# Add flags for adding LLVM sanitizer coverage for fuzzing. This is added by
+# the host_clang_fuzz toolchains.
+config("instrumentation") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz manipulates compiler flags directly. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+ cflags_c = string_split(getenv("CFLAGS"))
+ cflags_cc = string_split(getenv("CXXFLAGS"))
+
+ # OSS-Fuzz sets "-stdlib=libc++", which conflicts with the "-nostdinc++" set
+ # by `pw_minimal_cpp_stdlib`.
+ cflags_cc += [ "-Wno-unused-command-line-argument" ]
+ } else {
+ cflags = [ "-fsanitize=fuzzer-no-link" ]
+ }
}
-# OSS-Fuzz needs to be able to specify its own compilers and add flags.
-config("oss_fuzz") {
- # OSS-Fuzz doesn't always link with -fsanitize=fuzzer, sometimes it uses
- #-fsanitize=fuzzer-no-link and provides the fuzzing engine explicitly to be
- # passed to the linker.
- ldflags = [ getenv("LIB_FUZZING_ENGINE") ]
-}
-
-config("oss_fuzz_extra") {
- cflags_c = oss_fuzz_extra_cflags_c
- cflags_cc = oss_fuzz_extra_cflags_cc
- ldflags = oss_fuzz_extra_ldflags
+# Add flags for linking against compiler-rt's libFuzzer. This is added
+# automatically by `pw_fuzzer`.
+config("engine") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz manipulates linker flags directly. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+ ldflags = string_split(getenv("LDFLAGS")) + [ getenv("LIB_FUZZING_ENGINE") ]
+ } else {
+ ldflags = [ "-fsanitize=fuzzer" ]
+ }
}
pw_source_set("pw_fuzzer") {
@@ -81,12 +88,15 @@
pw_fuzzer("toy_fuzzer") {
sources = [ "examples/toy_fuzzer.cc" ]
deps = [
- dir_pw_result,
- dir_pw_span,
- dir_pw_string,
+ ":pw_fuzzer",
+ dir_pw_status,
]
}
pw_test_group("tests") {
- tests = [ ":toy_fuzzer" ]
+ tests = [ ":toy_fuzzer_test" ]
+}
+
+group("fuzzers") {
+ deps = [ ":toy_fuzzer" ]
}
diff --git a/pw_fuzzer/docs.rst b/pw_fuzzer/docs.rst
index e52b9f1..02bf05f 100644
--- a/pw_fuzzer/docs.rst
+++ b/pw_fuzzer/docs.rst
@@ -68,8 +68,9 @@
To build a fuzzer, do the following:
-1. Add the GN target using ``pw_fuzzer`` GN template, and add it to your the
- test group of the module:
+1. Add the GN target to the module using ``pw_fuzzer`` GN template. If you wish
+ to limit when the generated unit test is run, you can set `enable_test_if` in
+ the same manner as `enable_if` for `pw_test`:
.. code::
@@ -79,25 +80,65 @@
pw_fuzzer("my_fuzzer") {
sources = [ "my_fuzzer.cc" ]
deps = [ ":my_lib" ]
+ enable_test_if = device_has_1m_flash
}
+2. Add the generated unit test to the module's test group. This test verifies
+ the fuzzer can build and run, even when not being built in a fuzzing
+ toolchain.
+
+.. code::
+
+ # In $dir_my_module/BUILD.gn
pw_test_group("tests") {
tests = [
- ":existing_tests", ...
- ":my_fuzzer", # <- Added!
+ ...
+ ":my_fuzzer_test",
]
}
-2. Select your choice of sanitizers ("address" is also the current default).
+3. If your module does not already have a group of fuzzers, add it and include
+ it in the top level fuzzers target. Depending on your project, the specific
+ toolchain may differ. Fuzzer toolchains are those with
+ ``pw_toolchain_FUZZING_ENABLED`` set to true. Examples include
+ ``host_clang_fuzz`` and any toolchains that extend it.
+
+.. code::
+
+ # In //BUILD.gn
+ group("fuzzers") {
+ deps = [
+ ...
+ "$dir_my_module:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ ]
+ }
+
+4. Add your fuzzer to the module's group of fuzzers.
+
+.. code::
+
+ group("fuzzers") {
+ deps = [
+ ...
+ ":my_fuzzer",
+ ]
+ }
+
+5. If desired, select a sanitizer runtime. By default,
+ `//targets/host:host_clang_fuzz` uses "address" if no sanitizer is specified.
See LLVM for `valid options`_.
.. code:: sh
$ gn gen out --args='pw_toolchain_SANITIZERS=["address"]'
-3. Build normally, e.g. using ``pw watch``.
+6. Build the fuzzers!
-.. _run:
+.. code:: sh
+
+ $ ninja -C out fuzzers
+
+.. _bazel:
Building and running fuzzers with Bazel
=======================================
@@ -142,6 +183,8 @@
bazel test //my_module:my_fuzz_test --config asan-libfuzzer
+.. _run:
+
Running fuzzers locally
=======================
diff --git a/pw_fuzzer/examples/toy_fuzzer.cc b/pw_fuzzer/examples/toy_fuzzer.cc
index 2576e2a..99b04df 100644
--- a/pw_fuzzer/examples/toy_fuzzer.cc
+++ b/pw_fuzzer/examples/toy_fuzzer.cc
@@ -21,70 +21,32 @@
#include <cstddef>
#include <cstdint>
-#include <cstring>
+#include <string_view>
-#include "pw_result/result.h"
-#include "pw_span/span.h"
-#include "pw_string/util.h"
+#include "pw_fuzzer/fuzzed_data_provider.h"
+#include "pw_status/status.h"
+namespace pw::fuzzer::example {
namespace {
// The code to fuzz. This would normally be in separate library.
-void toy_example(const char* word1, const char* word2) {
- bool greeted = false;
- if (word1[0] == 'h') {
- if (word1[1] == 'e') {
- if (word1[2] == 'l') {
- if (word1[3] == 'l') {
- if (word1[4] == 'o') {
- greeted = true;
- }
- }
- }
+Status SomeAPI(std::string_view s1, std::string_view s2) {
+ if (s1 == "hello") {
+ if (s2 == "world") {
+ abort();
}
}
- if (word2[0] == 'w') {
- if (word2[1] == 'o') {
- if (word2[2] == 'r') {
- if (word2[3] == 'l') {
- if (word2[4] == 'd') {
- if (greeted) {
- // Our "defect", simulating a crash.
- __builtin_trap();
- }
- }
- }
- }
- }
- }
+ return OkStatus();
}
} // namespace
+} // namespace pw::fuzzer::example
// The fuzz target function
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- // We want to split our input into two strings.
- const pw::span<const char> input(reinterpret_cast<const char*>(data), size);
-
- // If that's not feasible, toss this input. The fuzzer will quickly learn that
- // inputs without null-terminators are uninteresting.
- const pw::Result<size_t> possible_word1_size =
- pw::string::NullTerminatedLength(input);
- if (!possible_word1_size.ok()) {
- return 0;
- }
- const pw::span<const char> word1 =
- input.first(possible_word1_size.value() + 1);
-
- // Actually, inputs without TWO null terminators are uninteresting.
- pw::span<const char> remaining_input = input.subspan(word1.size());
- if (!pw::string::NullTerminatedLength(remaining_input).ok()) {
- return 0;
- }
-
- // Call the code we're targeting!
- toy_example(word1.data(), remaining_input.data());
-
- // By convention, the fuzzer always returns zero.
+ FuzzedDataProvider provider(data, size);
+ std::string s1 = provider.ConsumeRandomLengthString();
+ std::string s2 = provider.ConsumeRemainingBytesAsString();
+ pw::fuzzer::example::SomeAPI(s1, s2).IgnoreError();
return 0;
}
diff --git a/pw_fuzzer/fuzzer.gni b/pw_fuzzer/fuzzer.gni
index beb3a1c..a43a2d9 100644
--- a/pw_fuzzer/fuzzer.gni
+++ b/pw_fuzzer/fuzzer.gni
@@ -14,10 +14,11 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/error.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_unit_test/test.gni")
-# Creates a libFuzzer-based fuzzer executable target.
+# Creates a libFuzzer-based fuzzer executable target and unit test
#
# This will link `sources` and `deps` with the libFuzzer compiler runtime. The
# `sources` and `deps` should include a definition of the standard LLVM fuzz
@@ -25,82 +26,47 @@
# //pw_fuzzer/docs.rst
# https://llvm.org/docs/LibFuzzer.html
#
+# Additionally, this creates a unit test that does not generate fuzzer inputs
+# and simply executes the fuzz target function with fixed inputs. This is useful
+# for verifying the fuzz target function compiles, links, and runs even when not
+# using a fuzzing-capable host or toolchain.
+#
+# Args:
+# - enable_test_if: (optional) Passed as `enable_if` to the unit test.
+# Remaining arguments are the same as `pw_executable`.
+#
template("pw_fuzzer") {
- # This currently is ONLY supported on Linux and Mac using clang (debug).
- # TODO(pwbug/179): Add Windows here after testing.
- fuzzing_platforms = [
- "linux",
- "mac",
- ]
-
- fuzzing_toolchains =
- [ get_path_info("$dir_pigweed/targets/host:host_clang_fuzz", "abspath") ]
-
- # This is how GN says 'elem in list':
- can_fuzz = fuzzing_platforms + [ host_os ] - [ host_os ] != fuzzing_platforms
-
- can_fuzz = fuzzing_toolchains + [ current_toolchain ] -
- [ current_toolchain ] != fuzzing_toolchains && can_fuzz
-
- if (can_fuzz && pw_toolchain_SANITIZERS != []) {
- # Build the actual fuzzer using the fuzzing config.
- pw_executable(target_name) {
- forward_variables_from(invoker, "*", [ "visibility" ])
- forward_variables_from(invoker, [ "visibility" ])
-
- if (!defined(deps)) {
- deps = []
- }
- deps += [ dir_pw_fuzzer ]
-
- if (!defined(configs)) {
- configs = []
- }
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- configs += [ "$dir_pw_fuzzer:oss_fuzz" ]
- } else {
- configs += [ "$dir_pw_fuzzer:fuzzing" ]
- }
-
- _fuzzer_output_dir = "${target_out_dir}/bin"
- if (defined(invoker.output_dir)) {
- _fuzzer_output_dir = invoker.output_dir
- }
- output_dir = _fuzzer_output_dir
-
- # Metadata for this fuzzer when used as part of a pw_test_group target.
- metadata = {
- tests = [
- {
- type = "fuzzer"
- test_name = target_name
- test_directory = rebase_path(output_dir, root_build_dir)
- },
- ]
- }
+ if (!pw_toolchain_FUZZING_ENABLED) {
+ pw_error(target_name) {
+ message_lines = [ "Toolchain does not enable fuzzing." ]
}
-
- # No-op target to satisfy `pw_test_group`. It is empty as we don't want to
- # automatically run fuzzers.
- group(target_name + ".run") {
+ not_needed(invoker, "*")
+ } else if (pw_toolchain_SANITIZERS == []) {
+ pw_error(target_name) {
+ message_lines = [ "No sanitizer runtime set." ]
}
-
- # No-op target to satisfy `pw_test`. It is empty as we don't need a separate
- # lib target.
- group(target_name + ".lib") {
- }
+ not_needed(invoker, "*")
} else {
- # Build a unit test that exercise the fuzz target function.
- pw_test(target_name) {
- # TODO(b/234891784): Re-enable when there's better configurability for
- # on-device fuzz testing.
- enable_if = false
- sources = []
+ pw_executable(target_name) {
+ configs = []
deps = []
- forward_variables_from(invoker, "*", [ "visibility" ])
+ forward_variables_from(invoker,
+ "*",
+ [
+ "enable_test_if",
+ "visibility",
+ ])
forward_variables_from(invoker, [ "visibility" ])
- sources += [ "$dir_pw_fuzzer/pw_fuzzer_disabled.cc" ]
- deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
+ configs += [ "$dir_pw_fuzzer:engine" ]
+ deps += [ dir_pw_fuzzer ]
}
}
+
+ pw_test("${target_name}_test") {
+ deps = []
+ forward_variables_from(invoker, "*", [ "visibility" ])
+ forward_variables_from(invoker, [ "visibility" ])
+ deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
+ enable_if = !defined(enable_test_if) || enable_test_if
+ }
}
diff --git a/pw_fuzzer/oss_fuzz.gni b/pw_fuzzer/oss_fuzz.gni
deleted file mode 100644
index 3a21438..0000000
--- a/pw_fuzzer/oss_fuzz.gni
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright 2019 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-# TODO(aarongreen): Do some minimal parsing on the environment variables to
-# identify conflicting configs.
-oss_fuzz_extra_cflags_c = string_split(getenv("CFLAGS"))
-oss_fuzz_extra_cflags_cc = string_split(getenv("CXXFLAGS"))
-oss_fuzz_extra_ldflags = string_split(getenv("LDFLAGS"))
-
-# TODO(pwbug/184): OSS-Fuzz sets -stdlib=libc++, but pw_minimal_cpp_stdlib
-# sets -nostdinc++. Find a more flexible mechanism to achieve this and
-# similar needs (like removing -fno-rtti fro UBSan).
-oss_fuzz_extra_cflags_cc += [ "-Wno-unused-command-line-argument" ]
diff --git a/pw_hdlc/encoder_test.cc b/pw_hdlc/encoder_test.cc
index df7a125..3f23c3d 100644
--- a/pw_hdlc/encoder_test.cc
+++ b/pw_hdlc/encoder_test.cc
@@ -46,11 +46,11 @@
class WriteUnnumberedFrame : public ::testing::Test {
protected:
- WriteUnnumberedFrame() : writer_(buffer_) {}
+ WriteUnnumberedFrame() : buffer_{}, writer_(buffer_) {}
- stream::MemoryWriter writer_;
// Allocate a buffer that will fit any 7-byte payload.
std::array<byte, MaxEncodedFrameSize(7)> buffer_;
+ stream::MemoryWriter writer_;
};
constexpr byte kUnnumberedControl = byte{0x3};
diff --git a/pw_hdlc/rpc_example/example_script.py b/pw_hdlc/rpc_example/example_script.py
index ec071e9..36a3cf8 100755
--- a/pw_hdlc/rpc_example/example_script.py
+++ b/pw_hdlc/rpc_example/example_script.py
@@ -18,7 +18,7 @@
import os
from pathlib import Path
-import serial # type: ignore
+import serial
from pw_hdlc.rpc import HdlcRpcClient, default_channels
diff --git a/pw_ide/py/cpp_test.py b/pw_ide/py/cpp_test.py
index d0d9e70..87d57f3 100644
--- a/pw_ide/py/cpp_test.py
+++ b/pw_ide/py/cpp_test.py
@@ -808,7 +808,6 @@
with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
cache_symlink_path
):
-
# Set the second target, which should replace the symlinks
CppIdeFeaturesState(settings).current_target = targets[1]
@@ -868,7 +867,6 @@
with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
cache_symlink_path
):
-
# Set the second target, which should replace the symlinks
CppIdeFeaturesState(settings).current_target = targets[1]
diff --git a/pw_ide/py/pw_ide/editors.py b/pw_ide/py/pw_ide/editors.py
index 311f9a9..f59e353 100644
--- a/pw_ide/py/pw_ide/editors.py
+++ b/pw_ide/py/pw_ide/editors.py
@@ -123,14 +123,14 @@
# Allows constraining to dicts and dict subclasses, while also constraining to
# the *same* dict subclass.
-_TDictLike = TypeVar('_TDictLike', bound=Dict)
+_DictLike = TypeVar('_DictLike', bound=Dict)
def dict_deep_merge(
- src: _TDictLike,
- dest: _TDictLike,
- ctor: Optional[Callable[[], _TDictLike]] = None,
-) -> _TDictLike:
+ src: _DictLike,
+ dest: _DictLike,
+ ctor: Optional[Callable[[], _DictLike]] = None,
+) -> _DictLike:
"""Deep merge dict-like `src` into dict-like `dest`.
`dest` is mutated in place and also returned.
@@ -480,20 +480,20 @@
# name of that settings file, without the extension.
# TODO(chadnorvell): Would be great to constrain this to enums, but bound=
# doesn't do what we want with Enum or EnumMeta.
-_TSettingsType = TypeVar('_TSettingsType')
+_SettingsTypeT = TypeVar('_SettingsTypeT')
# Maps each settings type with the callback that generates the default settings
# for that settings type.
-EditorSettingsTypesWithDefaults = Dict[_TSettingsType, DefaultSettingsCallback]
+EditorSettingsTypesWithDefaults = Dict[_SettingsTypeT, DefaultSettingsCallback]
-class EditorSettingsManager(Generic[_TSettingsType]):
+class EditorSettingsManager(Generic[_SettingsTypeT]):
"""Manages all settings for a particular editor.
This is where you interact with an editor's settings (actually in a
subclass of this class, not here). Initializing this class sets up access
to one or more settings files for an editor (determined by
- ``_TSettingsType``, fulfilled by an enum that defines each of an editor's
+ ``_SettingsTypeT``, fulfilled by an enum that defines each of an editor's
settings files), along with the cascading settings levels.
"""
@@ -509,7 +509,7 @@
# These must be overridden in child classes.
default_settings_dir: Path = None # type: ignore
file_format: _StructuredFileFormat = _StructuredFileFormat()
- types_with_defaults: EditorSettingsTypesWithDefaults[_TSettingsType] = {}
+ types_with_defaults: EditorSettingsTypesWithDefaults[_SettingsTypeT] = {}
def __init__(
self,
@@ -517,7 +517,7 @@
settings_dir: Optional[Path] = None,
file_format: Optional[_StructuredFileFormat] = None,
types_with_defaults: Optional[
- EditorSettingsTypesWithDefaults[_TSettingsType]
+ EditorSettingsTypesWithDefaults[_SettingsTypeT]
] = None,
):
if SettingsLevel.ACTIVE in self.__class__.prefixes:
@@ -564,7 +564,7 @@
# each settings type. Those settings definitions may be stored in files
# or not.
self._settings_definitions: Dict[
- SettingsLevel, Dict[_TSettingsType, EditorSettingsDefinition]
+ SettingsLevel, Dict[_SettingsTypeT, EditorSettingsDefinition]
] = {}
self._settings_types = tuple(self._types_with_defaults.keys())
@@ -597,19 +597,19 @@
self._settings_dir, name, self._file_format
)
- def default(self, settings_type: _TSettingsType):
+ def default(self, settings_type: _SettingsTypeT):
"""Default settings for the provided settings type."""
return self._settings_definitions[SettingsLevel.DEFAULT][settings_type]
- def project(self, settings_type: _TSettingsType):
+ def project(self, settings_type: _SettingsTypeT):
"""Project settings for the provided settings type."""
return self._settings_definitions[SettingsLevel.PROJECT][settings_type]
- def user(self, settings_type: _TSettingsType):
+ def user(self, settings_type: _SettingsTypeT):
"""User settings for the provided settings type."""
return self._settings_definitions[SettingsLevel.USER][settings_type]
- def active(self, settings_type: _TSettingsType):
+ def active(self, settings_type: _SettingsTypeT):
"""Active settings for the provided settings type."""
return self._settings_definitions[SettingsLevel.ACTIVE][settings_type]
diff --git a/pw_ide/py/setup.cfg b/pw_ide/py/setup.cfg
index b166ce1..c7334c3 100644
--- a/pw_ide/py/setup.cfg
+++ b/pw_ide/py/setup.cfg
@@ -21,7 +21,7 @@
[options]
packages = find:
install_requires =
- json5 ==0.9.10
+ json5>=0.9.10
[options.entry_points]
console_scripts = pw-ide = pw_ide.__main__:main
diff --git a/pw_log/protobuf.rst b/pw_log/protobuf.rst
index c65171f..ff7335f 100644
--- a/pw_log/protobuf.rst
+++ b/pw_log/protobuf.rst
@@ -89,11 +89,14 @@
provided in the ``pw_log/proto_utils.h`` header. Separate helpers are provided
for encoding tokenized logs and string-based logs.
+The following example shows a :c:func:`pw_log_tokenized_HandleLog`
+implementation that encodes the results to a protobuf.
+
.. code-block:: cpp
#include "pw_log/proto_utils.h"
- extern "C" void pw_log_tokenized_HandleLog((
+ extern "C" void pw_log_tokenized_HandleLog(
uint32_t payload, const uint8_t data[], size_t size) {
pw::log_tokenized::Metadata metadata(payload);
std::byte log_buffer[kLogBufferSize];
diff --git a/pw_log_rpc/docs.rst b/pw_log_rpc/docs.rst
index 4a9db13..0b30356 100644
--- a/pw_log_rpc/docs.rst
+++ b/pw_log_rpc/docs.rst
@@ -28,9 +28,8 @@
3. Connect the tokenized logging handler to the MultiSink
---------------------------------------------------------
Create a :ref:`MultiSink <module-pw_multisink>` instance to buffer log entries.
-Then, make the log backend handler, :cpp:func:`pw_log_tokenized_HandleLog`,
-encode log entries in the ``log::LogEntry`` format, and add them to the
-``MultiSink``.
+Then, make the log backend handler, :c:func:`pw_log_tokenized_HandleLog`, encode
+log entries in the ``log::LogEntry`` format, and add them to the ``MultiSink``.
4. Create log drains and filters
--------------------------------
diff --git a/pw_log_rpc/log_filter_service_test.cc b/pw_log_rpc/log_filter_service_test.cc
index fe8db8f..942d371 100644
--- a/pw_log_rpc/log_filter_service_test.cc
+++ b/pw_log_rpc/log_filter_service_test.cc
@@ -41,7 +41,6 @@
FilterServiceTest() : filter_map_(filters_) {}
protected:
- FilterMap filter_map_;
static constexpr size_t kMaxFilterRules = 4;
std::array<Filter::Rule, kMaxFilterRules> rules1_;
std::array<Filter::Rule, kMaxFilterRules> rules2_;
@@ -58,6 +57,7 @@
Filter(filter_id2_, rules2_),
Filter(filter_id3_, rules3_),
};
+ FilterMap filter_map_;
};
TEST_F(FilterServiceTest, GetFilterIds) {
diff --git a/pw_log_tokenized/BUILD.gn b/pw_log_tokenized/BUILD.gn
index cc1b742..3e2b161 100644
--- a/pw_log_tokenized/BUILD.gn
+++ b/pw_log_tokenized/BUILD.gn
@@ -42,20 +42,12 @@
# This target provides the backend for pw_log.
pw_source_set("pw_log_tokenized") {
- public_configs = [
- ":backend_config",
- ":public_include_path",
- ]
+ public_configs = [ ":backend_config" ]
public_deps = [
- ":config",
":handler.facade", # Depend on the facade to avoid circular dependencies.
- ":metadata",
- dir_pw_tokenizer,
+ ":headers",
]
- public = [
- "public/pw_log_tokenized/log_tokenized.h",
- "public_overrides/pw_log_backend/log_backend.h",
- ]
+ public = [ "public_overrides/pw_log_backend/log_backend.h" ]
sources = [ "log_tokenized.cc" ]
}
@@ -65,6 +57,22 @@
visibility = [ ":*" ]
}
+pw_source_set("headers") {
+ visibility = [ ":*" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":config",
+ ":metadata",
+
+ # TODO(hepler): Remove this dependency when all projects have migrated to
+ # the new pw_log_tokenized handler.
+ "$dir_pw_tokenizer:global_handler_with_payload",
+ dir_pw_preprocessor,
+ dir_pw_tokenizer,
+ ]
+ public = [ "public/pw_log_tokenized/log_tokenized.h" ]
+}
+
# The old pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND backend may still be
# in use by projects that have not switched to the new pw_log_tokenized facade.
# Use the old backend as a stand-in for the new backend if it is set.
@@ -156,16 +164,10 @@
}
pw_test_group("tests") {
- tests = [ ":metadata_test" ]
-
- # TODO(b/269354373): The Windows MinGW compiler fails to link
- # log_tokenized_test.cc because log_tokenized.cc refers to the log handler,
- # which is not defined, even though _pw_log_tokenized_EncodeTokenizedLog is
- # never called. Remove this check when the Windows build is consistent with
- # other builds.
- if (current_os != "win" || _new_backend_is_set || _old_backend_is_set) {
- tests += [ ":log_tokenized_test" ]
- }
+ tests = [
+ ":log_tokenized_test",
+ ":metadata_test",
+ ]
}
pw_test("log_tokenized_test") {
@@ -175,7 +177,7 @@
"pw_log_tokenized_private/test_utils.h",
]
deps = [
- ":pw_log_tokenized",
+ ":headers",
dir_pw_preprocessor,
]
}
diff --git a/pw_log_tokenized/docs.rst b/pw_log_tokenized/docs.rst
index 662c1df..24a6d8d 100644
--- a/pw_log_tokenized/docs.rst
+++ b/pw_log_tokenized/docs.rst
@@ -11,15 +11,10 @@
``pw_log_tokenized`` provides a backend for ``pw_log`` that tokenizes log
messages with the ``pw_tokenizer`` module. The log level, 16-bit tokenized
module name, and flags bits are passed through the payload argument. The macro
-eventually passes logs to the ``pw_log_tokenized_HandleLog`` function, which
-must be implemented by the application.
+eventually passes logs to the :c:func:`pw_log_tokenized_HandleLog` function,
+which must be implemented by the application.
-.. c:function: void pw_log_tokenized_HandleLog(uint32_t metadata, const uint8_t[] message, size_t size_bytes)
-
- Function that is called for each log message. The metadata uint32_t can be
- converted to a :cpp:class`pw::log::tokenized::Metadata`. The message is passed
- as a pointer to a buffer and a size. The pointer is invalidated after this
- function returns, so the buffer must be copied.
+.. doxygenfunction:: pw_log_tokenized_HandleLog
Example implementation:
@@ -135,20 +130,18 @@
Creating and reading Metadata payloads
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-A C++ class is provided to facilitate the creation and interpretation of packed
-log metadata payloads. The ``GenericMetadata`` class allows flags, log level,
-line number, and a module identifier to be packed into bit fields of
-configurable size. The ``Metadata`` class simplifies the bit field width
-templatization of ``GenericMetadata`` by pulling from this module's
-configuration options. In most cases, it's recommended to use ``Metadata`` to
-create or read metadata payloads.
+``pw_log_tokenized`` provides a C++ class to facilitate the creation and
+interpretation of packed log metadata payloads.
-A ``Metadata`` object can be created from a ``uint32_t`` allowing
-various peices of metadata to be read from the payload as seen below:
+.. doxygenclass:: pw::log_tokenized::GenericMetadata
+.. doxygentypedef:: pw::log_tokenized::Metadata
+
+The following example shows that a ``Metadata`` object can be created from a
+``uint32_t`` log metadata payload.
.. code-block:: cpp
- extern "C" void pw_log_tokenized_HandleLog((
+ extern "C" void pw_log_tokenized_HandleLog(
uint32_t payload,
const uint8_t message[],
size_t size_bytes) {
diff --git a/pw_log_tokenized/metadata_test.cc b/pw_log_tokenized/metadata_test.cc
index 74de0a5..4d84c5a 100644
--- a/pw_log_tokenized/metadata_test.cc
+++ b/pw_log_tokenized/metadata_test.cc
@@ -20,7 +20,7 @@
namespace {
TEST(Metadata, NoLineBits) {
- using NoLineBits = internal::GenericMetadata<6, 0, 10, 16>;
+ using NoLineBits = GenericMetadata<6, 0, 10, 16>;
constexpr NoLineBits test1 = NoLineBits::Set<0, 0, 0>();
static_assert(test1.level() == 0);
@@ -42,7 +42,7 @@
}
TEST(Metadata, NoFlagBits) {
- using NoFlagBits = internal::GenericMetadata<3, 13, 0, 16>;
+ using NoFlagBits = GenericMetadata<3, 13, 0, 16>;
constexpr NoFlagBits test1 = NoFlagBits::Set<0, 0, 0, 0>();
static_assert(test1.level() == 0);
diff --git a/pw_log_tokenized/public/pw_log_tokenized/handler.h b/pw_log_tokenized/public/pw_log_tokenized/handler.h
index f15ee5b..40ac0f7 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/handler.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/handler.h
@@ -20,6 +20,10 @@
PW_EXTERN_C_START
+/// Function that is called for each log message. The metadata `uint32_t` can be
+/// converted to a @cpp_type{pw::log_tokenized::Metadata}. The message is passed
+/// as a pointer to a buffer and a size. The pointer is invalidated after this
+/// function returns, so the buffer must be copied.
void pw_log_tokenized_HandleLog(uint32_t metadata,
const uint8_t encoded_message[],
size_t size_bytes);
diff --git a/pw_log_tokenized/public/pw_log_tokenized/metadata.h b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
index 7a87bd4..5d78e3b 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/metadata.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
@@ -41,8 +41,17 @@
static constexpr T Shift(T) { return 0; }
};
+} // namespace internal
+
// This class, which is aliased to pw::log_tokenized::Metadata below, is used to
// access the log metadata packed into the tokenizer's payload argument.
+//
+/// `GenericMetadata` facilitates the creation and interpretation of packed
+/// log metadata payloads. The `GenericMetadata` class allows flags, log level,
+/// line number, and a module identifier to be packed into bit fields of
+/// configurable size.
+///
+/// Typically, the `Metadata` alias should be used instead.
template <unsigned kLevelBits,
unsigned kLineBits,
unsigned kFlagBits,
@@ -60,35 +69,37 @@
return GenericMetadata(BitsFromMetadata(log_level, module, flags, line));
}
- // Only use this constructor for creating metadata from runtime values. This
- // constructor is unable to warn at compilation when values will not fit in
- // the specified bit field widths.
+ /// Only use this constructor for creating metadata from runtime values. This
+ /// constructor is unable to warn at compilation when values will not fit in
+ /// the specified bit field widths.
constexpr GenericMetadata(T log_level, T module, T flags, T line)
: value_(BitsFromMetadata(log_level, module, flags, line)) {}
constexpr GenericMetadata(T value) : value_(value) {}
- // The log level of this message.
+ /// The log level of this message.
constexpr T level() const { return Level::Get(value_); }
- // The line number of the log call. The first line in a file is 1. If the line
- // number is 0, it was too large to be stored.
+ /// The line number of the log call. The first line in a file is 1. If the
+ /// line number is 0, it was too large to be stored.
constexpr T line_number() const { return Line::Get(value_); }
- // The flags provided to the log call.
+ /// The flags provided to the log call.
constexpr T flags() const { return Flags::Get(value_); }
- // The 16 bit tokenized version of the module name (PW_LOG_MODULE_NAME).
+ /// The 16-bit tokenized version of the module name
+ /// (@c_macro{PW_LOG_MODULE_NAME}).
constexpr T module() const { return Module::Get(value_); }
- // The underlying packed metadata.
+ /// The underlying packed metadata.
constexpr T value() const { return value_; }
private:
- using Level = BitField<T, kLevelBits, 0>;
- using Line = BitField<T, kLineBits, kLevelBits>;
- using Flags = BitField<T, kFlagBits, kLevelBits + kLineBits>;
- using Module = BitField<T, kModuleBits, kLevelBits + kLineBits + kFlagBits>;
+ using Level = internal::BitField<T, kLevelBits, 0>;
+ using Line = internal::BitField<T, kLineBits, kLevelBits>;
+ using Flags = internal::BitField<T, kFlagBits, kLevelBits + kLineBits>;
+ using Module =
+ internal::BitField<T, kModuleBits, kLevelBits + kLineBits + kFlagBits>;
static constexpr T BitsFromMetadata(T log_level, T module, T flags, T line) {
return Level::Shift(log_level) | Module::Shift(module) |
@@ -101,12 +112,16 @@
sizeof(value_) * 8);
};
-} // namespace internal
-
-using Metadata = internal::GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
- PW_LOG_TOKENIZED_LINE_BITS,
- PW_LOG_TOKENIZED_FLAG_BITS,
- PW_LOG_TOKENIZED_MODULE_BITS>;
+/// The `Metadata` alias simplifies the bit field width templatization of
+/// `GenericMetadata` by pulling from this module's configuration options. In
+/// most cases, it's recommended to use `Metadata` to create or read metadata
+/// payloads.
+///
+/// A `Metadata` object can be created from a `uint32_t`.
+using Metadata = GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
+ PW_LOG_TOKENIZED_LINE_BITS,
+ PW_LOG_TOKENIZED_FLAG_BITS,
+ PW_LOG_TOKENIZED_MODULE_BITS>;
} // namespace log_tokenized
} // namespace pw
diff --git a/pw_log_zephyr/CMakeLists.txt b/pw_log_zephyr/CMakeLists.txt
index 08946fa..3f39df9 100644
--- a/pw_log_zephyr/CMakeLists.txt
+++ b/pw_log_zephyr/CMakeLists.txt
@@ -14,24 +14,34 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(NOT CONFIG_PIGWEED_LOG)
- return()
+if(CONFIG_PIGWEED_LOG_ZEPHYR)
+ pw_add_library(pw_log_zephyr STATIC
+ HEADERS
+ public/pw_log_zephyr/log_zephyr.h
+ public/pw_log_zephyr/config.h
+ public_overrides/pw_log_backend/log_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_log.facade
+ zephyr_interface
+ SOURCES
+ log_zephyr.cc
+ PRIVATE_DEPS
+ pw_preprocessor
+ )
+ zephyr_link_libraries(pw_log_zephyr)
endif()
-pw_add_library(pw_log_zephyr STATIC
- HEADERS
- public/pw_log_zephyr/log_zephyr.h
- public/pw_log_zephyr/config.h
- public_overrides/pw_log_backend/log_backend.h
- PUBLIC_INCLUDES
- public
- public_overrides
- PUBLIC_DEPS
- pw_log.facade
- zephyr_interface
- SOURCES
- log_zephyr.cc
- PRIVATE_DEPS
- pw_preprocessor
-)
-zephyr_link_libraries(pw_log_zephyr)
+if(CONFIG_PIGWEED_LOG_TOKENIZED)
+ pw_add_library(pw_log_zephyr.tokenized_handler STATIC
+ SOURCES
+ pw_log_zephyr_tokenized_handler.cc
+ PRIVATE_DEPS
+ pw_log_tokenized.handler
+ pw_tokenizer
+ )
+ zephyr_link_libraries(pw_log pw_log_zephyr.tokenized_handler)
+ zephyr_include_directories(public_overrides)
+endif()
diff --git a/pw_log_zephyr/Kconfig b/pw_log_zephyr/Kconfig
index 472b54c..59a971b 100644
--- a/pw_log_zephyr/Kconfig
+++ b/pw_log_zephyr/Kconfig
@@ -12,17 +12,41 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_LOG
- bool "Enable Pigweed logging library (pw_log)"
+choice PIGWEED_LOG
+ prompt "Logging backend used"
+ help
+ The type of Zephyr pw_log backend to use.
+
+config PIGWEED_LOG_ZEPHYR
+ bool "Zephyr logging for PW_LOG_* statements"
select PIGWEED_PREPROCESSOR
help
Once the Pigweed logging is enabled, all Pigweed logs via PW_LOG_*() will
- go to the "pigweed" Zephyr logging module.
+ be routed to the Zephyr logging system. This means that:
+ - PW_LOG_LEVEL_DEBUG maps to Zephyr's LOG_LEVEL_DBG
+ - PW_LOG_LEVEL_INFO maps to Zephyr's LOG_LEVEL_INF
+ - PW_LOG_LEVEL_WARN maps to Zephyr's LOG_LEVEL_WRN
+ - PW_LOG_LEVEL_ERROR maps to Zephyr's LOG_LEVEL_ERR
+ - PW_LOG_LEVEL_CRITICAL maps to Zephyr's LOG_LEVEL_ERR
+ - PW_LOG_LEVEL_FATAL maps to Zephyr's LOG_LEVEL_ERR
-if PIGWEED_LOG
+config PIGWEED_LOG_TOKENIZED
+ bool "Maps all Zephyr log macros to tokenized PW_LOG_* macros"
+ select PIGWEED_PREPROCESSOR
+ select PIGWEED_TOKENIZER
+ select LOG_CUSTOM_HEADER
+ help
+ Map all the Zephyr log macros to use Pigweed's then use the
+ 'pw_log_tokenized' target as the logging backend in order to
+ automatically tokenize all the logging strings. This means that Pigweed
+ will also tokenize all of Zephyr's logging statements.
+
+endchoice
+
+if PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
module = PIGWEED
module-str = "pigweed"
source "subsys/logging/Kconfig.template.log_config"
-endif # PIGWEED_LOG
+endif # PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
diff --git a/pw_log_zephyr/docs.rst b/pw_log_zephyr/docs.rst
index 84b8db9..beb6a69 100644
--- a/pw_log_zephyr/docs.rst
+++ b/pw_log_zephyr/docs.rst
@@ -1,17 +1,31 @@
.. _module-pw_log_zephyr:
-================
+=============
pw_log_zephyr
-================
+=============
--------
Overview
--------
-This interrupt backend implements the ``pw_log`` facade. To enable, set
-``CONFIG_PIGWEED_LOG=y``. After that, logging can be controlled via the standard
-`Kconfig options <https://docs.zephyrproject.org/latest/reference/logging/index.html#global-kconfig-options>`_.
-All logs made through `PW_LOG_*` are logged to the Zephyr logging module
-``pigweed``.
+This interrupt backend implements the ``pw_log`` facade. Currently, two
+separate Pigweed backends are implemented. One that uses the plain Zephyr
+logging framework and routes Pigweed's logs to Zephyr. While another maps
+the Zephyr logging macros to Pigweed's tokenized logging.
+
+Using Zephyr logging
+--------------------
+To enable, set ``CONFIG_PIGWEED_LOG_ZEPHYR=y``. After that, logging can be
+controlled via the standard `Kconfig options`_. All logs made through
+`PW_LOG_*` are logged to the Zephyr logging module ``pigweed``. In this
+model, the Zephyr logging is set as ``pw_log``'s backend.
+
+Using Pigweed tokenized logging
+-------------------------------
+Using the pigweed logging can be done by enabling
+``CONFIG_PIGWEED_LOG_TOKENIZED=y``. At that point ``pw_log_tokenized`` is set
+as the backedn for ``pw_log`` and all Zephyr logs are routed to Pigweed's
+logging facade. This means that any logging statements made in Zephyr itself
+are also tokenized.
Setting the log level
---------------------
@@ -37,3 +51,5 @@
Alternatively, it is also possible to set the Zephyr logging level directly via
``CONFIG_PIGWEED_LOG_LEVEL``.
+
+.. _`Kconfig options`: https://docs.zephyrproject.org/latest/reference/logging/index.html#global-kconfig-options
diff --git a/pw_log_zephyr/public_overrides/zephyr_custom_log.h b/pw_log_zephyr/public_overrides/zephyr_custom_log.h
new file mode 100644
index 0000000..8485d5e
--- /dev/null
+++ b/pw_log_zephyr/public_overrides/zephyr_custom_log.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <zephyr/sys/__assert.h>
+
+// If static_assert wasn't defined by zephyr/sys/__assert.h that means it's not
+// supported, just ignore it.
+#ifndef static_assert
+#define static_assert(...)
+#endif
+
+#include <pw_log/log.h>
+
+#undef LOG_DBG
+#undef LOG_INF
+#undef LOG_WRN
+#undef LOG_ERR
+
+#define LOG_DBG(format, ...) PW_LOG_DEBUG(format, ##__VA_ARGS__)
+#define LOG_INF(format, ...) PW_LOG_INFO(format, ##__VA_ARGS__)
+#define LOG_WRN(format, ...) PW_LOG_WARN(format, ##__VA_ARGS__)
+#define LOG_ERR(format, ...) PW_LOG_ERROR(format, ##__VA_ARGS__)
diff --git a/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
new file mode 100644
index 0000000..bdc64ed
--- /dev/null
+++ b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include <zephyr/logging/log_backend.h>
+#include <zephyr/logging/log_msg.h>
+
+#include "pw_log_tokenized/handler.h"
+
+namespace pw::log_tokenized {
+
+extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
+ const uint8_t log_buffer[],
+ size_t size_bytes) {
+ ARG_UNUSED(metadata);
+ ARG_UNUSED(log_buffer);
+ ARG_UNUSED(size_bytes);
+ // TODO(asemjonovs): implement this function
+}
+
+} // namespace pw::log_tokenized
diff --git a/pw_module/py/pw_module/create.py b/pw_module/py/pw_module/create.py
index b5ff6d3..d53dca0 100644
--- a/pw_module/py/pw_module/create.py
+++ b/pw_module/py/pw_module/create.py
@@ -899,7 +899,7 @@
root=project_root,
modules_list=modules_file,
modules_gni_file=modules_gni_file,
- warn_only=None,
+ mode=generate_modules_lists.Mode.UPDATE,
)
print(' modify ' + str(modules_gni_file.relative_to(Path.cwd())))
diff --git a/pw_multisink/multisink_test.cc b/pw_multisink/multisink_test.cc
index b466ba0..3489a06 100644
--- a/pw_multisink/multisink_test.cc
+++ b/pw_multisink/multisink_test.cc
@@ -52,7 +52,7 @@
static constexpr size_t kEntryBufferSize = 1024;
static constexpr size_t kBufferSize = 5 * kEntryBufferSize;
- MultiSinkTest() : multisink_(buffer_) {}
+ MultiSinkTest() : buffer_{}, multisink_(buffer_) {}
// Expects the peeked or popped message to equal the provided non-empty
// message, and the drop count to match. If `expected_message` is empty, the
diff --git a/pw_perf_test/public/pw_perf_test/perf_test.h b/pw_perf_test/public/pw_perf_test/perf_test.h
index 64f6e64..e092087 100644
--- a/pw_perf_test/public/pw_perf_test/perf_test.h
+++ b/pw_perf_test/public/pw_perf_test/perf_test.h
@@ -23,7 +23,7 @@
#include "pw_preprocessor/arguments.h"
#define PW_PERF_TEST(name, function, ...) \
- ::pw::perf_test::internal::TestInfo PwPerfTest_##name( \
+ const ::pw::perf_test::internal::TestInfo PwPerfTest_##name( \
#name, [](::pw::perf_test::State& pw_perf_test_state) { \
static_cast<void>( \
function(pw_perf_test_state PW_COMMA_ARGS(__VA_ARGS__))); \
@@ -59,6 +59,7 @@
: event_handler_(nullptr),
tests_(nullptr),
run_info_{.total_tests = 0, .default_iterations = kDefaultIterations} {}
+
static Framework& Get() { return framework_; }
void RegisterEventHandler(EventHandler& event_handler) {
@@ -84,7 +85,7 @@
class TestInfo {
public:
- constexpr TestInfo(const char* test_name, void (*function_body)(State&))
+ TestInfo(const char* test_name, void (*function_body)(State&))
: run_(function_body), test_name_(test_name) {
// Once a TestInfo object is created by the macro, this adds itself to the
// list of registered tests
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index 3e48ad1..5be527b 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -192,6 +192,34 @@
These will suggest fixes using ``pw format --fix``.
+Options for code formatting can be specified in the ``pigweed.json`` file
+(see also :ref:`SEED-0101 <seed-0101>`). These apply to both ``pw presubmit``
+steps that check code formatting and ``pw format`` commands that either check
+or fix code formatting.
+
+* ``python_formatter``: Choice of Python formatter. Options are ``black`` (used
+ by Pigweed itself) and ``yapf`` (the default).
+* ``black_path``: If ``python_formatter`` is ``black``, use this as the
+ executable instead of ``black``.
+
+.. TODO(b/264578594) Add exclude to pigweed.json file.
+.. * ``exclude``: List of path regular expressions to ignore.
+
+Example section from a ``pigweed.json`` file:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_presubmit": {
+ "format": {
+ "python_formatter": "black",
+ "black_path": "black"
+ }
+ }
+ }
+ }
+
Sorted Blocks
^^^^^^^^^^^^^
Blocks of code can be required to be kept in sorted order using comments like
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index 167de40..6ef59e8 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -520,7 +520,7 @@
yield
finally:
- proc.terminate()
+ proc.terminate() # pylint: disable=used-before-assignment
@filter_paths(
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 03f1f37..9b073ea 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -58,6 +58,7 @@
from pw_presubmit import (
cli,
FormatContext,
+ FormatOptions,
git_repo,
owners_checks,
PresubmitContext,
@@ -266,9 +267,6 @@
return {}
-BLACK = 'black'
-
-
def _enumerate_black_configs() -> Iterable[Path]:
if directory := os.environ.get('PW_PROJECT_ROOT'):
yield Path(directory, '.black.toml')
@@ -293,10 +291,11 @@
def _black_multiple_files(ctx: _Context) -> Tuple[str, ...]:
+ black = ctx.format_options.black_path
changed_paths: List[str] = []
for line in (
log_run(
- [BLACK, '--check', *_black_config_args(), *ctx.paths],
+ [black, '--check', *_black_config_args(), *ctx.paths],
capture_output=True,
)
.stderr.decode()
@@ -324,7 +323,7 @@
build.write_bytes(data)
proc = log_run(
- [BLACK, *_black_config_args(), build],
+ [ctx.format_options.black_path, *_black_config_args(), build],
capture_output=True,
)
if proc.returncode:
@@ -354,7 +353,7 @@
continue
proc = log_run(
- [BLACK, *_black_config_args(), path],
+ [ctx.format_options.black_path, *_black_config_args(), path],
capture_output=True,
)
if proc.returncode:
@@ -362,6 +361,22 @@
return errors
+def check_py_format(ctx: _Context) -> Dict[Path, str]:
+ if ctx.format_options.python_formatter == 'black':
+ return check_py_format_black(ctx)
+ if ctx.format_options.python_formatter == 'yapf':
+ return check_py_format_yapf(ctx)
+ raise ValueError(ctx.format_options.python_formatter)
+
+
+def fix_py_format(ctx: _Context) -> Dict[Path, str]:
+ if ctx.format_options.python_formatter == 'black':
+ return fix_py_format_black(ctx)
+ if ctx.format_options.python_formatter == 'yapf':
+ return fix_py_format_yapf(ctx)
+ raise ValueError(ctx.format_options.python_formatter)
+
+
_TRAILING_SPACE = re.compile(rb'[ \t]+$', flags=re.MULTILINE)
@@ -484,19 +499,11 @@
'Go', FileFilter(endswith=('.go',)), check_go_format, fix_go_format
)
-# TODO(b/259595799) Remove yapf support.
-PYTHON_FORMAT_YAPF: CodeFormat = CodeFormat(
+PYTHON_FORMAT: CodeFormat = CodeFormat(
'Python',
FileFilter(endswith=('.py',)),
- check_py_format_yapf,
- fix_py_format_yapf,
-)
-
-PYTHON_FORMAT_BLACK: CodeFormat = CodeFormat(
- 'Python',
- FileFilter(endswith=('.py',)),
- check_py_format_black,
- fix_py_format_black,
+ check_py_format,
+ fix_py_format,
)
GN_FORMAT: CodeFormat = CodeFormat(
@@ -546,7 +553,7 @@
fix=fix_owners_format,
)
-_CODE_FORMATS_WITHOUT_PYTHON: Tuple[CodeFormat, ...] = (
+CODE_FORMATS: Tuple[CodeFormat, ...] = (
# keep-sorted: start
BAZEL_FORMAT,
CMAKE_FORMAT,
@@ -559,23 +566,14 @@
MARKDOWN_FORMAT,
OWNERS_CODE_FORMAT,
PROTO_FORMAT,
+ PYTHON_FORMAT,
RST_FORMAT,
# keep-sorted: end
)
-# TODO(b/259595799) Remove yapf support.
-CODE_FORMATS_WITH_YAPF: Tuple[CodeFormat, ...] = (
- *_CODE_FORMATS_WITHOUT_PYTHON,
- PYTHON_FORMAT_YAPF,
-)
-
-CODE_FORMATS_WITH_BLACK: Tuple[CodeFormat, ...] = (
- *_CODE_FORMATS_WITHOUT_PYTHON,
- PYTHON_FORMAT_BLACK,
-)
-
-# TODO(b/259595799) For downstream compatibility only.
-CODE_FORMATS = CODE_FORMATS_WITH_YAPF
+# TODO(b/264578594) Remove these lines when these globals aren't referenced.
+CODE_FORMATS_WITH_BLACK: Tuple[CodeFormat, ...] = CODE_FORMATS
+CODE_FORMATS_WITH_YAPF: Tuple[CodeFormat, ...] = CODE_FORMATS
def presubmit_check(
@@ -624,7 +622,7 @@
def presubmit_checks(
*,
exclude: Collection[Union[str, Pattern[str]]] = (),
- code_formats: Collection[CodeFormat] = CODE_FORMATS_WITH_YAPF,
+ code_formats: Collection[CodeFormat] = CODE_FORMATS,
) -> Tuple[Callable, ...]:
"""Returns a tuple with all supported code format presubmit checks.
@@ -673,6 +671,7 @@
output_dir=outdir,
paths=tuple(self._formats[code_format]),
package_root=self.package_root,
+ format_options=FormatOptions.load(),
)
def check(self) -> Dict[Path, str]:
@@ -718,7 +717,7 @@
exclude: Collection[Pattern[str]],
fix: bool,
base: str,
- code_formats: Collection[CodeFormat] = CODE_FORMATS_WITH_YAPF,
+ code_formats: Collection[CodeFormat] = CODE_FORMATS,
output_directory: Optional[Path] = None,
package_root: Optional[Path] = None,
) -> int:
@@ -833,10 +832,7 @@
return 0
-def arguments(
- git_paths: bool,
- use_black: bool = False,
-) -> argparse.ArgumentParser:
+def arguments(git_paths: bool) -> argparse.ArgumentParser:
"""Creates an argument parser for format_files or format_paths_in_repo."""
parser = argparse.ArgumentParser(description=__doc__)
@@ -866,18 +862,6 @@
'--fix', action='store_true', help='Apply formatting fixes in place.'
)
- # TODO(b/259595799, b/261025545) Remove --code-formats option when
- # downstream projects have switched away from yapf.
- default_code_formats = CODE_FORMATS_WITH_YAPF
- if use_black:
- default_code_formats = CODE_FORMATS_WITH_BLACK
- parser.add_argument(
- '--code-formats',
- choices=(CODE_FORMATS_WITH_YAPF, CODE_FORMATS_WITH_BLACK),
- default=default_code_formats,
- help=argparse.SUPPRESS,
- )
-
parser.add_argument(
'--output-directory',
type=Path,
@@ -903,7 +887,7 @@
Excludes third party sources.
"""
- args = arguments(git_paths=True, use_black=True).parse_args()
+ args = arguments(git_paths=True).parse_args()
# Exclude paths with third party code from formatting.
args.exclude.append(re.compile('^third_party/fuchsia/repo/'))
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 1b4f671..1369fbc 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -145,6 +145,7 @@
def _gn_combined_build_check_targets() -> Sequence[str]:
build_targets = [
+ 'check_modules',
*_at_all_optimization_levels('stm32f429i'),
*_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
'python.tests',
@@ -491,6 +492,27 @@
*targets,
)
+ # Provide some coverage of the FreeRTOS build.
+ #
+ # This is just a minimal presubmit intended to ensure we don't break what
+ # support we have.
+ #
+ # TODO(b/271465588): Eventually just build the entire repo for this
+ # platform.
+ build.bazel(
+ ctx,
+ 'build',
+ # Designated initializers produce a warning-treated-as-error when
+ # compiled with -std=c++17.
+ #
+ # TODO(b/271299438): Remove this.
+ '--copt=-Wno-pedantic',
+ '--platforms=//pw_build/platforms:testonly_freertos',
+ '//pw_sync/...',
+ '//pw_thread/...',
+ '//pw_thread_freertos/...',
+ )
+
def pw_transfer_integration_test(ctx: PresubmitContext) -> None:
"""Runs the pw_transfer cross-language integration test only.
@@ -884,7 +906,6 @@
r'\bpw_doctor/py/pw_doctor/doctor.py',
r'\bpw_env_setup/util.sh',
r'\bpw_fuzzer/fuzzer.gni',
- r'\bpw_fuzzer/oss_fuzz.gni',
r'\bpw_i2c/BUILD.gn',
r'\bpw_i2c/public/pw_i2c/register_device.h',
r'\bpw_kvs/flash_memory.cc',
@@ -980,9 +1001,7 @@
_LINTFORMAT = (
commit_message_format,
copyright_notice,
- format_code.presubmit_checks(
- code_formats=format_code.CODE_FORMATS_WITH_BLACK
- ),
+ format_code.presubmit_checks(),
inclusive_language.presubmit_check.with_filter(
exclude=(
r'\byarn.lock$',
@@ -992,7 +1011,6 @@
cpp_checks.pragma_once,
build.bazel_lint,
owners_lint_checks,
- source_in_build.bazel(SOURCE_FILES_FILTER),
source_in_build.gn(SOURCE_FILES_FILTER),
source_is_in_cmake_build_warn_only,
shell_checks.shellcheck if shutil.which('shellcheck') else (),
@@ -1002,6 +1020,11 @@
LINTFORMAT = (
_LINTFORMAT,
+ # This check is excluded from _LINTFORMAT because it's not quick: it issues
+ # a bazel query that pulls in all of Pigweed's external dependencies
+ # (https://stackoverflow.com/q/71024130/1224002). These are cached, but
+ # after a roll it can be quite slow.
+ source_in_build.bazel(SOURCE_FILES_FILTER),
pw_presubmit.python_checks.check_python_versions,
pw_presubmit.python_checks.gn_python_lint,
)
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index bf08020..ef10a3a 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -77,6 +77,7 @@
import pw_cli.color
import pw_cli.env
+import pw_env_setup.config_file
from pw_package import package_manager
from pw_presubmit import git_repo, tools
from pw_presubmit.tools import plural
@@ -138,7 +139,6 @@
class PresubmitResult(enum.Enum):
-
PASS = 'PASSED' # Check completed successfully.
FAIL = 'FAILED' # Check failed.
CANCEL = 'CANCEL' # Check didn't complete.
@@ -214,6 +214,25 @@
return len(self._programs)
+@dataclasses.dataclass(frozen=True)
+class FormatOptions:
+ python_formatter: Optional[str] = 'yapf'
+ black_path: Optional[str] = 'black'
+
+ # TODO(b/264578594) Add exclude to pigweed.json file.
+ # exclude: Sequence[re.Pattern] = dataclasses.field(default_factory=list)
+
+ @staticmethod
+ def load() -> 'FormatOptions':
+ config = pw_env_setup.config_file.load()
+ fmt = config.get('pw', {}).get('pw_presubmit', {}).get('format', {})
+ return FormatOptions(
+ python_formatter=fmt.get('python_formatter', 'yapf'),
+ black_path=fmt.get('black_path', 'black'),
+ # exclude=tuple(re.compile(x) for x in fmt.get('exclude', ())),
+ )
+
+
@dataclasses.dataclass
class LuciPipeline:
round: int
@@ -503,12 +522,14 @@
paths: Modified files for the presubmit step to check (often used in
formatting steps but ignored in compile steps)
package_root: Root directory for pw package installations
+ format_options: Formatting options, derived from pigweed.json
"""
root: Optional[Path]
output_dir: Path
paths: Tuple[Path, ...]
package_root: Path
+ format_options: FormatOptions
@dataclasses.dataclass
@@ -530,6 +551,7 @@
package_root: Root directory for pw package installations
override_gn_args: Additional GN args processed by build.gn_gen()
luci: Information about the LUCI build or None if not running in LUCI
+ format_options: Formatting options, derived from pigweed.json
num_jobs: Number of jobs to run in parallel
continue_after_build_error: For steps that compile, don't exit on the
first compilation error
@@ -544,6 +566,7 @@
package_root: Path
luci: Optional[LuciContext]
override_gn_args: Dict[str, str]
+ format_options: FormatOptions
num_jobs: Optional[int] = None
continue_after_build_error: bool = False
_failed: bool = False
@@ -581,6 +604,7 @@
package_root=root / 'environment' / 'packages',
luci=None,
override_gn_args={},
+ format_options=FormatOptions(),
)
@@ -859,6 +883,7 @@
override_gn_args=self._override_gn_args,
continue_after_build_error=self._continue_after_build_error,
luci=LuciContext.create_from_environment(),
+ format_options=FormatOptions.load(),
)
finally:
@@ -1296,7 +1321,7 @@
_LOG.warning('%s', failure)
return PresubmitResult.FAIL
- except Exception as failure: # pylint: disable=broad-except
+ except Exception as _failure: # pylint: disable=broad-except
_LOG.exception('Presubmit check %s failed!', self.name)
return PresubmitResult.FAIL
diff --git a/pw_presubmit/py/pw_presubmit/source_in_build.py b/pw_presubmit/py/pw_presubmit/source_in_build.py
index e3df834..f19dd11 100644
--- a/pw_presubmit/py/pw_presubmit/source_in_build.py
+++ b/pw_presubmit/py/pw_presubmit/source_in_build.py
@@ -11,7 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Check that source files are in the build."""
+"""Checks that source files are listed in build files, such as BUILD.bazel."""
import logging
from typing import Callable, Sequence
@@ -67,7 +67,7 @@
for miss in missing:
print(miss, file=outs)
- _LOG.warning('All source files must appear in BUILD files')
+ _LOG.warning('All source files must appear in BUILD.bazel files')
raise PresubmitFailure
return source_is_in_bazel_build
diff --git a/pw_presubmit/py/setup.cfg b/pw_presubmit/py/setup.cfg
index 82dcf55..5a71c4f 100644
--- a/pw_presubmit/py/setup.cfg
+++ b/pw_presubmit/py/setup.cfg
@@ -22,8 +22,8 @@
packages = find:
zip_safe = False
install_requires =
- scan-build==2.0.19
- yapf==0.31.0
+ yapf>=0.31.0
+ black>=23.1.0
[options.package_data]
pw_presubmit = py.typed
diff --git a/pw_protobuf/BUILD.gn b/pw_protobuf/BUILD.gn
index 8c21beb..97f8c82 100644
--- a/pw_protobuf/BUILD.gn
+++ b/pw_protobuf/BUILD.gn
@@ -120,8 +120,8 @@
":codegen_message_test",
":decoder_test",
":encoder_test",
- ":encoder_fuzzer",
- ":decoder_fuzzer",
+ ":encoder_fuzzer_test",
+ ":decoder_fuzzer_test",
":find_test",
":map_utils_test",
":message_test",
@@ -131,6 +131,13 @@
]
}
+group("fuzzers") {
+ deps = [
+ ":decoder_fuzzer",
+ ":encoder_fuzzer",
+ ]
+}
+
pw_test("decoder_test") {
deps = [ ":pw_protobuf" ]
sources = [ "decoder_test.cc" ]
@@ -252,12 +259,27 @@
]
}
+# The tests below have a large amount of global and static data.
+# TODO(b/234883746): Replace this with a better size-based check.
+_small_executable_target_types = [
+ "stm32f429i_executable",
+ "lm3s6965evb_executable",
+]
+_supports_large_tests =
+ _small_executable_target_types + [ pw_build_EXECUTABLE_TARGET_TYPE ] -
+ _small_executable_target_types != []
+
pw_fuzzer("encoder_fuzzer") {
sources = [
"encoder_fuzzer.cc",
"fuzz.h",
]
- deps = [ ":pw_protobuf" ]
+ deps = [
+ ":pw_protobuf",
+ dir_pw_fuzzer,
+ dir_pw_span,
+ ]
+ enable_test_if = _supports_large_tests
}
pw_fuzzer("decoder_fuzzer") {
@@ -265,5 +287,12 @@
"decoder_fuzzer.cc",
"fuzz.h",
]
- deps = [ ":pw_protobuf" ]
+ deps = [
+ ":pw_protobuf",
+ dir_pw_fuzzer,
+ dir_pw_span,
+ dir_pw_status,
+ dir_pw_stream,
+ ]
+ enable_test_if = _supports_large_tests
}
diff --git a/pw_protobuf/CMakeLists.txt b/pw_protobuf/CMakeLists.txt
index 4d1ecec..77d0ccf 100644
--- a/pw_protobuf/CMakeLists.txt
+++ b/pw_protobuf/CMakeLists.txt
@@ -192,6 +192,11 @@
pw_protobuf_protos/status.proto
)
+pw_proto_library(pw_protobuf.field_options_proto
+ SOURCES
+ pw_protobuf_protos/field_options.proto
+)
+
pw_proto_library(pw_protobuf.codegen_protos
SOURCES
pw_protobuf_codegen_protos/codegen_options.proto
diff --git a/pw_protobuf/decoder_fuzzer.cc b/pw_protobuf/decoder_fuzzer.cc
index 1f70e9e..7866b43 100644
--- a/pw_protobuf/decoder_fuzzer.cc
+++ b/pw_protobuf/decoder_fuzzer.cc
@@ -27,10 +27,11 @@
#include "pw_stream/memory_stream.h"
#include "pw_stream/stream.h"
+namespace pw::protobuf::fuzz {
namespace {
void recursive_fuzzed_decode(FuzzedDataProvider& provider,
- pw::protobuf::StreamDecoder& decoder,
+ StreamDecoder& decoder,
uint32_t depth = 0) {
constexpr size_t kMaxRepeatedRead = 1024;
constexpr size_t kMaxDepth = 3;
@@ -191,22 +192,28 @@
}
} break;
case kPush: {
- pw::protobuf::StreamDecoder nested_decoder = decoder.GetNestedDecoder();
+ StreamDecoder nested_decoder = decoder.GetNestedDecoder();
recursive_fuzzed_decode(provider, nested_decoder, depth + 1);
} break;
}
}
}
-} // namespace
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- FuzzedDataProvider provider(data, size);
+void TestOneInput(FuzzedDataProvider& provider) {
constexpr size_t kMaxFuzzedProtoSize = 4096;
std::vector<std::byte> proto_message_data = provider.ConsumeBytes<std::byte>(
provider.ConsumeIntegralInRange<size_t>(0, kMaxFuzzedProtoSize));
- pw::stream::MemoryReader memory_reader(proto_message_data);
- pw::protobuf::StreamDecoder decoder(memory_reader);
+ stream::MemoryReader memory_reader(proto_message_data);
+ StreamDecoder decoder(memory_reader);
recursive_fuzzed_decode(provider, decoder);
+}
+
+} // namespace
+} // namespace pw::protobuf::fuzz
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider provider(data, size);
+ pw::protobuf::fuzz::TestOneInput(provider);
return 0;
}
diff --git a/pw_protobuf/encoder_fuzzer.cc b/pw_protobuf/encoder_fuzzer.cc
index d616364..ebd0984 100644
--- a/pw_protobuf/encoder_fuzzer.cc
+++ b/pw_protobuf/encoder_fuzzer.cc
@@ -23,6 +23,7 @@
#include "pw_protobuf/encoder.h"
#include "pw_span/span.h"
+namespace pw::protobuf::fuzz {
namespace {
// TODO(b/235289495): Move this to pw_fuzzer/fuzzed_data_provider.h
@@ -30,68 +31,63 @@
// Uses the given |provider| to pick and return a number between 0 and the
// maximum numbers of T that can be generated from the remaining input data.
template <typename T>
-size_t ConsumeSize(FuzzedDataProvider* provider) {
- size_t max = provider->remaining_bytes() / sizeof(T);
- return provider->ConsumeIntegralInRange<size_t>(0, max);
+size_t ConsumeSize(FuzzedDataProvider& provider) {
+ size_t max = provider.remaining_bytes() / sizeof(T);
+ return provider.ConsumeIntegralInRange<size_t>(0, max);
}
// Uses the given |provider| to generate several instances of T, store them in
-// |data|, and then return a pw::span to them. It is the caller's responsbility
-// to ensure |data| remains in scope as long as the returned pw::span.
+// |data|, and then return a span to them. It is the caller's responsbility
+// to ensure |data| remains in scope as long as the returned span.
template <typename T>
-pw::span<const T> ConsumeSpan(FuzzedDataProvider* provider,
- std::vector<T>* data) {
+span<const T> ConsumeSpan(FuzzedDataProvider& provider, std::vector<T>* data) {
size_t num = ConsumeSize<T>(provider);
size_t off = data->size();
data->reserve(off + num);
for (size_t i = 0; i < num; ++i) {
if constexpr (std::is_floating_point<T>::value) {
- data->push_back(provider->ConsumeFloatingPoint<T>());
+ data->push_back(provider.ConsumeFloatingPoint<T>());
} else {
- data->push_back(provider->ConsumeIntegral<T>());
+ data->push_back(provider.ConsumeIntegral<T>());
}
}
- return pw::span(&((*data)[off]), num);
+ return span(&((*data)[off]), num);
}
// Uses the given |provider| to generate a string, store it in |data|, and
// return a C-style representation. It is the caller's responsbility to
// ensure |data| remains in scope as long as the returned char*.
-const char* ConsumeString(FuzzedDataProvider* provider,
+const char* ConsumeString(FuzzedDataProvider& provider,
std::vector<std::string>* data) {
size_t off = data->size();
// OSS-Fuzz's clang doesn't have the zero-parameter version of
// ConsumeRandomLengthString yet.
size_t max_length = std::numeric_limits<size_t>::max();
- data->push_back(provider->ConsumeRandomLengthString(max_length));
+ data->push_back(provider.ConsumeRandomLengthString(max_length));
return (*data)[off].c_str();
}
// Uses the given |provider| to generate non-arithmetic bytes, store them in
-// |data|, and return a pw::span to them. It is the caller's responsbility to
-// ensure |data| remains in scope as long as the returned pw::span.
-pw::span<const std::byte> ConsumeBytes(FuzzedDataProvider* provider,
- std::vector<std::byte>* data) {
+// |data|, and return a span to them. It is the caller's responsbility to
+// ensure |data| remains in scope as long as the returned span.
+span<const std::byte> ConsumeBytes(FuzzedDataProvider& provider,
+ std::vector<std::byte>* data) {
size_t num = ConsumeSize<std::byte>(provider);
- auto added = provider->ConsumeBytes<std::byte>(num);
+ auto added = provider.ConsumeBytes<std::byte>(num);
size_t off = data->size();
num = added.size();
data->insert(data->end(), added.begin(), added.end());
- return pw::span(&((*data)[off]), num);
+ return span(&((*data)[off]), num);
}
-} // namespace
-
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+void TestOneInput(FuzzedDataProvider& provider) {
static std::byte buffer[65536];
- FuzzedDataProvider provider(data, size);
-
// Pick a subset of the buffer that the fuzzer is allowed to use, and poison
// the rest.
size_t unpoisoned_length =
provider.ConsumeIntegralInRange<size_t>(0, sizeof(buffer));
- pw::span<std::byte> unpoisoned(buffer, unpoisoned_length);
+ ByteSpan unpoisoned(buffer, unpoisoned_length);
void* poisoned = &buffer[unpoisoned_length];
size_t poisoned_length = sizeof(buffer) - unpoisoned_length;
ASAN_POISON_MEMORY_REGION(poisoned, poisoned_length);
@@ -119,163 +115,163 @@
encoder
.WriteUint32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint32_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedUint32:
encoder
.WritePackedUint32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint32_t>(&provider, &u32s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<uint32_t>(provider, &u32s))
+ .IgnoreError();
break;
case kUint64:
encoder
.WriteUint64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint64_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedUint64:
encoder
.WritePackedUint64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint64_t>(&provider, &u64s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<uint64_t>(provider, &u64s))
+ .IgnoreError();
break;
case kInt32:
encoder
.WriteInt32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedInt32:
encoder
.WritePackedInt32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kInt64:
encoder
.WriteInt64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedInt64:
encoder
.WritePackedInt64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kSint32:
encoder
.WriteSint32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedSint32:
encoder
.WritePackedSint32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kSint64:
encoder
.WriteSint64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedSint64:
encoder
.WritePackedSint64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kBool:
encoder
.WriteBool(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeBool())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kFixed32:
encoder
.WriteFixed32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint32_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedFixed32:
encoder
.WritePackedFixed32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint32_t>(&provider, &u32s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<uint32_t>(provider, &u32s))
+ .IgnoreError();
break;
case kFixed64:
encoder
.WriteFixed64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint64_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedFixed64:
encoder
.WritePackedFixed64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint64_t>(&provider, &u64s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<uint64_t>(provider, &u64s))
+ .IgnoreError();
break;
case kSfixed32:
encoder
.WriteSfixed32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedSfixed32:
encoder
.WritePackedSfixed32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kSfixed64:
encoder
.WriteSfixed64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedSfixed64:
encoder
.WritePackedSfixed64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kFloat:
encoder
.WriteFloat(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeFloatingPoint<float>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedFloat:
encoder
.WritePackedFloat(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<float>(&provider, &floats))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<float>(provider, &floats))
+ .IgnoreError();
break;
case kDouble:
encoder
.WriteDouble(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeFloatingPoint<double>())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError();
break;
case kPackedDouble:
encoder
.WritePackedDouble(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<double>(&provider, &doubles))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeSpan<double>(provider, &doubles))
+ .IgnoreError();
break;
case kBytes:
encoder
.WriteBytes(provider.ConsumeIntegral<uint32_t>(),
- ConsumeBytes(&provider, &bytes))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeBytes(provider, &bytes))
+ .IgnoreError();
break;
case kString:
encoder
.WriteString(provider.ConsumeIntegral<uint32_t>(),
- ConsumeString(&provider, &strings))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ ConsumeString(provider, &strings))
+ .IgnoreError();
break;
case kPush:
// Special "field". The marks the start of a nested message.
@@ -286,5 +282,13 @@
// Don't forget to unpoison for the next iteration!
ASAN_UNPOISON_MEMORY_REGION(poisoned, poisoned_length);
+}
+
+} // namespace
+} // namespace pw::protobuf::fuzz
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider provider(data, size);
+ pw::protobuf::fuzz::TestOneInput(provider);
return 0;
}
diff --git a/pw_protobuf/fuzz.h b/pw_protobuf/fuzz.h
index 78dc34a..cc95db6 100644
--- a/pw_protobuf/fuzz.h
+++ b/pw_protobuf/fuzz.h
@@ -13,7 +13,9 @@
// the License.
#pragma once
-namespace {
+#include <cstdint>
+
+namespace pw::protobuf::fuzz {
// Encodable values. The fuzzer will iteratively choose different field types to
// generate and encode.
@@ -49,4 +51,4 @@
kMaxValue = kPush,
};
-} // namespace
+} // namespace pw::protobuf::fuzz
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index 196a029..dd86264 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -2467,6 +2467,24 @@
f'inline constexpr pw::span<const {_INTERNAL_NAMESPACE}::'
'MessageField> kMessageFields = _kMessageFields;'
)
+
+ member_list = ', '.join(
+ [f'message.{prop.struct_member()[1]}' for prop in properties]
+ )
+
+ # Generate std::tuple for Message fields.
+ output.write_line(
+ 'inline constexpr auto ToTuple(const Message &message) {'
+ )
+ output.write_line(f' return std::tie({member_list});')
+ output.write_line('}')
+
+ # Generate mutable std::tuple for Message fields.
+ output.write_line(
+ 'inline constexpr auto ToMutableTuple(Message &message) {'
+ )
+ output.write_line(f' return std::tie({member_list});')
+ output.write_line('}')
else:
output.write_line(
f'inline constexpr pw::span<const {_INTERNAL_NAMESPACE}::'
diff --git a/pw_protobuf/py/setup.cfg b/pw_protobuf/py/setup.cfg
index 8c2cc23..883edde 100644
--- a/pw_protobuf/py/setup.cfg
+++ b/pw_protobuf/py/setup.cfg
@@ -22,8 +22,8 @@
packages = find:
zip_safe = False
install_requires =
- protobuf==3.20.1
- googleapis-common-protos==1.56.2
+ protobuf>=3.20.1
+ googleapis-common-protos>=1.56.2
graphlib-backport;python_version<'3.9'
[options.entry_points]
diff --git a/pw_protobuf_compiler/proto.cmake b/pw_protobuf_compiler/proto.cmake
index 4c518ab..8ed7519 100644
--- a/pw_protobuf_compiler/proto.cmake
+++ b/pw_protobuf_compiler/proto.cmake
@@ -221,9 +221,11 @@
set(generated_outputs "${outputs}" PARENT_SCOPE)
if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
- get_filename_component(dir "${source_file}" DIRECTORY)
- get_filename_component(name "${source_file}" NAME_WE)
- set(arg_PLUGIN "${dir}/${name}.bat")
+ foreach(source_file IN LISTS SOURCES)
+ get_filename_component(dir "${source_file}" DIRECTORY)
+ get_filename_component(name "${source_file}" NAME_WE)
+ set(arg_PLUGIN "${dir}/${name}.bat")
+ endforeach()
endif()
set(script "$ENV{PW_ROOT}/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py")
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
index 729e3c5..5d379cb 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -36,36 +36,16 @@
Union,
)
-# Temporarily set the root logger level to critical while importing yapf.
-# This silences INFO level messages from
-# environment/cipd/packages/python/lib/python3.9/lib2to3/driver.py
-# when it writes Grammar3.*.pickle and PatternGrammar3.*.pickle files.
-_original_level = 0
-for handler in logging.getLogger().handlers:
- # pylint: disable=unidiomatic-typecheck
- if type(handler) == logging.StreamHandler:
- if handler.level > _original_level:
- _original_level = handler.level
- handler.level = logging.CRITICAL
- # pylint: enable=unidiomatic-typecheck
-
try:
# pylint: disable=wrong-import-position
- from yapf.yapflib import yapf_api # type: ignore[import]
+ import black
+
+ black_mode: Optional[black.Mode] = black.Mode(string_normalization=False)
# pylint: enable=wrong-import-position
except ImportError:
- yapf_api = None
-
-# Restore the original stderr/out log handler level.
-for handler in logging.getLogger().handlers:
- # Must use type() check here since isinstance returns True for FileHandlers
- # and StreamHandler: isinstance(logging.FileHandler, logging.StreamHandler)
- # pylint: disable=unidiomatic-typecheck
- if type(handler) == logging.StreamHandler:
- handler.level = _original_level
- # pylint: enable=unidiomatic-typecheck
-del _original_level
+ black = None # type: ignore
+ black_mode = None
_LOG = logging.getLogger(__name__)
@@ -471,12 +451,12 @@
Args:
message: The protobuf message to format
- wrap: If true and YAPF is available, the output is wrapped according to
- PEP8 using YAPF.
+ wrap: If true and black is available, the output is wrapped according to
+ PEP8 using black.
"""
raw = f'{message.DESCRIPTOR.full_name}({", ".join(_proto_repr(message))})'
- if wrap and yapf_api is not None:
- return yapf_api.FormatCode(raw, style_config='PEP8')[0].rstrip()
+ if wrap and black is not None and black_mode is not None:
+ return black.format_str(raw, mode=black_mode).strip()
return raw
diff --git a/pw_protobuf_compiler/py/python_protos_test.py b/pw_protobuf_compiler/py/python_protos_test.py
index 1c105f7..819bede 100755
--- a/pw_protobuf_compiler/py/python_protos_test.py
+++ b/pw_protobuf_compiler/py/python_protos_test.py
@@ -448,10 +448,12 @@
def test_wrap_multiple_lines(self):
self.assertEqual(
"""\
-pw.test3.Message(optional_int=0,
- optional_bytes=b'',
- optional_string='',
- optional_enum=pw.test3.Enum.ZERO)""",
+pw.test3.Message(
+ optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=pw.test3.Enum.ZERO,
+)""",
proto_repr(
self.message(
optional_int=0,
diff --git a/pw_protobuf_compiler/py/setup.cfg b/pw_protobuf_compiler/py/setup.cfg
index 98b45f6..8e3a6ec 100644
--- a/pw_protobuf_compiler/py/setup.cfg
+++ b/pw_protobuf_compiler/py/setup.cfg
@@ -22,11 +22,13 @@
packages = find:
zip_safe = False
install_requires =
- # NOTE: mypy needs to stay in sync with mypy-protobuf
- # Currently using mypy 0.991 and mypy-protobuf 3.3.0 (see constraint.list)
+ # NOTE: protobuf needs to stay in sync with mypy-protobuf
+ # Currently using mypy protobuf 3.20.1 and mypy-protobuf 3.3.0 (see
+ # constraint.list). These requirements should stay as >= the lowest version
+ # we support.
mypy-protobuf>=3.2.0
- protobuf==3.20.1
- types-protobuf==3.19.22
+ protobuf>=3.20.1
+ types-protobuf>=3.19.22
[options.package_data]
pw_protobuf_compiler = py.typed
diff --git a/pw_random/BUILD.gn b/pw_random/BUILD.gn
index 4b2a5c4..0563fc2 100644
--- a/pw_random/BUILD.gn
+++ b/pw_random/BUILD.gn
@@ -48,10 +48,14 @@
pw_test_group("tests") {
tests = [
":xor_shift_star_test",
- ":get_int_bounded_fuzzer",
+ ":get_int_bounded_fuzzer_test",
]
}
+group("fuzzers") {
+ deps = [ ":get_int_bounded_fuzzer" ]
+}
+
pw_test("xor_shift_star_test") {
deps = [
":pw_random",
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index ad7ff7b..18c8778 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -455,6 +455,7 @@
":service_test",
]
group_deps = [
+ "fuzz:tests",
"nanopb:tests",
"pwpb:tests",
"raw:tests",
diff --git a/pw_rpc/benchmark.rst b/pw_rpc/benchmark.rst
index 6e97ac1..5ecc136 100644
--- a/pw_rpc/benchmark.rst
+++ b/pw_rpc/benchmark.rst
@@ -49,3 +49,23 @@
server.RegisterService(benchmark_service);
}
+Stress Test
+===========
+.. attention::
+ This section is experimental and liable to change.
+
+The Benchmark service is also used as part of a stress test of the ``pw_rpc``
+module. This stress test is implemented as an unguided fuzzer that uses
+multiple worker threads to perform generated sequences of actions using RPC
+``Call`` objects. The test is included as an integration test, and can found and
+be run locally using GN:
+
+.. code-block:: bash
+
+ $ gn desc out //:integration_tests deps | grep fuzz
+ //pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)
+
+ $ gn outputs out '//pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)'
+ pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
+
+ $ ninja -C out pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
diff --git a/pw_rpc/client.cc b/pw_rpc/client.cc
index 4b5fc5c..0062642 100644
--- a/pw_rpc/client.cc
+++ b/pw_rpc/client.cc
@@ -87,9 +87,7 @@
case PacketType::REQUEST:
case PacketType::CLIENT_STREAM:
- case PacketType::DEPRECATED_SERVER_STREAM_END:
case PacketType::CLIENT_ERROR:
- case PacketType::DEPRECATED_CANCEL:
case PacketType::CLIENT_STREAM_END:
default:
internal::rpc_lock().unlock();
diff --git a/pw_rpc/fake_channel_output.cc b/pw_rpc/fake_channel_output.cc
index d652014..6404e68 100644
--- a/pw_rpc/fake_channel_output.cc
+++ b/pw_rpc/fake_channel_output.cc
@@ -70,8 +70,6 @@
return OkStatus();
case pwpb::PacketType::CLIENT_STREAM:
return OkStatus();
- case pwpb::PacketType::DEPRECATED_SERVER_STREAM_END:
- PW_CRASH("Deprecated PacketType %d", static_cast<int>(packet.type()));
case pwpb::PacketType::CLIENT_ERROR:
PW_LOG_WARN("FakeChannelOutput received client error: %s",
packet.status().str());
@@ -80,7 +78,6 @@
PW_LOG_WARN("FakeChannelOutput received server error: %s",
packet.status().str());
return OkStatus();
- case pwpb::PacketType::DEPRECATED_CANCEL:
case pwpb::PacketType::SERVER_STREAM:
case pwpb::PacketType::CLIENT_STREAM_END:
return OkStatus();
diff --git a/pw_rpc/fuzz/BUILD.gn b/pw_rpc/fuzz/BUILD.gn
new file mode 100644
index 0000000..bec42fe
--- /dev/null
+++ b/pw_rpc/fuzz/BUILD.gn
@@ -0,0 +1,140 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_rpc/internal/integration_test_ports.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [
+ "public",
+ "$dir_pw_rpc/public",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("alarm_timer") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/alarm_timer.h" ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_chrono:system_timer",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_test("alarm_timer_test") {
+ enable_if = pw_chrono_SYSTEM_TIMER_BACKEND != ""
+ sources = [ "alarm_timer_test.cc" ]
+ deps = [
+ ":alarm_timer",
+ "$dir_pw_sync:binary_semaphore",
+ ]
+}
+
+pw_source_set("argparse") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/argparse.h" ]
+ sources = [ "argparse.cc" ]
+ public_deps = [
+ "$dir_pw_containers:vector",
+ dir_pw_status,
+ ]
+ deps = [
+ "$dir_pw_string:builder",
+ dir_pw_assert,
+ dir_pw_log,
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_test("argparse_test") {
+ sources = [ "argparse_test.cc" ]
+ deps = [ ":argparse" ]
+}
+
+pw_source_set("engine") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/engine.h" ]
+ sources = [ "engine.cc" ]
+ public_deps = [
+ ":alarm_timer",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_rpc:benchmark",
+ "$dir_pw_rpc:log_config",
+ "$dir_pw_rpc:protos.raw_rpc",
+ "$dir_pw_string:format",
+ "$dir_pw_sync:condition_variable",
+ "$dir_pw_sync:timed_mutex",
+ "$dir_pw_thread:thread",
+ dir_pw_random,
+ ]
+ deps = [ "$dir_pw_rpc:client" ]
+ visibility = [ ":*" ]
+}
+
+pw_test("engine_test") {
+ enable_if =
+ pw_chrono_SYSTEM_TIMER_BACKEND == "$dir_pw_chrono_stl:system_timer" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ sources = [ "engine_test.cc" ]
+ deps = [
+ ":engine",
+ "$dir_pw_rpc:client_server_testing_threaded",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread_stl:test_threads",
+ dir_pw_log,
+ pw_chrono_SYSTEM_TIMER_BACKEND,
+ ]
+}
+
+pw_executable("client_fuzzer") {
+ sources = [ "client_fuzzer.cc" ]
+ deps = [
+ ":argparse",
+ ":engine",
+ "$dir_pw_rpc:client",
+ "$dir_pw_rpc:integration_testing",
+ ]
+}
+
+pw_python_action("cpp_client_server_fuzz_test") {
+ script = "../py/pw_rpc/testing.py"
+ args = [
+ "--server",
+ "<TARGET_FILE($dir_pw_rpc:test_rpc_server)>",
+ "--client",
+ "<TARGET_FILE(:client_fuzzer)>",
+ "--",
+ "$pw_rpc_CPP_CLIENT_FUZZER_TEST_PORT",
+ ]
+ deps = [
+ ":client_fuzzer",
+ "$dir_pw_rpc:test_rpc_server",
+ ]
+
+ stamp = true
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":argparse_test",
+ ":alarm_timer_test",
+ ":engine_test",
+ ]
+}
diff --git a/pw_rpc/fuzz/alarm_timer_test.cc b/pw_rpc/fuzz/alarm_timer_test.cc
new file mode 100644
index 0000000..ce8395a
--- /dev/null
+++ b/pw_rpc/fuzz/alarm_timer_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_rpc/fuzz/alarm_timer.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_sync/binary_semaphore.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::chrono_literals;
+
+TEST(AlarmTimerTest, Start) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(10ms);
+ sem.acquire();
+}
+
+TEST(AlarmTimerTest, Restart) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ for (size_t i = 0; i < 10; ++i) {
+ timer.Restart();
+ EXPECT_FALSE(sem.try_acquire_for(10us));
+ }
+ sem.acquire();
+}
+
+TEST(AlarmTimerTest, Cancel) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ timer.Cancel();
+ EXPECT_FALSE(sem.try_acquire_for(100us));
+}
+
+TEST(AlarmTimerTest, Destroy) {
+ sync::BinarySemaphore sem;
+ {
+ AlarmTimer timer(
+ [&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ }
+ EXPECT_FALSE(sem.try_acquire_for(100us));
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/argparse.cc b/pw_rpc/fuzz/argparse.cc
new file mode 100644
index 0000000..39a5dd6
--- /dev/null
+++ b/pw_rpc/fuzz/argparse.cc
@@ -0,0 +1,259 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_rpc/fuzz/argparse.h"
+
+#include <cctype>
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_log/log.h"
+#include "pw_string/string_builder.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+// Visitor to `ArgVariant` used by `ParseArgs` below.
+struct ParseVisitor {
+ std::string_view arg0;
+ std::string_view arg1;
+
+ template <typename Parser>
+ ParseStatus operator()(Parser& parser) {
+ return parser.Parse(arg0, arg1);
+ }
+};
+
+// Visitor to `ArgVariant` used by `GetArg` below.
+struct ValueVisitor {
+ std::string_view name;
+
+ template <typename Parser>
+ std::optional<ArgVariant> operator()(Parser& parser) {
+ std::optional<ArgVariant> result;
+ if (parser.short_name() == name || parser.long_name() == name) {
+ result.emplace(parser.value());
+ }
+ return result;
+ }
+};
+
+// Visitor to `ArgVariant` used by `PrintUsage` below.
+const size_t kMaxUsageLen = 256;
+struct UsageVisitor {
+ StringBuffer<kMaxUsageLen>* buffer;
+
+ void operator()(const BoolParser& parser) const {
+ auto short_name = parser.short_name();
+ auto long_name = parser.long_name();
+ *buffer << " [" << short_name << "|--[no-]" << long_name.substr(2) << "]";
+ }
+
+ template <typename T>
+ void operator()(const UnsignedParser<T>& parser) const {
+ auto short_name = parser.short_name();
+ auto long_name = parser.long_name();
+ *buffer << " ";
+ if (!parser.positional()) {
+ *buffer << "[";
+ if (!short_name.empty()) {
+ *buffer << short_name << "|";
+ }
+ *buffer << long_name << " ";
+ }
+ for (const auto& c : long_name) {
+ *buffer << static_cast<char>(toupper(c));
+ }
+ if (!parser.positional()) {
+ *buffer << "]";
+ }
+ }
+};
+
+// Visitor to `ArgVariant` used by `ResetArg` below.
+struct ResetVisitor {
+ std::string_view name;
+
+ template <typename Parser>
+ bool operator()(Parser& parser) {
+ if (parser.short_name() != name && parser.long_name() != name) {
+ return false;
+ }
+ parser.Reset();
+ return true;
+ }
+};
+
+} // namespace
+
+ArgParserBase::ArgParserBase(std::string_view name) : long_name_(name) {
+ PW_CHECK(!name.empty());
+ PW_CHECK(name != "--");
+ positional_ =
+ name[0] != '-' || (name.size() > 2 && name.substr(0, 2) != "--");
+}
+
+ArgParserBase::ArgParserBase(std::string_view shortopt,
+ std::string_view longopt)
+ : short_name_(shortopt), long_name_(longopt) {
+ PW_CHECK(shortopt.size() == 2);
+ PW_CHECK(shortopt[0] == '-');
+ PW_CHECK(shortopt != "--");
+ PW_CHECK(longopt.size() > 2);
+ PW_CHECK(longopt.substr(0, 2) == "--");
+ positional_ = false;
+}
+
+bool ArgParserBase::Match(std::string_view arg) {
+ if (arg.empty()) {
+ return false;
+ }
+ if (!positional_) {
+ return arg == short_name_ || arg == long_name_;
+ }
+ if (!std::holds_alternative<std::monostate>(value_)) {
+ return false;
+ }
+ if ((arg.size() == 2 && arg[0] == '-') ||
+ (arg.size() > 2 && arg.substr(0, 2) == "--")) {
+ PW_LOG_WARN("Argument parsed for '%s' appears to be a flag: '%s'",
+ long_name_.data(),
+ arg.data());
+ }
+ return true;
+}
+
+const ArgVariant& ArgParserBase::GetValue() const {
+ return std::holds_alternative<std::monostate>(value_) ? initial_ : value_;
+}
+
+BoolParser::BoolParser(std::string_view name) : ArgParserBase(name) {}
+BoolParser::BoolParser(std::string_view shortopt, std::string_view longopt)
+ : ArgParserBase(shortopt, longopt) {}
+
+BoolParser& BoolParser::set_default(bool value) {
+ set_initial(value);
+ return *this;
+}
+
+ParseStatus BoolParser::Parse(std::string_view arg0,
+ [[maybe_unused]] std::string_view arg1) {
+ if (Match(arg0)) {
+ set_value(true);
+ return kParsedOne;
+ }
+ if (arg0.size() > 5 && arg0.substr(0, 5) == "--no-" &&
+ arg0.substr(5) == long_name().substr(2)) {
+ set_value(false);
+ return kParsedOne;
+ }
+ return kParseMismatch;
+}
+
+UnsignedParserBase::UnsignedParserBase(std::string_view name)
+ : ArgParserBase(name) {}
+UnsignedParserBase::UnsignedParserBase(std::string_view shortopt,
+ std::string_view longopt)
+ : ArgParserBase(shortopt, longopt) {}
+
+ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
+ std::string_view arg1,
+ uint64_t max) {
+ auto result = kParsedOne;
+ if (!Match(arg0)) {
+ return kParseMismatch;
+ }
+ if (!positional()) {
+ if (arg1.empty()) {
+ PW_LOG_ERROR("Missing value for flag '%s'", arg0.data());
+ return kParseFailure;
+ }
+ arg0 = arg1;
+ result = kParsedTwo;
+ }
+ char* endptr;
+ auto value = strtoull(arg0.data(), &endptr, 0);
+ if (*endptr) {
+ PW_LOG_ERROR("Failed to parse number from '%s'", arg0.data());
+ return kParseFailure;
+ }
+ if (value > max) {
+ PW_LOG_ERROR("Parsed value is too large: %llu", value);
+ return kParseFailure;
+ }
+ set_value(value);
+ return result;
+}
+
+Status ParseArgs(Vector<ArgParserVariant>& parsers, int argc, char** argv) {
+ for (int i = 1; i < argc; ++i) {
+ auto arg0 = std::string_view(argv[i]);
+ auto arg1 =
+ i == (argc - 1) ? std::string_view() : std::string_view(argv[i + 1]);
+ bool parsed = false;
+ for (auto& parser : parsers) {
+ switch (std::visit(ParseVisitor{.arg0 = arg0, .arg1 = arg1}, parser)) {
+ case kParsedOne:
+ break;
+ case kParsedTwo:
+ ++i;
+ break;
+ case kParseMismatch:
+ continue;
+ case kParseFailure:
+ PW_LOG_ERROR("Failed to parse '%s'", arg0.data());
+ return Status::InvalidArgument();
+ }
+ parsed = true;
+ break;
+ }
+ if (!parsed) {
+ PW_LOG_ERROR("Unrecognized argument: '%s'", arg0.data());
+ return Status::InvalidArgument();
+ }
+ }
+ return OkStatus();
+}
+
+void PrintUsage(const Vector<ArgParserVariant>& parsers,
+ std::string_view argv0) {
+ StringBuffer<kMaxUsageLen> buffer;
+ buffer << "usage: " << argv0;
+ for (auto& parser : parsers) {
+ std::visit(UsageVisitor{.buffer = &buffer}, parser);
+ }
+ PW_LOG_INFO("%s", buffer.c_str());
+}
+
+std::optional<ArgVariant> GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name) {
+ for (auto& parser : parsers) {
+ if (auto result = std::visit(ValueVisitor{.name = name}, parser);
+ result.has_value()) {
+ return result;
+ }
+ }
+ return std::optional<ArgVariant>();
+}
+
+Status ResetArg(Vector<ArgParserVariant>& parsers, std::string_view name) {
+ for (auto& parser : parsers) {
+ if (std::visit(ResetVisitor{.name = name}, parser)) {
+ return OkStatus();
+ }
+ }
+ return Status::InvalidArgument();
+}
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/argparse_test.cc b/pw_rpc/fuzz/argparse_test.cc
new file mode 100644
index 0000000..6f0ab38
--- /dev/null
+++ b/pw_rpc/fuzz/argparse_test.cc
@@ -0,0 +1,196 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_rpc/fuzz/argparse.h"
+
+#include <cstdint>
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+TEST(ArgsParseTest, ParseBoolFlag) {
+ auto parser1 = BoolParser("-t", "--true").set_default(true);
+ auto parser2 = BoolParser("-f").set_default(false);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("-t"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("-t"), ParseStatus::kParseMismatch);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("--true"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("--true"), ParseStatus::kParseMismatch);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("--no-true"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("--no-true"), ParseStatus::kParseMismatch);
+ EXPECT_FALSE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("-f"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser2.Parse("-f"), ParseStatus::kParsedOne);
+ EXPECT_FALSE(parser1.value());
+ EXPECT_TRUE(parser2.value());
+}
+
+template <typename T>
+void ParseUnsignedFlag() {
+ auto parser = UnsignedParser<T>("-u", "--unsigned").set_default(137);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Wrong name.
+ EXPECT_EQ(parser.Parse("-s"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.Parse("--signed"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Missing values.
+ EXPECT_EQ(parser.Parse("-u"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Non-numeric values.
+ EXPECT_EQ(parser.Parse("-u", "foo"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned", "bar"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Minimum values.
+ EXPECT_EQ(parser.Parse("-u", "0"), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.Parse("--unsigned", "0"), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), 0u);
+
+ // Maximum values.
+ T max = std::numeric_limits<T>::max();
+ StringBuffer<32> buf;
+ buf << max;
+ EXPECT_EQ(parser.Parse("-u", buf.c_str()), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), max);
+ EXPECT_EQ(parser.Parse("--unsigned", buf.c_str()), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), max);
+
+ // Out of-range value.
+ if (max < std::numeric_limits<uint64_t>::max()) {
+ buf.clear();
+ buf << (max + 1ULL);
+ EXPECT_EQ(parser.Parse("-u", buf.c_str()), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned", buf.c_str()),
+ ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), max);
+ }
+}
+
+TEST(ArgsParseTest, ParseUnsignedFlags) {
+ ParseUnsignedFlag<uint8_t>();
+ ParseUnsignedFlag<uint16_t>();
+ ParseUnsignedFlag<uint32_t>();
+ ParseUnsignedFlag<uint64_t>();
+}
+
+TEST(ArgsParseTest, ParsePositional) {
+ auto parser = UnsignedParser<size_t>("positional").set_default(1);
+ EXPECT_EQ(parser.Parse("-p", "2"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 1u);
+
+ EXPECT_EQ(parser.Parse("--positional", "2"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 1u);
+
+ // Second arg is ignored..
+ EXPECT_EQ(parser.Parse("2", "3"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser.value(), 2u);
+
+ // Positional only matches once.
+ EXPECT_EQ(parser.Parse("3"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.value(), 2u);
+}
+
+TEST(ArgsParseTest, PrintUsage) {
+ // Just verify it compiles and runs.
+ Vector<ArgParserVariant, 3> parsers = {
+ BoolParser("-v", "--verbose").set_default(false),
+ UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+ UnsignedParser<size_t>("port").set_default(11111),
+ };
+ PrintUsage(parsers, "test-bin");
+}
+
+void CheckArgs(Vector<ArgParserVariant>& parsers,
+ bool verbose,
+ size_t runs,
+ uint16_t port) {
+ bool actual_verbose;
+ EXPECT_EQ(GetArg(parsers, "--verbose", &actual_verbose), OkStatus());
+ EXPECT_EQ(verbose, actual_verbose);
+ EXPECT_EQ(ResetArg(parsers, "--verbose"), OkStatus());
+
+ size_t actual_runs;
+ EXPECT_EQ(GetArg(parsers, "--runs", &actual_runs), OkStatus());
+ EXPECT_EQ(runs, actual_runs);
+ EXPECT_EQ(ResetArg(parsers, "--runs"), OkStatus());
+
+ uint16_t actual_port;
+ EXPECT_EQ(GetArg(parsers, "port", &actual_port), OkStatus());
+ EXPECT_EQ(port, actual_port);
+ EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
+}
+
+TEST(ArgsParseTest, ParseArgs) {
+ Vector<ArgParserVariant, 3> parsers{
+ BoolParser("-v", "--verbose").set_default(false),
+ UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+ UnsignedParser<uint16_t>("port").set_default(11111),
+ };
+
+ char const* argv1[] = {"test-bin"};
+ EXPECT_EQ(ParseArgs(parsers, 1, const_cast<char**>(argv1)), OkStatus());
+ CheckArgs(parsers, false, 1000, 11111);
+
+ char const* argv2[] = {"test-bin", "22222"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv2)), OkStatus());
+ CheckArgs(parsers, false, 1000, 22222);
+
+ // Out of range argument.
+ char const* argv3[] = {"test-bin", "65536"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv3)),
+ Status::InvalidArgument());
+
+ // Extra argument.
+ char const* argv4[] = {"test-bin", "1", "2"};
+ EXPECT_EQ(ParseArgs(parsers, 3, const_cast<char**>(argv4)),
+ Status::InvalidArgument());
+ EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
+
+ // Flag missing value.
+ char const* argv5[] = {"test-bin", "--runs"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv5)),
+ Status::InvalidArgument());
+
+ char const* argv6[] = {"test-bin", "-v", "33333", "--runs", "300"};
+ EXPECT_EQ(ParseArgs(parsers, 5, const_cast<char**>(argv6)), OkStatus());
+ CheckArgs(parsers, true, 300, 33333);
+
+ char const* argv7[] = {"test-bin", "-r", "400", "--verbose"};
+ EXPECT_EQ(ParseArgs(parsers, 4, const_cast<char**>(argv7)), OkStatus());
+ CheckArgs(parsers, true, 400, 11111);
+
+ char const* argv8[] = {"test-bin", "--no-verbose", "-r", "5000", "55555"};
+ EXPECT_EQ(ParseArgs(parsers, 5, const_cast<char**>(argv8)), OkStatus());
+ CheckArgs(parsers, false, 5000, 55555);
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/client_fuzzer.cc b/pw_rpc/fuzz/client_fuzzer.cc
new file mode 100644
index 0000000..c5086be
--- /dev/null
+++ b/pw_rpc/fuzz/client_fuzzer.cc
@@ -0,0 +1,111 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// clang-format off
+#include "pw_rpc/internal/log_config.h" // PW_LOG_* macros must be first.
+// clang-format on
+
+#include <sys/socket.h>
+
+#include <cstring>
+
+#include "pw_log/log.h"
+#include "pw_rpc/fuzz/argparse.h"
+#include "pw_rpc/fuzz/engine.h"
+#include "pw_rpc/integration_testing.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+// This client configures a socket read timeout to allow the RPC dispatch thread
+// to exit gracefully.
+constexpr timeval kSocketReadTimeout = {.tv_sec = 1, .tv_usec = 0};
+
+int FuzzClient(int argc, char** argv) {
+ // TODO(aarongreen): Incorporate descriptions into usage message.
+ Vector<ArgParserVariant, 5> parsers{
+ // Enables additional logging.
+ BoolParser("-v", "--verbose").set_default(false),
+
+ // The number of actions to perform as part of the test. A value of 0 runs
+ // indefinitely.
+ UnsignedParser<size_t>("-n", "--num-actions").set_default(256),
+
+ // The seed value for the PRNG. A value of 0 generates a seed.
+ UnsignedParser<uint64_t>("-s", "--seed").set_default(0),
+
+ // The time, in milliseconds, that can elapse without triggering an error.
+ UnsignedParser<size_t>("-t", "--timeout").set_default(5000),
+
+ // The port use to connect to the `test_rpc_server`.
+ UnsignedParser<uint16_t>("port").set_default(48000)};
+
+ if (!ParseArgs(parsers, argc, argv).ok()) {
+ PrintUsage(parsers, argv[0]);
+ return 1;
+ }
+
+ bool verbose;
+ size_t num_actions;
+ uint64_t seed;
+ size_t timeout_ms;
+ uint16_t port;
+ if (!GetArg(parsers, "--verbose", &verbose).ok() ||
+ !GetArg(parsers, "--num-actions", &num_actions).ok() ||
+ !GetArg(parsers, "--seed", &seed).ok() ||
+ !GetArg(parsers, "--timeout", &timeout_ms).ok() ||
+ !GetArg(parsers, "port", &port).ok()) {
+ return 1;
+ }
+
+ if (!seed) {
+ seed = chrono::SystemClock::now().time_since_epoch().count();
+ }
+
+ if (auto status = integration_test::InitializeClient(port); !status.ok()) {
+ PW_LOG_ERROR("Failed to initialize client: %s", pw_StatusString(status));
+ return 1;
+ }
+
+ // Set read timout on socket to allow
+ // pw::rpc::integration_test::TerminateClient() to complete.
+ int fd = integration_test::GetClientSocketFd();
+ if (setsockopt(fd,
+ SOL_SOCKET,
+ SO_RCVTIMEO,
+ &kSocketReadTimeout,
+ sizeof(kSocketReadTimeout)) != 0) {
+ PW_LOG_ERROR("Failed to configure socket receive timeout with errno=%d",
+ errno);
+ return 1;
+ }
+
+ if (num_actions == 0) {
+ num_actions = std::numeric_limits<size_t>::max();
+ }
+
+ Fuzzer fuzzer(integration_test::client(), integration_test::kChannelId);
+ fuzzer.set_verbose(verbose);
+ fuzzer.set_timeout(std::chrono::milliseconds(timeout_ms));
+ fuzzer.Run(seed, num_actions);
+ integration_test::TerminateClient();
+ return 0;
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
+
+int main(int argc, char** argv) {
+ return pw::rpc::fuzz::FuzzClient(argc, argv);
+}
diff --git a/pw_rpc/fuzz/engine.cc b/pw_rpc/fuzz/engine.cc
new file mode 100644
index 0000000..b19dfa3
--- /dev/null
+++ b/pw_rpc/fuzz/engine.cc
@@ -0,0 +1,553 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// clang-format off
+#include "pw_rpc/internal/log_config.h" // PW_LOG_* macros must be first.
+
+#include "pw_rpc/fuzz/engine.h"
+// clang-format on
+
+#include <algorithm>
+#include <cctype>
+#include <chrono>
+#include <cinttypes>
+#include <limits>
+#include <mutex>
+
+#include "pw_assert/check.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_string/format.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::chrono_literals;
+
+// Maximum number of bytes written in a single unary or stream request.
+constexpr size_t kMaxWriteLen = MaxSafePayloadSize();
+static_assert(kMaxWriteLen * 0x7E <= std::numeric_limits<uint16_t>::max());
+
+struct ActiveVisitor final {
+ using result_type = bool;
+ result_type operator()(std::monostate&) { return false; }
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) {
+ return call.active();
+ }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ return call.active();
+ }
+};
+
+struct CloseClientStreamVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver&) {}
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.CloseClientStream().IgnoreError();
+ }
+};
+
+struct WriteVisitor final {
+ using result_type = bool;
+ result_type operator()(std::monostate&) { return false; }
+ result_type operator()(pw::rpc::RawUnaryReceiver&) { return false; }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ if (!call.active()) {
+ return false;
+ }
+ call.Write(data).IgnoreError();
+ return true;
+ }
+ ConstByteSpan data;
+};
+
+struct CancelVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) {
+ call.Cancel().IgnoreError();
+ }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.Cancel().IgnoreError();
+ }
+};
+
+struct AbandonVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) { call.Abandon(); }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.Abandon();
+ }
+};
+
+} // namespace
+
+// `Action` methods.
+
+Action::Action(uint32_t encoded) {
+ // The first byte is used to determine the operation. The ranges used set the
+ // relative likelihood of each result, e.g. `kWait` is more likely than
+ // `kAbandon`.
+ uint32_t raw = encoded & 0xFF;
+ if (raw == 0) {
+ op = kSkip;
+ } else if (raw < 0x60) {
+ op = kWait;
+ } else if (raw < 0x80) {
+ op = kWriteUnary;
+ } else if (raw < 0xA0) {
+ op = kWriteStream;
+ } else if (raw < 0xC0) {
+ op = kCloseClientStream;
+ } else if (raw < 0xD0) {
+ op = kCancel;
+ } else if (raw < 0xE0) {
+ op = kAbandon;
+ } else if (raw < 0xF0) {
+ op = kSwap;
+ } else {
+ op = kDestroy;
+ }
+ target = ((encoded & 0xFF00) >> 8) % Fuzzer::kMaxConcurrentCalls;
+ value = encoded >> 16;
+}
+
+Action::Action(Op op_, size_t target_, uint16_t value_)
+ : op(op_), target(target_), value(value_) {}
+
+Action::Action(Op op_, size_t target_, char val, size_t len)
+ : op(op_), target(target_) {
+ PW_ASSERT(op == kWriteUnary || op == kWriteStream);
+ value = static_cast<uint16_t>(((val % 0x80) * kMaxWriteLen) +
+ (len % kMaxWriteLen));
+}
+
+char Action::DecodeWriteValue(uint16_t value) {
+ return static_cast<char>((value / kMaxWriteLen) % 0x7F);
+}
+
+size_t Action::DecodeWriteLength(uint16_t value) {
+ return value % kMaxWriteLen;
+}
+
+uint32_t Action::Encode() const {
+ uint32_t encoded = 0;
+ switch (op) {
+ case kSkip:
+ encoded = 0x00;
+ break;
+ case kWait:
+ encoded = 0x5F;
+ break;
+ case kWriteUnary:
+ encoded = 0x7F;
+ break;
+ case kWriteStream:
+ encoded = 0x9F;
+ break;
+ case kCloseClientStream:
+ encoded = 0xBF;
+ break;
+ case kCancel:
+ encoded = 0xCF;
+ break;
+ case kAbandon:
+ encoded = 0xDF;
+ break;
+ case kSwap:
+ encoded = 0xEF;
+ break;
+ case kDestroy:
+ encoded = 0xFF;
+ break;
+ }
+ encoded |=
+ ((target < Fuzzer::kMaxConcurrentCalls ? target
+ : Fuzzer::kMaxConcurrentCalls) %
+ 0xFF)
+ << 8;
+ encoded |= (static_cast<uint32_t>(value) << 16);
+ return encoded;
+}
+
+void Action::Log(bool verbose, size_t num_actions, const char* fmt, ...) const {
+ if (!verbose) {
+ return;
+ }
+ char s1[16];
+ auto result = callback_id < Fuzzer::kMaxConcurrentCalls
+ ? string::Format(s1, "%-3zu", callback_id)
+ : string::Format(s1, "n/a");
+ va_list ap;
+ va_start(ap, fmt);
+ char s2[128];
+ if (result.ok()) {
+ result = string::FormatVaList(s2, fmt, ap);
+ }
+ va_end(ap);
+ if (result.ok()) {
+ PW_LOG_INFO("#%-12zu\tthread: %zu\tcallback for: %s\ttarget call: %zu\t%s",
+ num_actions,
+ thread_id,
+ s1,
+ target,
+ s2);
+ } else {
+ LogFailure(verbose, num_actions, result.status());
+ }
+}
+
+void Action::LogFailure(bool verbose, size_t num_actions, Status status) const {
+ if (verbose && !status.ok()) {
+ PW_LOG_INFO("#%-12zu\tthread: %zu\tFailed to log action: %s",
+ num_actions,
+ thread_id,
+ pw_StatusString(status));
+ }
+}
+
+// FuzzyCall methods.
+
+void FuzzyCall::RecordWrite(size_t num, bool append) {
+ std::lock_guard lock(mutex_);
+ if (append) {
+ last_write_ += num;
+ } else {
+ last_write_ = num;
+ }
+ total_written_ += num;
+ pending_ = true;
+}
+
+void FuzzyCall::Await() {
+ std::unique_lock<sync::Mutex> lock(mutex_);
+ cv_.wait(lock, [this]() PW_NO_LOCK_SAFETY_ANALYSIS { return !pending_; });
+}
+
+void FuzzyCall::Notify() {
+ if (pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+}
+
+void FuzzyCall::Swap(FuzzyCall& other) {
+ if (index_ == other.index_) {
+ return;
+ }
+ // Manually acquire locks in an order based on call IDs to prevent deadlock.
+ if (index_ < other.index_) {
+ mutex_.lock();
+ other.mutex_.lock();
+ } else {
+ other.mutex_.lock();
+ mutex_.lock();
+ }
+ call_.swap(other.call_);
+ std::swap(id_, other.id_);
+ pending_ = other.pending_.exchange(pending_);
+ std::swap(last_write_, other.last_write_);
+ std::swap(total_written_, other.total_written_);
+ mutex_.unlock();
+ other.mutex_.unlock();
+ cv_.notify_all();
+ other.cv_.notify_all();
+}
+
+void FuzzyCall::Reset(Variant call) {
+ {
+ std::lock_guard lock(mutex_);
+ call_ = std::move(call);
+ }
+ cv_.notify_all();
+}
+
+void FuzzyCall::Log() {
+ if (mutex_.try_lock_for(100ms)) {
+ PW_LOG_INFO("call %zu:", index_);
+ PW_LOG_INFO(" active: %s",
+ std::visit(ActiveVisitor(), call_) ? "true" : "false");
+ PW_LOG_INFO(" request pending: %s ", pending_ ? "true" : "false");
+ PW_LOG_INFO(" last write: %zu bytes", last_write_);
+ PW_LOG_INFO(" total written: %zu bytes", total_written_);
+ mutex_.unlock();
+ } else {
+ PW_LOG_WARN("call %zu: failed to acquire lock", index_);
+ }
+}
+
+// `Fuzzer` methods.
+
+#define FUZZ_LOG_VERBOSE(...) \
+ if (verbose_) { \
+ PW_LOG_INFO(__VA_ARGS__); \
+ }
+
+Fuzzer::Fuzzer(Client& client, uint32_t channel_id)
+ : client_(client, channel_id),
+ timer_([this](chrono::SystemClock::time_point) {
+ PW_LOG_ERROR(
+ "Workers performed %zu actions before timing out without an "
+ "update.",
+ num_actions_.load());
+ PW_LOG_INFO("Additional call details:");
+ for (auto& call : fuzzy_calls_) {
+ call.Log();
+ }
+ PW_CRASH("Fuzzer found a fatal error condition: TIMEOUT.");
+ }) {
+ for (size_t index = 0; index < kMaxConcurrentCalls; ++index) {
+ fuzzy_calls_.emplace_back(index);
+ indices_.push_back(index);
+ contexts_.push_back(CallbackContext{.id = index, .fuzzer = this});
+ }
+}
+
+void Fuzzer::Run(uint64_t seed, size_t num_actions) {
+ FUZZ_LOG_VERBOSE("Fuzzing RPC client with:");
+ FUZZ_LOG_VERBOSE(" num_actions: %zu", num_actions);
+ FUZZ_LOG_VERBOSE(" seed: %" PRIu64, seed);
+ num_actions_.store(0);
+ random::XorShiftStarRng64 rng(seed);
+ while (true) {
+ {
+ size_t actions_done = num_actions_.load();
+ if (actions_done >= num_actions) {
+ FUZZ_LOG_VERBOSE("Fuzzing complete; %zu actions performed.",
+ actions_done);
+ break;
+ }
+ FUZZ_LOG_VERBOSE("%zu actions remaining.", num_actions - actions_done);
+ }
+ FUZZ_LOG_VERBOSE("Generating %zu random actions.", kMaxActions);
+ pw::Vector<uint32_t, kMaxActions> actions;
+ for (size_t i = 0; i < kNumThreads; ++i) {
+ size_t num_actions_for_thread;
+ rng.GetInt(num_actions_for_thread, kMaxActionsPerThread + 1);
+ for (size_t j = 0; j < num_actions_for_thread; ++j) {
+ uint32_t encoded = 0;
+ while (!encoded) {
+ rng.GetInt(encoded);
+ }
+ actions.push_back(encoded);
+ }
+ actions.push_back(0);
+ }
+ Run(actions);
+ }
+}
+
+void Fuzzer::Run(const pw::Vector<uint32_t>& actions) {
+ FUZZ_LOG_VERBOSE("Starting %zu threads to perform %zu actions:",
+ kNumThreads - 1,
+ actions.size());
+ FUZZ_LOG_VERBOSE(" timeout: %lldms", timer_.timeout() / 1ms);
+ auto iter = actions.begin();
+ timer_.Restart();
+ for (size_t thread_id = 0; thread_id < kNumThreads; ++thread_id) {
+ pw::Vector<uint32_t, kMaxActionsPerThread> thread_actions;
+ while (thread_actions.size() < kMaxActionsPerThread &&
+ iter != actions.end()) {
+ uint32_t encoded = *iter++;
+ if (!encoded) {
+ break;
+ }
+ thread_actions.push_back(encoded);
+ }
+ if (thread_id == 0) {
+ std::lock_guard lock(mutex_);
+ callback_actions_ = std::move(thread_actions);
+ callback_iterator_ = callback_actions_.begin();
+ } else {
+ threads_.emplace_back(
+ [this, thread_id, actions = std::move(thread_actions)]() {
+ for (const auto& encoded : actions) {
+ Action action(encoded);
+ action.set_thread_id(thread_id);
+ Perform(action);
+ }
+ });
+ }
+ }
+ for (auto& t : threads_) {
+ t.join();
+ }
+ for (auto& fuzzy_call : fuzzy_calls_) {
+ fuzzy_call.Reset();
+ }
+ timer_.Cancel();
+}
+
+void Fuzzer::Perform(const Action& action) {
+ FuzzyCall& fuzzy_call = FindCall(action.target);
+ switch (action.op) {
+ case Action::kSkip: {
+ if (action.thread_id == 0) {
+ action.Log(verbose_, ++num_actions_, "Callback chain completed");
+ }
+ break;
+ }
+ case Action::kWait: {
+ if (action.callback_id == action.target) {
+ // Don't wait in a callback of the target call.
+ break;
+ }
+ if (fuzzy_call.pending()) {
+ action.Log(verbose_, ++num_actions_, "Waiting for call.");
+ fuzzy_call.Await();
+ }
+ break;
+ }
+ case Action::kWriteUnary:
+ case Action::kWriteStream: {
+ if (action.callback_id == action.target) {
+ // Don't create a new call from the call's own callback.
+ break;
+ }
+ char buf[kMaxWriteLen];
+ char val = Action::DecodeWriteValue(action.value);
+ size_t len = Action::DecodeWriteLength(action.value);
+ memset(buf, val, len);
+ if (verbose_) {
+ char msg_buf[64];
+ span msg(msg_buf);
+ auto result = string::Format(
+ msg,
+ "Writing %s request of ",
+ action.op == Action::kWriteUnary ? "unary" : "stream");
+ if (result.ok()) {
+ size_t off = result.size();
+ result = string::Format(
+ msg.subspan(off),
+ isprint(val) ? "['%c'; %zu]." : "['\\x%02x'; %zu].",
+ val,
+ len);
+ }
+ size_t num_actions = ++num_actions_;
+ if (result.ok()) {
+ action.Log(verbose_, num_actions, "%s", msg.data());
+ } else if (verbose_) {
+ action.LogFailure(verbose_, num_actions, result.status());
+ }
+ }
+ bool append = false;
+ if (action.op == Action::kWriteUnary) {
+ // Send a unary request.
+ fuzzy_call.Reset(client_.UnaryEcho(
+ as_bytes(span(buf, len)),
+ /* on completed */
+ [context = GetContext(action.target)](ConstByteSpan, Status) {
+ context->fuzzer->OnCompleted(context->id);
+ },
+ /* on error */
+ [context = GetContext(action.target)](Status status) {
+ context->fuzzer->OnError(context->id, status);
+ }));
+
+ } else if (fuzzy_call.Visit(
+ WriteVisitor{.data = as_bytes(span(buf, len))})) {
+ // Append to an existing stream
+ append = true;
+ } else {
+ // .Open a new stream.
+ fuzzy_call.Reset(client_.BidirectionalEcho(
+ /* on next */
+ [context = GetContext(action.target)](ConstByteSpan) {
+ context->fuzzer->OnNext(context->id);
+ },
+ /* on completed */
+ [context = GetContext(action.target)](Status) {
+ context->fuzzer->OnCompleted(context->id);
+ },
+ /* on error */
+ [context = GetContext(action.target)](Status status) {
+ context->fuzzer->OnError(context->id, status);
+ }));
+ }
+ fuzzy_call.RecordWrite(len, append);
+ break;
+ }
+ case Action::kCloseClientStream:
+ action.Log(verbose_, ++num_actions_, "Closing stream.");
+ fuzzy_call.Visit(CloseClientStreamVisitor());
+ break;
+ case Action::kCancel:
+ action.Log(verbose_, ++num_actions_, "Canceling call.");
+ fuzzy_call.Visit(CancelVisitor());
+ break;
+ case Action::kAbandon: {
+ action.Log(verbose_, ++num_actions_, "Abandoning call.");
+ fuzzy_call.Visit(AbandonVisitor());
+ break;
+ }
+ case Action::kSwap: {
+ size_t other_target = action.value % kMaxConcurrentCalls;
+ if (action.callback_id == action.target ||
+ action.callback_id == other_target) {
+ // Don't move a call from within its own callback.
+ break;
+ }
+ action.Log(verbose_,
+ ++num_actions_,
+ "Swapping call with call %zu.",
+ other_target);
+ std::lock_guard lock(mutex_);
+ FuzzyCall& other = FindCallLocked(other_target);
+ std::swap(indices_[fuzzy_call.id()], indices_[other.id()]);
+ fuzzy_call.Swap(other);
+ break;
+ }
+ case Action::kDestroy: {
+ if (action.callback_id == action.target) {
+ // Don't destroy a call from within its own callback.
+ break;
+ }
+ action.Log(verbose_, ++num_actions_, "Destroying call.");
+ fuzzy_call.Reset();
+ break;
+ }
+ default:
+ break;
+ }
+ timer_.Restart();
+}
+
+void Fuzzer::OnNext(size_t callback_id) { FindCall(callback_id).Notify(); }
+
+void Fuzzer::OnCompleted(size_t callback_id) {
+ uint32_t encoded = 0;
+ {
+ std::lock_guard lock(mutex_);
+ if (callback_iterator_ != callback_actions_.end()) {
+ encoded = *callback_iterator_++;
+ }
+ }
+ Action action(encoded);
+ action.set_callback_id(callback_id);
+ Perform(action);
+ FindCall(callback_id).Notify();
+}
+
+void Fuzzer::OnError(size_t callback_id, Status status) {
+ FuzzyCall& call = FindCall(callback_id);
+ PW_LOG_WARN("Call %zu received an error from the server: %s",
+ call.id(),
+ pw_StatusString(status));
+ call.Notify();
+}
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/engine_test.cc b/pw_rpc/fuzz/engine_test.cc
new file mode 100644
index 0000000..5ac1a49
--- /dev/null
+++ b/pw_rpc/fuzz/engine_test.cc
@@ -0,0 +1,264 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_rpc/fuzz/engine.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_containers/vector.h"
+#include "pw_log/log.h"
+#include "pw_rpc/benchmark.h"
+#include "pw_rpc/internal/client_server_testing_threaded.h"
+#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_thread/test_threads.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::literals::chrono_literals;
+
+// Maximum time, in milliseconds, that can elapse without a call completing or
+// being dropped in some way..
+const chrono::SystemClock::duration kTimeout = 5s;
+
+// These are fairly tight constraints in order to fit within the default
+// `PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE`.
+constexpr size_t kMaxPackets = 128;
+constexpr size_t kMaxPayloadSize = 64;
+
+using BufferedChannelOutputBase =
+ internal::test::FakeChannelOutputBuffer<kMaxPackets, kMaxPayloadSize>;
+
+/// Channel output backed by a fixed buffer.
+class BufferedChannelOutput : public BufferedChannelOutputBase {
+ public:
+ BufferedChannelOutput() : BufferedChannelOutputBase() {}
+};
+
+using FuzzerChannelOutputBase =
+ internal::WatchableChannelOutput<BufferedChannelOutput,
+ kMaxPayloadSize,
+ kMaxPackets,
+ kMaxPayloadSize>;
+
+/// Channel output that can be waited on by the server.
+class FuzzerChannelOutput : public FuzzerChannelOutputBase {
+ public:
+ FuzzerChannelOutput() : FuzzerChannelOutputBase() {}
+};
+
+using FuzzerContextBase =
+ internal::ClientServerTestContextThreaded<FuzzerChannelOutput,
+ kMaxPayloadSize,
+ kMaxPackets,
+ kMaxPayloadSize>;
+class FuzzerContext : public FuzzerContextBase {
+ public:
+ static constexpr uint32_t kChannelId = 1;
+
+ FuzzerContext() : FuzzerContextBase(thread::test::TestOptionsThread0()) {}
+};
+
+class RpcFuzzTestingTest : public testing::Test {
+ protected:
+ void SetUp() override { context_.server().RegisterService(service_); }
+
+ void Add(Action::Op op, size_t target, uint16_t value) {
+ actions_.push_back(Action(op, target, value).Encode());
+ }
+
+ void Add(Action::Op op, size_t target, char val, size_t len) {
+ actions_.push_back(Action(op, target, val, len).Encode());
+ }
+
+ void NextThread() { actions_.push_back(0); }
+
+ void Run() {
+ Fuzzer fuzzer(context_.client(), FuzzerContext::kChannelId);
+ fuzzer.set_verbose(true);
+ fuzzer.set_timeout(kTimeout);
+ fuzzer.Run(actions_);
+ }
+
+ private:
+ FuzzerContext context_;
+ BenchmarkService service_;
+ Vector<uint32_t, Fuzzer::kMaxActions> actions_;
+};
+
+TEST_F(RpcFuzzTestingTest, SequentialRequests) {
+ // Callback thread
+ Add(Action::kWriteStream, 1, 'B', 1);
+ Add(Action::kSkip, 0, 0);
+ Add(Action::kWriteStream, 2, 'B', 2);
+ Add(Action::kSkip, 0, 0);
+ Add(Action::kWriteStream, 3, 'B', 3);
+ Add(Action::kSkip, 0, 0);
+ NextThread();
+
+ // Thread 1
+ Add(Action::kWriteStream, 0, 'A', 2);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWriteStream, 1, 'A', 4);
+ NextThread();
+
+ // Thread 2
+ NextThread();
+ Add(Action::kWait, 2, 0);
+ Add(Action::kWriteStream, 2, 'A', 6);
+
+ // Thread 3
+ NextThread();
+ Add(Action::kWait, 3, 0);
+
+ Run();
+}
+
+// TODO(b/274437709): Re-enable.
+TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ Add(Action::kWriteUnary, 1, 'A', 1);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ Add(Action::kWriteUnary, 2, 'B', 2);
+ Add(Action::kWait, 3, 0);
+ NextThread();
+
+ // Thread 3
+ Add(Action::kWriteUnary, 3, 'C', 3);
+ Add(Action::kWait, 1, 0);
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kCancel, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kAbandon, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
+ Vector<uint32_t, Fuzzer::kMaxActions> actions;
+ // Callback thread
+ NextThread();
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+ // Thread 2
+ for (size_t i = 0; i < 100; ++i) {
+ auto j = i % 3;
+ Add(Action::kSwap, j, j + 1);
+ }
+ NextThread();
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_DestroyedRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 100; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 100; ++i) {
+ Add(Action::kDestroy, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h
new file mode 100644
index 0000000..9ccd7ce
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h
@@ -0,0 +1,56 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_chrono/system_clock.h"
+#include "pw_chrono/system_timer.h"
+
+namespace pw::rpc::fuzz {
+
+/// Represents a timer that invokes a callback on timeout. Once started, it will
+/// invoke the callback after a provided duration unless it is restarted,
+/// canceled, or destroyed.
+class AlarmTimer {
+ public:
+ AlarmTimer(chrono::SystemTimer::ExpiryCallback&& on_timeout)
+ : timer_(std::move(on_timeout)) {}
+
+ chrono::SystemClock::duration timeout() const { return timeout_; }
+
+ /// "Arms" the timer. The callback will be invoked if `timeout` elapses
+ /// without a call to `Restart`, `Cancel`, or the destructor. Calling `Start`
+ /// again restarts the timer, possibly with a different `timeout` value.
+ void Start(chrono::SystemClock::duration timeout) {
+ timeout_ = timeout;
+ Restart();
+ }
+
+ /// Restarts the timer. This is equivalent to calling `Start` with the same
+ /// `timeout` as passed previously. Does nothing if `Start` has not been
+ /// called.
+ void Restart() {
+ Cancel();
+ timer_.InvokeAfter(timeout_);
+ }
+
+ /// "Disarms" the timer. The callback will not be invoked unless `Start` is
+ /// called again. Does nothing if `Start` has not been called.
+ void Cancel() { timer_.Cancel(); }
+
+ private:
+ chrono::SystemTimer timer_;
+ chrono::SystemClock::duration timeout_;
+};
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
new file mode 100644
index 0000000..05a7633
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
@@ -0,0 +1,230 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+/// Command line argument parsing.
+///
+/// The objects defined below can be used to parse command line arguments of
+/// different types. These objects are "just enough" defined for current use
+/// cases, but the design is intended to be extensible as new types and traits
+/// are needed.
+///
+/// Example:
+///
+/// Given a boolean flag "verbose", a numerical flag "runs", and a positional
+/// "port" argument to be parsed, we can create a vector of parsers. In this
+/// example, we modify the parsers during creation to set default values:
+///
+/// @code
+/// Vector<ArgParserVariant, 3> parsers = {
+/// BoolParser("-v", "--verbose").set_default(false),
+/// UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+/// UnsignedParser<uint16_t>("port").set_default(11111),
+/// };
+/// @endcode
+///
+/// With this vector, we can then parse command line arguments and extract
+/// the values of arguments that were set, e.g.:
+///
+/// @code
+/// if (!ParseArgs(parsers, argc, argv).ok()) {
+/// PrintUsage(parsers, argv[0]);
+/// return 1;
+/// }
+/// bool verbose;
+/// size_t runs;
+/// uint16_t port;
+/// if (!GetArg(parsers, "--verbose", &verbose).ok() ||
+/// !GetArg(parsers, "--runs", &runs).ok() ||
+/// !GetArg(parsers, "port", &port).ok()) {
+/// // Shouldn't happen unless names do not match.
+/// return 1;
+/// }
+///
+/// // Do stuff with `verbose`, `runs`, and `port`...
+/// @endcode
+
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+#include <variant>
+
+#include "pw_containers/vector.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::fuzz {
+
+/// Enumerates the results of trying to parse a specific command line argument
+/// with a particular parsers.
+enum ParseStatus {
+ /// The argument matched the parser and was successfully parsed without a
+ /// value.
+ kParsedOne,
+
+ /// The argument matched the parser and was successfully parsed with a value.
+ kParsedTwo,
+
+ /// The argument did not match the parser. This is not necessarily an error;
+ /// the argument may match a different parser.
+ kParseMismatch,
+
+ /// The argument matched a parser, but could not be parsed. This may be due to
+ /// a missing value for a flag, a value of the wrong type, a provided value
+ /// being out of range, etc. Parsers should log additional details before
+ /// returning this value.
+ kParseFailure,
+};
+
+/// Holds parsed argument values of different types.
+using ArgVariant = std::variant<std::monostate, bool, uint64_t>;
+
+/// Base class for argument parsers.
+class ArgParserBase {
+ public:
+ virtual ~ArgParserBase() = default;
+
+ std::string_view short_name() const { return short_name_; }
+ std::string_view long_name() const { return long_name_; }
+ bool positional() const { return positional_; }
+
+ /// Clears the value. Typically, command line arguments are only parsed once,
+ /// but this method is useful for testing.
+ void Reset() { value_ = std::monostate(); }
+
+ protected:
+ /// Defines an argument parser with a single name. This may be a positional
+ /// argument or a flag.
+ ArgParserBase(std::string_view name);
+
+ /// Defines an argument parser for a flag with short and long names.
+ ArgParserBase(std::string_view shortopt, std::string_view longopt);
+
+ void set_initial(ArgVariant initial) { initial_ = initial; }
+ void set_value(ArgVariant value) { value_ = value; }
+
+ /// Examines if the given `arg` matches this parser. A parser for a flag can
+ /// match the short name (e.g. '-f') if set, or the long name (e.g. '--foo').
+ /// A parser for a positional argument will match anything until it has a
+ /// value set.
+ bool Match(std::string_view arg);
+
+ /// Returns the parsed value.
+ template <typename T>
+ T Get() const {
+ return std::get<T>(GetValue());
+ }
+
+ private:
+ const ArgVariant& GetValue() const;
+
+ std::string_view short_name_;
+ std::string_view long_name_;
+ bool positional_;
+
+ ArgVariant initial_;
+ ArgVariant value_;
+};
+
+// Argument parsers for boolean arguments. These arguments are always flags, and
+// can be specified as, e.g. "-f" (true), "--foo" (true) or "--no-foo" (false).
+class BoolParser : public ArgParserBase {
+ public:
+ BoolParser(std::string_view optname);
+ BoolParser(std::string_view shortopt, std::string_view longopt);
+
+ bool value() const { return Get<bool>(); }
+ BoolParser& set_default(bool value);
+
+ ParseStatus Parse(std::string_view arg0,
+ std::string_view arg1 = std::string_view());
+};
+
+// Type-erasing argument parser for unsigned integer arguments. This object
+// always parses values as `uint64_t`s and should not be used directly.
+// Instead, use `UnsignedParser<T>` with a type to explicitly narrow to.
+class UnsignedParserBase : public ArgParserBase {
+ protected:
+ UnsignedParserBase(std::string_view name);
+ UnsignedParserBase(std::string_view shortopt, std::string_view longopt);
+
+ ParseStatus Parse(std::string_view arg0, std::string_view arg1, uint64_t max);
+};
+
+// Argument parser for unsigned integer arguments. These arguments may be flags
+// or positional arguments.
+template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0>
+class UnsignedParser : public UnsignedParserBase {
+ public:
+ UnsignedParser(std::string_view name) : UnsignedParserBase(name) {}
+ UnsignedParser(std::string_view shortopt, std::string_view longopt)
+ : UnsignedParserBase(shortopt, longopt) {}
+
+ T value() const { return static_cast<T>(Get<uint64_t>()); }
+
+ UnsignedParser& set_default(T value) {
+ set_initial(static_cast<uint64_t>(value));
+ return *this;
+ }
+
+ ParseStatus Parse(std::string_view arg0,
+ std::string_view arg1 = std::string_view()) {
+ return UnsignedParserBase::Parse(arg0, arg1, std::numeric_limits<T>::max());
+ }
+};
+
+// Holds argument parsers of different types.
+using ArgParserVariant =
+ std::variant<BoolParser, UnsignedParser<uint16_t>, UnsignedParser<size_t>>;
+
+// Parses the command line arguments and sets the values of the given `parsers`.
+Status ParseArgs(Vector<ArgParserVariant>& parsers, int argc, char** argv);
+
+// Logs a usage message based on the given `parsers` and the program name given
+// by `argv0`.
+void PrintUsage(const Vector<ArgParserVariant>& parsers,
+ std::string_view argv0);
+
+// Attempts to find the parser in `parsers` with the given `name`, and returns
+// its value if found.
+std::optional<ArgVariant> GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name);
+
+inline void GetArgValue(const ArgVariant& arg, bool* out) {
+ *out = std::get<bool>(arg);
+}
+
+template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0>
+void GetArgValue(const ArgVariant& arg, T* out) {
+ *out = static_cast<T>(std::get<uint64_t>(arg));
+}
+
+// Like `GetArgVariant` above, but extracts the typed value from the variant
+// into `out`. Returns an error if no parser exists in `parsers` with the given
+// `name`.
+template <typename T>
+Status GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name,
+ T* out) {
+ const auto& arg = GetArg(parsers, name);
+ if (!arg.has_value()) {
+ return Status::InvalidArgument();
+ }
+ GetArgValue(*arg, out);
+ return OkStatus();
+}
+
+// Resets the parser with the given name. Returns an error if not found.
+Status ResetArg(Vector<ArgParserVariant>& parsers, std::string_view name);
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h
new file mode 100644
index 0000000..34e92c0
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h
@@ -0,0 +1,339 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <atomic>
+#include <cstdarg>
+#include <cstddef>
+#include <cstdint>
+#include <thread>
+#include <variant>
+
+#include "pw_containers/vector.h"
+#include "pw_random/xor_shift.h"
+#include "pw_rpc/benchmark.h"
+#include "pw_rpc/benchmark.raw_rpc.pb.h"
+#include "pw_rpc/fuzz/alarm_timer.h"
+#include "pw_sync/condition_variable.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/timed_mutex.h"
+
+namespace pw::rpc::fuzz {
+
+/// Describes an action a fuzzing thread can perform on a call.
+struct Action {
+ enum Op : uint8_t {
+ /// No-op.
+ kSkip,
+
+ /// Waits for the call indicated by `target` to complete.
+ kWait,
+
+ /// Makes a new unary request using the call indicated by `target`. The data
+ /// written is derived from `value`.
+ kWriteUnary,
+
+ /// Writes to a stream request using the call indicated by `target`, or
+ /// makes
+ /// a new one if not currently a stream call. The data written is derived
+ /// from `value`.
+ kWriteStream,
+
+ /// Closes the stream if the call indicated by `target` is a stream call.
+ kCloseClientStream,
+
+ /// Cancels the call indicated by `target`.
+ kCancel,
+
+ /// Abandons the call indicated by `target`.
+ kAbandon,
+
+ /// Swaps the call indicated by `target` with a call indicated by `value`.
+ kSwap,
+
+ /// Sets the call indicated by `target` to an initial, unset state.
+ kDestroy,
+ };
+
+ constexpr Action() = default;
+ Action(uint32_t encoded);
+ Action(Op op, size_t target, uint16_t value);
+ Action(Op op, size_t target, char val, size_t len);
+ ~Action() = default;
+
+ void set_thread_id(size_t thread_id_) {
+ thread_id = thread_id_;
+ callback_id = std::numeric_limits<size_t>::max();
+ }
+
+ void set_callback_id(size_t callback_id_) {
+ thread_id = 0;
+ callback_id = callback_id_;
+ }
+
+ // For a write action's value, returns the character value to be written.
+ static char DecodeWriteValue(uint16_t value);
+
+ // For a write action's value, returns the number of characters to be written.
+ static size_t DecodeWriteLength(uint16_t value);
+
+ /// Returns a value that represents the fields of an action. Constructing an
+ /// `Action` with this value will produce the same fields.
+ uint32_t Encode() const;
+
+ /// Records details of the action being performed if verbose logging is
+ /// enabled.
+ void Log(bool verbose, size_t num_actions, const char* fmt, ...) const;
+
+ /// Records an encountered when trying to log an action.
+ void LogFailure(bool verbose, size_t num_actions, Status status) const;
+
+ Op op = kSkip;
+ size_t target = 0;
+ uint16_t value = 0;
+
+ size_t thread_id = 0;
+ size_t callback_id = std::numeric_limits<size_t>::max();
+};
+
+/// Wraps an RPC call that may be either a `RawUnaryReceiver` or
+/// `RawClientReaderWriter`. Allows applying `Action`s to each possible
+/// type of call.
+class FuzzyCall {
+ public:
+ using Variant =
+ std::variant<std::monostate, RawUnaryReceiver, RawClientReaderWriter>;
+
+ explicit FuzzyCall(size_t index) : index_(index), id_(index) {}
+ ~FuzzyCall() = default;
+
+ size_t id() {
+ std::lock_guard lock(mutex_);
+ return id_;
+ }
+
+ bool pending() {
+ std::lock_guard lock(mutex_);
+ return pending_;
+ }
+
+ /// Applies the given visitor to the call variant. If the action taken by the
+ /// visitor is expected to complete the call, it will notify any threads
+ /// waiting for the call to complete. This version of the method does not
+ /// return the result of the visiting the variant.
+ template <typename Visitor,
+ typename std::enable_if_t<
+ std::is_same_v<typename Visitor::result_type, void>,
+ int> = 0>
+ typename Visitor::result_type Visit(Visitor visitor, bool completes = true) {
+ {
+ std::lock_guard lock(mutex_);
+ std::visit(std::move(visitor), call_);
+ }
+ if (completes && pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+ }
+
+ /// Applies the given visitor to the call variant. If the action taken by the
+ /// visitor is expected to complete the call, it will notify any threads
+ /// waiting for the call to complete. This version of the method returns the
+ /// result of the visiting the variant.
+ template <typename Visitor,
+ typename std::enable_if_t<
+ !std::is_same_v<typename Visitor::result_type, void>,
+ int> = 0>
+ typename Visitor::result_type Visit(Visitor visitor, bool completes = true) {
+ typename Visitor::result_type result;
+ {
+ std::lock_guard lock(mutex_);
+ result = std::visit(std::move(visitor), call_);
+ }
+ if (completes && pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+ return result;
+ }
+
+ // Records the number of bytes written as part of a request. If `append` is
+ // true, treats the write as a continuation of a streaming request.
+ void RecordWrite(size_t num, bool append = false);
+
+ /// Waits to be notified that a callback has been invoked.
+ void Await() PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Completes the call, notifying any waiters.
+ void Notify() PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Exchanges the call represented by this object with another.
+ void Swap(FuzzyCall& other);
+
+ /// Resets the call wrapped by this object with a new one. Destorys the
+ /// previous call.
+ void Reset(Variant call = Variant()) PW_LOCKS_EXCLUDED(mutex_);
+
+ // Reports the state of this object.
+ void Log() PW_LOCKS_EXCLUDED(mutex_);
+
+ private:
+ /// This represents the index in the engine's list of calls. It is used to
+ /// ensure a consistent order of locking multiple calls.
+ const size_t index_;
+
+ sync::TimedMutex mutex_;
+ sync::ConditionVariable cv_;
+
+ /// An identifier that can be used find this object, e.g. by a callback, even
+ /// when it has been swapped with another call.
+ size_t id_ PW_GUARDED_BY(mutex_);
+
+ /// Holds the actual pw::rpc::Call object, when present.
+ Variant call_ PW_GUARDED_BY(mutex_);
+
+ /// Set when a request is sent, and cleared when a callback is invoked.
+ std::atomic_bool pending_ = false;
+
+ /// Bytes sent in the last unary request or stream write.
+ size_t last_write_ PW_GUARDED_BY(mutex_) = 0;
+
+ /// Total bytes sent using this call object.
+ size_t total_written_ PW_GUARDED_BY(mutex_) = 0;
+};
+
+/// The main RPC fuzzing engine.
+///
+/// This class takes or generates a sequence of actions, and dsitributes them to
+/// a number of threads that can perform them using an RPC client. Passing the
+/// same seed to the engine at construction will allow it to generate the same
+/// sequence of actions.
+class Fuzzer {
+ public:
+ /// Number of fuzzing threads. The first thread counted is the RPC dispatch
+ /// thread.
+ static constexpr size_t kNumThreads = 4;
+
+ /// Maximum number of actions that a single thread will try to perform before
+ /// exiting.
+ static constexpr size_t kMaxActionsPerThread = 255;
+
+ /// The number of call objects available to be used for fuzzing.
+ static constexpr size_t kMaxConcurrentCalls = 8;
+
+ /// The mxiumum number of individual fuzzing actions that the fuzzing threads
+ /// can perform. The `+ 1` is to allow the inclusion of a special `0` action
+ /// to separate each thread's actions when concatenated into a single list.
+ static constexpr size_t kMaxActions =
+ kNumThreads * (kMaxActionsPerThread + 1);
+
+ explicit Fuzzer(Client& client, uint32_t channel_id);
+
+ /// The fuzzer engine should remain pinned in memory since it is referenced by
+ /// the `CallbackContext`s.
+ Fuzzer(const Fuzzer&) = delete;
+ Fuzzer(Fuzzer&&) = delete;
+ Fuzzer& operator=(const Fuzzer&) = delete;
+ Fuzzer& operator=(Fuzzer&&) = delete;
+
+ void set_verbose(bool verbose) { verbose_ = verbose; }
+
+ /// Sets the timeout and starts the timer.
+ void set_timeout(chrono::SystemClock::duration timeout) {
+ timer_.Start(timeout);
+ }
+
+ /// Generates encoded actions from the RNG and `Run`s them.
+ void Run(uint64_t seed, size_t num_actions);
+
+ /// Splits the provided `actions` between the fuzzing threads and runs them to
+ /// completion.
+ void Run(const Vector<uint32_t>& actions);
+
+ private:
+ /// Information passed to the RPC callbacks, including the index of the
+ /// associated call and a pointer to the fuzzer object.
+ struct CallbackContext {
+ size_t id;
+ Fuzzer* fuzzer;
+ };
+
+ /// Restarts the alarm timer, delaying it from detecting a timeout. This is
+ /// called whenever actions complete and indicates progress is still being
+ /// made.
+ void ResetTimerLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// Decodes the `encoded` action and performs it. The `thread_id` is used for
+ /// verbose diagnostics. When invoked from `PerformCallback` the `callback_id`
+ /// will be set to the index of the associated call. This allows avoiding
+ /// specific, prohibited actions, e.g. destroying a call from its own
+ /// callback.
+ void Perform(const Action& action) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Returns the call with the matching `id`.
+ FuzzyCall& FindCall(size_t id) PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return FindCallLocked(id);
+ }
+
+ FuzzyCall& FindCallLocked(size_t id) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
+ return fuzzy_calls_[indices_[id]];
+ }
+
+ /// Returns a pointer to callback context for the given call index.
+ CallbackContext* GetContext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return &contexts_[callback_id];
+ }
+
+ /// Callback for stream write made by the call with the given `callback_id`.
+ void OnNext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Callback for completed request for the call with the given `callback_id`.
+ void OnCompleted(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Callback for an error for the call with the given `callback_id`.
+ void OnError(size_t callback_id, Status status) PW_LOCKS_EXCLUDED(mutex_);
+
+ bool verbose_ = false;
+ pw_rpc::raw::Benchmark::Client client_;
+ BenchmarkService service_;
+
+ /// Alarm thread that detects when no workers have made recent progress.
+ AlarmTimer timer_;
+
+ sync::Mutex mutex_;
+
+ /// Worker threads. The first thread is the RPC response dispatcher.
+ Vector<std::thread, kNumThreads> threads_;
+
+ /// RPC call objects.
+ Vector<FuzzyCall, kMaxConcurrentCalls> fuzzy_calls_;
+
+ /// Maps each call's IDs to its index. Since calls may be move before their
+ /// callbacks are invoked, this list can be used to find the original call.
+ Vector<size_t, kMaxConcurrentCalls> indices_ PW_GUARDED_BY(mutex_);
+
+ /// Context objects used to reference the engine and call.
+ Vector<CallbackContext, kMaxConcurrentCalls> contexts_ PW_GUARDED_BY(mutex_);
+
+ /// Set of actions performed as callbacks from other calls.
+ Vector<uint32_t, kMaxActionsPerThread> callback_actions_
+ PW_GUARDED_BY(mutex_);
+ Vector<uint32_t>::iterator callback_iterator_ PW_GUARDED_BY(mutex_);
+
+ /// Total actions performed by all workers.
+ std::atomic<size_t> num_actions_ = 0;
+};
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/internal/integration_test_ports.gni b/pw_rpc/internal/integration_test_ports.gni
index 5413f87..5197aab 100644
--- a/pw_rpc/internal/integration_test_ports.gni
+++ b/pw_rpc/internal/integration_test_ports.gni
@@ -16,4 +16,5 @@
# in one place to prevent accidental conflicts between tests.
pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT = 30576
pw_rpc_CPP_CLIENT_INTEGRATION_TEST_PORT = 30577
+pw_rpc_CPP_CLIENT_FUZZER_TEST_PORT = 30578
pw_unit_test_RPC_SERVICE_TEST_PORT = 30580
diff --git a/pw_rpc/internal/packet.proto b/pw_rpc/internal/packet.proto
index eb29072..606efb9 100644
--- a/pw_rpc/internal/packet.proto
+++ b/pw_rpc/internal/packet.proto
@@ -33,10 +33,6 @@
// The client received a packet for an RPC it did not request.
CLIENT_ERROR = 4;
- // Deprecated, do not use. Send a CLIENT_ERROR with status CANCELLED instead.
- // TODO(b/234879973): Remove this packet type.
- DEPRECATED_CANCEL = 6;
-
// A client stream has completed.
CLIENT_STREAM_END = 8;
@@ -45,16 +41,15 @@
// The RPC has finished.
RESPONSE = 1;
- // Deprecated, do not use. Formerly was used as the last packet in a server
- // stream.
- // TODO(b/234879973): Remove this packet type.
- DEPRECATED_SERVER_STREAM_END = 3;
-
// The server was unable to process a request.
SERVER_ERROR = 5;
// A message in a server stream.
SERVER_STREAM = 7;
+
+ // Reserve field numbers for deprecated PacketTypes.
+ reserved 3; // SERVER_STREAM_END (equivalent to RESPONSE now)
+ reserved 6; // CANCEL (replaced by CLIENT_ERROR with status CANCELLED)
}
message RpcPacket {
diff --git a/pw_rpc/packet.cc b/pw_rpc/packet.cc
index 2b92850..3e9803c 100644
--- a/pw_rpc/packet.cc
+++ b/pw_rpc/packet.cc
@@ -79,12 +79,6 @@
return status;
}
- // TODO(b/234879973): CANCEL is equivalent to CLIENT_ERROR with status
- // CANCELLED. Remove this workaround when CANCEL is removed.
- if (packet.type() == PacketType::DEPRECATED_CANCEL) {
- packet.set_status(Status::Cancelled());
- }
-
return packet;
}
diff --git a/pw_rpc/public/pw_rpc/internal/config.h b/pw_rpc/public/pw_rpc/internal/config.h
index 45fc799..24a2271 100644
--- a/pw_rpc/public/pw_rpc/internal/config.h
+++ b/pw_rpc/public/pw_rpc/internal/config.h
@@ -24,7 +24,7 @@
///
/// This option controls whether or not include a callback that is called when
/// the client stream ends. The callback is included in all ServerReader/Writer
-/// objects as a @cpp_class{pw::Function}, so may have a significant cost.
+/// objects as a @cpp_type{pw::Function}, so may have a significant cost.
///
/// This is disabled by default.
#ifndef PW_RPC_CLIENT_STREAM_END_CALLBACK
@@ -160,13 +160,16 @@
/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this macro must expand to
/// a container capable of storing objects of the provided type. This container
-/// will be used internally be pw_rpc. Defaults to `std::vector<type>`, but may
-/// be set to any type that supports the following std::vector operations:
+/// will be used internally by pw_rpc to allocate the channels list and encoding
+/// buffer. Defaults to `std::vector<type>`, but may be set to any type that
+/// supports the following `std::vector` operations:
///
/// - Default construction
/// - `emplace_back()`
/// - `pop_back()`
/// - `back()`
+/// - `resize()`
+/// - `clear()`
/// - Range-based for loop iteration (`begin()`, `end()`)
///
#ifndef PW_RPC_DYNAMIC_CONTAINER
diff --git a/pw_rpc/public/pw_rpc/internal/encoding_buffer.h b/pw_rpc/public/pw_rpc/internal/encoding_buffer.h
index c3b8033..8ae752f 100644
--- a/pw_rpc/public/pw_rpc/internal/encoding_buffer.h
+++ b/pw_rpc/public/pw_rpc/internal/encoding_buffer.h
@@ -21,20 +21,47 @@
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
+#include "pw_rpc/internal/config.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/packet.h"
#include "pw_status/status_with_size.h"
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+#include PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
namespace pw::rpc::internal {
constexpr ByteSpan ResizeForPayload(ByteSpan buffer) {
return buffer.subspan(Packet::kMinEncodedSizeWithoutPayload);
}
+// Wraps a statically allocated encoding buffer.
+class StaticEncodingBuffer {
+ public:
+ constexpr StaticEncodingBuffer() : buffer_{} {}
+
+ ByteSpan AllocatePayloadBuffer() { return ResizeForPayload(buffer_); }
+ ByteSpan GetPacketBuffer(size_t /* payload_size */) { return buffer_; }
+
+ void Release() {}
+ void ReleaseIfAllocated() {}
+
+ private:
+ static_assert(MaxSafePayloadSize() > 0,
+ "pw_rpc's encode buffer is too small to fit any data");
+
+ std::array<std::byte, cfg::kEncodingBufferSizeBytes> buffer_;
+};
+
+#if PW_RPC_DYNAMIC_ALLOCATION
+
// Wraps a dynamically allocated encoding buffer.
class DynamicEncodingBuffer {
public:
- constexpr DynamicEncodingBuffer() = default;
+ DynamicEncodingBuffer() = default;
~DynamicEncodingBuffer() { PW_DASSERT(buffer_.empty()); }
@@ -56,8 +83,7 @@
// Frees the payload buffer, which MUST have been allocated previously.
void Release() {
PW_DASSERT(!buffer_.empty());
- delete[] buffer_.data();
- buffer_ = {};
+ buffer_.clear();
}
// Frees the payload buffer, if one was allocated.
@@ -72,37 +98,23 @@
const size_t buffer_size =
payload_size + Packet::kMinEncodedSizeWithoutPayload;
PW_DASSERT(buffer_.empty());
- buffer_ = span(new std::byte[buffer_size], buffer_size);
+ buffer_.resize(buffer_size);
}
- ByteSpan buffer_;
+ PW_RPC_DYNAMIC_CONTAINER(std::byte) buffer_;
};
-// Wraps a statically allocated encoding buffer.
-class StaticEncodingBuffer {
- public:
- constexpr StaticEncodingBuffer() : buffer_{} {}
+using EncodingBuffer = DynamicEncodingBuffer;
- ByteSpan AllocatePayloadBuffer() { return ResizeForPayload(buffer_); }
- ByteSpan GetPacketBuffer(size_t /* payload_size */) { return buffer_; }
+#else
- void Release() {}
- void ReleaseIfAllocated() {}
+using EncodingBuffer = StaticEncodingBuffer;
- private:
- static_assert(MaxSafePayloadSize() > 0,
- "pw_rpc's encode buffer is too small to fit any data");
-
- std::array<std::byte, cfg::kEncodingBufferSizeBytes> buffer_;
-};
+#endif // PW_RPC_DYNAMIC_ALLOCATION
// Instantiate the global encoding buffer variable, depending on whether dynamic
// allocation is enabled or not.
-#if PW_RPC_DYNAMIC_ALLOCATION
-inline DynamicEncodingBuffer encoding_buffer PW_GUARDED_BY(rpc_lock());
-#else
-inline StaticEncodingBuffer encoding_buffer PW_GUARDED_BY(rpc_lock());
-#endif // PW_RPC_DYNAMIC_ALLOCATION
+inline EncodingBuffer encoding_buffer PW_GUARDED_BY(rpc_lock());
// Successful calls to EncodeToPayloadBuffer MUST send the returned buffer,
// without releasing the RPC lock.
diff --git a/pw_rpc/py/pw_rpc/callback_client/call.py b/pw_rpc/py/pw_rpc/callback_client/call.py
index 0378aeb..6cb09aa 100644
--- a/pw_rpc/py/pw_rpc/callback_client/call.py
+++ b/pw_rpc/py/pw_rpc/callback_client/call.py
@@ -46,17 +46,17 @@
VALUE = 0
-CallType = TypeVar(
- 'CallType',
+CallTypeT = TypeVar(
+ 'CallTypeT',
'UnaryCall',
'ServerStreamingCall',
'ClientStreamingCall',
'BidirectionalStreamingCall',
)
-OnNextCallback = Callable[[CallType, Any], Any]
-OnCompletedCallback = Callable[[CallType, Any], Any]
-OnErrorCallback = Callable[[CallType, Any], Any]
+OnNextCallback = Callable[[CallTypeT, Any], Any]
+OnCompletedCallback = Callable[[CallTypeT, Any], Any]
+OnErrorCallback = Callable[[CallTypeT, Any], Any]
OptionalTimeout = Union[UseDefault, float, None]
diff --git a/pw_rpc/py/pw_rpc/callback_client/impl.py b/pw_rpc/py/pw_rpc/callback_client/impl.py
index e74e304..4747573 100644
--- a/pw_rpc/py/pw_rpc/callback_client/impl.py
+++ b/pw_rpc/py/pw_rpc/callback_client/impl.py
@@ -28,7 +28,7 @@
from pw_rpc.callback_client.call import (
UseDefault,
OptionalTimeout,
- CallType,
+ CallTypeT,
UnaryResponse,
StreamResponse,
Call,
@@ -106,14 +106,14 @@
def _start_call(
self,
- call_type: Type[CallType],
+ call_type: Type[CallTypeT],
request: Optional[Message],
timeout_s: OptionalTimeout,
on_next: Optional[OnNextCallback],
on_completed: Optional[OnCompletedCallback],
on_error: Optional[OnErrorCallback],
ignore_errors: bool = False,
- ) -> CallType:
+ ) -> CallTypeT:
"""Creates the Call object and invokes the RPC using it."""
if timeout_s is UseDefault.VALUE:
timeout_s = self.default_timeout_s
@@ -125,8 +125,8 @@
return call
def _client_streaming_call_type(
- self, base: Type[CallType]
- ) -> Type[CallType]:
+ self, base: Type[CallTypeT]
+ ) -> Type[CallTypeT]:
"""Creates a client or bidirectional stream call type.
Applies the signature from the request protobuf to the send method.
diff --git a/pw_rpc/py/pw_rpc/client.py b/pw_rpc/py/pw_rpc/client.py
index 45dd98e..d8c3390 100644
--- a/pw_rpc/py/pw_rpc/client.py
+++ b/pw_rpc/py/pw_rpc/client.py
@@ -151,11 +151,11 @@
packets.encode_client_stream_end(rpc)
)
- def cancel(self, rpc: PendingRpc) -> Optional[bytes]:
- """Cancels the RPC. Returns the CANCEL packet to send.
+ def cancel(self, rpc: PendingRpc) -> bytes:
+ """Cancels the RPC.
Returns:
- True if the RPC was cancelled; False if it was not pending
+ The CLIENT_ERROR packet to send.
Raises:
KeyError if the RPC is not pending
@@ -163,9 +163,6 @@
_LOG.debug('Cancelling %s', rpc)
del self._pending[rpc]
- if rpc.method.type is Method.Type.UNARY:
- return None
-
return packets.encode_cancel(rpc)
def send_cancel(self, rpc: PendingRpc) -> bool:
@@ -400,12 +397,6 @@
if rpc.method.type is not Method.Type.SERVER_STREAMING:
return
- # SERVER_STREAM_END packets are deprecated. They are equivalent to a
- # RESPONSE packet.
- if packet.type == PacketType.DEPRECATED_SERVER_STREAM_END:
- packet.type = PacketType.RESPONSE
- return
-
# Prior to the introduction of SERVER_STREAM packets, RESPONSE packets with
# a payload were used instead. If a non-zero payload is present, assume this
# RESPONSE is equivalent to a SERVER_STREAM packet.
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 4e91dff..01e0346 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -185,7 +185,6 @@
def server_streaming_signature(
self, method: ProtoServiceMethod, prefix: str
) -> str:
-
return (
f'void {prefix}{method.name()}('
'pw::ConstByteSpan request, RawServerWriter& writer)'
diff --git a/pw_rpc/py/pw_rpc/lossy_channel.py b/pw_rpc/py/pw_rpc/lossy_channel.py
index feb46c1..db8908a 100644
--- a/pw_rpc/py/pw_rpc/lossy_channel.py
+++ b/pw_rpc/py/pw_rpc/lossy_channel.py
@@ -112,27 +112,22 @@
def next_packet_duplicated(self) -> bool:
return False
- @staticmethod
- def next_packet_out_of_order() -> bool:
+ def next_packet_out_of_order(self) -> bool:
return False
- @staticmethod
- def next_packet_delayed() -> bool:
+ def next_packet_delayed(self) -> bool:
return False
def next_packet_dropped(self) -> bool:
return not self.keep_packet()
- @staticmethod
- def next_packet_delay() -> int:
+ def next_packet_delay(self) -> int:
return 0
- @staticmethod
- def next_num_dupes() -> int:
+ def next_num_dupes(self) -> int:
return 0
- @staticmethod
- def choose_out_of_order_packet(max_idx) -> int:
+ def choose_out_of_order_packet(self, max_idx) -> int:
return 0
diff --git a/pw_rpc/py/pw_rpc/testing.py b/pw_rpc/py/pw_rpc/testing.py
index 0f14758..de8ab62 100644
--- a/pw_rpc/py/pw_rpc/testing.py
+++ b/pw_rpc/py/pw_rpc/testing.py
@@ -14,6 +14,7 @@
"""Utilities for testing pw_rpc."""
import argparse
+import shlex
import subprocess
import sys
import tempfile
@@ -68,8 +69,22 @@
parser = argparse.ArgumentParser(
description='Executes a test between two subprocesses'
)
- parser.add_argument('--client', required=True, help='Client binary to run')
- parser.add_argument('--server', required=True, help='Server binary to run')
+ parser.add_argument(
+ '--client',
+ required=True,
+ help=(
+ 'Client command to run. '
+ 'Use quotes and whitespace to pass client-specifc arguments.'
+ ),
+ )
+ parser.add_argument(
+ '--server',
+ required=True,
+ help=(
+ 'Server command to run. '
+ 'Use quotes and whitespace to pass client-specifc arguments.'
+ ),
+ )
parser.add_argument(
'common_args',
metavar='-- ...',
@@ -96,6 +111,7 @@
common_args: Sequence[str],
setup_time_s: float = 0.2,
) -> int:
+ """Runs an RPC server and client as part of an integration test."""
temp_dir: Optional[tempfile.TemporaryDirectory] = None
if TEMP_DIR_MARKER in common_args:
@@ -105,11 +121,17 @@
]
try:
- server_process = subprocess.Popen([server, *common_args])
+ server_cmdline = shlex.split(server)
+ client_cmdline = shlex.split(client)
+ if common_args:
+ server_cmdline += [*common_args]
+ client_cmdline += [*common_args]
+
+ server_process = subprocess.Popen(server_cmdline)
# TODO(b/234879791): Replace this delay with some sort of IPC.
time.sleep(setup_time_s)
- result = subprocess.run([client, *common_args]).returncode
+ result = subprocess.run(client_cmdline).returncode
server_process.terminate()
server_process.communicate()
diff --git a/pw_rpc/py/tests/callback_client_test.py b/pw_rpc/py/tests/callback_client_test.py
index 0f74961..4061829 100755
--- a/pw_rpc/py/tests/callback_client_test.py
+++ b/pw_rpc/py/tests/callback_client_test.py
@@ -421,8 +421,10 @@
self.assertTrue(call.cancel())
self.assertFalse(call.cancel()) # Already cancelled, returns False
- # Unary RPCs do not send a cancel request to the server.
- self.assertFalse(self.requests)
+ self.assertEqual(
+ self.last_request().type, packet_pb2.PacketType.CLIENT_ERROR
+ )
+ self.assertEqual(self.last_request().status, Status.CANCELLED.value)
callback.assert_not_called()
@@ -501,38 +503,6 @@
4, self._sent_payload(self.method.request_type).magic_number
)
- def test_deprecated_packet_format(self) -> None:
- rep1 = self.method.response_type(payload='!!!')
- rep2 = self.method.response_type(payload='?')
-
- for _ in range(3):
- # The original packet format used RESPONSE packets for the server
- # stream and a SERVER_STREAM_END packet as the last packet. These
- # are converted to SERVER_STREAM packets followed by a RESPONSE.
- self._enqueue_response(1, self.method, payload=rep1)
- self._enqueue_response(1, self.method, payload=rep2)
-
- self._next_packets.append(
- (
- packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.DEPRECATED_SERVER_STREAM_END,
- channel_id=1,
- service_id=self.method.service.id,
- method_id=self.method.id,
- status=Status.INVALID_ARGUMENT.value,
- ).SerializeToString(),
- Status.OK,
- )
- )
-
- status, replies = self._service.SomeServerStreaming(magic_number=4)
- self.assertEqual([rep1, rep2], replies)
- self.assertIs(status, Status.INVALID_ARGUMENT)
-
- self.assertEqual(
- 4, self._sent_payload(self.method.request_type).magic_number
- )
-
def test_nonblocking_call(self) -> None:
rep1 = self.method.response_type(payload='!!!')
rep2 = self.method.response_type(payload='?')
diff --git a/pw_rpc/server.cc b/pw_rpc/server.cc
index b953424..4af9ff4 100644
--- a/pw_rpc/server.cc
+++ b/pw_rpc/server.cc
@@ -86,7 +86,6 @@
HandleClientStreamPacket(packet, *channel, call);
break;
case PacketType::CLIENT_ERROR:
- case PacketType::DEPRECATED_CANCEL:
if (call != calls_end()) {
call->HandleError(packet.status());
} else {
@@ -98,7 +97,6 @@
break;
case PacketType::REQUEST: // Handled above
case PacketType::RESPONSE:
- case PacketType::DEPRECATED_SERVER_STREAM_END:
case PacketType::SERVER_ERROR:
case PacketType::SERVER_STREAM:
default:
diff --git a/pw_rpc/ts/client_test.ts b/pw_rpc/ts/client_test.ts
index dba716c..0dfed21 100644
--- a/pw_rpc/ts/client_test.ts
+++ b/pw_rpc/ts/client_test.ts
@@ -361,6 +361,9 @@
requests = [];
expect(call.cancel()).toBe(true);
+ expect(lastRequest().getType()).toEqual(PacketType.CLIENT_ERROR);
+ expect(lastRequest().getStatus()).toEqual(Status.CANCELLED);
+
expect(call.cancel()).toBe(false);
expect(onNext).not.toHaveBeenCalled();
}
diff --git a/pw_rpc/ts/rpc_classes.ts b/pw_rpc/ts/rpc_classes.ts
index 2cd62ce..703612e 100644
--- a/pw_rpc/ts/rpc_classes.ts
+++ b/pw_rpc/ts/rpc_classes.ts
@@ -112,13 +112,10 @@
rpc.channel.send(packets.encodeClientStreamEnd(rpc.idSet));
}
- /** Cancels the RPC. Returns the CANCEL packet to send. */
- cancel(rpc: Rpc): Uint8Array | undefined {
+ /** Cancels the RPC. Returns the CLIENT_ERROR packet to send. */
+ cancel(rpc: Rpc): Uint8Array {
console.debug(`Cancelling ${rpc}`);
this.pending.delete(rpc.idString);
- if (rpc.method.clientStreaming && rpc.method.serverStreaming) {
- return undefined;
- }
return packets.encodeCancel(rpc.idSet);
}
diff --git a/pw_rust/examples/host_executable/BUILD.gn b/pw_rust/examples/host_executable/BUILD.gn
index 99013ec..fca1f74 100644
--- a/pw_rust/examples/host_executable/BUILD.gn
+++ b/pw_rust/examples/host_executable/BUILD.gn
@@ -16,6 +16,33 @@
import("$dir_pw_build/target_types.gni")
-pw_executable("hello") {
- sources = [ "main.rs" ]
+pw_rust_executable("hello") {
+ sources = [
+ "main.rs",
+ "other.rs",
+ ]
+
+ deps = [
+ ":a",
+ ":c",
+ ]
+}
+
+# The dep chain hello->a->b will exercise the functionality of both direct and
+# transitive deps for A
+pw_rust_library("a") {
+ crate_root = "a/lib.rs"
+ sources = [ "a/lib.rs" ]
+ deps = [ ":b" ]
+}
+
+pw_rust_library("b") {
+ crate_root = "b/lib.rs"
+ sources = [ "b/lib.rs" ]
+ deps = [ ":c" ]
+}
+
+pw_rust_library("c") {
+ crate_root = "c/lib.rs"
+ sources = [ "c/lib.rs" ]
}
diff --git a/pw_rust/examples/host_executable/a/lib.rs b/pw_rust/examples/host_executable/a/lib.rs
new file mode 100644
index 0000000..e49ddb4
--- /dev/null
+++ b/pw_rust/examples/host_executable/a/lib.rs
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+use b::RequiredB;
+
+#[derive(Copy, Clone, Default)]
+pub struct RequiredA {
+ pub required_b: RequiredB,
+}
diff --git a/pw_rust/examples/host_executable/b/lib.rs b/pw_rust/examples/host_executable/b/lib.rs
new file mode 100644
index 0000000..6e32a14
--- /dev/null
+++ b/pw_rust/examples/host_executable/b/lib.rs
@@ -0,0 +1,20 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+#[derive(Copy, Clone, Debug, Default)]
+pub struct RequiredB {
+ pub value: i32,
+}
diff --git a/pw_rust/examples/host_executable/c/lib.rs b/pw_rust/examples/host_executable/c/lib.rs
new file mode 100644
index 0000000..3c2cbff
--- /dev/null
+++ b/pw_rust/examples/host_executable/c/lib.rs
@@ -0,0 +1,19 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+pub fn value() -> i32 {
+ 1
+}
diff --git a/pw_rust/examples/host_executable/main.rs b/pw_rust/examples/host_executable/main.rs
index b78a752..79d72df 100644
--- a/pw_rust/examples/host_executable/main.rs
+++ b/pw_rust/examples/host_executable/main.rs
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// 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
@@ -12,6 +12,31 @@
// License for the specific language governing permissions and limitations under
// the License.
+#![warn(clippy::all)]
+
+mod other;
+
fn main() {
println!("Hello, Pigweed!");
+
+ // ensure we can run code from other modules in the main crate
+ println!("{}", other::foo());
+
+ // ensure we can run code from dependent libraries
+ println!("{}", a::RequiredA::default().required_b.value);
+ println!("{}", c::value());
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_simple() {
+ let x = 3.14;
+ assert!(x > 0.0);
+ }
+
+ #[test]
+ fn test_other_module() {
+ assert!(a::RequiredA::default().required_b.value == 0);
+ }
}
diff --git a/pw_rust/examples/host_executable/other.rs b/pw_rust/examples/host_executable/other.rs
new file mode 100644
index 0000000..fd57109
--- /dev/null
+++ b/pw_rust/examples/host_executable/other.rs
@@ -0,0 +1,18 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#[allow(unused)]
+pub fn foo() -> i32 {
+ return 42;
+}
diff --git a/pw_software_update/py/pw_software_update/cli.py b/pw_software_update/py/pw_software_update/cli.py
index d499939..a099bef 100644
--- a/pw_software_update/py/pw_software_update/cli.py
+++ b/pw_software_update/py/pw_software_update/cli.py
@@ -198,7 +198,7 @@
arg.bundle.write_bytes(updated_bundle.SerializeToString())
- except (IOError) as error:
+ except IOError as error:
print(error)
diff --git a/pw_software_update/py/pw_software_update/generate_test_bundle.py b/pw_software_update/py/pw_software_update/generate_test_bundle.py
index 0c52439..14409ae 100644
--- a/pw_software_update/py/pw_software_update/generate_test_bundle.py
+++ b/pw_software_update/py/pw_software_update/generate_test_bundle.py
@@ -280,11 +280,11 @@
return parser.parse_args()
+# TODO(b/237580538): Refactor the code so that each test bundle generation
+# is done in a separate function or script.
+# pylint: disable=too-many-locals
def main() -> int:
"""Main"""
- # TODO(b/237580538): Refactor the code so that each test bundle generation
- # is done in a separate function or script.
- # pylint: disable=too-many-locals
args = parse_args()
test_bundle = Bundle()
@@ -515,11 +515,13 @@
],
check=True,
)
- # TODO(b/237580538): Refactor the code so that each test bundle generation
- # is done in a separate function or script.
- # pylint: enable=too-many-locals
return 0
+# TODO(b/237580538): Refactor the code so that each test bundle generation
+# is done in a separate function or script.
+# pylint: enable=too-many-locals
+
+
if __name__ == "__main__":
sys.exit(main())
diff --git a/pw_software_update/py/pw_software_update/metadata.py b/pw_software_update/py/pw_software_update/metadata.py
index 86def49..0816d7c 100644
--- a/pw_software_update/py/pw_software_update/metadata.py
+++ b/pw_software_update/py/pw_software_update/metadata.py
@@ -54,7 +54,6 @@
def gen_target_file(
file_name: str, file_contents: bytes, hash_funcs=DEFAULT_HASHES
) -> TargetFile:
-
return TargetFile(
file_name=file_name,
length=len(file_contents),
diff --git a/pw_software_update/update_bundle_test.cc b/pw_software_update/update_bundle_test.cc
index c2bbf66..e35987a 100644
--- a/pw_software_update/update_bundle_test.cc
+++ b/pw_software_update/update_bundle_test.cc
@@ -69,7 +69,7 @@
void SetManifestWriter(stream::Writer* writer) { manifest_writer_ = writer; }
- virtual Result<stream::SeekableReader*> GetRootMetadataReader() override {
+ Result<stream::SeekableReader*> GetRootMetadataReader() override {
return &trusted_root_reader_;
}
@@ -105,7 +105,7 @@
return manifest_writer_;
}
- virtual Status SafelyPersistRootMetadata(
+ Status SafelyPersistRootMetadata(
[[maybe_unused]] stream::IntervalReader root_metadata) override {
new_root_persisted_ = true;
trusted_root_reader_ = root_metadata;
@@ -273,7 +273,7 @@
TEST_F(UpdateBundleTest, SelfVerificationWithIncomingRoot) {
StageTestBundle(kTestDevBundleWithRoot);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
// Self verification must not persist anything.
@@ -293,7 +293,7 @@
TEST_F(UpdateBundleTest, SelfVerificationWithoutIncomingRoot) {
StageTestBundle(kTestDevBundle);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
}
@@ -301,7 +301,7 @@
TEST_F(UpdateBundleTest, SelfVerificationWithMessedUpRoot) {
StageTestBundle(kTestDevBundleWithProdRoot);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -309,7 +309,7 @@
TEST_F(UpdateBundleTest, SelfVerificationChecksMissingHashes) {
StageTestBundle(kTestBundleMissingTargetHashFile0);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -317,7 +317,7 @@
TEST_F(UpdateBundleTest, SelfVerificationChecksBadHashes) {
StageTestBundle(kTestBundleMismatchedTargetHashFile0);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -325,7 +325,7 @@
TEST_F(UpdateBundleTest, SelfVerificationIgnoresUnsignedBundle) {
StageTestBundle(kTestUnsignedBundleWithRoot);
UpdateBundleAccessor update_bundle(
- blob_reader(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
}
diff --git a/pw_stream/BUILD.bazel b/pw_stream/BUILD.bazel
index c4d910a..524e208 100644
--- a/pw_stream/BUILD.bazel
+++ b/pw_stream/BUILD.bazel
@@ -51,6 +51,7 @@
deps = [
":pw_stream",
"//pw_log",
+ "//pw_string",
"//pw_sys_io",
],
)
@@ -143,3 +144,12 @@
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "socket_stream_test",
+ srcs = ["socket_stream_test.cc"],
+ deps = [
+ ":socket_stream",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_stream/BUILD.gn b/pw_stream/BUILD.gn
index 1ac95a3..9f4eb2f 100644
--- a/pw_stream/BUILD.gn
+++ b/pw_stream/BUILD.gn
@@ -46,7 +46,11 @@
pw_source_set("socket_stream") {
public_configs = [ ":public_include_path" ]
public_deps = [ ":pw_stream" ]
- deps = [ dir_pw_log ]
+ deps = [
+ dir_pw_assert,
+ dir_pw_log,
+ dir_pw_string,
+ ]
sources = [ "socket_stream.cc" ]
public = [ "public/pw_stream/socket_stream.h" ]
}
@@ -95,6 +99,11 @@
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
pw_toolchain_SCOPE.is_host_toolchain) {
tests += [ ":std_file_stream_test" ]
+
+ # socket_stream_test doesn't compile on Windows.
+ if (host_os != "win") {
+ tests += [ ":socket_stream_test" ]
+ }
}
}
@@ -136,3 +145,8 @@
sources = [ "interval_reader_test.cc" ]
deps = [ ":interval_reader" ]
}
+
+pw_test("socket_stream_test") {
+ sources = [ "socket_stream_test.cc" ]
+ deps = [ ":socket_stream" ]
+}
diff --git a/pw_stream/CMakeLists.txt b/pw_stream/CMakeLists.txt
index 5f71629..dfa4582 100644
--- a/pw_stream/CMakeLists.txt
+++ b/pw_stream/CMakeLists.txt
@@ -47,6 +47,7 @@
socket_stream.cc
PRIVATE_DEPS
pw_log
+ pw_string
)
pw_add_library(pw_stream.sys_io_stream INTERFACE
diff --git a/pw_stream/docs.rst b/pw_stream/docs.rst
index dd4fef1..a4aaeb9 100644
--- a/pw_stream/docs.rst
+++ b/pw_stream/docs.rst
@@ -209,7 +209,7 @@
-----------------
.. cpp:class:: Reader : public Stream
- A Stream that supports writing but not reading. The Write() method is hidden.
+ A Stream that supports reading but not writing. The Write() method is hidden.
Use in APIs when:
* Must read from, but not write to, a stream.
@@ -426,8 +426,14 @@
.. cpp:class:: SocketStream : public NonSeekableReaderWriter
- ``SocketStream`` wraps posix-style sockets with the :cpp:class:`Reader` and
- :cpp:class:`Writer` interfaces.
+ ``SocketStream`` wraps posix-style TCP sockets with the :cpp:class:`Reader`
+ and :cpp:class:`Writer` interfaces. It can be used to connect to a TCP server,
+ or to communicate with a client via the ``ServerSocket`` class.
+
+.. cpp:class:: ServerSocket
+
+ ``ServerSocket`` wraps a posix server socket, and produces a
+ :cpp:class:`SocketStream` for each accepted client connection.
------------------
Why use pw_stream?
diff --git a/pw_stream/public/pw_stream/socket_stream.h b/pw_stream/public/pw_stream/socket_stream.h
index d757822..0092fc8 100644
--- a/pw_stream/public/pw_stream/socket_stream.h
+++ b/pw_stream/public/pw_stream/socket_stream.h
@@ -17,6 +17,7 @@
#include <cstdint>
+#include "pw_result/result.h"
#include "pw_span/span.h"
#include "pw_stream/stream.h"
@@ -26,13 +27,38 @@
public:
constexpr SocketStream() = default;
+ // SocketStream objects are moveable but not copyable.
+ SocketStream& operator=(SocketStream&& other) {
+ listen_port_ = other.listen_port_;
+ socket_fd_ = other.socket_fd_;
+ other.socket_fd_ = kInvalidFd;
+ connection_fd_ = other.connection_fd_;
+ other.connection_fd_ = kInvalidFd;
+ sockaddr_client_ = other.sockaddr_client_;
+ return *this;
+ }
+ SocketStream(SocketStream&& other) noexcept
+ : listen_port_(other.listen_port_),
+ socket_fd_(other.socket_fd_),
+ connection_fd_(other.connection_fd_),
+ sockaddr_client_(other.sockaddr_client_) {
+ other.socket_fd_ = kInvalidFd;
+ other.connection_fd_ = kInvalidFd;
+ }
+ SocketStream(const SocketStream&) = delete;
+ SocketStream& operator=(const SocketStream&) = delete;
+
~SocketStream() override { Close(); }
// Listen to the port and return after a client is connected
+ //
+ // DEPRECATED: Use the ServerSocket class instead.
+ // TODO(b/271323032): Remove when this method is no longer used.
Status Serve(uint16_t port);
- // Connect to a local or remote endpoint. Host must be an IPv4 address. If
- // host is nullptr then the locahost address is used instead.
+ // Connect to a local or remote endpoint. Host may be either an IPv4 or IPv6
+ // address. If host is nullptr then the IPv4 localhost address is used
+ // instead.
Status Connect(const char* host, uint16_t port);
// Close the socket stream and release all resources
@@ -46,6 +72,8 @@
int connection_fd() { return connection_fd_; }
private:
+ friend class ServerSocket;
+
static constexpr int kInvalidFd = -1;
Status DoWrite(span<const std::byte> data) override;
@@ -58,4 +86,39 @@
struct sockaddr_in sockaddr_client_ = {};
};
+/// `ServerSocket` wraps a POSIX-style server socket, producing a `SocketStream`
+/// for each accepted client connection.
+///
+/// Call `Listen` to create the socket and start listening for connections.
+/// Then call `Accept` any number of times to accept client connections.
+class ServerSocket {
+ public:
+ ServerSocket() = default;
+ ~ServerSocket() { Close(); }
+
+ ServerSocket(const ServerSocket& other) = delete;
+ ServerSocket& operator=(const ServerSocket& other) = delete;
+
+ // Listen for connections on the given port.
+ // If port is 0, a random unused port is chosen and can be retrieved with
+ // port().
+ Status Listen(uint16_t port = 0);
+
+ // Accept a connection. Blocks until after a client is connected.
+ // On success, returns a SocketStream connected to the new client.
+ Result<SocketStream> Accept();
+
+ // Close the server socket, preventing further connections.
+ void Close();
+
+ // Returns the port this socket is listening on.
+ uint16_t port() const { return port_; }
+
+ private:
+ static constexpr int kInvalidFd = -1;
+
+ uint16_t port_ = -1;
+ int socket_fd_ = kInvalidFd;
+};
+
} // namespace pw::stream
diff --git a/pw_stream/socket_stream.cc b/pw_stream/socket_stream.cc
index 564857b..3978e49 100644
--- a/pw_stream/socket_stream.cc
+++ b/pw_stream/socket_stream.cc
@@ -15,18 +15,37 @@
#include "pw_stream/socket_stream.h"
#include <arpa/inet.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
#include <unistd.h>
+#include <cerrno>
#include <cstring>
+#include "pw_assert/check.h"
#include "pw_log/log.h"
+#include "pw_string/to_string.h"
namespace pw::stream {
namespace {
-constexpr uint32_t kMaxConcurrentUser = 1;
+constexpr uint32_t kServerBacklogLength = 1;
constexpr const char* kLocalhostAddress = "127.0.0.1";
+// Set necessary options on a socket file descriptor.
+void ConfigureSocket([[maybe_unused]] int socket) {
+#if defined(__APPLE__)
+ // Use SO_NOSIGPIPE to avoid getting a SIGPIPE signal when the remote peer
+ // drops the connection. This is supported on macOS only.
+ constexpr int value = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int)) < 0) {
+ PW_LOG_WARN("Failed to set SO_NOSIGPIPE: %s", std::strerror(errno));
+ }
+#endif // defined(__APPLE__)
+}
+
} // namespace
// TODO(b/240982565): Implement SocketStream for Windows.
@@ -65,7 +84,7 @@
return Status::Unknown();
}
- if (listen(socket_fd_, kMaxConcurrentUser) < 0) {
+ if (listen(socket_fd_, kServerBacklogLength) < 0) {
PW_LOG_ERROR("Failed to listen to socket: %s", std::strerror(errno));
return Status::Unknown();
}
@@ -77,28 +96,36 @@
if (connection_fd_ < 0) {
return Status::Unknown();
}
+ ConfigureSocket(connection_fd_);
return OkStatus();
}
Status SocketStream::SocketStream::Connect(const char* host, uint16_t port) {
- connection_fd_ = socket(AF_INET, SOCK_STREAM, 0);
-
- sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_port = htons(port);
-
if (host == nullptr || std::strcmp(host, "localhost") == 0) {
host = kLocalhostAddress;
}
- if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
+ struct addrinfo hints = {};
+ struct addrinfo* res;
+ char port_buffer[6];
+ PW_CHECK(ToString(port, port_buffer).ok());
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV | AI_PASSIVE;
+ if (getaddrinfo(host, port_buffer, &hints, &res) != 0) {
PW_LOG_ERROR("Failed to configure connection address for socket");
return Status::InvalidArgument();
}
- if (connect(connection_fd_,
- reinterpret_cast<sockaddr*>(&addr),
- sizeof(addr)) < 0) {
+ connection_fd_ = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ ConfigureSocket(connection_fd_);
+ if (connect(connection_fd_, res->ai_addr, res->ai_addrlen) < 0) {
+ close(connection_fd_);
+ connection_fd_ = kInvalidFd;
+ }
+ freeaddrinfo(res);
+
+ if (connection_fd_ == kInvalidFd) {
PW_LOG_ERROR(
"Failed to connect to %s:%d: %s", host, port, std::strerror(errno));
return Status::Unknown();
@@ -120,10 +147,15 @@
}
Status SocketStream::DoWrite(span<const std::byte> data) {
+ int send_flags = 0;
+#if defined(__linux__)
// Use MSG_NOSIGNAL to avoid getting a SIGPIPE signal when the remote
- // peer drops the connection.
+ // peer drops the connection. This is supported on Linux only.
+ send_flags |= MSG_NOSIGNAL;
+#endif // defined(__linux__)
+
ssize_t bytes_sent =
- send(connection_fd_, data.data(), data.size_bytes(), MSG_NOSIGNAL);
+ send(connection_fd_, data.data(), data.size_bytes(), send_flags);
if (bytes_sent < 0 || static_cast<size_t>(bytes_sent) != data.size()) {
if (errno == EPIPE) {
@@ -156,4 +188,73 @@
return StatusWithSize(bytes_rcvd);
}
+// Listen for connections on the given port.
+// If port is 0, a random unused port is chosen and can be retrieved with
+// port().
+Status ServerSocket::Listen(uint16_t port) {
+ socket_fd_ = socket(AF_INET6, SOCK_STREAM, 0);
+ if (socket_fd_ == kInvalidFd) {
+ return Status::Unknown();
+ }
+
+ // Allow binding to an address that may still be in use by a closed socket.
+ constexpr int value = 1;
+ setsockopt(socket_fd_, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(int));
+
+ if (port != 0) {
+ struct sockaddr_in6 addr = {};
+ socklen_t addr_len = sizeof(addr);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = htons(port);
+ addr.sin6_addr = in6addr_any;
+ if (bind(socket_fd_, reinterpret_cast<sockaddr*>(&addr), addr_len) < 0) {
+ return Status::Unknown();
+ }
+ }
+
+ if (listen(socket_fd_, kServerBacklogLength) < 0) {
+ return Status::Unknown();
+ }
+
+ // Find out which port the socket is listening on, and fill in port_.
+ struct sockaddr_in6 addr = {};
+ socklen_t addr_len = sizeof(addr);
+ if (getsockname(socket_fd_, reinterpret_cast<sockaddr*>(&addr), &addr_len) <
+ 0 ||
+ addr_len > sizeof(addr)) {
+ close(socket_fd_);
+ return Status::Unknown();
+ }
+
+ port_ = ntohs(addr.sin6_port);
+
+ return OkStatus();
+}
+
+// Accept a connection. Blocks until after a client is connected.
+// On success, returns a SocketStream connected to the new client.
+Result<SocketStream> ServerSocket::Accept() {
+ struct sockaddr_in6 sockaddr_client_ = {};
+ socklen_t len = sizeof(sockaddr_client_);
+
+ int connection_fd =
+ accept(socket_fd_, reinterpret_cast<sockaddr*>(&sockaddr_client_), &len);
+ if (connection_fd == kInvalidFd) {
+ return Status::Unknown();
+ }
+ ConfigureSocket(connection_fd);
+
+ SocketStream client_stream;
+ client_stream.connection_fd_ = connection_fd;
+ return client_stream;
+}
+
+// Close the server socket, preventing further connections.
+void ServerSocket::Close() {
+ if (socket_fd_ != kInvalidFd) {
+ close(socket_fd_);
+ socket_fd_ = kInvalidFd;
+ }
+}
+
} // namespace pw::stream
diff --git a/pw_stream/socket_stream_test.cc b/pw_stream/socket_stream_test.cc
new file mode 100644
index 0000000..bd8e58e
--- /dev/null
+++ b/pw_stream/socket_stream_test.cc
@@ -0,0 +1,194 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_stream/socket_stream.h"
+
+#include <thread>
+
+#include "gtest/gtest.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::stream {
+namespace {
+
+// Helper function to create a ServerSocket and connect to it via loopback.
+void RunConnectTest(const char* hostname) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect(hostname, server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ server_stream.value().Close();
+ server.Close();
+ client.Close();
+}
+
+TEST(SocketStreamTest, ConnectIpv4) { RunConnectTest("127.0.0.1"); }
+
+TEST(SocketStreamTest, ConnectIpv6) { RunConnectTest("::1"); }
+
+TEST(SocketStreamTest, ConnectSpecificPort) {
+ // We want to test the "listen on a specific port" functionality,
+ // but hard-coding a port number in a test is inherently problematic, as
+ // port numbers are a global resource.
+ //
+ // We use the automatic port assignment initially to get a port assignment,
+ // close that server, and then use that port explicitly in a new server.
+ //
+ // There's still the possibility that the port will get swiped, but it
+ // shouldn't happen by chance.
+ ServerSocket initial_server;
+ EXPECT_EQ(initial_server.Listen(), OkStatus());
+ uint16_t port = initial_server.port();
+ initial_server.Close();
+
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(port), OkStatus());
+ EXPECT_EQ(server.port(), port);
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ server_stream.value().Close();
+ server.Close();
+ client.Close();
+}
+
+// Helper function to test exchanging data on a pair of sockets.
+void ExchangeData(SocketStream& stream1, SocketStream& stream2) {
+ auto kPayload1 = as_bytes(span("some data"));
+ auto kPayload2 = as_bytes(span("other bytes"));
+ std::array<char, 100> read_buffer{};
+
+ // Write data from stream1 and read it from stream2.
+ auto write_status = Status::Unavailable();
+ auto write_thread =
+ std::thread{[&]() { write_status = stream1.Write(kPayload1); }};
+ Result<ByteSpan> read_result =
+ stream2.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(read_result.status(), OkStatus());
+ EXPECT_EQ(read_result.value().size(), kPayload1.size());
+ EXPECT_TRUE(
+ std::equal(kPayload1.begin(), kPayload1.end(), read_result->begin()));
+
+ write_thread.join();
+ EXPECT_EQ(write_status, OkStatus());
+
+ // Read data in the client and write it from the server.
+ auto read_thread = std::thread{[&]() {
+ read_result = stream1.Read(as_writable_bytes(span(read_buffer)));
+ }};
+ EXPECT_EQ(stream2.Write(kPayload2), OkStatus());
+
+ read_thread.join();
+ EXPECT_EQ(read_result.status(), OkStatus());
+ EXPECT_EQ(read_result.value().size(), kPayload2.size());
+ EXPECT_TRUE(
+ std::equal(kPayload2.begin(), kPayload2.end(), read_result->begin()));
+
+ // Close stream1 and attempt to read from stream2.
+ stream1.Close();
+ read_result = stream2.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(read_result.status(), Status::OutOfRange());
+
+ stream2.Close();
+}
+
+TEST(SocketStreamTest, ReadWrite) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ ExchangeData(client, server_stream.value());
+ server.Close();
+}
+
+TEST(SocketStreamTest, MultipleClients) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream1 = Status::Unavailable();
+ Result<SocketStream> server_stream2 = Status::Unavailable();
+ Result<SocketStream> server_stream3 = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() {
+ server_stream1 = server.Accept();
+ server_stream2 = server.Accept();
+ server_stream3 = server.Accept();
+ }};
+
+ SocketStream client1;
+ SocketStream client2;
+ SocketStream client3;
+ EXPECT_EQ(client1.Connect("localhost", server.port()), OkStatus());
+ EXPECT_EQ(client2.Connect("localhost", server.port()), OkStatus());
+ EXPECT_EQ(client3.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream1.status(), OkStatus());
+ EXPECT_EQ(server_stream2.status(), OkStatus());
+ EXPECT_EQ(server_stream3.status(), OkStatus());
+
+ ExchangeData(client1, server_stream1.value());
+ ExchangeData(client2, server_stream2.value());
+ ExchangeData(client3, server_stream3.value());
+ server.Close();
+}
+
+TEST(SocketStreamTest, ReuseAutomaticServerPort) {
+ uint16_t server_port = 0;
+ SocketStream client_stream;
+ ServerSocket server;
+
+ EXPECT_EQ(server.Listen(0), OkStatus());
+ server_port = server.port();
+ EXPECT_NE(server_port, 0);
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ EXPECT_EQ(client_stream.Connect(nullptr, server_port), OkStatus());
+ accept_thread.join();
+ ASSERT_EQ(server_stream.status(), OkStatus());
+
+ server_stream->Close();
+ server.Close();
+
+ ServerSocket server2;
+ EXPECT_EQ(server2.Listen(server_port), OkStatus());
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_string/api.rst b/pw_string/api.rst
index 3dc701d..a526023 100644
--- a/pw_string/api.rst
+++ b/pw_string/api.rst
@@ -1,94 +1,71 @@
.. _module-pw_string-api:
-=======================
-pw_string API reference
-=======================
+=============
+API Reference
+=============
-.. _pw_inlinebasicstring-api:
+--------
+Overview
+--------
+This module provides two types of strings, and utility functions for working
+with strings.
----------------------
-pw::InlineBasicString
----------------------
-:cpp:class:`pw::InlineBasicString` and :cpp:type:`pw::InlineString` are
-C++14-compatible, fixed-capacity, null-terminated string classes. They are
-equivalent to ``std::basic_string<T>`` and ``std::string``, but store the string
-contents inline and use no dynamic memory.
+**pw::StringBuilder**
-:cpp:type:`pw::InlineString` takes the fixed capacity as a template argument,
-but may be used generically without specifying the capacity. The capacity value
-is stored in a member variable, which the generic ``pw::InlineString<>`` /
-``pw::InlineBasicString<T>`` specialization uses in place of the template
-parameter.
+.. doxygenfile:: pw_string/string_builder.h
+ :sections: briefdescription
-:cpp:type:`pw::InlineString` is efficient and compact. The current size and
-capacity are stored in a single word. Accessing the contents of a
-:cpp:type:`pw::InlineString` is a simple array access within the object, with no
-pointer indirection, even when working from a generic ``pw::InlineString<>``
-reference.
+**pw::InlineString**
-.. cpp:class:: template <typename T, unsigned short kCapacity> pw::InlineBasicString
+.. doxygenfile:: pw_string/string.h
+ :sections: briefdescription
- Represents a fixed-capacity string of a generic character type. Equivalent to
- ``std::basic_string<T>``. Always null (``T()``) terminated.
+**String utility functions**
+
+.. doxygenfile:: pw_string/util.h
+ :sections: briefdescription
+
+-----------------
+pw::StringBuilder
+-----------------
+.. doxygenfile:: pw_string/string_builder.h
+ :sections: briefdescription
+.. doxygenclass:: pw::StringBuilder
+ :members:
----------------
pw::InlineString
----------------
-See also :ref:`pw_inlinebasicstring-api`.
+.. doxygenfile:: pw_string/string.h
+ :sections: detaileddescription
-.. cpp:type:: template <unsigned short kCapacity> pw::InlineString = pw::InlineBasicString<char, kCapacity>
+.. doxygenclass:: pw::InlineBasicString
+ :members:
- Represents a fixed-capacity string of ``char`` characters. Equivalent to
- ``std::string``. Always null (``'\0'``) terminated.
+.. doxygentypedef:: pw::InlineString
-----------
-pw::string
-----------
+------------------------
+String utility functions
+------------------------
-pw::string::Assign
+pw::string::Assign()
+--------------------
+.. doxygenfunction:: pw::string::Assign(InlineString<> &string, const std::string_view &view)
+
+pw::string::Append()
+--------------------
+.. doxygenfunction:: pw::string::Append(InlineString<>& string, const std::string_view& view)
+
+pw::string::ClampedCString()
+----------------------------
+.. doxygenfunction:: pw::string::ClampedCString(const char* str, size_t max_len)
+.. doxygenfunction:: pw::string::ClampedCString(span<const char> str)
+
+pw::string::Copy()
------------------
-.. cpp:function:: pw::Status pw::string::Assign(pw::InlineString<>& string, const std::string_view& view)
-
- Assigns ``view`` to ``string``. Truncates and returns ``RESOURCE_EXHAUSTED``
- if ``view`` is too large for ``string``.
-
-pw::string::Append
-------------------
-.. cpp:function:: pw::Status pw::string::Append(pw::InlineString<>& string, const std::string_view& view)
-
- Appends ``view`` to ``string``. Truncates and returns ``RESOURCE_EXHAUSTED``
- if ``view`` does not fit within the remaining capacity of ``string``.
-
-pw::string::ClampedCString
---------------------------
-.. cpp:function:: constexpr std::string_view pw::string::ClampedCString(span<const char> str)
-.. cpp:function:: constexpr std::string_view pw::string::ClampedCString(const char* str, size_t max_len)
-
- Safe alternative to the string_view constructor to avoid the risk of an
- unbounded implicit or explicit use of strlen.
-
- This is strongly recommended over using something like C11's strnlen_s as
- a string_view does not require null-termination.
-
-pw::string::Copy
-----------------
-The ``pw::string::Copy`` functions provide a safer alternative to
-``std::strncpy`` as it always null-terminates whenever the destination
-buffer has a non-zero size.
-
-.. cpp:function:: StatusWithSize Copy(const std::string_view& source, span<char> dest)
-.. cpp:function:: StatusWithSize Copy(const char* source, span<char> dest)
-.. cpp:function:: StatusWithSize Copy(const char* source, char* dest, size_t num)
-.. cpp:function:: StatusWithSize Copy(const pw::Vector<char>& source, span<char> dest)
-
- Copies the source string to the dest, truncating if the full string does not
- fit. Always null terminates if dest.size() or num > 0.
-
- Returns the number of characters written, excluding the null terminator. If
- the string is truncated, the status is ResourceExhausted.
-
- Precondition: The destination and source shall not overlap.
- Precondition: The source shall be a valid pointer.
+.. doxygenfunction:: pw::string::Copy(const char* source, char* dest, size_t num)
+.. doxygenfunction:: pw::string::Copy(const char* source, Span&& dest)
+.. doxygenfunction:: pw::string::Copy(const std::string_view& source, Span&& dest)
It also has variants that provide a destination of ``pw::Vector<char>``
(see :ref:`module-pw_containers` for details) that do not store the null
@@ -97,43 +74,18 @@
.. cpp:function:: StatusWithSize Copy(const std::string_view& source, pw::Vector<char>& dest)
.. cpp:function:: StatusWithSize Copy(const char* source, pw::Vector<char>& dest)
-pw::string::NullTerminatedLength
---------------------------------
-.. cpp:function:: constexpr pw::Result<size_t> pw::string::NullTerminatedLength(span<const char> str)
-.. cpp:function:: pw::Result<size_t> pw::string::NullTerminatedLength(const char* str, size_t max_len)
+pw::string::Format()
+--------------------
+.. doxygenfile:: pw_string/format.h
+ :sections: briefdescription
+.. doxygenfunction:: pw::string::Format
+.. doxygenfunction:: pw::string::FormatVaList
- Safe alternative to strlen to calculate the null-terminated length of the
- string within the specified span, excluding the null terminator. Like C11's
- strnlen_s, the scan for the null-terminator is bounded.
+pw::string::NullTerminatedLength()
+----------------------------------
+.. doxygenfunction:: pw::string::NullTerminatedLength(const char* str, size_t max_len)
+.. doxygenfunction:: pw::string::NullTerminatedLength(span<const char> str)
- Returns:
- null-terminated length of the string excluding the null terminator.
- OutOfRange - if the string is not null-terminated.
-
- Precondition: The string shall be at a valid pointer.
-
-pw::string::PrintableCopy
--------------------------
-The ``pw::string::PrintableCopy`` function provides a safe printable copy of a
-string. It functions with the same safety of ``pw::string::Copy`` while also
-converting any non-printable characters to a ``.`` char.
-
-.. cpp:function:: StatusWithSize PrintableCopy(const std::string_view& source, span<char> dest)
-
------------------
-pw::StringBuilder
------------------
-.. cpp:namespace-push:: pw::StringBuilder
-
-:cpp:class:`StringBuilder` facilitates creating formatted strings in a
-fixed-sized buffer or :cpp:type:`pw::InlineString`. It is designed to give the
-flexibility of ``std::ostringstream``, but with a small footprint.
-
-:cpp:class:`StringBuilder` supports C++ ``<<``-style output, printf formatting,
-and a few ``std::string`` functions (:cpp:func:`append()`,
-:cpp:func:`push_back()`, :cpp:func:`pop_back`.
-
-.. cpp:namespace-pop::
-
-.. doxygenclass:: pw::StringBuilder
- :members:
+pw::string::PrintableCopy()
+---------------------------
+.. doxygenfunction:: pw::string::PrintableCopy(const std::string_view& source, span<char> dest)
diff --git a/pw_string/design.rst b/pw_string/design.rst
index c84465c..91202e1 100644
--- a/pw_string/design.rst
+++ b/pw_string/design.rst
@@ -3,45 +3,65 @@
================
pw_string design
================
-..
- This doc provides background on how a module works internally, the assumptions
- inherent in its design, why this particular design was chosen over others, and
- other topics of that nature.
+``pw_string`` provides string classes and utility functions designed to
+prioritize safety and static allocation. The APIs are broadly similar to those
+of the string classes in the C++ standard library, so familiarity with those
+classes will provide some context around ``pw_string`` design decisions.
-:cpp:type:`pw::InlineString` / :cpp:class:`pw::InlineBasicString` follows the
-``std::string`` / ``std::basic_string<T>`` API, with a few variations:
+------------
+InlineString
+------------
+:cpp:type:`pw::InlineString` / :cpp:class:`pw::InlineBasicString` are designed
+to match the ``std::string`` / ``std::basic_string<T>`` API as closely as
+possible, but with key differences to improve performance on embedded systems:
-- :cpp:type:`pw::InlineString` provides overloads specific to character arrays.
- These perform compile-time capacity checks and are used for class template
- argument deduction. Like ``std::string``, character arrays are treated as
- null-terminated strings.
-- :cpp:type:`pw::InlineString` allows implicit conversions from
- ``std::string_view``. Specifying the capacity parameter is cumbersome, so
- implicit conversions are helpful. Also, implicitly creating a
- :cpp:type:`pw::InlineString` is less costly than creating a ``std::string``.
- As with ``std::string``, explicit conversions are required from types that
- convert to ``std::string_view``.
-- Functions related to dynamic memory allocation are not present (``reserve()``,
- ``shrink_to_fit()``, ``get_allocator()``).
-- ``resize_and_overwrite()`` only takes the ``Operation`` argument, since the
- underlying string buffer cannot be resized.
-
-See the `std::string documentation
-<https://en.cppreference.com/w/cpp/string/basic_string>`_ for full details.
-
-Key differences from ``std::string``
-------------------------------------
-- **Fixed capacity** -- Operations that add characters to the string beyond its
+- **Fixed capacity:** Operations that add characters to the string beyond its
capacity are an error. These trigger a ``PW_ASSERT`` at runtime. When
detectable, these situations trigger a ``static_assert`` at compile time.
-- **Minimal overhead** -- :cpp:type:`pw::InlineString` operations never
+- **Minimal overhead:** :cpp:type:`pw::InlineString` operations never
allocate. Reading the contents of the string is a direct memory access within
the string object, without pointer indirection.
-- **Constexpr support** -- :cpp:type:`pw::InlineString` works in ``constexpr``
+- **Constexpr support:** :cpp:type:`pw::InlineString` works in ``constexpr``
contexts, which is not supported by ``std::string`` until C++20.
-Safe Length Checking
---------------------
+We don't aim to provide complete API compatibility with
+``std::string`` / ``std::basic_string<T>``. Some areas of deviation include:
+
+- **Compile-time capacity checks:** :cpp:type:`InlineString` provides overloads
+ specific to character arrays. These perform compile-time capacity checks and
+ are used for class template argument deduction.
+- **Implicit conversions from** ``std::string_view`` **:** Specifying the
+ capacity parameter is cumbersome, so implicit conversions are helpful. Also,
+ implicitly creating a :cpp:type:`InlineString` is less costly than creating a
+ ``std::string``. As with ``std::string``, explicit conversions are required
+ from types that convert to ``std::string_view``.
+- **No dynamic allocation functions:** Functions that allocate memory, like
+ ``reserve()``, ``shrink_to_fit()``, and ``get_allocator()``, are simply not
+ present.
+
+Capacity
+========
+:cpp:type:`InlineBasicString` has a template parameter for the capacity, but the
+capacity does not need to be known by the user to use the string safely. The
+:cpp:type:`InlineBasicString` template inherits from a
+:cpp:type:`InlineBasicString` specialization with capacity of the reserved value
+``pw::InlineString<>::npos``. The actual capacity is stored in a single word
+alongside the size. This allows code to work with strings of any capacity
+through a ``InlineString<>`` or ``InlineBasicString<T>`` reference.
+
+Exceeding the capacity
+----------------------
+Any :cpp:type:`pw::InlineString` operations that exceed the string's capacity
+fail an assertion, resulting in a crash. Helpers are provided in
+``pw_string/util.h`` that return ``pw::Status::ResourceExhausted()`` instead of
+failing an assert when the capacity would be exceeded.
+
+------------------------
+String utility functions
+------------------------
+
+Safe length checking
+====================
This module provides two safer alternatives to ``std::strlen`` in case the
string is extremely long and/or potentially not null-terminated.
@@ -53,10 +73,3 @@
Second, a constexpr specialized form is offered where null termination is
required through :cpp:func:`pw::string::NullTerminatedLength`. This will only
return a length if the string is null-terminated.
-
-Exceeding the capacity
-----------------------
-Any :cpp:type:`pw::InlineString` operations that exceed the string's capacity
-fail an assertion, resulting in a crash. Helpers are provided in
-``pw_string/util.h`` that return ``pw::Status::ResourceExhausted()`` instead of
-failing an assert when the capacity would be exceeded.
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index 7aa8000..e7b4660 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -1,108 +1,117 @@
.. _module-pw_string:
+.. rst-class:: with-subtitle
+
=========
pw_string
=========
-.. card::
- :octicon:`comment-discussion` Status:
- :bdg-secondary-line:`Experimental`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Unstable`
- :octicon:`chevron-right`
- :bdg-primary:`Stable`
- :octicon:`kebab-horizontal`
- :bdg-primary:`Current`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Deprecated`
+.. pigweed-module::
+ :name: pw_string
+ :tagline: Efficient, easy, and safe string manipulation
+ :status: stable
+ :languages: C++14, C++17
+ :code-size-impact: 500 to 1500 bytes
+ :get-started: module-pw_string-get-started
+ :design: module-pw_string-design
+ :guides: module-pw_string-guide
+ :api: module-pw_string-api
-Compatibility: C++17 (C++14 for :cpp:type:`pw::InlineString`)
+ - **Efficient**: No memory allocation, no pointer indirection.
+ - **Easy**: Use the string API you already know.
+ - **Safe**: Never worry about buffer overruns or undefined behavior.
-`API reference </pw_string/api.html>`_ | `Guide </pw_string/guide.html>`_ | `Design </pw_string/design.html>`_
+ *Pick three!* If you know how to use ``std::string``, just use
+ :cpp:type:`pw::InlineString` in the same way:
----------------------------------------------
-Efficient, easy, and safe string manipulation
----------------------------------------------
-- **Efficient**: No memory allocation, no pointer indirection.
-- **Easy**: Use the string API you already know.
-- **Safe**: Never worry about buffer overruns or undefined behavior.
+ .. code:: cpp
-*Pick three!* If you know how to use ``std::string``, just use
-:cpp:type:`pw::InlineString` in the same way:
+ // Create a string from a C-style char array; storage is pre-allocated!
+ pw::InlineString<16> my_string = "Literally";
-.. code:: cpp
+ // We have some space left, so let's add to the string.
+ my_string.append('?', 3); // "Literally???"
- // Create a string from a C-style char array; storage is pre-allocated!
- pw::InlineString<16> my_string = "Literally";
+ // Let's try something evil and extend this past its capacity 😈
+ my_string.append('!', 8);
+ // Foiled by a crash! No mysterious bugs or undefined behavior.
- // We have some space left, so let's add to the string.
- my_string.append('?', 3); // "Literally???"
+ Need to build up a string? :cpp:type:`pw::StringBuilder` works like
+ ``std::ostringstream``, but with most of the efficiency and memory benefits
+ of :cpp:type:`pw::InlineString`:
- // Let's try something evil and extend this past its capacity 😈
- my_string.append('!', 8);
- // Foiled by a crash! No mysterious bugs or undefined behavior.
+ .. code:: cpp
-Need to build up a string? :cpp:type:`pw::StringBuilder` works like
-``std::ostringstream``, but with most of the efficiency and memory benefits of
-:cpp:type:`pw::InlineString`:
+ // Create a pw::StringBuilder with a built-in buffer
+ pw::StringBuffer<32> my_string_builder = "Is it really this easy?";
-.. code:: cpp
+ // Add to it with idiomatic C++
+ my_string << " YES!";
- // Create a pw::StringBuilder with a built-in buffer
- pw::StringBuffer<32> my_string_builder = "Is it really this easy?";
+ // Use it like any other string
+ PW_LOG_DEBUG("%s", my_string_builder.c_str());
- // Add to it with idiomatic C++
- my_string << " YES!";
-
- // Use it like any other string
- PW_LOG_DEBUG("%s", my_string_builder.c_str());
-
-
-Check out :ref:`module-pw_string-guide` for more code samples.
+ Check out :ref:`module-pw_string-guide` for more code samples.
----------
Background
----------
-String manipulation is a very common operation, but the standard C and C++
-string libraries have drawbacks. The C++ functions are easy-to-use and powerful,
-but require too much flash and memory for many embedded projects. The C string
-functions are lighter weight, but can be difficult to use correctly. Mishandling
-of null terminators or buffer sizes can result in serious bugs.
+String manipulation on embedded systems can be surprisingly challenging.
+C strings are light weight but come with many pitfalls for those who don't know
+the standard library deeply. C++ provides string classes that are safe and easy
+to use, but they consume way too much code space and are designed to be used
+with dynamic memory allocation.
+
+Embedded systems need string functionality that is both safe and suitable for
+resource-constrained platforms.
------------
Our solution
------------
-The ``pw_string`` module provides the flexibility, ease-of-use, and safety of
-C++-style string manipulation, but with no dynamic memory allocation and a much
-smaller binary size impact. Using ``pw_string`` in place of the standard C
-functions eliminates issues related to buffer overflow or missing null
-terminators.
+``pw_string`` provides safe string handling functionality with an API that
+closely matches that of ``std::string``, but without dynamic memory allocation
+and with a *much* smaller :ref:`binary size impact <module-pw_string-size-reports>`.
---------------
Who this is for
---------------
-``pw_string`` is potentially useful for anyone who is working with strings in
-C++.
+``pw_string`` is useful any time you need to handle strings in embedded C++.
+--------------------
Is it right for you?
--------------------
+If your project written in C, ``pw_string`` is not a good fit since we don't
+currently expose a C API.
+
+For larger platforms where code space isn't in short supply and dynamic memory
+allocation isn't a problem, you may find that ``std::string`` meets your needs.
+
+.. tip::
+ ``pw_string`` works just as well on larger embedded platforms and host
+ systems. Using ``pw_string`` even when you might get away with ``std:string``
+ gives you the flexibility to move to smaller platforms later with much less
+ rework.
+
Here are some size reports that may affect whether ``pw_string`` is right for
you.
+.. _module-pw_string-size-reports:
+
Size comparison: snprintf versus pw::StringBuilder
--------------------------------------------------
-:cpp:type:`pw::StringBuilder` is safe, flexible, and results in much smaller code size than
-using ``std::ostringstream``. However, applications sensitive to code size
-should use :cpp:type:`pw::StringBuilder` with care.
+:cpp:type:`pw::StringBuilder` is safe, flexible, and results in much smaller
+code size than using ``std::ostringstream``. However, applications sensitive to
+code size should use :cpp:type:`pw::StringBuilder` with care.
-The fixed code size cost of :cpp:type:`pw::StringBuilder` is significant, though smaller than
-``std::snprintf``. Using :cpp:type:`pw::StringBuilder`'s ``<<`` and ``append`` methods exclusively in
-place of ``snprintf`` reduces code size, but ``snprintf`` may be difficult to
-avoid.
+The fixed code size cost of :cpp:type:`pw::StringBuilder` is significant, though
+smaller than ``std::snprintf``. Using :cpp:type:`pw::StringBuilder`'s ``<<`` and
+``append`` methods exclusively in place of ``snprintf`` reduces code size, but
+``snprintf`` may be difficult to avoid.
-The incremental code size cost of :cpp:type:`pw::StringBuilder` is comparable to ``snprintf`` if
-errors are handled. Each argument to :cpp:type:`pw::StringBuilder`'s ``<<`` method expands to a
-function call, but one or two :cpp:type:`pw::StringBuilder` appends may have a smaller code size
+The incremental code size cost of :cpp:type:`pw::StringBuilder` is comparable to
+``snprintf`` if errors are handled. Each argument to
+:cpp:type:`pw::StringBuilder`'s ``<<`` method expands to a function call, but
+one or two :cpp:type:`pw::StringBuilder` appends may have a smaller code size
impact than a single ``snprintf`` call.
.. include:: string_builder_size_report
@@ -115,6 +124,18 @@
.. include:: format_size_report
+Roadmap
+-------
+* StringBuilder's fixed size cost can be dramatically reduced by limiting
+ support for 64-bit integers.
+* Consider integrating with the tokenizer module.
+
+Compatibility
+-------------
+C++17, C++14 (:cpp:type:`pw::InlineString`)
+
+.. _module-pw_string-get-started:
+
---------------
Getting started
---------------
@@ -143,25 +164,15 @@
------
Add ``CONFIG_PIGWEED_STRING=y`` to the Zephyr project's configuration.
----------------------
-Design considerations
----------------------
-``pw_string`` is designed to prioritize safety and static allocation. It matches
-the ``std::string`` API as closely as possible, but isn't intended to provide
-complete API compatibility. See :ref:`module-pw_string-design` for more
-details.
-
-------
Roadmap
-------
-* The fixed size cost of :cpp:type:`pw::StringBuilder` can be dramatically reduced by
- limiting support for 64-bit integers.
+* The fixed size cost of :cpp:type:`pw::StringBuilder` can be dramatically
+ reduced by limiting support for 64-bit integers.
* ``pw_string`` may be integrated with :ref:`module-pw_tokenizer`.
-----------
-Learn more
-----------
.. toctree::
+ :hidden:
:maxdepth: 1
design
diff --git a/pw_string/guide.rst b/pw_string/guide.rst
index 38916ee..87ceec0 100644
--- a/pw_string/guide.rst
+++ b/pw_string/guide.rst
@@ -1,11 +1,67 @@
.. _module-pw_string-guide:
-===============
-pw_string guide
-===============
+================
+pw_string: Guide
+================
-Building strings with StringBuilder
------------------------------------
+InlineString and StringBuilder?
+===============================
+Use :cpp:type:`pw::InlineString` if you need:
+
+* Compatibility with ``std::string``
+* Storage internal to the object
+* A string object to persist in other data structures
+* Lower code size overhead
+
+Use :cpp:class:`pw::StringBuilder` if you need:
+
+* Compatibility with ``std::ostringstream``, including custom object support
+* Storage external to the object
+* Non-fatal handling of failed append/format operations
+* Tracking of the status of a series of operations
+* A temporary stack object to aid string construction
+* Medium code size overhead
+
+An example of when to prefer :cpp:type:`pw::InlineString` is wrapping a
+length-delimited string (e.g. ``std::string_view``) for APIs that require null
+termination:
+
+.. code-block:: cpp
+
+ #include <string>
+ #include "pw_log/log.h"
+ #include "pw_string/string_builder.h"
+
+ void ProcessName(std::string_view name) {
+ // %s format strings require null terminated strings, so create one on the
+ // stack with size up to kMaxNameLen, copy the string view `name` contents
+ // into it, add a null terminator, and log it.
+ PW_LOG_DEBUG("The name is %s",
+ pw::InlineString<kMaxNameLen>(name).c_str());
+ }
+
+An example of when to prefer :cpp:class:`pw::StringBuilder` is when
+constructing a string for external use.
+
+.. code-block:: cpp
+
+ #include "pw_string/string_builder.h"
+
+ pw::Status FlushSensorValueToUart(int32_t sensor_value) {
+ pw::StringBuffer<42> sb;
+ sb << "Sensor value: ";
+ sb << sensor_value; // Formats as int.
+ FlushCStringToUart(sb.c_str());
+
+ if (!sb.status().ok) {
+ format_error_metric.Increment(); // Track overflows.
+ }
+ return sb.status();
+ }
+
+
+Building strings with pw::StringBuilder
+=======================================
The following shows basic use of a :cpp:class:`pw::StringBuilder`.
.. code-block:: cpp
@@ -34,92 +90,94 @@
return sb.status();
}
-Constructing pw::InlineString objects
--------------------------------------
+Building strings with pw::InlineString
+======================================
:cpp:type:`pw::InlineString` objects must be constructed by specifying a fixed
capacity for the string.
.. code-block:: c++
- // Initialize from a C string.
- pw::InlineString<32> inline_string = "Literally";
- inline_string.append('?', 3); // contains "Literally???"
+ #include "pw_string/string.h"
- // Supports copying into known-capacity strings.
- pw::InlineString<64> other = inline_string;
+ // Initialize from a C string.
+ pw::InlineString<32> inline_string = "Literally";
+ inline_string.append('?', 3); // contains "Literally???"
- // Supports various helpful std::string functions
- if (inline_string.starts_with("Lit") || inline_string == "not\0literally"sv) {
- other += inline_string;
- }
+ // Supports copying into known-capacity strings.
+ pw::InlineString<64> other = inline_string;
- // Like std::string, InlineString is always null terminated when accessed
- // through c_str(). InlineString can be used to null-terminate
- // length-delimited strings for APIs that expect null-terminated strings.
- std::string_view file(".gif");
- if (std::fopen(pw::InlineString<kMaxNameLen>(file).c_str(), "r") == nullptr) {
- return;
- }
+ // Supports various helpful std::string functions
+ if (inline_string.starts_with("Lit") || inline_string == "not\0literally"sv) {
+ other += inline_string;
+ }
- // pw::InlineString integrates well with std::string_view. It supports
- // implicit conversions to and from std::string_view.
- inline_string = std::string_view("not\0literally", 12);
+ // Like std::string, InlineString is always null terminated when accessed
+ // through c_str(). InlineString can be used to null-terminate
+ // length-delimited strings for APIs that expect null-terminated strings.
+ std::string_view file(".gif");
+ if (std::fopen(pw::InlineString<kMaxNameLen>(file).c_str(), "r") == nullptr) {
+ return;
+ }
- FunctionThatTakesAStringView(inline_string);
+ // pw::InlineString integrates well with std::string_view. It supports
+ // implicit conversions to and from std::string_view.
+ inline_string = std::string_view("not\0literally", 12);
- FunctionThatTakesAnInlineString(std::string_view("1234", 4));
+ FunctionThatTakesAStringView(inline_string);
-Choosing between InlineString and StringBuilder
------------------------------------------------
-:cpp:type:`pw::InlineString` is comparable to ``std::string``, while
-:cpp:class:`pw::StringBuilder` is comparable to ``std::ostringstream``.
-Because :cpp:class:`pw::StringBuilder` provides high-level stream functionality,
-it has more overhead than :cpp:type:`pw::InlineString`.
+ FunctionThatTakesAnInlineString(std::string_view("1234", 4));
-Use :cpp:type:`pw::InlineString` unless :cpp:class:`pw::StringBuilder`'s
-capabilities are needed. Features unique to :cpp:class:`pw::StringBuilder`
-include:
-
-* Polymorphic C++ stream-style output, potentially supporting custom types.
-* Non-fatal handling of failed append/format operations.
-* Tracking the status of a series of operations.
-* Building a string in an external buffer.
-
-If those features are not required, use :cpp:type:`pw::InlineString`. A common
-example of when to prefer :cpp:type:`pw::InlineString` is wrapping a
-length-delimited string (e.g. ``std::string_view``) for APIs that require null
-termination.
-
-.. code-block:: cpp
-
- void ProcessName(std::string_view name) {
- PW_LOG_DEBUG("The name is %s", pw::InlineString<kMaxNameLen>(name).c_str());
-
-Operating on unknown size strings
----------------------------------
-All :cpp:type:`pw::InlineString` operations may be performed on strings without
-specifying their capacity.
+Building strings inside InlineString with a StringBuilder
+=========================================================
+:cpp:class:`pw::StringBuilder` can build a string in a
+:cpp:type:`pw::InlineString`:
.. code-block:: c++
- void RemoveSuffix(pw::InlineString<>& string, std::string_view suffix) {
- if (string.ends_with(suffix)) {
- string.resize(string.size() - suffix.size());
- }
- }
+ #include "pw_string/string.h"
- void DoStuff() {
- pw::InlineString<32> str1 = "Good morning!";
- RemoveSuffix(str1, " morning!");
+ void DoFoo() {
+ InlineString<32> inline_str;
+ StringBuilder sb(inline_str);
+ sb << 123 << "456";
+ // inline_str contains "456"
+ }
- pw::InlineString<40> str2 = "Good";
- RemoveSuffix(str2, " morning!");
+Passing InlineStrings as parameters
+===================================
+:cpp:type:`pw::InlineString` objects can be passed to non-templated functions
+via type erasure. This saves code size in most cases, since it avoids template
+expansions triggered by string size differences.
- PW_ASSERT(str1 == str2);
- }
+Unknown size strings
+--------------------
+To operate on :cpp:type:`pw::InlineString` objects without knowing their type,
+use the ``pw::InlineString<>`` type, shown in the examples below:
-Operating on known-size strings
--------------------------------
+.. code-block:: c++
+
+ // Note that the first argument is a generically-sized InlineString.
+ void RemoveSuffix(pw::InlineString<>& string, std::string_view suffix) {
+ if (string.ends_with(suffix)) {
+ string.resize(string.size() - suffix.size());
+ }
+ }
+
+ void DoStuff() {
+ pw::InlineString<32> str1 = "Good morning!";
+ RemoveSuffix(str1, " morning!");
+
+ pw::InlineString<40> str2 = "Good";
+ RemoveSuffix(str2, " morning!");
+
+ PW_ASSERT(str1 == str2);
+ }
+
+However, generically sized :cpp:type:`pw::InlineString` objects don't work in
+``constexpr`` contexts.
+
+Known size strings
+------------------
:cpp:type:`pw::InlineString` operations on known-size strings may be used in
``constexpr`` expressions.
@@ -135,13 +193,8 @@
return string;
}();
-Building strings
-----------------
-:cpp:class:`pw::StringBuilder` may be used to build a string in a
-:cpp:type:`pw::InlineString`.
-
-Deducing class template arguments with pw::InlineBasicString
-------------------------------------------------------------
+Compact initialization of InlineStrings
+=======================================
:cpp:type:`pw::InlineBasicString` supports class template argument deduction
(CTAD) in C++17 and newer. Since :cpp:type:`pw::InlineString` is an alias, CTAD
is not supported until C++20.
@@ -155,9 +208,8 @@
// In C++20, CTAD may be used with the pw::InlineString alias.
pw::InlineString my_other_string("123456789");
-
-Printing custom types
----------------------
+Supporting custom types with StringBuilder
+==========================================
As with ``std::ostream``, StringBuilder supports printing custom types by
overriding the ``<<`` operator. This is is done by defining ``operator<<`` in
the same namespace as the custom type. For example:
diff --git a/pw_string/public/pw_string/format.h b/pw_string/public/pw_string/format.h
index 7d05fe0..205210a 100644
--- a/pw_string/public/pw_string/format.h
+++ b/pw_string/public/pw_string/format.h
@@ -29,22 +29,24 @@
namespace pw::string {
-// Writes a printf-style formatted string to the provided buffer, similarly to
-// std::snprintf. Returns the number of characters written, excluding the null
-// terminator. The buffer is always null-terminated unless it is empty.
-//
-// The status is
-//
-// OkStatus() if the operation succeeded,
-// Status::ResourceExhausted() if the buffer was too small to fit the output,
-// Status::InvalidArgument() if there was a formatting error.
-//
+/// @brief Writes a printf-style formatted string to the provided buffer,
+/// similarly to `std::snprintf()`.
+///
+/// The `std::snprintf()` return value is awkward to interpret, and
+/// misinterpreting it can lead to serious bugs.
+///
+/// @returns The number of characters written, excluding the null
+/// terminator. The buffer is always null-terminated unless it is empty.
+/// The status is `OkStatus()` if the operation succeeded,
+/// `Status::ResourceExhausted()` if the buffer was too small to fit the output,
+/// or `Status::InvalidArgument()` if there was a formatting error.
PW_PRINTF_FORMAT(2, 3)
StatusWithSize Format(span<char> buffer, const char* format, ...);
-// Writes a printf-style formatted string with va_list-packed arguments to the
-// provided buffer, similarly to std::vsnprintf. The return value is the same as
-// above.
+/// @brief Writes a printf-style formatted string with va_list-packed arguments
+/// to the provided buffer, similarly to `std::vsnprintf()`.
+///
+/// @returns See `pw::string::Format()`.
PW_PRINTF_FORMAT(2, 0)
StatusWithSize FormatVaList(span<char> buffer,
const char* format,
diff --git a/pw_string/public/pw_string/string.h b/pw_string/public/pw_string/string.h
index 441b218..a6041f5 100644
--- a/pw_string/public/pw_string/string.h
+++ b/pw_string/public/pw_string/string.h
@@ -13,6 +13,11 @@
// the License.
#pragma once
+/// @file pw_string/string.h
+///
+/// @brief `pw::InlineBasicString` and `pw::InlineString` are safer alternatives
+/// to `std::basic_string` and `std::string`.
+
#include <cstddef>
#include <initializer_list>
#include <iterator>
@@ -36,21 +41,24 @@
namespace pw {
-// pw::InlineBasicString<T, kCapacity> is a fixed-capacity version of
-// std::basic_string. It implements mostly the same API as std::basic_string,
-// but the capacity of the string is fixed at construction and cannot grow.
-// Attempting to increase the size beyond the capacity triggers an assert.
-//
-// A pw::InlineString alias of pw::InlineBasicString<char>, equivalent to
-// std::string, is defined below.
-//
-// pw::InlineBasicString has a template parameter for the capacity, but the
-// capacity does not have to be known to use the string. The
-// pw::InlineBasicString template inherits from a pw::InlineBasicString
-// specialization with capacity of the reserved value pw::InlineString<>::npos.
-// The actual capacity is stored in a single word alongside the size. This
-// allows code to work with strings of any capacity through a pw::InlineString<>
-// or pw::InlineBasicString<T> reference.
+/// @brief `pw::InlineBasicString` is a fixed-capacity version of
+/// `std::basic_string`. In brief:
+///
+/// - It is C++14-compatible and null-terminated.
+/// - It stores the string contents inline and uses no dynamic memory.
+/// - It implements mostly the same API as `std::basic_string`, but the capacity
+/// of the string is fixed at construction and cannot grow. Attempting to
+/// increase the size beyond the capacity triggers an assert.
+///
+/// `pw::InlineBasicString` is efficient and compact. The current size and
+/// capacity are stored in a single word. Accessing its contents is a simple
+/// array access within the object, with no pointer indirection, even when
+/// working from a generic reference `pw::InlineBasicString<T>` where the
+/// capacity is not specified as a template argument. A string object can be
+/// used safely without the need to know its capacity.
+///
+/// See also `pw::InlineString`, which is an alias of
+/// `pw::InlineBasicString<char>` and is equivalent to `std::string`.
template <typename T, string_impl::size_type kCapacity = string_impl::kGeneric>
class InlineBasicString final
: public InlineBasicString<T, string_impl::kGeneric> {
@@ -566,6 +574,9 @@
// TODO(b/239996007): Implement other comparison operator overloads.
// Aliases
+
+/// @brief `pw::InlineString` is an alias of `pw::InlineBasicString<char>` and
+/// is equivalent to `std::string`.
template <string_impl::size_type kCapacity = string_impl::kGeneric>
using InlineString = InlineBasicString<char, kCapacity>;
diff --git a/pw_string/public/pw_string/string_builder.h b/pw_string/public/pw_string/string_builder.h
index 886aa60..c3c9d17 100644
--- a/pw_string/public/pw_string/string_builder.h
+++ b/pw_string/public/pw_string/string_builder.h
@@ -12,6 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
+/// @file pw_string/string_builder.h
+///
+/// @brief `pw::StringBuilder` facilitates creating formatted strings in a
+/// fixed-sized buffer or in a `pw::InlineString`. It is designed to give the
+/// flexibility of std::ostringstream, but with a small footprint.
#include <algorithm>
#include <cstdarg>
@@ -32,19 +37,18 @@
/// @class StringBuilder
///
-/// `StringBuilder` facilitates building formatted strings in a fixed-size
-/// buffer. StringBuilders are always null terminated (unless they are
+/// `pw::StringBuilder` instances are always null-terminated (unless they are
/// constructed with an empty buffer) and never overflow. Status is tracked for
/// each operation and an overall status is maintained, which reflects the most
/// recent error.
///
-/// A `StringBuilder` does not own the buffer it writes to. It can be used to
-/// write strings to any buffer. The StringBuffer template class, defined below,
-/// allocates a buffer alongside a `StringBuilder`.
+/// `pw::StringBuilder` does not own the buffer it writes to. It can be used
+/// to write strings to any buffer. The `pw::StringBuffer` template class,
+/// defined below, allocates a buffer alongside a `pw::StringBuilder`.
///
-/// `StringBuilder` supports C++-style << output, similar to
-/// `std::ostringstream`. It also supports std::string-like append functions and
-/// printf-style output.
+/// `pw::StringBuilder` supports C++-style `<<` output, similar to
+/// `std::ostringstream`. It also supports append functions like `std::string`
+/// and `printf`-style output.
///
/// Support for custom types is added by overloading `operator<<` in the same
/// namespace as the custom type. For example:
@@ -65,8 +69,8 @@
/// } // namespace my_project
/// @endcode
///
-/// The ToString template function can be specialized to support custom types
-/// with `StringBuilder`, though overloading `operator<<` is generally
+/// The `ToString` template function can be specialized to support custom types
+/// with `pw::StringBuilder`, though overloading `operator<<` is generally
/// preferred. For example:
///
/// @code
@@ -82,7 +86,7 @@
///
class StringBuilder {
public:
- /// Creates an empty StringBuilder.
+ /// Creates an empty `pw::StringBuilder`.
explicit constexpr StringBuilder(span<char> buffer)
: buffer_(buffer), size_(&inline_size_), inline_size_(0) {
NullTerminate();
@@ -98,7 +102,7 @@
inline_size_(0) {}
/// Disallow copy/assign to avoid confusion about where the string is actually
- /// stored. StringBuffers may be copied into one another.
+ /// stored. `pw::StringBuffer` instances may be copied into one another.
StringBuilder(const StringBuilder&) = delete;
StringBuilder& operator=(const StringBuilder&) = delete;
@@ -110,40 +114,41 @@
const char* data() const { return buffer_.data(); }
const char* c_str() const { return data(); }
- /// Returns a std::string_view of the contents of this StringBuilder. The
- /// std::string_view is invalidated if the StringBuilder contents change.
+ /// Returns a `std::string_view` of the contents of this `pw::StringBuilder`.
+ /// The `std::string_view` is invalidated if the `pw::StringBuilder` contents
+ /// change.
std::string_view view() const { return std::string_view(data(), size()); }
- /// Allow implicit conversions to std::string_view so StringBuilders can be
- /// passed into functions that take a std::string_view.
+ /// Allow implicit conversions to `std::string_view` so `pw::StringBuilder`
+ /// instances can be passed into functions that take a `std::string_view`.
operator std::string_view() const { return view(); }
- /// Returns a span<const std::byte> representation of this StringBuffer.
+ /// Returns a `span<const std::byte>` representation of this
+ /// `pw::StringBuffer`.
span<const std::byte> as_bytes() const {
return span(reinterpret_cast<const std::byte*>(buffer_.data()), size());
}
- /// Returns the StringBuilder's status, which reflects the most recent error
- /// that occurred while updating the string. After an update fails, the status
- /// remains non-OK until it is cleared with clear() or clear_status().
- /// Returns:
+ /// Returns the status of `pw::StringBuilder`, which reflects the most recent
+ /// error that occurred while updating the string. After an update fails, the
+ /// status remains non-OK until it is cleared with
+ /// `pw::StringBuilder::clear()` or `pw::StringBuilder::clear_status()`.
///
- /// OK if no errors have occurred
- /// RESOURCE_EXHAUSTED if output to the StringBuilder was truncated
- /// INVALID_ARGUMENT if printf-style formatting failed
- /// OUT_OF_RANGE if an operation outside the buffer was attempted
- ///
+ /// @returns `OK` if no errors have occurred; `RESOURCE_EXHAUSTED` if output
+ /// to the `StringBuilder` was truncated; `INVALID_ARGUMENT` if `printf`-style
+ /// formatting failed; `OUT_OF_RANGE` if an operation outside the buffer was
+ /// attempted.
Status status() const { return static_cast<Status::Code>(status_); }
- /// Returns status() and size() as a StatusWithSize.
+ /// Returns `status()` and `size()` as a `StatusWithSize`.
StatusWithSize status_with_size() const {
return StatusWithSize(status(), size());
}
- /// The status from the last operation. May be OK while status() is not OK.
+ /// The status from the last operation. May be OK while `status()` is not OK.
Status last_status() const { return static_cast<Status::Code>(last_status_); }
- /// True if status() is OkStatus().
+ /// True if `status()` is `OkStatus()`.
bool ok() const { return status().ok(); }
/// True if the string is empty.
@@ -158,57 +163,58 @@
/// Clears the string and resets its error state.
void clear();
- /// Sets the statuses to OkStatus();
+ /// Sets the statuses to `OkStatus()`;
void clear_status() {
status_ = static_cast<unsigned char>(OkStatus().code());
last_status_ = static_cast<unsigned char>(OkStatus().code());
}
- /// Appends a single character. Stets the status to RESOURCE_EXHAUSTED if the
+ /// Appends a single character. Sets the status to `RESOURCE_EXHAUSTED` if the
/// character cannot be added because the buffer is full.
void push_back(char ch) { append(1, ch); }
- /// Removes the last character. Sets the status to OUT_OF_RANGE if the buffer
- /// is empty (in which case the unsigned overflow is intentional).
+ /// Removes the last character. Sets the status to `OUT_OF_RANGE` if the
+ /// buffer is empty (in which case the unsigned overflow is intentional).
void pop_back() PW_NO_SANITIZE("unsigned-integer-overflow") {
resize(size() - 1);
}
- /// Appends the provided character count times.
+ /// Appends the provided character `count` times.
StringBuilder& append(size_t count, char ch);
- /// Appends count characters from str to the end of the StringBuilder. If
- /// count exceeds the remaining space in the StringBuffer, max_size() - size()
- /// characters are appended and the status is set to RESOURCE_EXHAUSTED.
+ /// Appends `count` characters from `str` to the end of the `StringBuilder`.
+ /// If count exceeds the remaining space in the `StringBuffer`,
+ /// `max_size() - size()` characters are appended and the status is set to
+ /// `RESOURCE_EXHAUSTED`.
///
- /// str is not considered null-terminated and may contain null characters.
+ /// `str` is not considered null-terminated and may contain null characters.
StringBuilder& append(const char* str, size_t count);
/// Appends characters from the null-terminated string to the end of the
- /// StringBuilder. If the string's length exceeds the remaining space in the
- /// buffer, max_size() - size() characters are copied and the status is set to
- /// RESOURCE_EXHAUSTED.
+ /// `StringBuilder`. If the string's length exceeds the remaining space in the
+ /// buffer, `max_size() - size()` characters are copied and the status is
+ /// set to `RESOURCE_EXHAUSTED`.
///
- /// This function uses string::Length instead of std::strlen to avoid
- /// unbounded reads if the string is not null terminated.
+ /// This function uses `string::Length` instead of `std::strlen` to avoid
+ /// unbounded reads if the string is not null-terminated.
StringBuilder& append(const char* str);
- /// Appends a std::string_view to the end of the StringBuilder.
+ /// Appends a `std::string_view` to the end of the `StringBuilder`.
StringBuilder& append(const std::string_view& str);
- /// Appends a substring from the std::string_view to the StringBuilder. Copies
- /// up to count characters starting from pos to the end of the StringBuilder.
- /// If pos > str.size(), sets the status to OUT_OF_RANGE.
+ /// Appends a substring from the `std::string_view` to the `StringBuilder`.
+ /// Copies up to count characters starting from `pos` to the end of the
+ /// `StringBuilder`. If `pos > str.size()`, sets the status to `OUT_OF_RANGE`.
StringBuilder& append(const std::string_view& str,
size_t pos,
size_t count = std::string_view::npos);
- /// Appends to the end of the StringBuilder using the << operator. This
- /// enables C++ stream-style formatted to StringBuilders.
+ /// Appends to the end of the `StringBuilder` using the `<<` operator. This
+ /// enables C++ stream-style formatted to `StringBuilder` instances.
template <typename T>
StringBuilder& operator<<(const T& value) {
- /// For std::string_view-compatible types, use the append function, which
- /// gives smaller code size.
+ /// For types compatible with `std::string_view`, use the `append` function,
+ /// which gives smaller code size.
if constexpr (std::is_convertible_v<T, std::string_view>) {
append(value);
} else if constexpr (std::is_convertible_v<T, span<const std::byte>>) {
@@ -219,7 +225,7 @@
return *this;
}
- /// Provide a few additional operator<< overloads that reduce code size.
+ /// Provide a few additional `operator<<` overloads that reduce code size.
StringBuilder& operator<<(bool value) {
return append(value ? "true" : "false");
}
@@ -236,32 +242,32 @@
StringBuilder& operator<<(Status status) { return *this << status.str(); }
/// @fn pw::StringBuilder::Format
- /// Appends a printf-style string to the end of the StringBuilder. If the
+ /// Appends a `printf`-style string to the end of the `StringBuilder`. If the
/// formatted string does not fit, the results are truncated and the status is
- /// set to RESOURCE_EXHAUSTED.
+ /// set to `RESOURCE_EXHAUSTED`.
///
/// @param format The format string
/// @param ... Arguments for format specification
///
- /// @return StringBuilder&
+ /// @returns `StringBuilder&`
///
- /// @note Internally, calls string::Format, which calls std::vsnprintf.
+ /// @note Internally, calls `string::Format`, which calls `std::vsnprintf`.
PW_PRINTF_FORMAT(2, 3) StringBuilder& Format(const char* format, ...);
- /// Appends a vsnprintf-style string with va_list arguments to the end of the
- /// StringBuilder. If the formatted string does not fit, the results are
- /// truncated and the status is set to RESOURCE_EXHAUSTED.
+ /// Appends a `vsnprintf`-style string with `va_list` arguments to the end of
+ /// the `StringBuilder`. If the formatted string does not fit, the results are
+ /// truncated and the status is set to `RESOURCE_EXHAUSTED`.
///
- /// Internally, calls string::Format, which calls std::vsnprintf.
+ /// @note Internally, calls `string::Format`, which calls `std::vsnprintf`.
PW_PRINTF_FORMAT(2, 0)
StringBuilder& FormatVaList(const char* format, va_list args);
- /// Sets the StringBuilder's size. This function only truncates; if
- /// new_size > size(), it sets status to OUT_OF_RANGE and does nothing.
+ /// Sets the size of the `StringBuilder`. This function only truncates; if
+ /// `new_size > size()`, it sets status to `OUT_OF_RANGE` and does nothing.
void resize(size_t new_size);
protected:
- /// Functions to support StringBuffer copies.
+ /// Functions to support `StringBuffer` copies.
constexpr StringBuilder(span<char> buffer, const StringBuilder& other)
: buffer_(buffer),
size_(&inline_size_),
@@ -272,7 +278,7 @@
void CopySizeAndStatus(const StringBuilder& other);
private:
- /// Statuses are stored as an unsigned char so they pack into a single word.
+ /// Statuses are stored as an `unsigned char` so they pack into a single word.
static constexpr unsigned char StatusCode(Status status) {
return static_cast<unsigned char>(status.code());
}
@@ -295,16 +301,16 @@
InlineString<>::size_type* size_;
- /// Place the inline_size_, status_, and last_status_ members together and use
- /// unsigned char for the status codes so these members can be packed into a
- /// single word.
+ // Place the `inline_size_`, `status_`, and `last_status_` members together
+ // and use `unsigned char` for the status codes so these members can be
+ // packed into a single word.
InlineString<>::size_type inline_size_;
unsigned char status_ = StatusCode(OkStatus());
unsigned char last_status_ = StatusCode(OkStatus());
};
-// StringBuffers declare a buffer along with a StringBuilder. StringBuffer can
-// be used as a statically allocated replacement for std::ostringstream or
+// StringBuffer declares a buffer along with a StringBuilder. StringBuffer
+// can be used as a statically allocated replacement for std::ostringstream or
// std::string. For example:
//
// StringBuffer<32> str;
diff --git a/pw_string/public/pw_string/util.h b/pw_string/public/pw_string/util.h
index 2e06b6e..d95bb79 100644
--- a/pw_string/public/pw_string/util.h
+++ b/pw_string/public/pw_string/util.h
@@ -12,6 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
+/// @file pw_string/util.h
+///
+/// @brief The `pw::string::*` functions provide safer alternatives to
+/// C++ standard library string functions.
#include <cctype>
#include <cstddef>
@@ -46,11 +50,11 @@
} // namespace internal
-// Safe alternative to the string_view constructor to avoid the risk of an
-// unbounded implicit or explicit use of strlen.
-//
-// This is strongly recommended over using something like C11's strnlen_s as
-// a string_view does not require null-termination.
+/// @brief Safe alternative to the `string_view` constructor that avoids the
+/// risk of an unbounded implicit or explicit use of `strlen`.
+///
+/// This is strongly recommended over using something like C11's `strnlen_s` as
+/// a `string_view` does not require null-termination.
constexpr std::string_view ClampedCString(span<const char> str) {
return std::string_view(str.data(),
internal::ClampedLength(str.data(), str.size()));
@@ -60,15 +64,16 @@
return ClampedCString(span<const char>(str, max_len));
}
-// Safe alternative to strlen to calculate the null-terminated length of the
-// string within the specified span, excluding the null terminator. Like C11's
-// strnlen_s, the scan for the null-terminator is bounded.
-//
-// Returns:
-// null-terminated length of the string excluding the null terminator.
-// OutOfRange - if the string is not null-terminated.
-//
-// Precondition: The string shall be at a valid pointer.
+/// @brief `pw::string::NullTerminatedLength` is a safer alternative to
+/// `strlen` for calculating the null-terminated length of the
+/// string within the specified span, excluding the null terminator.
+///
+/// Like `strnlen_s` in C11, the scan for the null-terminator is bounded.
+///
+/// @pre The string shall be at a valid pointer.
+///
+/// @returns the null-terminated length of the string excluding the null
+/// terminator or `OutOfRange` if the string is not null-terminated.
constexpr Result<size_t> NullTerminatedLength(span<const char> str) {
PW_DASSERT(str.data() != nullptr);
@@ -84,14 +89,17 @@
return NullTerminatedLength(span<const char>(str, max_len));
}
-// Copies the source string to the dest, truncating if the full string does not
-// fit. Always null terminates if dest.size() or num > 0.
-//
-// Returns the number of characters written, excluding the null terminator. If
-// the string is truncated, the status is ResourceExhausted.
-//
-// Precondition: The destination and source shall not overlap.
-// Precondition: The source shall be a valid pointer.
+/// @brief `pw::string::Copy` is a safer alternative to `std::strncpy` as it
+/// always null-terminates whenever the destination buffer has a non-zero size.
+///
+/// Copies the `source` string to the `dest`, truncating if the full string does
+/// not fit. Always null terminates if `dest.size()` or `num` is greater than 0.
+///
+/// @pre The destination and source shall not overlap. The source
+/// shall be a valid pointer.
+///
+/// @returns the number of characters written, excluding the null terminator. If
+/// the string is truncated, the status is `RESOURCE_EXHAUSTED`.
template <typename Span>
PW_CONSTEXPR_CPP20 inline StatusWithSize Copy(const std::string_view& source,
Span&& dest) {
@@ -116,13 +124,14 @@
return Copy(source, span<char>(dest, num));
}
-// Assigns a std::string_view to a pw::InlineString, truncating if it does not
-// fit. pw::InlineString's assign() function asserts if the string's requested
-// size exceeds its capacity; pw::string::Assign() returns a Status instead.
-//
-// Returns:
-// OK - the entire std::string_view was copied to the end of the InlineString
-// RESOURCE_EXHAUSTED - the std::string_view was truncated to fit
+/// Assigns a `std::string_view` to a `pw::InlineString`, truncating if it does
+/// not fit. The `assign()` function of `pw::InlineString` asserts if the
+/// string's requested size exceeds its capacity; `pw::string::Assign()`
+/// returns a `Status` instead.
+///
+/// @return `OK` if the entire `std::string_view` was copied to the end of the
+/// `pw::InlineString`. `RESOURCE_EXHAUSTED` if the `std::string_view` was
+/// truncated to fit.
inline Status Assign(InlineString<>& string, const std::string_view& view) {
const size_t chars_copied =
std::min(view.size(), static_cast<size_t>(string.capacity()));
@@ -136,13 +145,13 @@
return Assign(string, ClampedCString(c_string, string.capacity() + 1));
}
-// Appends a std::string_view to a pw::InlineString, truncating if it does not
-// fit. pw::InlineString's append() function asserts if the string's requested
-// size exceeds its capacity; pw::string::Append() returns a Status instead.
-//
-// Returns:
-// OK - the entire std::string_view was assigned
-// RESOURCE_EXHAUSTED - the std::string_view was truncated to fit
+/// Appends a `std::string_view` to a `pw::InlineString`, truncating if it
+/// does not fit. The `append()` function of `pw::InlineString` asserts if the
+/// string's requested size exceeds its capacity; `pw::string::Append()` returns
+/// a `Status` instead.
+///
+/// @return `OK` if the entire `std::string_view` was assigned.
+/// `RESOURCE_EXHAUSTED` if the `std::string_view` was truncated to fit.
inline Status Append(InlineString<>& string, const std::string_view& view) {
const size_t chars_copied = std::min(
view.size(), static_cast<size_t>(string.capacity() - string.size()));
@@ -156,8 +165,11 @@
return Append(string, ClampedCString(c_string, string.capacity() + 1));
}
-// Copies source string to the dest with same behavior as Copy, with the
-// difference that any non-printable characters are changed to '.'.
+/// @brief Provides a safe, printable copy of a string.
+///
+/// Copies the `source` string to the `dest` string with same behavior as
+/// `pw::string::Copy`, with the difference that any non-printable characters
+/// are changed to `.`.
PW_CONSTEXPR_CPP20 inline StatusWithSize PrintableCopy(
const std::string_view& source, span<char> dest) {
StatusWithSize copy_result = Copy(source, dest);
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index 78fc7b2..5d48dac 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -54,7 +54,6 @@
name = "binary_semaphore_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:binary_semaphore"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:binary_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:binary_semaphore"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:binary_semaphore"],
@@ -89,7 +88,6 @@
name = "counting_semaphore_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:counting_semaphore"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:counting_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:counting_semaphore"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:counting_semaphore"],
@@ -175,7 +173,6 @@
name = "mutex_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:mutex"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:mutex"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:mutex"],
@@ -215,7 +212,6 @@
name = "timed_mutex_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:timed_mutex"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:timed_mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_mutex"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:timed_mutex"],
@@ -280,7 +276,6 @@
name = "interrupt_spin_lock_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:interrupt_spin_lock"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:interrupt_spin_lock"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:interrupt_spin_lock"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:interrupt_spin_lock"],
@@ -308,7 +303,8 @@
name = "thread_notification_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "//conditions:default": ["//pw_sync:binary_semaphore_thread_notification_backend"],
+ "//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:thread_notification"],
+ "//conditions:default": [":binary_semaphore_thread_notification_backend"],
}),
)
@@ -319,7 +315,7 @@
],
includes = ["public"],
deps = [
- ":thread_notification_facade",
+ ":thread_notification",
"//pw_chrono:system_clock",
],
)
@@ -337,6 +333,7 @@
name = "timed_thread_notification_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
+ "//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_thread_notification"],
"//conditions:default": ["//pw_sync:binary_semaphore_timed_thread_notification_backend"],
}),
)
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index 6e1adb4..e30c01f 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -33,8 +33,9 @@
relevant
`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_ C++
named requirements. This means that they are compatible with existing helpers in
-the STL's ``<mutex>`` thread support library. For example `std::lock_guard <https://en.cppreference.com/w/cpp/thread/lock_guard>`_
-and `std::unique_lock <https://en.cppreference.com/w/cpp/thread/unique_lock>`_ can be directly used.
+the STL's ``<mutex>`` thread support library. For example `std::lock_guard
+<https://en.cppreference.com/w/cpp/thread/lock_guard>`_ and `std::unique_lock
+<https://en.cppreference.com/w/cpp/thread/unique_lock>`_ can be directly used.
Mutex
=====
@@ -186,21 +187,25 @@
TimedMutex
==========
-The TimedMutex is an extension of the Mutex which offers timeout and deadline
-based semantics.
+.. cpp:namespace-push:: pw::sync
-The TimedMutex's API is C++11 STL
+The :cpp:class:`TimedMutex` is an extension of the Mutex which offers timeout
+and deadline based semantics.
+
+The :cpp:class:`TimedMutex`'s API is C++11 STL
`std::timed_mutex <https://en.cppreference.com/w/cpp/thread/timed_mutex>`_ like,
meaning it is a
`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_,
`Lockable <https://en.cppreference.com/w/cpp/named_req/Lockable>`_, and
`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_.
-Note that the ``TimedMutex`` is a derived ``Mutex`` class, meaning that
-a ``TimedMutex`` can be used by someone who needs the basic ``Mutex``. This is
-in contrast to the C++ STL's
+Note that the :cpp:class:`TimedMutex` is a derived :cpp:class:`Mutex` class,
+meaning that a :cpp:class:`TimedMutex` can be used by someone who needs the
+basic :cpp:class:`Mutex`. This is in contrast to the C++ STL's
`std::timed_mutex <https://en.cppreference.com/w/cpp/thread/timed_mutex>`_.
+.. cpp:namespace-pop::
+
.. list-table::
:header-rows: 1
@@ -1028,19 +1033,22 @@
This simpler but highly portable class of signaling primitives is intended to
ensure that a portability efficiency tradeoff does not have to be made up front.
Today this is class of simpler signaling primitives is limited to the
-``pw::sync::ThreadNotification`` and ``pw::sync::TimedThreadNotification``.
+:cpp:class:`pw::sync::ThreadNotification` and
+:cpp:class:`pw::sync::TimedThreadNotification`.
ThreadNotification
==================
-The ThreadNotification is a synchronization primitive that can be used to
+.. cpp:namespace-push:: pw::sync
+
+The :cpp:class:`ThreadNotification` is a synchronization primitive that can be used to
permit a SINGLE thread to block and consume a latching, saturating
notification from multiple notifiers.
.. Note::
- Although only a single thread can block on a ThreadNotification at a time,
- many instances may be used by a single thread just like binary semaphores.
- This is in contrast to some native RTOS APIs, such as direct task
- notifications, which re-use the same state within a thread's context.
+ Although only a single thread can block on a :cpp:class:`ThreadNotification`
+ at a time, many instances may be used by a single thread just like binary
+ semaphores. This is in contrast to some native RTOS APIs, such as direct
+ task notifications, which re-use the same state within a thread's context.
.. Warning::
This is a single consumer/waiter, multiple producer/notifier API!
@@ -1048,7 +1056,7 @@
result, having multiple threads receiving notifications via the acquire API
is unsupported.
-This is effectively a subset of the ``pw::sync::BinarySemaphore`` API, except
+This is effectively a subset of the :cpp:class:`BinarySemaphore` API, except
that only a single thread can be notified and block at a time.
The single consumer aspect of the API permits the use of a smaller and/or
@@ -1057,13 +1065,17 @@
whether that is a semaphore, event flag group, condition variable, or something
else.
-The ThreadNotification is initialized to being empty (latch is not set).
+The :cpp:class:`ThreadNotification` is initialized to being empty (latch is not
+set).
+
+.. cpp:namespace-pop::
Generic BinarySemaphore-based Backend
-------------------------------------
-This module provides a generic backend for ``pw::sync::ThreadNotification`` via
+This module provides a generic backend for
+:cpp:class:`pw::sync::ThreadNotification` via
``pw_sync:binary_semaphore_thread_notification`` which uses a
-``pw::sync::BinarySemaphore`` as the backing primitive. See
+:cpp:class:`pw::sync::BinarySemaphore` as the backing primitive. See
:ref:`BinarySemaphore <module-pw_sync-binary-semaphore>` for backend
availability.
@@ -1157,10 +1169,12 @@
TimedThreadNotification
=======================
-The TimedThreadNotification is an extension of the ThreadNotification which
-offers timeout and deadline based semantics.
+The :cpp:class:`TimedThreadNotification` is an extension of the
+:cpp:class:`ThreadNotification` which offers timeout and deadline based
+semantics.
-The TimedThreadNotification is initialized to being empty (latch is not set).
+The :cpp:class:`TimedThreadNotification` is initialized to being empty (latch is
+not set).
.. Warning::
This is a single consumer/waiter, multiple producer/notifier API! The
@@ -1170,9 +1184,10 @@
Generic BinarySemaphore-based Backend
-------------------------------------
-This module provides a generic backend for ``pw::sync::TimedThreadNotification``
-via ``pw_sync:binary_semaphore_timed_thread_notification`` which uses a
-``pw::sync::BinarySemaphore`` as the backing primitive. See
+This module provides a generic backend for
+:cpp:class:`pw::sync::TimedThreadNotification` via
+``pw_sync:binary_semaphore_timed_thread_notification`` which uses a
+:cpp:class:`pw::sync::BinarySemaphore` as the backing primitive. See
:ref:`BinarySemaphore <module-pw_sync-binary-semaphore>` for backend
availability.
@@ -1274,22 +1289,27 @@
CountingSemaphore
=================
-The CountingSemaphore is a synchronization primitive that can be used for
-counting events and/or resource management where receiver(s) can block on
-acquire until notifier(s) signal by invoking release.
+.. cpp:namespace-push:: pw::sync
-Note that unlike Mutexes, priority inheritance is not used by semaphores meaning
-semaphores are subject to unbounded priority inversions. Due to this, Pigweed
-does not recommend semaphores for mutual exclusion.
+The :cpp:class:`CountingSemaphore` is a synchronization primitive that can be
+used for counting events and/or resource management where receiver(s) can block
+on acquire until notifier(s) signal by invoking release.
-The CountingSemaphore is initialized to being empty or having no tokens.
+Note that unlike :cpp:class:`Mutex`, priority inheritance is not used by
+semaphores meaning semaphores are subject to unbounded priority inversions. Due
+to this, Pigweed does not recommend semaphores for mutual exclusion.
+
+The :cpp:class:`CountingSemaphore` is initialized to being empty or having no
+tokens.
The entire API is thread safe, but only a subset is interrupt safe.
.. Note::
If there is only a single consuming thread, we recommend using a
- ThreadNotification instead which can be much more efficient on some RTOSes
- such as FreeRTOS.
+ :cpp:class:`ThreadNotification` instead which can be much more efficient on
+ some RTOSes such as FreeRTOS.
+
+.. cpp:namespace-pop::
.. Warning::
Releasing multiple tokens is often not natively supported, meaning you may
@@ -1407,14 +1427,19 @@
BinarySemaphore
===============
-BinarySemaphore is a specialization of CountingSemaphore with an arbitrary token
-limit of 1. Note that that ``max()`` is >= 1, meaning it may be released up to
-``max()`` times but only acquired once for those N releases.
+.. cpp:namespace-push:: pw::sync
-Implementations of BinarySemaphore are typically more efficient than the
-default implementation of CountingSemaphore.
+:cpp:class:`BinarySemaphore` is a specialization of CountingSemaphore with an
+arbitrary token limit of 1. Note that that ``max()`` is >= 1, meaning it may be
+released up to ``max()`` times but only acquired once for those N releases.
-The BinarySemaphore is initialized to being empty or having no tokens.
+Implementations of :cpp:class:`BinarySemaphore` are typically more
+efficient than the default implementation of :cpp:class:`CountingSemaphore`.
+
+The :cpp:class:`BinarySemaphore` is initialized to being empty or having no
+tokens.
+
+.. cpp:namespace-pop::
The entire API is thread safe, but only a subset is interrupt safe.
@@ -1523,7 +1548,8 @@
Conditional Variables
=====================
-``pw::sync::ConditionVariable`` provides a condition variable implementation
-that provides semantics and an API very similar to `std::condition_variable
+:cpp:class:`pw::sync::ConditionVariable` provides a condition variable
+implementation that provides semantics and an API very similar to
+`std::condition_variable
<https://en.cppreference.com/w/cpp/thread/condition_variable>`_ in the C++
Standard Library.
diff --git a/pw_sync/public/pw_sync/binary_semaphore.h b/pw_sync/public/pw_sync/binary_semaphore.h
index 8ae96a0..c3e025d 100644
--- a/pw_sync/public/pw_sync/binary_semaphore.h
+++ b/pw_sync/public/pw_sync/binary_semaphore.h
@@ -25,13 +25,11 @@
namespace pw::sync {
-/// @class BinarySemaphore
-///
-/// BinarySemaphore is a specialization of CountingSemaphore with an arbitrary
-/// token limit of 1. Note that that max() is >= 1, meaning it may be
-/// released up to max() times but only acquired once for those N releases.
-/// Implementations of BinarySemaphore are typically more efficient than the
-/// default implementation of CountingSemaphore. The entire API is thread safe
+/// `BinarySemaphore` is a specialization of `CountingSemaphore` with an
+/// arbitrary token limit of 1. Note that that max() is >= 1, meaning it may be
+/// released up to `max()` times but only acquired once for those `N` releases.
+/// Implementations of `BinarySemaphore` are typically more efficient than the
+/// default implementation of `CountingSemaphore`. The entire API is thread safe
/// but only a subset is IRQ safe.
///
/// WARNING: In order to support global statically constructed BinarySemaphores,
@@ -39,7 +37,7 @@
/// environment is done prior to the creation and/or initialization of the
/// native synchronization primitives (e.g. kernel initialization).
///
-/// The BinarySemaphore is initialized to being empty or having no tokens.
+/// The `BinarySemaphore` is initialized to being empty or having no tokens.
class BinarySemaphore {
public:
using native_handle_type = backend::NativeBinarySemaphoreHandle;
diff --git a/pw_sync/public/pw_sync/borrow.h b/pw_sync/public/pw_sync/borrow.h
index cdb1c4e..204b888 100644
--- a/pw_sync/public/pw_sync/borrow.h
+++ b/pw_sync/public/pw_sync/borrow.h
@@ -23,9 +23,7 @@
namespace pw::sync {
-/// @class BorrowedPointer
-///
-/// The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed
+/// The `BorrowedPointer` is an RAII handle which wraps a pointer to a borrowed
/// object along with a held lock which is guarding the object. When destroyed,
/// the lock is released.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
@@ -73,7 +71,7 @@
///
/// @rst
/// .. note::
- /// The member of pointer member access operator, operator->(), is
+ /// The member of pointer member access operator, ``operator->()``, is
/// recommended over this API as this is prone to leaking references.
/// However, this is sometimes necessary.
///
@@ -103,16 +101,14 @@
GuardedType* object_;
};
-/// @class Borrowable
-///
-/// The Borrowable is a helper construct that enables callers to borrow an
+/// The `Borrowable` is a helper construct that enables callers to borrow an
/// object which is guarded by a lock.
///
/// Users who need access to the guarded object can ask to acquire a
-/// BorrowedPointer which permits access while the lock is held.
+/// `BorrowedPointer` which permits access while the lock is held.
///
-/// This class is compatible with locks which comply with BasicLockable,
-/// Lockable, and TimedLockable C++ named requirements.
+/// This class is compatible with locks which comply with `BasicLockable`,
+/// `Lockable`, and `TimedLockable` C++ named requirements.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable {
public:
@@ -141,7 +137,7 @@
/// Tries to borrow the object. Blocks until the specified timeout has elapsed
/// or the object has been borrowed, whichever comes first. Returns a
- /// BorrowedPointer on success, otherwise `std::nullopt` (nothing).
+ /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
template <class Rep, class Period>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(
std::chrono::duration<Rep, Period> timeout) {
@@ -153,7 +149,7 @@
/// Tries to borrow the object. Blocks until the specified deadline has passed
/// or the object has been borrowed, whichever comes first. Returns a
- /// BorrowedPointer on success, otherwise `std::nullopt` (nothing).
+ /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
template <class Clock, class Duration>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(
std::chrono::time_point<Clock, Duration> deadline) {
diff --git a/pw_sync/public/pw_sync/counting_semaphore.h b/pw_sync/public/pw_sync/counting_semaphore.h
index ec54556..b0ab876 100644
--- a/pw_sync/public/pw_sync/counting_semaphore.h
+++ b/pw_sync/public/pw_sync/counting_semaphore.h
@@ -25,9 +25,7 @@
namespace pw::sync {
-/// @class CountingSemaphore
-///
-/// The CountingSemaphore is a synchronization primitive that can be used for
+/// The `CountingSemaphore` is a synchronization primitive that can be used for
/// counting events and/or resource management where receiver(s) can block on
/// acquire until notifier(s) signal by invoking release.
/// Note that unlike Mutexes, priority inheritance is not used by semaphores
@@ -37,13 +35,13 @@
///
/// @rst
/// .. WARNING::
-/// In order to support global statically constructed CountingSemaphores the
-/// user and/or backend MUST ensure that any initialization required in your
-/// environment is done prior to the creation and/or initialization of the
-/// native synchronization primitives (e.g. kernel initialization).
-///
-/// The CountingSemaphore is initialized to being empty or having no tokens.
+/// In order to support global statically constructed ``CountingSemaphores``
+/// the user and/or backend MUST ensure that any initialization required in
+/// your environment is done prior to the creation and/or initialization of
+/// the native synchronization primitives (e.g. kernel initialization).
/// @endrst
+///
+/// The `CountingSemaphore` is initialized to being empty or having no tokens.
class CountingSemaphore {
public:
using native_handle_type = backend::NativeCountingSemaphoreHandle;
diff --git a/pw_sync/public/pw_sync/inline_borrowable.h b/pw_sync/public/pw_sync/inline_borrowable.h
index 546baf8..0a423f4 100644
--- a/pw_sync/public/pw_sync/inline_borrowable.h
+++ b/pw_sync/public/pw_sync/inline_borrowable.h
@@ -23,14 +23,12 @@
namespace pw::sync {
-/// @class InlineBorrowable
-///
-/// InlineBorrowable holds an object of GuardedType and a Lock that guards
+/// `InlineBorrowable` holds an object of `GuardedType` and a Lock that guards
/// access to the object. It should be used when an object should be guarded for
/// its entire lifecycle by a single lock.
///
/// This object should be shared with other componetns as a reference of type
-/// Borrowable<GuardedType, LockInterface>.
+/// `Borrowable<GuardedType, LockInterface>`.
///
template <typename GuardedType,
typename Lock = pw::sync::VirtualMutex,
diff --git a/pw_sync/public/pw_sync/interrupt_spin_lock.h b/pw_sync/public/pw_sync/interrupt_spin_lock.h
index 01f93d8..7726c92 100644
--- a/pw_sync/public/pw_sync/interrupt_spin_lock.h
+++ b/pw_sync/public/pw_sync/interrupt_spin_lock.h
@@ -25,16 +25,14 @@
namespace pw::sync {
-/// @class InterruptSpinLock
-///
-/// The InterruptSpinLock is a synchronization primitive that can be used to
+/// The `InterruptSpinLock` is a synchronization primitive that can be used to
/// protect shared data from being simultaneously accessed by multiple threads
/// and/or interrupts as a targeted global lock, with the exception of
/// Non-Maskable Interrupts (NMIs).
/// It offers exclusive, non-recursive ownership semantics where IRQs up to a
/// backend defined level of "NMIs" will be masked to solve priority-inversion.
///
-/// @note This InterruptSpinLock relies on built-in local interrupt masking to
+/// @note This `InterruptSpinLock` relies on built-in local interrupt masking to
/// make it interrupt safe without requiring the caller to separately mask
/// and unmask interrupts when using this primitive.
///
@@ -45,7 +43,7 @@
///
/// This entire API is IRQ safe, but NOT NMI safe.
///
-/// @b Precondition: Code that holds a specific InterruptSpinLock must not try
+/// @b Precondition: Code that holds a specific `InterruptSpinLock` must not try
/// to re-acquire it. However, it is okay to nest distinct spinlocks.
class PW_LOCKABLE("pw::sync::InterruptSpinLock") InterruptSpinLock {
public:
@@ -124,22 +122,16 @@
PW_EXTERN_C_START
-/// @fn pw_sync_InterruptSpinLock_Lock
-///
/// Invokes the `InterruptSpinLock::lock` member function on the given
/// `interrupt_spin_lock`.
void pw_sync_InterruptSpinLock_Lock(pw_sync_InterruptSpinLock* spin_lock)
PW_NO_LOCK_SAFETY_ANALYSIS;
-/// @fn pw_sync_InterruptSpinLock_TryLock
-///
/// Invokes the `InterruptSpinLock::try_lock` member function on the given
/// `interrupt_spin_lock`.
bool pw_sync_InterruptSpinLock_TryLock(pw_sync_InterruptSpinLock* spin_lock)
PW_NO_LOCK_SAFETY_ANALYSIS;
-/// @fn pw_sync_InterruptSpinLock_Unlock
-///
/// Invokes the `InterruptSpinLock::unlock` member function on the given
/// `interrupt_spin_lock`.
void pw_sync_InterruptSpinLock_Unlock(pw_sync_InterruptSpinLock* spin_lock)
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index f1137b0..5b42468 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -25,13 +25,11 @@
namespace pw::sync {
-/// @class Mutex
-///
-/// The Mutex is a synchronization primitive that can be used to protect shared
-/// data from being simultaneously accessed by multiple threads. It offers
-/// exclusive, non-recursive ownership semantics where priority inheritance is
-/// used to solve the classic priority-inversion problem. This is thread safe,
-/// but NOT IRQ safe.
+/// The `Mutex` is a synchronization primitive that can be used to protect
+/// shared data from being simultaneously accessed by multiple threads. It
+/// offers exclusive, non-recursive ownership semantics where priority
+/// inheritance is used to solve the classic priority-inversion problem. This
+/// is thread safe, but NOT IRQ safe.
///
/// @rst
/// .. warning::
@@ -129,18 +127,12 @@
PW_EXTERN_C_START
-/// @fn pw_sync_Mutex_Lock
-///
/// Invokes the `Mutex::lock` member function on the given `mutex`.
void pw_sync_Mutex_Lock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
-/// @fn pw_sync_Mutex_TryLock
-///
/// Invokes the `Mutex::try_lock` member function on the given `mutex`.
bool pw_sync_Mutex_TryLock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
-/// @fn pw_sync_Mutex_Unlock
-///
/// Invokes the `Mutex::unlock` member function on the given `mutex`.
void pw_sync_Mutex_Unlock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
diff --git a/pw_sync/public/pw_sync/thread_notification.h b/pw_sync/public/pw_sync/thread_notification.h
index e82ed77..85e6e1b 100644
--- a/pw_sync/public/pw_sync/thread_notification.h
+++ b/pw_sync/public/pw_sync/thread_notification.h
@@ -17,9 +17,7 @@
namespace pw::sync {
-/// @class ThreadNotification
-///
-/// The ThreadNotification is a synchronization primitive that can be used to
+/// The `ThreadNotification` is a synchronization primitive that can be used to
/// permit a SINGLE thread to block and consume a latching, saturating
/// notification from multiple notifiers.
///
@@ -34,7 +32,7 @@
/// The single consumer aspect of the API permits the use of a smaller and/or
/// faster native APIs such as direct thread signaling.
///
-/// The ThreadNotification is initialized to being empty (latch is not set).
+/// The `ThreadNotification` is initialized to being empty (latch is not set).
class ThreadNotification {
public:
using native_handle_type = backend::NativeThreadNotificationHandle;
diff --git a/pw_sync/public/pw_sync/timed_mutex.h b/pw_sync/public/pw_sync/timed_mutex.h
index 0640b37..abb48c1 100644
--- a/pw_sync/public/pw_sync/timed_mutex.h
+++ b/pw_sync/public/pw_sync/timed_mutex.h
@@ -26,13 +26,11 @@
namespace pw::sync {
-/// @class TimedMutex
-///
-/// The TimedMutex is a synchronization primitive that can be used to protect
+/// The `TimedMutex` is a synchronization primitive that can be used to protect
/// shared data from being simultaneously accessed by multiple threads with
-/// timeouts and deadlines, extending the Mutex. It offers exclusive,
+/// timeouts and deadlines, extending the `Mutex`. It offers exclusive,
/// non-recursive ownership semantics where priority inheritance is used to
-/// solve the classic priority-inversion problem. This is thread safe, but NOT
+/// solve the classic priority-inversion problem. This is thread safe, but NOT
/// IRQ safe.
///
/// @rst
diff --git a/pw_sync/public/pw_sync/timed_thread_notification.h b/pw_sync/public/pw_sync/timed_thread_notification.h
index 945c0e0..7b98b39 100644
--- a/pw_sync/public/pw_sync/timed_thread_notification.h
+++ b/pw_sync/public/pw_sync/timed_thread_notification.h
@@ -18,10 +18,8 @@
namespace pw::sync {
-/// @class TimedThreadNotification
-///
-/// The TimedThreadNotification is a synchronization primitive that can be used
-/// to permit a SINGLE thread to block and consume a latching, saturating
+/// The `TimedThreadNotification` is a synchronization primitive that can be
+/// used to permit a SINGLE thread to block and consume a latching, saturating
/// notification from multiple notifiers.
///
/// @b IMPORTANT: This is a single consumer/waiter, multiple producer/notifier
@@ -35,7 +33,7 @@
/// The single consumer aspect of the API permits the use of a smaller and/or
/// faster native APIs such as direct thread signaling.
///
-/// The TimedThreadNotification is initialized to being empty (latch is not
+/// The `TimedThreadNotification` is initialized to being empty (latch is not
/// set).
class TimedThreadNotification : public ThreadNotification {
public:
diff --git a/pw_sync/public/pw_sync/virtual_basic_lockable.h b/pw_sync/public/pw_sync/virtual_basic_lockable.h
index 3a262b2..5010365 100644
--- a/pw_sync/public/pw_sync/virtual_basic_lockable.h
+++ b/pw_sync/public/pw_sync/virtual_basic_lockable.h
@@ -18,10 +18,8 @@
namespace pw::sync {
-/// @class VirtualBasicLockable
-///
-/// The VirtualBasicLockable is a virtual lock abstraction for locks which meet
-/// the C++ named BasicLockable requirements of lock() and unlock().
+/// The `VirtualBasicLockable` is a virtual lock abstraction for locks which
+/// meet the C++ named BasicLockable requirements of lock() and unlock().
///
/// This virtual indirection is useful in case you need configurable lock
/// selection in a portable module where the final type is not defined upstream
@@ -46,14 +44,12 @@
private:
/// Uses a single virtual method with an enum to minimize the vtable cost per
- /// implementation of VirtualBasicLockable.
+ /// implementation of `VirtualBasicLockable`.
virtual void DoLockOperation(Operation operation) = 0;
};
-/// @class NoOpLock
-///
-/// The NoOpLock is a type of VirtualBasicLockable that does nothing, i.e. lock
-/// operations are no-ops.
+/// The `NoOpLock` is a type of `VirtualBasicLockable` that does nothing, i.e.
+/// lock operations are no-ops.
class PW_LOCKABLE("pw::sync::NoOpLock") NoOpLock final
: public VirtualBasicLockable {
public:
diff --git a/pw_sync_baremetal/BUILD.bazel b/pw_sync_baremetal/BUILD.bazel
index a137c69..d401d99 100644
--- a/pw_sync_baremetal/BUILD.bazel
+++ b/pw_sync_baremetal/BUILD.bazel
@@ -85,9 +85,9 @@
"public_overrides",
],
target_compatible_with = ["@platforms//os:none"],
- visibility = ["//visibility:private"],
+ visibility = ["//pw_sync:__pkg__"],
deps = [
"//pw_assert",
- "//pw_sync:recursive_mutex",
+ "//pw_sync:recursive_mutex_facade",
],
)
diff --git a/pw_sync_freertos/BUILD.bazel b/pw_sync_freertos/BUILD.bazel
index 5609b5f..1be8b9f 100644
--- a/pw_sync_freertos/BUILD.bazel
+++ b/pw_sync_freertos/BUILD.bazel
@@ -37,11 +37,10 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "@freertos",
],
)
@@ -77,12 +76,11 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
- "//pw_sync:counting_semaphore_facade",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "//pw_sync:counting_semaphore_facade",
+ "@freertos",
],
)
@@ -91,9 +89,17 @@
srcs = [
"counting_semaphore.cc",
],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
+ target_compatible_with = select({
+ # Not compatible with this FreeRTOS config, because it does not enable
+ # FreeRTOS counting semaphores. We mark it explicitly incompatible to
+ # that this library is skipped when you
+ # `bazel build //pw_sync_freertos/...` for a platform using that
+ # config.
+ "//targets/stm32f429i_disc1_stm32cube:freertos_config_cv": ["@platforms//:incompatible"],
+ "//conditions:default": [
+ "//pw_build/constraints/rtos:freertos",
+ ],
+ }),
deps = [
":counting_semaphore_headers",
"//pw_assert",
@@ -118,9 +124,9 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
+ "//pw_interrupt:context",
+ "@freertos",
],
)
@@ -153,12 +159,11 @@
],
deps = [
"//pw_assert",
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
"//pw_interrupt:context",
"//pw_polyfill",
"//pw_sync:interrupt_spin_lock",
"//pw_sync:lock_annotations",
+ "@freertos",
],
)
@@ -192,10 +197,9 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_thread_notification_facade",
+ "@freertos",
],
)
@@ -230,10 +234,9 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_mutex_facade",
+ "@freertos",
],
)
@@ -246,6 +249,7 @@
"//pw_build/constraints/rtos:freertos",
],
deps = [
+ ":mutex_headers",
":timed_mutex_headers",
"//pw_assert",
"//pw_chrono_freertos:system_clock_headers",
@@ -269,8 +273,9 @@
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ deps = [
+ "@freertos",
+ ],
)
pw_cc_library(
diff --git a/pw_system/BUILD.bazel b/pw_system/BUILD.bazel
index a54b21b..d9fb2e9 100644
--- a/pw_system/BUILD.bazel
+++ b/pw_system/BUILD.bazel
@@ -263,26 +263,18 @@
],
)
-# TODO(b/234877642): This is broken out into a separate pw_cc_library target as
-# a workaround for pw_cc_binary not supporting `select` in its deps.
-pw_cc_library(
- name = "boot",
- deps = select({
- "//pw_build/constraints/rtos:freertos": [],
- "//conditions:default": ["//targets/host_device_simulator:boot"],
- }),
-)
-
pw_cc_binary(
name = "system_example",
srcs = ["example_user_app_init.cc"],
deps = [
- ":boot",
":init",
":io",
":target_hooks",
"//pw_stream",
"//pw_stream:sys_io_stream",
"//pw_unit_test:rpc_service",
- ],
+ ] + select({
+ "//pw_build/constraints/rtos:freertos": [],
+ "//conditions:default": ["//targets/host_device_simulator:boot"],
+ }),
)
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py
index 80fd2c3..4995088 100644
--- a/pw_system/py/pw_system/console.py
+++ b/pw_system/py/pw_system/console.py
@@ -51,7 +51,7 @@
)
import socket
-import serial # type: ignore
+import serial
import IPython # type: ignore
from pw_cli import log as pw_cli_log
diff --git a/pw_system/py/setup.cfg b/pw_system/py/setup.cfg
index 9db8b71..fea637c 100644
--- a/pw_system/py/setup.cfg
+++ b/pw_system/py/setup.cfg
@@ -23,6 +23,7 @@
zip_safe = False
install_requires =
pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.entry_points]
console_scripts = pw-system-console = pw_system.console:main
diff --git a/pw_system/socket_target_io.cc b/pw_system/socket_target_io.cc
index 55f1054..441de02 100644
--- a/pw_system/socket_target_io.cc
+++ b/pw_system/socket_target_io.cc
@@ -29,11 +29,15 @@
stream::SocketStream& GetStream() {
static bool running = false;
static std::mutex socket_open_lock;
+ static stream::ServerSocket server_socket;
static stream::SocketStream socket_stream;
std::lock_guard guard(socket_open_lock);
if (!running) {
printf("Awaiting connection on port %d\n", static_cast<int>(kPort));
- PW_CHECK_OK(socket_stream.Serve(kPort));
+ PW_CHECK_OK(server_socket.Listen(kPort));
+ auto accept_result = server_socket.Accept();
+ PW_CHECK_OK(accept_result.status());
+ socket_stream = *std::move(accept_result);
printf("Client connected\n");
running = true;
}
diff --git a/pw_thread/BUILD.bazel b/pw_thread/BUILD.bazel
index c61f095..c54848a 100644
--- a/pw_thread/BUILD.bazel
+++ b/pw_thread/BUILD.bazel
@@ -141,6 +141,7 @@
includes = ["public"],
deps = [
":id_facade",
+ ":thread_core",
],
)
diff --git a/pw_thread_freertos/BUILD.bazel b/pw_thread_freertos/BUILD.bazel
index 3cc1e93..c401c2a 100644
--- a/pw_thread_freertos/BUILD.bazel
+++ b/pw_thread_freertos/BUILD.bazel
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
load(
"//pw_build:pigweed.bzl",
"pw_cc_facade",
@@ -35,6 +36,10 @@
"id_public_overrides",
"public",
],
+ deps = [
+ "//pw_interrupt:context",
+ "@freertos",
+ ],
)
pw_cc_library(
@@ -46,8 +51,6 @@
":id_headers",
"//pw_thread:id_facade",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -78,10 +81,9 @@
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "//pw_thread:id",
"//pw_thread:sleep_facade",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
# This target provides the FreeRTOS specific headers needs for thread creation.
@@ -108,13 +110,8 @@
"//pw_assert",
"//pw_string",
"//pw_sync:binary_semaphore",
-
- # There's a circular dependency here, need to have the header part of
- # the facade visibile to this library.
- "//pw_thread:thread",
+ "//pw_thread:thread_facade",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -130,8 +127,6 @@
":thread_headers",
"//pw_assert",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -152,6 +147,8 @@
pw_cc_test(
name = "dynamic_thread_backend_test",
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
deps = [
":dynamic_test_threads",
"//pw_thread:thread_facade_test",
@@ -176,6 +173,8 @@
pw_cc_test(
name = "static_thread_backend_test",
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
deps = [
":static_test_threads",
"//pw_thread:thread_facade_test",
@@ -195,8 +194,9 @@
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ deps = [
+ "@freertos",
+ ],
)
pw_cc_library(
@@ -213,10 +213,6 @@
"pw_thread_freertos_private/thread_iteration.h",
"thread_iteration.cc",
],
- hdrs = [
- "public/pw_thread_freertos/thread_iteration.h",
- "public_overrides/pw_thread_backend/thread_iteration.h",
- ],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
@@ -226,10 +222,9 @@
"//pw_span",
"//pw_status",
"//pw_thread:thread_info",
+ "//pw_thread:thread_iteration_facade",
"//pw_thread_freertos:util",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_test(
@@ -238,9 +233,8 @@
"pw_thread_freertos_private/thread_iteration.h",
"thread_iteration_test.cc",
],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
deps = [
":freertos_tasktcb",
":static_test_threads",
@@ -255,8 +249,6 @@
"//pw_thread:thread_info",
"//pw_thread:thread_iteration",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -267,15 +259,16 @@
hdrs = [
"public/pw_thread_freertos/util.h",
],
+ includes = ["public"],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
deps = [
"//pw_function",
+ "//pw_log",
"//pw_status",
+ "@freertos",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -286,6 +279,11 @@
hdrs = [
"public/pw_thread_freertos/snapshot.h",
],
+ # TODO(b/269204725): Put this in the toolchain configuration instead. I
+ # would like to say `copts = ["-Wno-c++20-designator"]`, but arm-gcc tells
+ # me that's an "unrecognized command line option"; I think it may be a
+ # clang-only flag.
+ copts = ["-Wno-pedantic"],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
@@ -299,8 +297,6 @@
"//pw_thread:snapshot",
"//pw_thread:thread_cc.pwpb",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_facade(
@@ -312,13 +308,28 @@
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
name = "freertos_tasktcb",
+ hdrs = [
+ ":generate_freertos_tsktcb",
+ ],
+ includes = ["thread_public_overrides"],
deps = [
":freertos_tasktcb_facade",
],
)
+
+run_binary(
+ name = "generate_freertos_tsktcb",
+ srcs = [
+ "@freertos//:tasks.c",
+ ],
+ outs = [":thread_public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h"],
+ args = [
+ "--freertos-tasks-c=$(location @freertos//:tasks.c)",
+ "--output=$(location :thread_public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h)",
+ ],
+ tool = "//pw_thread_freertos/py:generate_freertos_tsktcb",
+)
diff --git a/pw_thread_freertos/py/BUILD.bazel b/pw_thread_freertos/py/BUILD.bazel
new file mode 100644
index 0000000..a6b3f45
--- /dev/null
+++ b/pw_thread_freertos/py/BUILD.bazel
@@ -0,0 +1,21 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Python utilities for code generation.
+
+py_binary(
+ name = "generate_freertos_tsktcb",
+ srcs = ["pw_thread_freertos/generate_freertos_tsktcb.py"],
+ visibility = ["//pw_thread_freertos:__subpackages__"],
+)
diff --git a/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py b/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
index 69a81a9..c37a376 100644
--- a/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
+++ b/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
@@ -20,7 +20,7 @@
import argparse
import re
import sys
-from typing import TextIO
+from typing import Optional, TextIO
from pathlib import Path
_GENERATED_HEADER = """\
@@ -40,7 +40,18 @@
parser.add_argument(
'--freertos-src-dir',
type=Path,
- help='Path to the FreeRTOS source directory.',
+ help=(
+ 'Path to the FreeRTOS source directory. Required unless'
+ ' --freertos-tasks-c is provided.'
+ ),
+ )
+ parser.add_argument(
+ '--freertos-tasks-c',
+ type=Path,
+ help=(
+ 'Path to the tasks.c file in the FreeRTOS source directory. '
+ 'Required unless --freertos-src-dir is provided.'
+ ),
)
parser.add_argument(
'--output',
@@ -62,8 +73,15 @@
raise ValueError('Could not find tskTCB struct in tasks.c')
-def _main(freertos_src_dir: Path, output: TextIO):
- with open(freertos_src_dir / 'tasks.c', 'r') as tasks_c:
+def _main(
+ freertos_src_dir: Optional[Path],
+ freertos_tasks_c: Optional[Path],
+ output: TextIO,
+):
+ if freertos_tasks_c is None or not freertos_tasks_c.is_file():
+ assert freertos_src_dir is not None
+ freertos_tasks_c = freertos_src_dir / 'tasks.c'
+ with open(freertos_tasks_c, 'r') as tasks_c:
output.write(_GENERATED_HEADER)
output.write(_extract_struct(tasks_c.read()))
diff --git a/pw_thread_zephyr/BUILD.gn b/pw_thread_zephyr/BUILD.gn
new file mode 100644
index 0000000..8cbd453
--- /dev/null
+++ b/pw_thread_zephyr/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_thread_zephyr/CMakeLists.txt b/pw_thread_zephyr/CMakeLists.txt
new file mode 100644
index 0000000..41e3573
--- /dev/null
+++ b/pw_thread_zephyr/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+pw_add_library(pw_thread_zephyr.sleep STATIC
+ HEADERS
+ public/pw_thread_zephyr/sleep_inline.h
+ sleep_public_overrides/pw_thread_backend/sleep_inline.h
+ PUBLIC_INCLUDES
+ public
+ sleep_public_overrides
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_thread.sleep.facade
+ SOURCES
+ sleep.cc
+ PRIVATE_DEPS
+ pw_chrono_zephyr.system_clock
+ pw_assert.check
+)
diff --git a/pw_thread_zephyr/Kconfig b/pw_thread_zephyr/Kconfig
new file mode 100644
index 0000000..65b9c3a
--- /dev/null
+++ b/pw_thread_zephyr/Kconfig
@@ -0,0 +1,18 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+config PIGWEED_THREAD_SLEEP
+ bool "Enabled the Zephyr pw_thread.sleep backend"
+ select PIGWEED_CHRONO_SYSTEM_CLOCK
+ select PIGWEED_ASSERT
diff --git a/pw_thread_zephyr/docs.rst b/pw_thread_zephyr/docs.rst
new file mode 100644
index 0000000..9608eb1
--- /dev/null
+++ b/pw_thread_zephyr/docs.rst
@@ -0,0 +1,8 @@
+.. _module-pw_thread_zephyr:
+
+----------------
+pw_thread_zephyr
+----------------
+This is a set of backends for pw_thread based on the Zephyr RTOS. Currently,
+only the pw_thread.sleep facade is implemented which is enabled via
+``CONFIG_PIGWEED_THREAD_SLEEP=y``.
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h b/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h
new file mode 100644
index 0000000..f7f9abd
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <zephyr/kernel.h>
+#include <zephyr/sys/util.h>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_thread/sleep.h"
+
+namespace pw::this_thread {
+
+inline void sleep_for(chrono::SystemClock::duration sleep_duration) {
+ sleep_until(chrono::SystemClock::TimePointAfterAtLeast(sleep_duration));
+}
+
+} // namespace pw::this_thread
diff --git a/pw_thread_zephyr/sleep.cc b/pw_thread_zephyr/sleep.cc
new file mode 100644
index 0000000..7a3c78d
--- /dev/null
+++ b/pw_thread_zephyr/sleep.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_thread/sleep.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_chrono_zephyr/system_clock_constants.h"
+
+using pw::chrono::SystemClock;
+
+namespace pw::this_thread {
+
+void sleep_until(SystemClock::time_point wakeup_time) {
+ SystemClock::time_point now = chrono::SystemClock::now();
+
+ // Check if the expiration deadline has already passed, yield.
+ if (wakeup_time <= now) {
+ k_yield();
+ return;
+ }
+
+ // The maximum amount of time we should sleep for in a single command.
+ constexpr chrono::SystemClock::duration kMaxTimeoutMinusOne =
+ pw::chrono::zephyr::kMaxTimeout - SystemClock::duration(1);
+
+ while (now < wakeup_time) {
+ // Sleep either the full remaining duration or the maximum timout
+ k_sleep(Z_TIMEOUT_TICKS(
+ std::min((wakeup_time - now).count(), kMaxTimeoutMinusOne.count())));
+
+ // Check how much time has passed, the scheduler can wake us up early.
+ now = SystemClock::now();
+ }
+}
+
+} // namespace pw::this_thread
diff --git a/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h
new file mode 100644
index 0000000..3dcf011
--- /dev/null
+++ b/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_thread_zephyr/sleep_inline.h"
diff --git a/pw_tokenizer/BUILD.bazel b/pw_tokenizer/BUILD.bazel
index b776067..26ce5dc 100644
--- a/pw_tokenizer/BUILD.bazel
+++ b/pw_tokenizer/BUILD.bazel
@@ -213,6 +213,15 @@
)
pw_cc_test(
+ name = "encode_args_test",
+ srcs = ["encode_args_test.cc"],
+ deps = [
+ ":pw_tokenizer",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "hash_test",
srcs = [
"hash_test.cc",
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index abc73bb..fc276f4 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -37,7 +37,11 @@
}
config("linker_script") {
- inputs = [ "pw_tokenizer_linker_sections.ld" ]
+ inputs = [
+ "pw_tokenizer_linker_sections.ld",
+ "pw_tokenizer_linker_rules.ld",
+ ]
+ lib_dirs = [ "." ]
# Automatically add the tokenizer linker sections when cross-compiling or
# building for Linux. macOS and Windows executables are not supported.
@@ -56,7 +60,6 @@
rebase_path("add_tokenizer_sections_to_default_script.ld",
root_build_dir),
]
- lib_dirs = [ "." ]
inputs += [ "add_tokenizer_sections_to_default_script.ld" ]
}
@@ -169,17 +172,25 @@
":argument_types_test",
":base64_test",
":decode_test",
- ":detokenize_fuzzer",
+ ":detokenize_fuzzer_test",
":detokenize_test",
+ ":encode_args_test",
":hash_test",
":simple_tokenize_test",
- ":token_database_fuzzer",
+ ":token_database_fuzzer_test",
":token_database_test",
":tokenize_test",
]
group_deps = [ "$dir_pw_preprocessor:tests" ]
}
+group("fuzzers") {
+ deps = [
+ ":detokenize_fuzzer",
+ ":token_database_fuzzer",
+ ]
+}
+
pw_test("argument_types_test") {
sources = [
"argument_types_test.cc",
@@ -226,6 +237,11 @@
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "arduino_executable"
}
+pw_test("encode_args_test") {
+ sources = [ "encode_args_test.cc" ]
+ deps = [ ":pw_tokenizer" ]
+}
+
pw_test("hash_test") {
sources = [
"hash_test.cc",
@@ -264,6 +280,7 @@
"$dir_pw_preprocessor",
dir_pw_span,
]
+ enable_test_if = false
}
pw_fuzzer("detokenize_fuzzer") {
diff --git a/pw_tokenizer/CMakeLists.txt b/pw_tokenizer/CMakeLists.txt
index b3d52bd..cd4a419 100644
--- a/pw_tokenizer/CMakeLists.txt
+++ b/pw_tokenizer/CMakeLists.txt
@@ -54,12 +54,15 @@
pw_varint
)
-if("${CMAKE_SYSTEM_NAME}" STREQUAL "")
+if(Zephyr_FOUND)
+ zephyr_linker_sources(SECTIONS "${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_linker_rules.ld")
+elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "")
target_link_options(pw_tokenizer
PUBLIC
"-T${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_linker_sections.ld"
+ "-L${CMAKE_CURRENT_SOURCE_DIR}"
)
-elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux" OR Zephyr_FOUND)
+elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
target_link_options(pw_tokenizer
PUBLIC
"-T${CMAKE_CURRENT_SOURCE_DIR}/add_tokenizer_sections_to_default_script.ld"
@@ -169,6 +172,16 @@
pw_tokenizer
)
+pw_add_test(pw_tokenizer.encode_args_test
+ SOURCES
+ encode_args_test.cc
+ PRIVATE_DEPS
+ pw_tokenizer
+ GROUPS
+ modules
+ pw_tokenizer
+)
+
pw_add_test(pw_tokenizer.hash_test
SOURCES
hash_test.cc
diff --git a/pw_tokenizer/argument_types_test.cc b/pw_tokenizer/argument_types_test.cc
index 696886e..10c3095 100644
--- a/pw_tokenizer/argument_types_test.cc
+++ b/pw_tokenizer/argument_types_test.cc
@@ -18,6 +18,7 @@
#include "gtest/gtest.h"
#include "pw_preprocessor/concat.h"
+#include "pw_tokenizer/tokenize.h"
#include "pw_tokenizer_private/argument_types_test.h"
namespace pw::tokenizer {
diff --git a/pw_tokenizer/argument_types_test_c.c b/pw_tokenizer/argument_types_test_c.c
index 4308fcb..aa2b52f 100644
--- a/pw_tokenizer/argument_types_test_c.c
+++ b/pw_tokenizer/argument_types_test_c.c
@@ -18,6 +18,7 @@
#include <assert.h>
#include <stddef.h>
+#include "pw_tokenizer/tokenize.h"
#include "pw_tokenizer_private/argument_types_test.h"
#ifdef __cplusplus
diff --git a/pw_tokenizer/database.gni b/pw_tokenizer/database.gni
index f5cc21b..a0cb024 100644
--- a/pw_tokenizer/database.gni
+++ b/pw_tokenizer/database.gni
@@ -94,13 +94,12 @@
pw_python_action(target_name) {
script = "$dir_pw_tokenizer/py/pw_tokenizer/database.py"
- # Restrict parallelism for updating this database file to one thread. This
- # makes it safe to update it from multiple toolchains.
- pool = "$dir_pw_tokenizer/pool:database($default_toolchain)"
-
inputs = _input_databases
if (_create == "") {
+ # Restrict parallelism for updating this database file to one thread. This
+ # makes it safe to update it from multiple toolchains.
+ pool = "$dir_pw_tokenizer/pool:database($default_toolchain)"
args = [ "add" ]
if (defined(invoker.commit)) {
args += [
diff --git a/pw_tokenizer/docs.rst b/pw_tokenizer/docs.rst
index dc4d66d..944f4cb 100644
--- a/pw_tokenizer/docs.rst
+++ b/pw_tokenizer/docs.rst
@@ -154,6 +154,11 @@
#include <pw_tokenizer/tokenize.h>
+.. note::
+ Zephyr handles the additional linker sections via
+ ``pw_tokenizer_linker_rules.ld`` which is added to the end of the linker file
+ via a call to ``zephyr_linker_sources(SECTIONS ...)``.
+
------------
Tokenization
------------
@@ -161,6 +166,8 @@
string, its arguments are encoded along with it. The results of tokenization can
be sent off device or stored in place of a full string.
+.. doxygentypedef:: pw_tokenizer_Token
+
Tokenization macros
===================
Adding tokenization to a project is simple. To tokenize a string, include
@@ -171,25 +178,9 @@
``pw_tokenizer`` provides macros for tokenizing string literals with no
arguments.
-.. c:macro:: PW_TOKENIZE_STRING(string_literal)
-
- Converts a string literal to a ``uint32_t`` token in a standalone statement.
- C and C++ compatible.
-
- .. code-block:: cpp
-
- constexpr uint32_t token = PW_TOKENIZE_STRING("Any string literal!");
-
-.. c:macro:: PW_TOKENIZE_STRING_DOMAIN(domain, string_literal)
-
- Tokenizes a string literal in a standalone statement using the specified
- :ref:`domain <module-pw_tokenizer-domains>`. C and C++ compatible.
-
-.. c:macro:: PW_TOKENIZE_STRING_MASK(domain, mask, string_literal)
-
- Tokenizes a string literal in a standalone stateemnt using the specified
- :ref:`domain <module-pw_tokenizer-domains>` and :ref:`bit mask
- <module-pw_tokenizer-masks>`. C and C++ compatible.
+.. doxygendefine:: PW_TOKENIZE_STRING
+.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN
+.. doxygendefine:: PW_TOKENIZE_STRING_MASK
The tokenization macros above cannot be used inside other expressions.
@@ -220,25 +211,9 @@
require C++ and cannot be assigned to constexpr variables or be used with
special function variables like ``__func__``.
-.. c:macro:: PW_TOKENIZE_STRING_EXPR(string_literal)
-
- Converts a string literal to a ``uint32_t`` token within an expression.
- Requires C++.
-
- .. code-block:: cpp
-
- DoSomething(PW_TOKENIZE_STRING_EXPR("Succeed"));
-
-.. c:macro:: PW_TOKENIZE_STRING_DOMAIN_EXPR(domain, string_literal)
-
- Tokenizes a string literal using the specified :ref:`domain
- <module-pw_tokenizer-domains>` within an expression. Requires C++.
-
-.. c:macro:: PW_TOKENIZE_STRING_MASK_EXPR(domain, mask, string_literal)
-
- Tokenizes a string literal using the specified :ref:`domain
- <module-pw_tokenizer-domains>` and :ref:`bit mask
- <module-pw_tokenizer-masks>` within an expression. Requires C++.
+.. doxygendefine:: PW_TOKENIZE_STRING_EXPR
+.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN_EXPR
+.. doxygendefine:: PW_TOKENIZE_STRING_MASK_EXPR
.. admonition:: When to use these macros
@@ -288,30 +263,16 @@
``pw_tokenizer`` provides two low-level macros for projects to use
to create custom tokenization macros.
-.. c:macro:: PW_TOKENIZE_FORMAT_STRING(domain, mask, format_string, ...)
-
- Tokenizes a format string and sets the ``_pw_tokenizer_token`` variable to the
- token. Must be used in its own scope, since the same variable is used in every
- invocation.
-
- The tokenized string uses the specified :ref:`tokenization domain
- <module-pw_tokenizer-domains>`. Use ``PW_TOKENIZER_DEFAULT_DOMAIN`` for the
- default. The token also may be masked; use ``UINT32_MAX`` to keep all bits.
-
-.. c:macro:: PW_TOKENIZER_ARG_TYPES(...)
-
- Converts a series of arguments to a compact format that replaces the format
- string literal. Evaluates to a ``pw_tokenizer_ArgTypes`` value.
+.. doxygendefine:: PW_TOKENIZE_FORMAT_STRING
+.. doxygendefine:: PW_TOKENIZER_ARG_TYPES
The outputs of these macros are typically passed to an encoding function. That
function encodes the token, argument types, and argument data to a buffer using
helpers provided by ``pw_tokenizer/encode_args.h``.
.. doxygenfunction:: pw::tokenizer::EncodeArgs
-
.. doxygenclass:: pw::tokenizer::EncodedMessage
:members:
-
.. doxygenfunction:: pw_tokenizer_EncodeArgs
Example
@@ -382,18 +343,9 @@
Tokenize a message with arguments to a buffer
---------------------------------------------
-.. c:macro:: PW_TOKENIZE_TO_BUFFER(buffer_pointer, buffer_size_pointer, format_string, arguments...)
-
- ``PW_TOKENIZE_TO_BUFFER`` encodes to a caller-provided buffer.
-
- .. code-block:: cpp
-
- uint8_t buffer[BUFFER_SIZE];
- size_t size_bytes = sizeof(buffer);
- PW_TOKENIZE_TO_BUFFER(buffer, &size_bytes, format_string_literal, arguments...);
-
- While ``PW_TOKENIZE_TO_BUFFER`` is very flexible, it must be passed a buffer,
- which increases its call site overhead.
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_DOMAIN
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_MASK
.. admonition:: Why use this macro
@@ -477,6 +429,10 @@
arguments short or avoid encoding them as strings (e.g. encode an enum as an
integer instead of a string). See also `Tokenized strings as %s arguments`_.
+Buffer sizing helper
+--------------------
+.. doxygenfunction:: pw::tokenizer::MinEncodingBufferSizeBytes
+
Encoding command line utility
-----------------------------
The ``pw_tokenizer.encode`` command line tool can be used to encode tokenized
diff --git a/pw_tokenizer/encode_args_test.cc b/pw_tokenizer/encode_args_test.cc
new file mode 100644
index 0000000..131f27b
--- /dev/null
+++ b/pw_tokenizer/encode_args_test.cc
@@ -0,0 +1,42 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_tokenizer/encode_args.h"
+
+#include "gtest/gtest.h"
+
+namespace pw {
+namespace tokenizer {
+
+static_assert(MinEncodingBufferSizeBytes<>() == 4);
+static_assert(MinEncodingBufferSizeBytes<bool>() == 4 + 2);
+static_assert(MinEncodingBufferSizeBytes<char>() == 4 + 2);
+static_assert(MinEncodingBufferSizeBytes<short>() == 4 + 3);
+static_assert(MinEncodingBufferSizeBytes<int>() == 4 + 5);
+static_assert(MinEncodingBufferSizeBytes<long long>() == 4 + 10);
+static_assert(MinEncodingBufferSizeBytes<float>() == 4 + 4);
+static_assert(MinEncodingBufferSizeBytes<double>() == 4 + 4);
+static_assert(MinEncodingBufferSizeBytes<const char*>() == 4 + 1);
+static_assert(MinEncodingBufferSizeBytes<void*>() == 4 + 5 ||
+ MinEncodingBufferSizeBytes<void*>() == 4 + 10);
+
+static_assert(MinEncodingBufferSizeBytes<int, double>() == 4 + 5 + 4);
+static_assert(MinEncodingBufferSizeBytes<int, int, const char*>() ==
+ 4 + 5 + 5 + 1);
+static_assert(
+ MinEncodingBufferSizeBytes<const char*, long long, int, short>() ==
+ 4 + 1 + 10 + 5 + 3);
+
+} // namespace tokenizer
+} // namespace pw
diff --git a/pw_tokenizer/public/pw_tokenizer/encode_args.h b/pw_tokenizer/public/pw_tokenizer/encode_args.h
index 43124e5..07e6e31 100644
--- a/pw_tokenizer/public/pw_tokenizer/encode_args.h
+++ b/pw_tokenizer/public/pw_tokenizer/encode_args.h
@@ -25,12 +25,52 @@
#include <cstring>
+#include "pw_polyfill/standard.h"
#include "pw_span/span.h"
#include "pw_tokenizer/config.h"
#include "pw_tokenizer/tokenize.h"
-namespace pw {
-namespace tokenizer {
+namespace pw::tokenizer {
+namespace internal {
+
+// Returns the maximum encoded size of an argument of the specified type.
+template <typename T>
+constexpr size_t ArgEncodedSizeBytes() {
+ constexpr pw_tokenizer_ArgTypes kType = VarargsType<T>();
+ if constexpr (kType == PW_TOKENIZER_ARG_TYPE_DOUBLE) {
+ return sizeof(float);
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_STRING) {
+ return 1; // Size of the length byte only
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_INT64) {
+ return 10; // Max size of a varint-encoded 64-bit integer
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_INT) {
+ return sizeof(T) + 1; // Max size of zig-zag varint integer <= 32-bits
+ } else {
+ static_assert(sizeof(T) != sizeof(T), "Unsupported argument type");
+ }
+}
+
+} // namespace internal
+
+/// Calculates the minimum buffer size to allocate that is guaranteed to support
+/// encoding the specified arguments.
+///
+/// The contents of strings are NOT included in this total. The string's
+/// length/status byte is guaranteed to fit, but the string contents may be
+/// truncated. Encoding is considered to succeed as long as the string's
+/// length/status byte is written, even if the actual string is truncated.
+///
+/// Examples:
+///
+/// - Message with no arguments:
+/// `MinEncodingBufferSizeBytes() == 4`
+/// - Message with an int argument
+/// `MinEncodingBufferSizeBytes<int>() == 9 (4 + 5)`
+template <typename... ArgTypes>
+constexpr size_t MinEncodingBufferSizeBytes() {
+ return (sizeof(pw_tokenizer_Token) + ... +
+ internal::ArgEncodedSizeBytes<ArgTypes>());
+}
/// Encodes a tokenized string's arguments to a buffer. The
/// @cpp_type{pw_tokenizer_ArgTypes} parameter specifies the argument types, in
@@ -97,8 +137,7 @@
size_t size_;
};
-} // namespace tokenizer
-} // namespace pw
+} // namespace pw::tokenizer
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
diff --git a/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h b/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
index 0fdf96b..64c9a4c 100644
--- a/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
+++ b/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
@@ -174,14 +174,4 @@
#endif // __cplusplus
-// Encodes the types of the provided arguments as a pw_tokenizer_ArgTypes
-// value. Depending on the size of pw_tokenizer_ArgTypes, the bottom 4 or 6
-// bits store the number of arguments and the remaining bits store the types,
-// two bits per type.
-//
-// The arguments are not evaluated; only their types are used to
-// select the set their corresponding PW_TOKENIZER_ARG_TYPEs.
-#define PW_TOKENIZER_ARG_TYPES(...) \
- PW_DELEGATE_BY_ARG_COUNT(_PW_TOKENIZER_TYPES_, __VA_ARGS__)
-
#define _PW_TOKENIZER_TYPES_0() ((pw_tokenizer_ArgTypes)0)
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize.h b/pw_tokenizer/public/pw_tokenizer/tokenize.h
index fd96715..6b8e62c 100644
--- a/pw_tokenizer/public/pw_tokenizer/tokenize.h
+++ b/pw_tokenizer/public/pw_tokenizer/tokenize.h
@@ -33,58 +33,54 @@
#include "pw_tokenizer/internal/argument_types.h"
#include "pw_tokenizer/internal/tokenize_string.h"
-// The type of the token used in place of a format string. Also available as
-// pw::tokenizer::Token.
+/// The type of the 32-bit token used in place of a string. Also available as
+/// `pw::tokenizer::Token`.
typedef uint32_t pw_tokenizer_Token;
-// Strings may optionally be tokenized to a domain. Strings in different domains
-// can be processed separately by the token database tools. Each domain in use
-// must have a corresponding section declared in the linker script. See
-// pw_tokenizer_linker_sections.ld for more details.
+// Strings may optionally be tokenized to a domain. Strings in different
+// domains can be processed separately by the token database tools. Each domain
+// in use must have a corresponding section declared in the linker script. See
+// `pw_tokenizer_linker_sections.ld` for more details.
//
// The default domain is an empty string.
#define PW_TOKENIZER_DEFAULT_DOMAIN ""
-// Tokenizes a string and converts it to a pw_tokenizer_Token. In C++, the
-// string may be a literal or a constexpr char array. In C, the argument must be
-// a string literal. In either case, the string must be null terminated, but may
-// contain any characters (including '\0').
-//
-// Two different versions are provided, PW_TOKENIZE_STRING and
-// PW_TOKENIZE_STRING_EXPR. PW_TOKENIZE_STRING can be assigned to a local or
-// global variable, including constexpr variables. PW_TOKENIZE_STRING can be
-// used with special function variables like __func__.
-//
-// PW_TOKENIZE_STRING_EXPR can be used inside an expression.
-// PW_TOKENIZE_STRING_EXPR is implemented using a lambda function, so it will
-// not work as expected with special function variables like __func__. It is
-// also only usable with C++.
-//
-// constexpr uint32_t global = PW_TOKENIZE_STRING("Wow!"); // This works.
-//
-// void SomeFunction() {
-// constexpr uint32_t token = PW_TOKENIZE_STRING("Cool!"); // This works.
-//
-// DoSomethingElse(PW_TOKENIZE_STRING("Lame!")); // This does NOT work.
-// DoSomethingElse(PW_TOKENIZE_STRING_EXPR("Yay!")); // This works.
-//
-// constexpr uint32_t token2 = PW_TOKENIZE_STRING(__func__); // This works.
-// DoSomethingElse(PW_TOKENIZE_STRING_EXPR(__func__)); // Does NOT work.
-// }
-//
+/// Converts a string literal to a `pw_tokenizer_Token` (`uint32_t`) token in a
+/// standalone statement. C and C++ compatible. In C++, the string may be a
+/// literal or a constexpr char array, including function variables like
+/// `__func__`. In C, the argument must be a string literal. In either case, the
+/// string must be null terminated, but may contain any characters (including
+/// '\0').
+///
+/// @code
+///
+/// constexpr uint32_t token = PW_TOKENIZE_STRING("Any string literal!");
+///
+/// @endcode
#define PW_TOKENIZE_STRING(string_literal) \
PW_TOKENIZE_STRING_DOMAIN(PW_TOKENIZER_DEFAULT_DOMAIN, string_literal)
+/// Converts a string literal to a ``uint32_t`` token within an expression.
+/// Requires C++.
+///
+/// @code
+///
+/// DoSomething(PW_TOKENIZE_STRING_EXPR("Succeed"));
+///
+/// @endcode
#define PW_TOKENIZE_STRING_EXPR(string_literal) \
[&] { \
constexpr uint32_t lambda_ret_token = PW_TOKENIZE_STRING(string_literal); \
return lambda_ret_token; \
}()
-// Same as PW_TOKENIZE_STRING, but tokenizes to the specified domain.
+/// Tokenizes a string literal in a standalone statement using the specified
+/// @rstref{domain <module-pw_tokenizer-domains>}. C and C++ compatible.
#define PW_TOKENIZE_STRING_DOMAIN(domain, string_literal) \
PW_TOKENIZE_STRING_MASK(domain, UINT32_MAX, string_literal)
+/// Tokenizes a string literal using the specified @rstref{domain
+/// <module-pw_tokenizer-domains>} within an expression. Requires C++.
#define PW_TOKENIZE_STRING_DOMAIN_EXPR(domain, string_literal) \
[&] { \
constexpr uint32_t lambda_ret_token = \
@@ -92,7 +88,9 @@
return lambda_ret_token; \
}()
-// Same as PW_TOKENIZE_STRING_DOMAIN, but applies a mask to the token.
+/// Tokenizes a string literal in a standalone statement using the specified
+/// @rstref{domain <module-pw_tokenizer-domains>} and @rstref{bit mask
+/// <module-pw_tokenizer-masks>}. C and C++ compatible.
#define PW_TOKENIZE_STRING_MASK(domain, mask, string_literal) \
/* assign to a variable */ _PW_TOKENIZER_MASK_TOKEN(mask, string_literal); \
\
@@ -102,6 +100,9 @@
_PW_TOKENIZER_RECORD_ORIGINAL_STRING( \
_PW_TOKENIZER_MASK_TOKEN(mask, string_literal), domain, string_literal)
+/// Tokenizes a string literal using the specified @rstref{domain
+/// <module-pw_tokenizer-domains>} and @rstref{bit mask
+/// <module-pw_tokenizer-masks>} within an expression. Requires C++.
#define PW_TOKENIZE_STRING_MASK_EXPR(domain, mask, string_literal) \
[&] { \
constexpr uint32_t lambda_ret_token = \
@@ -112,26 +113,36 @@
#define _PW_TOKENIZER_MASK_TOKEN(mask, string_literal) \
((pw_tokenizer_Token)(mask)&PW_TOKENIZER_STRING_TOKEN(string_literal))
-// Encodes a tokenized string and arguments to the provided buffer. The size of
-// the buffer is passed via a pointer to a size_t. After encoding is complete,
-// the size_t is set to the number of bytes written to the buffer.
-//
-// The macro's arguments are equivalent to the following function signature:
-//
-// TokenizeToBuffer(void* buffer,
-// size_t* buffer_size_pointer,
-// const char* format,
-// ...); /* printf-style arguments */
-//
-// For example, the following encodes a tokenized string with a temperature to a
-// buffer. The buffer is passed to a function to send the message over a UART.
-//
-// uint8_t buffer[32];
-// size_t size_bytes = sizeof(buffer);
-// PW_TOKENIZE_TO_BUFFER(
-// buffer, &size_bytes, "Temperature (C): %0.2f", temperature_c);
-// MyProject_EnqueueMessageForUart(buffer, size);
-//
+/// Encodes a tokenized string and arguments to the provided buffer. The size of
+/// the buffer is passed via a pointer to a `size_t`. After encoding is
+/// complete, the `size_t` is set to the number of bytes written to the buffer.
+///
+/// The macro's arguments are equivalent to the following function signature:
+///
+/// @code
+///
+/// TokenizeToBuffer(void* buffer,
+/// size_t* buffer_size_pointer,
+/// const char* format,
+/// ...); // printf-style arguments
+/// @endcode
+///
+/// For example, the following encodes a tokenized string with a temperature to
+/// a buffer. The buffer is passed to a function to send the message over a
+/// UART.
+///
+/// @code
+///
+/// uint8_t buffer[32];
+/// size_t size_bytes = sizeof(buffer);
+/// PW_TOKENIZE_TO_BUFFER(
+/// buffer, &size_bytes, "Temperature (C): %0.2f", temperature_c);
+/// MyProject_EnqueueMessageForUart(buffer, size);
+///
+/// @endcode
+///
+/// While `PW_TOKENIZE_TO_BUFFER` is very flexible, it must be passed a buffer,
+/// which increases its code size footprint at the call site.
#define PW_TOKENIZE_TO_BUFFER(buffer, buffer_size_pointer, format, ...) \
PW_TOKENIZE_TO_BUFFER_DOMAIN(PW_TOKENIZER_DEFAULT_DOMAIN, \
buffer, \
@@ -139,13 +150,15 @@
format, \
__VA_ARGS__)
-// Same as PW_TOKENIZE_TO_BUFFER, but tokenizes to the specified domain.
+/// Same as @c_macro{PW_TOKENIZE_TO_BUFFER}, but tokenizes to the specified
+/// @rstref{domain <module-pw_tokenizer-domains>}.
#define PW_TOKENIZE_TO_BUFFER_DOMAIN( \
domain, buffer, buffer_size_pointer, format, ...) \
PW_TOKENIZE_TO_BUFFER_MASK( \
domain, UINT32_MAX, buffer, buffer_size_pointer, format, __VA_ARGS__)
-// Same as PW_TOKENIZE_TO_BUFFER_DOMAIN, but applies a mask to the token.
+/// Same as @c_macro{PW_TOKENIZE_TO_BUFFER_DOMAIN}, but applies a
+/// @rstref{bit mask <module-pw_tokenizer-masks>} to the token.
#define PW_TOKENIZE_TO_BUFFER_MASK( \
domain, mask, buffer, buffer_size_pointer, format, ...) \
do { \
@@ -157,6 +170,15 @@
PW_COMMA_ARGS(__VA_ARGS__)); \
} while (0)
+/// Converts a series of arguments to a compact format that replaces the format
+/// string literal. Evaluates to a `pw_tokenizer_ArgTypes` value.
+///
+/// Depending on the size of `pw_tokenizer_ArgTypes`, the bottom 4 or 6 bits
+/// store the number of arguments and the remaining bits store the types, two
+/// bits per type. The arguments are not evaluated; only their types are used.
+#define PW_TOKENIZER_ARG_TYPES(...) \
+ PW_DELEGATE_BY_ARG_COUNT(_PW_TOKENIZER_TYPES_, __VA_ARGS__)
+
PW_EXTERN_C_START
// These functions encode the tokenized strings. These should not be called
@@ -177,12 +199,17 @@
PW_EXTERN_C_END
-// These macros implement string tokenization. They should not be used directly;
-// use one of the PW_TOKENIZE_* macros above instead.
-
-// This macro takes a printf-style format string and corresponding arguments. It
-// checks that the arguments are correct, stores the format string in a special
-// section, and calculates the string's token at compile time. This
+/// Tokenizes a format string with optional arguments and sets the
+/// `_pw_tokenizer_token` variable to the token. Must be used in its own scope,
+/// since the same variable is used in every invocation.
+///
+/// The tokenized string uses the specified @rstref{tokenization domain
+/// <module-pw_tokenizer-domains>}. Use `PW_TOKENIZER_DEFAULT_DOMAIN` for the
+/// default. The token also may be masked; use `UINT32_MAX` to keep all bits.
+///
+/// This macro checks that the printf-style format string matches the arguments,
+/// stores the format string in a special section, and calculates the string's
+/// token at compile time.
// clang-format off
#define PW_TOKENIZE_FORMAT_STRING(domain, mask, format, ...) \
if (0) { /* Do not execute to prevent double evaluation of the arguments. */ \
diff --git a/pw_tokenizer/pw_tokenizer_linker_rules.ld b/pw_tokenizer/pw_tokenizer_linker_rules.ld
new file mode 100644
index 0000000..d1f0aa7
--- /dev/null
+++ b/pw_tokenizer/pw_tokenizer_linker_rules.ld
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/*
+ * This file is separate from pw_tokenizer_linker_sections.ld because Zephyr
+ * already defines the top level SECTIONS label and requires new linker
+ * scripts to only add the individual sections.
+ */
+
+/*
+ * This section stores metadata that may be used during tokenized string
+ * decoding. This metadata describes properties that may affect how the
+ * tokenized string is encoded or decoded -- the maximum length of the hash
+ * function and the sizes of certain integer types.
+ *
+ * Metadata is declared as key-value pairs. See the metadata variable in
+ * tokenize.cc for further details.
+ */
+.pw_tokenizer.info 0x0 (INFO) :
+{
+ KEEP(*(.pw_tokenizer.info))
+}
+
+/*
+ * Tokenized string entries are stored in this section. Each entry contains
+ * the original string literal and the calculated token that represents it. In
+ * the compiled code, the token and a compact argument list encoded in a
+ * uint32_t are used in place of the format string. The compiled code
+ * contains no references to the tokenized string entries in this section.
+ *
+ * The tokenized string entry format is specified by the
+ * pw::tokenizer::internal::Entry class in
+ * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
+ *
+ * The section contents are declared with KEEP so that they are not removed
+ * from the ELF. These are never emitted in the final binary or loaded into
+ * memory.
+ */
+.pw_tokenizer.entries 0x0 (INFO) :
+{
+ KEEP(*(.pw_tokenizer.entries.*))
+}
diff --git a/pw_tokenizer/pw_tokenizer_linker_sections.ld b/pw_tokenizer/pw_tokenizer_linker_sections.ld
index ae17f47..a48a804 100644
--- a/pw_tokenizer/pw_tokenizer_linker_sections.ld
+++ b/pw_tokenizer/pw_tokenizer_linker_sections.ld
@@ -34,37 +34,5 @@
SECTIONS
{
- /*
- * This section stores metadata that may be used during tokenized string
- * decoding. This metadata describes properties that may affect how the
- * tokenized string is encoded or decoded -- the maximum length of the hash
- * function and the sizes of certain integer types.
- *
- * Metadata is declared as key-value pairs. See the metadata variable in
- * tokenize.cc for further details.
- */
- .pw_tokenizer.info 0x0 (INFO) :
- {
- KEEP(*(.pw_tokenizer.info))
- }
-
- /*
- * Tokenized string entries are stored in this section. Each entry contains
- * the original string literal and the calculated token that represents it. In
- * the compiled code, the token and a compact argument list encoded in a
- * uint32_t are used in place of the format string. The compiled code
- * contains no references to the tokenized string entries in this section.
- *
- * The tokenized string entry format is specified by the
- * pw::tokenizer::internal::Entry class in
- * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
- *
- * The section contents are declared with KEEP so that they are not removed
- * from the ELF. These are never emitted in the final binary or loaded into
- * memory.
- */
- .pw_tokenizer.entries 0x0 (INFO) :
- {
- KEEP(*(.pw_tokenizer.entries.*))
- }
+ INCLUDE pw_tokenizer_linker_rules.ld
}
diff --git a/pw_tokenizer/py/pw_tokenizer/database.py b/pw_tokenizer/py/pw_tokenizer/database.py
index 0931868..26a32a7 100755
--- a/pw_tokenizer/py/pw_tokenizer/database.py
+++ b/pw_tokenizer/py/pw_tokenizer/database.py
@@ -594,7 +594,7 @@
def replacement(value: str) -> Tuple[Pattern, 'str']:
try:
find, sub = unescaped_slash.split(value, 1)
- except ValueError as err:
+ except ValueError as _err:
raise argparse.ArgumentTypeError(
'replacements must be specified as "search_regex/replacement"'
)
diff --git a/pw_tokenizer/py/pw_tokenizer/decode.py b/pw_tokenizer/py/pw_tokenizer/decode.py
index 4263507..4796fb0 100644
--- a/pw_tokenizer/py/pw_tokenizer/decode.py
+++ b/pw_tokenizer/py/pw_tokenizer/decode.py
@@ -787,7 +787,7 @@
# Start with the part of the format string up to the first specifier.
string_pieces = [self.format_string[: spec_spans[0][0]]]
- for ((_, end1), (start2, _)) in zip(spec_spans[:-1], spec_spans[1:]):
+ for (_, end1), (start2, _) in zip(spec_spans[:-1], spec_spans[1:]):
string_pieces.append(self.format_string[end1:start2])
# Append the format string segment after the last format specifier.
diff --git a/pw_tokenizer/py/pw_tokenizer/detokenize.py b/pw_tokenizer/py/pw_tokenizer/detokenize.py
index fa60212..3aa7a3a 100755
--- a/pw_tokenizer/py/pw_tokenizer/detokenize.py
+++ b/pw_tokenizer/py/pw_tokenizer/detokenize.py
@@ -73,6 +73,8 @@
BASE64_PREFIX = encode.BASE64_PREFIX.encode()
DEFAULT_RECURSION = 9
+_RawIO = Union[io.RawIOBase, BinaryIO]
+
class DetokenizedString:
"""A detokenized string, with all results if there are collisions."""
@@ -264,7 +266,7 @@
def detokenize_base64_live(
self,
- input_file: BinaryIO,
+ input_file: _RawIO,
output: BinaryIO,
prefix: Union[str, bytes] = BASE64_PREFIX,
recursion: int = DEFAULT_RECURSION,
@@ -320,6 +322,7 @@
_PathOrStr = Union[Path, str]
+
# TODO(b/265334753): Reuse this function in database.py:LoadTokenDatabases
def _parse_domain(path: _PathOrStr) -> Tuple[Path, Optional[Pattern[str]]]:
"""Extracts an optional domain regex pattern suffix from a path"""
@@ -427,16 +430,14 @@
self.data = bytearray()
- def _read_next(self, fd: BinaryIO) -> Tuple[bytes, int]:
+ def _read_next(self, fd: _RawIO) -> Tuple[bytes, int]:
"""Returns the next character and its index."""
- char = fd.read(1)
+ char = fd.read(1) or b''
index = len(self.data)
self.data += char
return char, index
- def read_messages(
- self, binary_fd: BinaryIO
- ) -> Iterator[Tuple[bool, bytes]]:
+ def read_messages(self, binary_fd: _RawIO) -> Iterator[Tuple[bool, bytes]]:
"""Parses prefixed messages; yields (is_message, contents) chunks."""
message_start = None
@@ -463,7 +464,7 @@
yield False, char
def transform(
- self, binary_fd: BinaryIO, transform: Callable[[bytes], bytes]
+ self, binary_fd: _RawIO, transform: Callable[[bytes], bytes]
) -> Iterator[bytes]:
"""Yields the file with a transformation applied to the messages."""
for is_message, chunk in self.read_messages(binary_fd):
diff --git a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
index 793f95b..ab673a5 100644
--- a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
+++ b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
@@ -21,7 +21,7 @@
import sys
from typing import BinaryIO, Iterable
-import serial # type: ignore
+import serial
from pw_tokenizer import database, detokenize, tokens
@@ -80,7 +80,7 @@
def _detokenize_serial(
databases: Iterable,
- device: serial.Serial,
+ device: str,
baudrate: int,
show_errors: bool,
output: BinaryIO,
diff --git a/pw_tokenizer/py/setup.cfg b/pw_tokenizer/py/setup.cfg
index 99e075d..f7a20b7 100644
--- a/pw_tokenizer/py/setup.cfg
+++ b/pw_tokenizer/py/setup.cfg
@@ -21,6 +21,9 @@
[options]
packages = pw_tokenizer
zip_safe = False
+install_requires =
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.package_data]
pw_tokenizer = py.typed
diff --git a/pw_tokenizer/ts/detokenizer.ts b/pw_tokenizer/ts/detokenizer.ts
index 90bff2b..fe6ea91 100644
--- a/pw_tokenizer/ts/detokenizer.ts
+++ b/pw_tokenizer/ts/detokenizer.ts
@@ -115,7 +115,7 @@
data.byteOffset,
4
).getUint32(0, true);
- const args = new Uint8Array(data.buffer.slice(4));
+ const args = new Uint8Array(data.buffer.slice(data.byteOffset + 4));
return {token, args};
}
diff --git a/pw_tokenizer/ts/int_testdata.ts b/pw_tokenizer/ts/int_testdata.ts
new file mode 100644
index 0000000..36b5a21
--- /dev/null
+++ b/pw_tokenizer/ts/int_testdata.ts
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+const IntDB = [
+ ["%d", "-128", "%u", "4294967168", '\xff\x01'],
+ ["%d", "-10", "%u", "4294967286", '\x13'],
+ ["%d", "-9", "%u", "4294967287", '\x11'],
+ ["%d", "-8", "%u", "4294967288", '\x0f'],
+ ["%d", "-7", "%u", "4294967289", '\x0d'],
+ ["%d", "-6", "%u", "4294967290", '\x0b'],
+ ["%d", "-5", "%u", "4294967291", '\x09'],
+ ["%d", "-4", "%u", "4294967292", '\x07'],
+ ["%d", "-3", "%u", "4294967293", '\x05'],
+ ["%d", "-2", "%u", "4294967294", '\x03'],
+ ["%d", "-1", "%u", "4294967295", '\x01'],
+ ["%d", "0", "%u", "0", '\x00'],
+ ["%d", "1", "%u", "1", '\x02'],
+ ["%d", "2", "%u", "2", '\x04'],
+ ["%d", "3", "%u", "3", '\x06'],
+ ["%d", "4", "%u", "4", '\x08'],
+ ["%d", "5", "%u", "5", '\x0a'],
+ ["%d", "6", "%u", "6", '\x0c'],
+ ["%d", "7", "%u", "7", '\x0e'],
+ ["%d", "8", "%u", "8", '\x10'],
+ ["%d", "9", "%u", "9", '\x12'],
+ ["%d", "10", "%u", "10", '\x14'],
+ ["%d", "127", "%u", "127", '\xfe\x01'],
+ ["%d", "-32768", "%u", "4294934528", '\xff\xff\x03'],
+ ["%d", "652344632", "%u", "652344632", '\xf0\xf4\x8f\xee\x04'],
+ ["%d", "18567", "%u", "18567", '\x8e\xa2\x02'],
+ ["%d", "-14", "%u", "4294967282", '\x1b'],
+ ["%d", "-2147483648", "%u", "2147483648", '\xff\xff\xff\xff\x0f'],
+ ["%ld", "-14", "%lu", "4294967282", '\x1b'],
+ ["%d", "2075650855", "%u", "2075650855", '\xce\xac\xbf\xbb\x0f'],
+ ["%lld", "5922204476835468009", "%llu", "5922204476835468009", '\xd2\xcb\x8c\x90\x86\xe6\xf2\xaf\xa4\x01'],
+ ["%lld", "-9223372036854775808", "%llu", "9223372036854775808", '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+ ["%lld", "3273441488341945355", "%llu", "3273441488341945355", '\x96\xb0\xae\x9a\x96\xec\xcc\xed\x5a'],
+ ["%lld", "-9223372036854775807", "%llu", "9223372036854775809", '\xfd\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+]
+
+export default IntDB;
diff --git a/pw_tokenizer/ts/printf_decoder.ts b/pw_tokenizer/ts/printf_decoder.ts
index 127d358..7b02030 100644
--- a/pw_tokenizer/ts/printf_decoder.ts
+++ b/pw_tokenizer/ts/printf_decoder.ts
@@ -13,9 +13,9 @@
// the License.
/** Decodes arguments and formats them with the provided format string. */
+import Long from "long";
-const SPECIFIER_REGEX = /%(\.([0-9]+))?([%csdioxXufFeEaAgGnp])/g;
-
+const SPECIFIER_REGEX = /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g;
// Conversion specifiers by type; n is not supported.
const SIGNED_INT = 'di'.split('');
const UNSIGNED_INT = 'oxXup'.split('');
@@ -33,14 +33,24 @@
interface DecodedArg {
size: number;
- value: string | number | null;
+ value: string | number | Long | null;
}
// ZigZag decode function from protobuf's wire_format module.
-function zigzagDecode(value: number): number {
- if (!(value & 0x1)) return value >> 1;
- return (value >> 1) ^ ~0;
-}
+function zigzagDecode(value: Long, unsigned: boolean = false): Long {
+ // 64 bit math is:
+ // signmask = (zigzag & 1) ? -1 : 0;
+ // twosComplement = (zigzag >> 1) ^ signmask;
+ //
+ // To work with 32 bit, we can operate on both but "carry" the lowest bit
+ // from the high word by shifting it up 31 bits to be the most significant bit
+ // of the low word.
+ var bitsLow = value.low, bitsHigh = value.high;
+ var signFlipMask = -(bitsLow & 1);
+ bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask;
+ bitsHigh = (bitsHigh >>> 1) ^ signFlipMask;
+ return new Long(bitsLow, bitsHigh, unsigned);
+};
export class PrintfDecoder {
// Reads a unicode string from the encoded data.
@@ -65,16 +75,19 @@
}
private decodeSignedInt(args: Uint8Array): DecodedArg {
- if (args.length === 0) return {size: 0, value: null};
+ return this._decodeInt(args);
+ }
+ private _decodeInt(args: Uint8Array, unsigned: boolean = false): DecodedArg {
+ if (args.length === 0) return {size: 0, value: null};
let count = 0;
- let result = 0;
+ let result = new Long(0);
let shift = 0;
for (count = 0; count < args.length; count++) {
const byte = args[count];
- result |= (byte & 0x7f) << shift;
+ result = result.or((Long.fromInt(byte, unsigned).and(0x7f)).shiftLeft(shift));
if (!(byte & 0x80)) {
- return {value: zigzagDecode(result), size: count + 1};
+ return {value: zigzagDecode(result, unsigned), size: count + 1};
}
shift += 7;
if (shift >= 64) break;
@@ -83,15 +96,21 @@
return {size: 0, value: null};
}
- private decodeUnsignedInt(args: Uint8Array): DecodedArg {
- const arg = this.decodeSignedInt(args);
+ private decodeUnsignedInt(args: Uint8Array, lengthSpecifier: string): DecodedArg {
+ const arg = this._decodeInt(args, true);
+ const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32;
// Since ZigZag encoding is used, unsigned integers must be masked off to
// their original bit length.
if (arg.value !== null) {
- let num = arg.value as number;
- num = num >>> 0;
- arg.value = num;
+ let num = arg.value as Long;
+ if (bits === 32) {
+ num = num.and((Long.fromInt(1).shiftLeft(bits)).add(-1));
+ }
+ else {
+ num = num.and(-1);
+ }
+ arg.value = num.toString();
}
return arg;
}
@@ -100,8 +119,8 @@
const arg = this.decodeSignedInt(args);
if (arg.value !== null) {
- const num = arg.value as number;
- arg.value = String.fromCharCode(num);
+ const num = arg.value as Long;
+ arg.value = String.fromCharCode(num.toInt());
}
return arg;
}
@@ -116,7 +135,7 @@
return {size: 4, value: floatValue};
}
- private format(specifierType: string, args: Uint8Array, precision: string): DecodedArg {
+ private format(specifierType: string, args: Uint8Array, precision: string, lengthSpecifier: string): DecodedArg {
if (specifierType == '%') return {size: 0, value: '%'}; // literal %
if (specifierType === 's') {
return this.decodeString(args);
@@ -128,7 +147,7 @@
return this.decodeSignedInt(args);
}
if (UNSIGNED_INT.indexOf(specifierType) !== -1) {
- return this.decodeUnsignedInt(args);
+ return this.decodeUnsignedInt(args, lengthSpecifier);
}
if (FLOATING_POINT.indexOf(specifierType) !== -1) {
return this.decodeFloat(args, precision);
@@ -141,11 +160,11 @@
decode(formatString: string, args: Uint8Array): string {
return formatString.replace(
SPECIFIER_REGEX,
- (_specifier, _precisionFull, precision, specifierType) => {
- const decodedArg = this.format(specifierType, args, precision);
- args = args.slice(decodedArg.size);
- if (decodedArg === null) return '';
- return String(decodedArg.value);
- });
+ (_specifier, _precisionFull, precision, lengthSpecifier, specifierType) => {
+ const decodedArg = this.format(specifierType, args, precision, lengthSpecifier);
+ args = args.slice(decodedArg.size);
+ if (decodedArg === null) return '';
+ return String(decodedArg.value);
+ });
}
}
diff --git a/pw_tokenizer/ts/printf_decoder_test.ts b/pw_tokenizer/ts/printf_decoder_test.ts
index 80814ff..db28e48 100644
--- a/pw_tokenizer/ts/printf_decoder_test.ts
+++ b/pw_tokenizer/ts/printf_decoder_test.ts
@@ -14,6 +14,7 @@
/* eslint-env browser */
import {PrintfDecoder} from './printf_decoder';
+import IntDB from './int_testdata';
function argFromString(arg: string): Uint8Array {
const data = new TextEncoder().encode(arg);
@@ -52,6 +53,41 @@
).toEqual('Hello Mac and PC');
});
+ it('formats string + number correctly', () => {
+ expect(
+ printfDecoder.decode(
+ 'Hello %s and %u',
+ argsConcat(argFromString('Computer'), argFromStringBinary('\xff\xff\x03'))
+ )).toEqual(
+ 'Hello Computer and 4294934528');
+ });
+
+ it('formats integers correctly', () => {
+ for (let index = 0; index < IntDB.length; index++) {
+ const testcase = IntDB[index];
+ // Test signed
+ expect(
+ printfDecoder
+ .decode(testcase[0], argFromStringBinary(testcase[4])))
+ .toEqual(testcase[1]);
+
+ // Test unsigned
+ expect(
+ printfDecoder
+ .decode(testcase[2], argFromStringBinary(testcase[4])))
+ .toEqual(testcase[3]);
+ }
+ });
+
+ it('formats string correctly', () => {
+ expect(
+ printfDecoder.decode(
+ 'Hello %s and %s',
+ argsConcat(argFromString('Mac'), argFromString('PC'))
+ )
+ ).toEqual('Hello Mac and PC');
+ });
+
it('formats varint correctly', () => {
const arg = argFromStringBinary('\xff\xff\x03');
expect(printfDecoder.decode('Number %d', arg)).toEqual('Number -32768');
diff --git a/pw_toolchain/arm_clang/BUILD.gn b/pw_toolchain/arm_clang/BUILD.gn
index 0e8cdaf..9660142 100644
--- a/pw_toolchain/arm_clang/BUILD.gn
+++ b/pw_toolchain/arm_clang/BUILD.gn
@@ -40,6 +40,12 @@
cortex_m_hardware_fpu_v5_sp_flags =
cortex_m_hardware_fpu_flags_common + [ "-mfpu=fpv5-sp-d16" ]
+# Default config added to all the ARM cortex M targets to link `nosys` library.
+config("nosys") {
+ # TODO(prabhukr): libs = ["nosys"] did not work as expected (pwrev/133110).
+ ldflags = [ "-lnosys" ]
+}
+
config("enable_float_printf") {
ldflags = [ "-Wl,-u_printf_float" ]
}
diff --git a/pw_toolchain/arm_clang/toolchains.gni b/pw_toolchain/arm_clang/toolchains.gni
index 2b893c8..4020821 100644
--- a/pw_toolchain/arm_clang/toolchains.gni
+++ b/pw_toolchain/arm_clang/toolchains.gni
@@ -29,21 +29,45 @@
}
# Configs specific to different architectures.
-_cortex_m0plus = [ "$dir_pw_toolchain/arm_clang:cortex_m0plus" ]
+_cortex_m0plus = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m0plus",
+]
-_cortex_m3 = [ "$dir_pw_toolchain/arm_clang:cortex_m3" ]
+_cortex_m3 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m3",
+]
-_cortex_m4 = [ "$dir_pw_toolchain/arm_clang:cortex_m4" ]
+_cortex_m4 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m4",
+]
-_cortex_m4f = [ "$dir_pw_toolchain/arm_clang:cortex_m4f" ]
+_cortex_m4f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m4f",
+]
-_cortex_m7 = [ "$dir_pw_toolchain/arm_clang:cortex_m7" ]
+_cortex_m7 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m7",
+]
-_cortex_m7f = [ "$dir_pw_toolchain/arm_clang:cortex_m7f" ]
+_cortex_m7f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m7f",
+]
-_cortex_m33 = [ "$dir_pw_toolchain/arm_clang:cortex_m33" ]
+_cortex_m33 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m33",
+]
-_cortex_m33f = [ "$dir_pw_toolchain/arm_clang:cortex_m33f" ]
+_cortex_m33f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m33f",
+]
# Describes ARM clang toolchains for specific targets.
pw_toolchain_arm_clang = {
diff --git a/pw_toolchain/host_clang/toolchains.gni b/pw_toolchain/host_clang/toolchains.gni
index e32b3af..3e847c4 100644
--- a/pw_toolchain/host_clang/toolchains.gni
+++ b/pw_toolchain/host_clang/toolchains.gni
@@ -28,6 +28,10 @@
# of the test binary itself cannot generate coverage reports.
pw_toolchain_COVERAGE_ENABLED = false
+ # Indicates if this toolchain supports building fuzzers. This is typically
+ # set by individual toolchains and not by GN args.
+ pw_toolchain_FUZZING_ENABLED = false
+
# Indicates if this build is a part of OSS-Fuzz, which needs to be able to
# provide its own compiler and flags. This violates the build hermeticisim and
# should only be used for OSS-Fuzz.
@@ -37,6 +41,9 @@
# Specifies the tools used by host Clang toolchains.
_host_clang_toolchain = {
if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz sets compiler and linker paths. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+
# Just use the "llvm-ar" on the system path.
ar = "llvm-ar"
cc = getenv("CC")
@@ -127,6 +134,9 @@
defaults = {
forward_variables_from(_defaults, "*")
+ pw_toolchain_FUZZING_ENABLED = true
+ default_configs += [ "$dir_pw_fuzzer:instrumentation" ]
+
# Always disable coverage generation.
pw_toolchain_COVERAGE_ENABLED = false
@@ -141,10 +151,6 @@
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
-
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- default_configs += [ "$dir_pw_fuzzer:oss_fuzz_extra" ]
- }
}
}
diff --git a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
index d1ba0d3..5a02f38 100644
--- a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
+++ b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
@@ -169,7 +169,6 @@
def get_ldflags(compiler_info: Dict[str, str]) -> List[str]:
ldflags: List[str] = [
- '-lnosys',
# Add library search paths.
'-L' + compiler_info['gcc_libs_dir'],
'-L'
diff --git a/pw_trace/docs.rst b/pw_trace/docs.rst
index 7e328aa..cfc4658 100644
--- a/pw_trace/docs.rst
+++ b/pw_trace/docs.rst
@@ -199,14 +199,23 @@
can be used to either provide a single value type, or provide multiple
different values with a variety of types. Options for format string types can
be found here: https://docs.python.org/3/library/struct.html#format-characters
+ . The data is always assumed to be packed with little-endian ordering if not
+ indicated otherwise::
+
+ // Example
+ data_format_string = "@pw_py_struct_fmt:ll"
+ data = 0x1400000014000000
+ args = {data_0: 20, data_1:20}
- *@pw_py_map_fmt:* - Interprets the string after the ":" as a dictionary
relating the data field name to the python struct format string. Once
collected, the format strings are concatenated and used to unpack the data
- elements as above::
+ elements as above. The data is always assumed to be packed with little-endian
+ ordering if not indicated otherwise. To specify a different ordering,
+ construct the format string as ``@pw_py_map_fmt:[@=<>!]{k:v,...}``::
// Example
data_format_string = "@pw_py_map_fmt:{Field: l, Field2: l }"
- data = 0x14000000000000001400000000000000 (little endian)
+ data = 0x1400000014000000
args = {Field: 20, Field2:20}
.. tip::
diff --git a/pw_trace/public/pw_trace/internal/trace_internal.h b/pw_trace/public/pw_trace/internal/trace_internal.h
index fc106b8..982765f 100644
--- a/pw_trace/public/pw_trace/internal/trace_internal.h
+++ b/pw_trace/public/pw_trace/internal/trace_internal.h
@@ -242,8 +242,10 @@
object_name(object_name&&) = delete; \
object_name& operator=(const object_name&) = delete; \
object_name& operator=(object_name&&) = delete; \
- object_name(uint32_t trace_id = PW_TRACE_TRACE_ID_DEFAULT) \
- : trace_id_(trace_id) { \
+ \
+ object_name(uint32_t PW_CONCAT(object_name, \
+ _trace_id) = PW_TRACE_TRACE_ID_DEFAULT) \
+ : trace_id_(PW_CONCAT(object_name, _trace_id)) { \
_PW_TRACE_IF_ENABLED(event_type_start, flag, label, group, trace_id_); \
} \
~object_name() { \
diff --git a/pw_trace/py/pw_trace/trace.py b/pw_trace/py/pw_trace/trace.py
index dbd2b7d..571d66f 100755
--- a/pw_trace/py/pw_trace/trace.py
+++ b/pw_trace/py/pw_trace/trace.py
@@ -28,6 +28,7 @@
from typing import Iterable, NamedTuple
_LOG = logging.getLogger('pw_trace')
+_ORDERING_CHARS = ("@", "=", "<", ">", "!")
class TraceType(Enum):
@@ -69,17 +70,47 @@
}
+def decode_struct_fmt_args(event):
+ """Decodes the trace's event data for struct-formatted data"""
+ args = {}
+ # we assume all data is packed, little-endian ordering if not specified
+ struct_fmt = event.data_fmt[len("@pw_py_struct_fmt:") :]
+ if not struct_fmt.startswith(_ORDERING_CHARS):
+ struct_fmt = "<" + struct_fmt
+ try:
+ # needed in case the buffer is larger than expected
+ assert struct.calcsize(struct_fmt) == len(event.data)
+ items = struct.unpack_from(struct_fmt, event.data)
+ for i, item in enumerate(items):
+ args["data_" + str(i)] = item
+ except (AssertionError, struct.error):
+ args["error"] = (
+ f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize(struct_fmt)} "
+ f"data {event.data.hex()} "
+ f"data len {len(event.data)}"
+ )
+ return args
+
+
def decode_map_fmt_args(event):
"""Decodes the trace's event data for map-formatted data"""
args = {}
- fmt_list = event.data_fmt[len("@pw_py_map_fmt:") :].strip("{}").split(",")
- fmt_bytes = ''
- fields = []
+ fmt = event.data_fmt[len("@pw_py_map_fmt:") :]
+
+ # we assume all data is packed, little-endian ordering if not specified
+ if not fmt.startswith(_ORDERING_CHARS):
+ fmt = '<' + fmt
+
try:
+ (fmt_bytes, fmt_list) = fmt.split("{")
+ fmt_list = fmt_list.strip("}").split(",")
+
+ names = []
for pair in fmt_list:
- (field, value) = (s.strip() for s in pair.split(":"))
- fields.append(field)
- fmt_bytes += value
+ (name, fmt_char) = (s.strip() for s in pair.split(":"))
+ names.append(name)
+ fmt_bytes += fmt_char
except ValueError:
args["error"] = f"Invalid map format {event.data_fmt}"
else:
@@ -88,11 +119,13 @@
assert struct.calcsize(fmt_bytes) == len(event.data)
items = struct.unpack_from(fmt_bytes, event.data)
for i, item in enumerate(items):
- args[fields[i]] = item
+ args[names[i]] = item
except (AssertionError, struct.error):
args["error"] = (
- f"Mismatched struct/data format {event.data_fmt}"
- f" data {event.data.hex()}"
+ f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize(fmt_bytes)} "
+ f"data {event.data.hex()} "
+ f"data len {len(event.data)}"
)
return args
@@ -171,13 +204,7 @@
line["name"]: int.from_bytes(event.data, "little")
}
elif event.data_fmt.startswith("@pw_py_struct_fmt:"):
- items = struct.unpack_from(
- event.data_fmt[len("@pw_py_struct_fmt:") :], event.data
- )
- args = {}
- for i, item in enumerate(items):
- args["data_" + str(i)] = item
- line["args"] = args
+ line["args"] = decode_struct_fmt_args(event)
elif event.data_fmt.startswith("@pw_py_map_fmt:"):
line["args"] = decode_map_fmt_args(event)
else:
diff --git a/pw_trace/py/trace_test.py b/pw_trace/py/trace_test.py
index 9cec920..6024962 100755
--- a/pw_trace/py/trace_test.py
+++ b/pw_trace/py/trace_test.py
@@ -195,7 +195,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_struct_fmt:Hl",
- data=struct.pack("Hl", 5, 2),
+ data=struct.pack("<Hl", 5, 2),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -211,6 +211,62 @@
},
)
+ def test_generate_error_json_data_struct_invalid_small_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("<H", 5),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "error": f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<Hl')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
+
+ def test_generate_error_json_data_struct_invalid_large_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("<Hll", 5, 2, 5),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "error": f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<Hl')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
+
def test_generate_json_data_map_fmt_single(self):
event = trace.TraceEvent(
event_type=trace.TraceType.INSTANTANEOUS,
@@ -219,7 +275,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_map_fmt:{Field:l}",
- data=struct.pack("l", 20),
+ data=struct.pack("<l", 20),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -243,7 +299,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_map_fmt:{Field: l, Field2: l }",
- data=struct.pack("ll", 20, 40),
+ data=struct.pack("<ll", 20, 40),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -267,7 +323,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_map_fmt:{Field;l,Field2;l}",
- data=struct.pack("ll", 20, 40),
+ data=struct.pack("<ll", 20, 40),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -291,7 +347,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_map_fmt:{Field:l,Field2:l}",
- data=struct.pack("l", 20),
+ data=struct.pack("<l", 20),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -304,8 +360,9 @@
"ts": 10,
"s": "p",
"args": {
- "error": f"Mismatched struct/data format {event.data_fmt} "
- f"data {event.data.hex()}"
+ "error": f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<ll')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
},
},
)
@@ -318,7 +375,7 @@
timestamp_us=10,
has_data=True,
data_fmt="@pw_py_map_fmt:{Field:H,Field2:H}",
- data=struct.pack("ll", 20, 40),
+ data=struct.pack("<ll", 20, 40),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -331,8 +388,9 @@
"ts": 10,
"s": "p",
"args": {
- "error": f"Mismatched struct/data format {event.data_fmt} "
- f"data {event.data.hex()}"
+ "error": f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<HH')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
},
},
)
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
index 38056f3..041c6bc 100755
--- a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
@@ -33,7 +33,7 @@
import sys
from typing import Collection, Iterable, Iterator
-import serial # type: ignore
+import serial
from pw_tokenizer import database
from pw_trace import trace
from pw_hdlc.rpc import HdlcRpcClient, default_channels
diff --git a/pw_trace_tokenized/py/setup.cfg b/pw_trace_tokenized/py/setup.cfg
index 6c643bb..98038d3 100644
--- a/pw_trace_tokenized/py/setup.cfg
+++ b/pw_trace_tokenized/py/setup.cfg
@@ -22,6 +22,8 @@
packages = find:
zip_safe = False
install_requires =
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.package_data]
pw_trace_tokenized = py.typed
diff --git a/pw_unit_test/py/pw_unit_test/test_runner.py b/pw_unit_test/py/pw_unit_test/test_runner.py
index bc88f99..a7b06ac 100644
--- a/pw_unit_test/py/pw_unit_test/test_runner.py
+++ b/pw_unit_test/py/pw_unit_test/test_runner.py
@@ -306,6 +306,7 @@
% self._result_sink['auth_token'],
},
data=json.dumps({'testResults': [test_result]}),
+ timeout=5.0,
).raise_for_status()
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
index 7944a77..86399f7 100644
--- a/pw_unit_test/test.gni
+++ b/pw_unit_test/test.gni
@@ -219,7 +219,60 @@
pw_internal_disableable_target("$target_name.lib") {
target_type = "pw_source_set"
enable_if = _test_is_enabled
- forward_variables_from(invoker, "*", [ "metadata" ])
+
+ # It is possible that the executable target type has been overriden by
+ # pw_unit_test_EXECUTABLE_TARGET_TYPE, which may allow for additional
+ # variables to be specified on the executable template. As such, we cannot
+ # forward all variables ("*") from the invoker to source_set library, as
+ # those additional variables would not be used and GN gen would error.
+ _source_set_relevant_variables = [
+ # GN source_set variables
+ # https://gn.googlesource.com/gn/+/main/docs/reference.md#target-declarations-source_set_declare-a-source-set-target-variables
+ "asmflags",
+ "cflags",
+ "cflags_c",
+ "cflags_cc",
+ "cflags_objc",
+ "cflags_objcc",
+ "defines",
+ "include_dirs",
+ "inputs",
+ "ldflags",
+ "lib_dirs",
+ "libs",
+ "precompiled_header",
+ "precompiled_source",
+ "rustenv",
+ "rustflags",
+ "swiftflags",
+ "testonly",
+ "assert_no_deps",
+ "data_deps",
+ "deps",
+ "public_deps",
+ "runtime_deps",
+ "write_runtime_deps",
+ "all_dependent_configs",
+ "public_configs",
+ "check_includes",
+ "configs",
+ "data",
+ "friend",
+ "inputs",
+ "metadata",
+ "output_extension",
+ "output_name",
+ "public",
+ "sources",
+ "testonly",
+ "visibility",
+
+ # pw_source_set variables
+ # https://pigweed.dev/pw_build/?highlight=pw_executable#target-types
+ "remove_configs",
+ "remove_public_deps",
+ ]
+ forward_variables_from(invoker, _source_set_relevant_variables)
if (!defined(deps)) {
deps = []
@@ -239,6 +292,17 @@
target_type = pw_unit_test_EXECUTABLE_TARGET_TYPE
enable_if = _test_is_enabled
+ # Include configs, deps, etc. from the pw_test in the executable as well as
+ # the library to ensure that linker flags propagate to the executable.
+ forward_variables_from(invoker,
+ "*",
+ [
+ "extra_metadata",
+ "metadata",
+ "sources",
+ "public",
+ ])
+
# Metadata for this test when used as part of a pw_test_group target.
metadata = {
tests = [
@@ -263,7 +327,10 @@
}
}
- deps = [ ":$_test_target_name.lib" ]
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ ":$_test_target_name.lib" ]
if (_test_main != "") {
deps += [ _test_main ]
}
diff --git a/pw_watch/py/pw_watch/debounce.py b/pw_watch/py/pw_watch/debounce.py
index c3559a1..ce09063 100644
--- a/pw_watch/py/pw_watch/debounce.py
+++ b/pw_watch/py/pw_watch/debounce.py
@@ -103,14 +103,14 @@
# re-try running afterwards.
error_message = ['Event while running: %s', event_description]
if BUILDER_CONTEXT.using_progress_bars():
- _LOG.error(*error_message)
+ _LOG.warning(*error_message)
else:
# Push an empty line to flush ongoing I/O in subprocess.
print('')
# Surround the error message with newlines to make it stand out.
print('')
- _LOG.error(*error_message)
+ _LOG.warning(*error_message)
print('')
self.function.cancel()
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index 0379a39..d956df3 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -252,7 +252,7 @@
log_message = f'File change detected: {os.path.relpath(matching_path)}'
if self.restart_on_changes:
if self.fullscreen_enabled and self.watch_app:
- self.watch_app.rebuild_on_filechange()
+ self.watch_app.clear_log_panes()
self.debouncer.press(f'{log_message} Triggering build...')
else:
_LOG.info('%s ; not rebuilding', log_message)
@@ -340,6 +340,11 @@
BUILDER_CONTEXT.set_idle()
def run_recipe(self, index: int, cfg: BuildRecipe, env) -> None:
+ if BUILDER_CONTEXT.interrupted():
+ return
+ if not cfg.enabled:
+ return
+
num_builds = len(self.project_builder)
index_message = f'[{index}/{num_builds}]'
@@ -438,7 +443,11 @@
_LOG.info('Build stopped.')
elif BUILDER_CONTEXT.interrupted():
pass # Don't print anything.
- elif all(recipe.status.passed() for recipe in self.project_builder):
+ elif all(
+ recipe.status.passed()
+ for recipe in self.project_builder
+ if recipe.enabled
+ ):
_LOG.info('Finished; all successful')
else:
_LOG.info('Finished; some builds failed')
diff --git a/pw_watch/py/pw_watch/watch_app.py b/pw_watch/py/pw_watch/watch_app.py
index df4dbf6..3851453 100644
--- a/pw_watch/py/pw_watch/watch_app.py
+++ b/pw_watch/py/pw_watch/watch_app.py
@@ -15,6 +15,7 @@
""" Prompt toolkit application for pw watch. """
import asyncio
+import functools
import logging
import os
import re
@@ -45,13 +46,14 @@
)
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.styles import (
+ ConditionalStyleTransformation,
DynamicStyle,
+ SwapLightAndDarkStyleTransformation,
+ merge_style_transformations,
merge_styles,
- Style,
style_from_pygments_cls,
)
from prompt_toolkit.formatted_text import StyleAndTextTuples
-from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from prompt_toolkit.lexers import PygmentsLexer
from pygments.lexers.markup import MarkdownLexer # type: ignore
@@ -70,6 +72,8 @@
ToolbarButton,
WindowPaneToolbar,
create_border,
+ mouse_handlers,
+ to_checkbox,
)
from pw_console.window_list import DisplayMode
from pw_console.window_manager import WindowManager
@@ -112,6 +116,33 @@
Balance all window sizes. ------------------------- Ctrl-U
+Bottom Toolbar Controls
+=======================
+
+Rebuild Enter --------------- Click or press Enter to trigger a rebuild.
+[x] Auto Rebuild ------------ Click to globaly enable or disable automatic
+ rebuilding when files change.
+Help F1 --------------------- Click or press F1 to open this help window.
+Quit Ctrl-d ----------------- Click or press Ctrl-d to quit pw_watch.
+Next Tab Ctrl-Alt-n --------- Switch to the next log tab.
+Previous Tab Ctrl-Alt-p ----- Switch to the previous log tab.
+
+
+Build Status Bar
+================
+
+The build status bar shows the current status of all build directories outlined
+in a colored frame.
+
+ ┏━━ BUILDING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+ ┃ [✓] out_directory Building Last line of standard out. ┃
+ ┃ [✓] out_dir2 Waiting Last line of standard out. ┃
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+Each checkbox on the far left controls whether that directory is built when
+files change and manual builds are run.
+
+
Copying Text
============
@@ -163,13 +194,12 @@
self.registered_commands = DEFAULT_KEY_BINDINGS
self.registered_commands.update(self.user_key_bindings)
- self.default_config.update(
- {
- 'key_bindings': DEFAULT_KEY_BINDINGS,
- 'show_python_logger': True,
- }
- )
- self.reset_config()
+ new_config_settings = {
+ 'key_bindings': DEFAULT_KEY_BINDINGS,
+ 'show_python_logger': True,
+ }
+ self.default_config.update(new_config_settings)
+ self._update_config(new_config_settings)
# Required pw_console preferences for key bindings and themes
@property
@@ -188,6 +218,10 @@
def theme_colors(self):
return get_theme_colors(self.ui_theme)
+ @property
+ def swap_light_and_dark(self) -> bool:
+ return self._config.get('swap_light_and_dark', False)
+
def get_function_keys(self, name: str) -> List:
"""Return the keys for the named function."""
try:
@@ -256,38 +290,6 @@
self.application.window_manager_container = self.create_root_container()
-class StatusBarControl(FormattedTextControl):
- """Handles switching build tabs in the UI on mouse click."""
-
- def __init__(self, watch_app: 'WatchApp', *args, **kwargs) -> None:
- self.watch_app = watch_app
- super().__init__(*args, **kwargs)
-
- def mouse_handler(self, mouse_event: MouseEvent):
- """Mouse handler for this control."""
- _click_x = mouse_event.position.x
- _click_y = mouse_event.position.y
-
- # On left click
- if mouse_event.event_type == MouseEventType.MOUSE_UP:
- tab_index = _click_y
- pane = self.watch_app.recipe_index_to_log_pane.get(tab_index, None)
- if not pane:
- return NotImplemented
-
- (
- window_list,
- pane_index,
- ) = self.watch_app.window_manager.find_window_list_and_pane_index(
- pane
- )
- window_list.switch_to_tab(pane_index)
- return None
-
- # Mouse event not handled, return NotImplemented.
- return NotImplemented
-
-
class WatchApp(PluginMixin):
"""Pigweed Watch main window application."""
@@ -371,9 +373,7 @@
self.status_bar_border_style = 'class:command-runner-border'
- self.status_bar_control = StatusBarControl(
- self, self.get_status_bar_text
- )
+ self.status_bar_control = FormattedTextControl(self.get_status_bar_text)
self.status_bar_container = create_border(
HSplit(
@@ -413,6 +413,17 @@
click_to_focus_text='',
)
self.help_toolbar.add_button(
+ ToolbarButton('Enter', 'Rebuild', self.run_build)
+ )
+ self.help_toolbar.add_button(
+ ToolbarButton(
+ description='Auto Rebuild',
+ mouse_handler=self.toggle_restart_on_filechange,
+ is_checkbox=True,
+ checked=lambda: self.restart_on_changes,
+ )
+ )
+ self.help_toolbar.add_button(
ToolbarButton('F1', 'Help', self.user_guide_window.toggle_display)
)
self.help_toolbar.add_button(ToolbarButton('Ctrl-d', 'Quit', self.exit))
@@ -491,11 +502,16 @@
)
self.current_theme = generate_styles(self.prefs.ui_theme)
- self.style_overrides = Style.from_dict(
- {
- # 'search': 'bg:ansired ansiblack',
- }
+
+ self.style_transformation = merge_style_transformations(
+ [
+ ConditionalStyleTransformation(
+ SwapLightAndDarkStyleTransformation(),
+ filter=Condition(lambda: self.prefs.swap_light_and_dark),
+ ),
+ ]
)
+
self.code_theme = style_from_pygments_cls(PigweedCodeStyle)
self.layout = Layout(
@@ -513,11 +529,11 @@
lambda: merge_styles(
[
self.current_theme,
- self.style_overrides,
self.code_theme,
]
)
),
+ style_transformation=self.style_transformation,
full_screen=True,
)
@@ -645,7 +661,7 @@
instead."""
self.window_manager.focus_first_visible_pane()
- def switch_to_root_log(self):
+ def switch_to_root_log(self) -> None:
(
window_list,
pane_index,
@@ -654,6 +670,17 @@
)
window_list.switch_to_tab(pane_index)
+ def switch_to_build_log(self, log_index: int) -> None:
+ pane = self.recipe_index_to_log_pane.get(log_index, None)
+ if not pane:
+ return
+
+ (
+ window_list,
+ pane_index,
+ ) = self.window_manager.find_window_list_and_pane_index(pane)
+ window_list.switch_to_tab(pane_index)
+
def command_runner_is_open(self) -> bool:
# pylint: disable=no-self-use
return False
@@ -663,25 +690,34 @@
if isinstance(pane, LogPane):
yield pane
- def clear_ninja_log(self) -> None:
+ def clear_log_panes(self) -> None:
+ """Erase all log pane content and turn on follow.
+
+ This is called whenever rebuilds occur. Either a manual build from
+ self.run_build or on file changes called from
+ pw_watch._handle_matched_event."""
for pane in self.all_log_panes():
+ pane.log_view.clear_visual_selection()
+ pane.log_view.clear_filters()
pane.log_view.log_store.clear_logs()
- pane.log_view._restart_filtering() # pylint: disable=protected-access
pane.log_view.view_mode_changed()
# Re-enable follow if needed
if not pane.log_view.follow:
pane.log_view.toggle_follow()
- def run_build(self):
- """Manually trigger a rebuild."""
- self.clear_ninja_log()
+ def run_build(self) -> None:
+ """Manually trigger a rebuild from the UI."""
+ self.clear_log_panes()
self.event_handler.rebuild()
- def rebuild_on_filechange(self):
- for pane in self.all_log_panes():
- pane.log_view.clear_visual_selection()
- pane.log_view.log_store.clear_logs()
- pane.log_view.view_mode_changed()
+ @property
+ def restart_on_changes(self) -> bool:
+ return self.event_handler.restart_on_changes
+
+ def toggle_restart_on_filechange(self) -> None:
+ self.event_handler.restart_on_changes = (
+ not self.event_handler.restart_on_changes
+ )
def get_status_bar_text(self) -> StyleAndTextTuples:
"""Return formatted text for build status bar."""
@@ -696,21 +732,48 @@
pane,
) = self.window_manager._get_active_window_list_and_pane()
# pylint: enable=protected-access
+ restarting = BUILDER_CONTEXT.restart_flag
- for cfg in self.event_handler.project_builder:
+ for i, cfg in enumerate(self.event_handler.project_builder):
# The build directory
name_style = ''
- if pane and pane.pane_title() == cfg.display_name:
+ if not pane:
+ formatted_text.append(('', '\n'))
+ continue
+
+ # Dim the build name if disabled
+ if not cfg.enabled:
+ name_style = 'class:theme-fg-inactive'
+
+ # If this build tab is selected, highlight with cyan.
+ if pane.pane_title() == cfg.display_name:
name_style = 'class:theme-fg-cyan'
+
+ formatted_text.append(
+ to_checkbox(
+ cfg.enabled,
+ functools.partial(
+ mouse_handlers.on_click,
+ cfg.toggle_enabled,
+ ),
+ end=' ',
+ unchecked_style='class:checkbox',
+ checked_style='class:checkbox-checked',
+ )
+ )
formatted_text.append(
(
name_style,
f'{cfg.display_name}'.ljust(name_width),
+ functools.partial(
+ mouse_handlers.on_click,
+ functools.partial(self.switch_to_build_log, i),
+ ),
)
)
formatted_text.append(separator)
# Status
- formatted_text.append(cfg.status.status_slug())
+ formatted_text.append(cfg.status.status_slug(restarting=restarting))
formatted_text.append(separator)
# Current stdout line
formatted_text.extend(cfg.status.current_step_formatted())
@@ -724,13 +787,15 @@
return formatted_text
def set_tab_bar_colors(self) -> None:
+ restarting = BUILDER_CONTEXT.restart_flag
+
for cfg in BUILDER_CONTEXT.recipes:
pane = self.recipe_name_to_log_pane.get(cfg.display_name, None)
if not pane:
continue
pane.extra_tab_style = None
- if cfg.status.failed():
+ if not restarting and cfg.status.failed():
pane.extra_tab_style = 'class:theme-fg-red'
def exit(
diff --git a/seed/0000-index.rst b/seed/0000-index.rst
index 142add4..35f1e75 100644
--- a/seed/0000-index.rst
+++ b/seed/0000-index.rst
@@ -10,5 +10,5 @@
0001-the-seed-process
0002-template
- 0101: pw_project.toml<https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128010>
+ 0101-pigweed.json
0102-module-docs
diff --git a/seed/0001-the-seed-process.rst b/seed/0001-the-seed-process.rst
index d697b16..3b7958b 100644
--- a/seed/0001-the-seed-process.rst
+++ b/seed/0001-the-seed-process.rst
@@ -8,11 +8,11 @@
:fas:`seedling` SEED-0001: :ref:`The SEED Process<seed-0001>`
:octicon:`comment-discussion` Status:
- :bdg-primary:`Open for Comments`
+ :bdg-secondary-line:`Open for Comments`
:octicon:`chevron-right`
:bdg-secondary-line:`Last Call`
:octicon:`chevron-right`
- :bdg-secondary-line:`Accepted`
+ :bdg-primary:`Accepted`
:octicon:`kebab-horizontal`
:bdg-secondary-line:`Rejected`
diff --git a/seed/0101-pigweed.json.rst b/seed/0101-pigweed.json.rst
new file mode 100644
index 0000000..b8def7f
--- /dev/null
+++ b/seed/0101-pigweed.json.rst
@@ -0,0 +1,210 @@
+.. _seed-0101:
+
+==================
+0101: pigweed.json
+==================
+
+.. card::
+ :fas:`seedling` SEED-0101: :ref:`pigweed.json<seed-0101>`
+
+ :octicon:`comment-discussion` Status:
+ :bdg-secondary-line:`Open for Comments`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Last Call`
+ :octicon:`chevron-right`
+ :bdg-primary:`Accepted`
+ :octicon:`kebab-horizontal`
+ :bdg-secondary-line:`Rejected`
+
+ :octicon:`calendar` Proposal Date: 2023-02-06
+
+ :octicon:`code-review` CL: `pwrev/128010 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128010>`_
+
+-------
+Summary
+-------
+Combine several of the configuration options downstream projects use to
+configure parts of Pigweed in one place, and use this place for further
+configuration options.
+
+----------
+Motivation
+----------
+Pigweed-based projects configure Pigweed and themselves in a variety of ways.
+The environment setup is controlled by a JSON file that's referenced in
+``bootstrap.sh`` files and in internal infrastructure repos that looks
+something like this:
+
+.. code-block::
+
+ {
+ "root_variable": "<PROJNAME>_ROOT",
+ "cipd_package_files": ["tools/default.json"],
+ "virtualenv": {
+ "gn_args": ["dir_pw_third_party_stm32cube=\"\""],
+ "gn_root": ".",
+ "gn_targets": [":python.install"]
+ },
+ "optional_submodules": ["vendor/shhh-secret"],
+ "gni_file": "build_overrides/pigweed_environment.gni"
+ }
+
+The plugins to the ``pw`` command-line utility are configured in ``PW_PLUGINS``,
+which looks like this:
+
+.. code-block::
+
+ # <name> <Python module> <function>
+ console pw_console.__main__ main
+ format pw_presubmit.format_code _pigweed_upstream_main
+
+In addition, changes have been proposed to configure some of the behavior of
+``pw format`` and the formatting steps of ``pw presubmit`` from config files,
+but there's no standard place to put these configuration options.
+
+---------------
+Guide reference
+---------------
+This proposal affects two sets of people: developers looking to use Pigweed,
+and developers looking to add configurable features to Pigweed.
+
+Developers looking to use Pigweed will have one config file that contains all
+the options they need to set. Documentation for individual Pigweed modules will
+show only the configuration options relevant for that module, and multiple of
+these examples can simply be concatenated to form a valid config file.
+
+Developers looking to add configurable features to Pigweed no longer need to
+define a new file format, figure out where to find it in the tree (or how to
+have Pigweed-projects specify a location), or parse this format.
+
+---------------------
+Problem investigation
+---------------------
+There are multiple issues with the current system that need to be addressed.
+
+* ``PW_PLUGINS`` works, but is a narrow custom format with exactly one purpose.
+* The environment config file is somewhat extensible, but is still specific to
+ environment setup.
+* There's no accepted place for other modules to retrieve configuration options.
+
+These should be combined into a single file. There are several formats that
+could be selected, and many more arguments for and against each. Only a subset
+of these arguments are reproduced here.
+
+* JSON does not support comments
+* JSON5 is not supported in the Python standard library
+* XML is too verbose
+* YAML is acceptable, but implicit type conversion could be a problem, and it's
+ not supported in the Python standard library
+* TOML is acceptable, and `was selected for a similar purpose by Python
+ <https://snarky.ca/what-the-heck-is-pyproject-toml/>`_, but it's
+ not supported in the Python standard library before Python v3.11
+* Protobuf Text Format is acceptable and widely used within Google, but is not
+ supported in the Python standard library
+
+The location of the file is also an issue. Environment config files can be found
+in a variety of locations depending on the project—all of the following paths
+are used by at least one internal Pigweed-based project.
+
+* ``build/environment.json``
+* ``build/pigweed/env_setup.json``
+* ``environment.json``
+* ``env_setup.json``
+* ``pw_env_setup.json``
+* ``scripts/environment.json``
+* ``tools/environment.json``
+* ``tools/env_setup.json``
+
+``PW_PLUGINS`` files can in theory be in any directory and ``pw`` will search up
+for them from the current directory, but in practice they only exist at the root
+of checkouts. Having this file in a fixed location with a fixed name makes it
+significantly easier to find as a user, and the fixed name (if not path) makes
+it easy to find programmatically too.
+
+---------------
+Detailed design
+---------------
+The ``pw_env_setup`` Python module will provide an API to retrieve a parsed
+``pigweed.json`` file from the root of the checkout. ``pw_env_setup`` is the
+correct location because it can't depend on anything else, but other modules can
+depend on it. Code in other languages does not yet depend on configuration
+files.
+
+A ``pigweed.json`` file might look like the following. Individual option names
+and structures are not final but will evolve as those options are
+implemented—this is merely an example of what an actual file could look like.
+The ``pw`` namespace is reserved for Pigweed, but other projects can use other
+namespaces for their own needs. Within the ``pw`` namespace all options are
+first grouped by their module name, which simplifies searching for the code and
+documentation related to the option in question.
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_cli": {
+ "plugins": {
+ "console": {
+ "module": "pw_console.__main__",
+ "function": "main"
+ },
+ "format": {
+ "module": "pw_presubmit.format_code",
+ "function": "_pigweed_upstream_main"
+ }
+ }
+ },
+ "pw_env_setup": {
+ "root_variable": "<PROJNAME>_ROOT",
+ "rosetta": "allow",
+ "gni_file": "build_overrides/pigweed_environment.gni",
+ "cipd": {
+ "package_files": [
+ "tools/default.json"
+ ]
+ },
+ "virtualenv": {
+ "gn_args": [
+ "dir_pw_third_party_stm32cube=\"\""
+ ],
+ "gn_targets": [
+ "python.install"
+ ],
+ "gn_root": "."
+ },
+ "submodules": {
+ "optional": [
+ "vendor/shhh-secret"
+ ]
+ }
+ },
+ "pw_presubmit": {
+ "format": {
+ "python": {
+ "formatter": "black",
+ "black_path": "pyink"
+ }
+ }
+ }
+ }
+ }
+
+Some teams will resist a new file at the root of their checkout, but this seed
+won't be adding any files, it'll be combining at least one top-level file, maybe
+two, into a new top-level file, so there won't be any additional files in the
+checkout root.
+
+------------
+Alternatives
+------------
+``pw format`` and the formatting steps of ``pw presubmit`` could read from yet
+another config file, further fracturing Pigweed's configuration.
+
+A different file format could be chosen over JSON. Since JSON is parsed into
+only Python lists, dicts, and primitives, switching to another format that can
+be parsed into the same internal structure should be trivial.
+
+--------------
+Open questions
+--------------
+None?
diff --git a/seed/BUILD.gn b/seed/BUILD.gn
index e2f12f6..6a5158f 100644
--- a/seed/BUILD.gn
+++ b/seed/BUILD.gn
@@ -21,6 +21,7 @@
group_deps = [
":0001",
":0002",
+ ":0101",
":0102",
]
}
@@ -34,6 +35,10 @@
sources = [ "0002-template.rst" ]
}
+pw_doc_group("0101") {
+ sources = [ "0101-pigweed.json.rst" ]
+}
+
pw_doc_group("0102") {
sources = [ "0102-module-docs.rst" ]
}
diff --git a/targets/default_config.BUILD b/targets/default_config.BUILD
index 5c7fcf9..b2d33d5 100644
--- a/targets/default_config.BUILD
+++ b/targets/default_config.BUILD
@@ -170,3 +170,8 @@
name = "pw_trace_backend",
build_setting_default = "@pigweed//pw_trace:backend_multiplexer",
)
+
+label_flag(
+ name = "freertos_config",
+ build_setting_default = "@pigweed//third_party/freertos:freertos_config",
+)
diff --git a/targets/host/system_rpc_server.cc b/targets/host/system_rpc_server.cc
index f3d870f..8ff7575 100644
--- a/targets/host/system_rpc_server.cc
+++ b/targets/host/system_rpc_server.cc
@@ -35,6 +35,7 @@
static_assert(kMaxTransmissionUnit ==
hdlc::MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes));
+stream::ServerSocket server_socket;
stream::SocketStream socket_stream;
hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
@@ -58,7 +59,10 @@
});
PW_LOG_INFO("Starting pw_rpc server on port %d", socket_port);
- PW_CHECK_OK(socket_stream.Serve(socket_port));
+ PW_CHECK_OK(server_socket.Listen(socket_port));
+ auto accept_result = server_socket.Accept();
+ PW_CHECK_OK(accept_result.status());
+ socket_stream = *std::move(accept_result);
}
rpc::Server& Server() { return server; }
diff --git a/targets/stm32f429i_disc1/py/setup.cfg b/targets/stm32f429i_disc1/py/setup.cfg
index 26ec850..a3f3772 100644
--- a/targets/stm32f429i_disc1/py/setup.cfg
+++ b/targets/stm32f429i_disc1/py/setup.cfg
@@ -23,6 +23,7 @@
zip_safe = False
install_requires =
pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
coloredlogs
[options.entry_points]
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
index 8574082..bc09d28 100644
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
@@ -16,9 +16,10 @@
import logging
import typing
+from typing import Optional
import coloredlogs # type: ignore
-import serial.tools.list_ports # type: ignore
+import serial.tools.list_ports
# Vendor/device ID to search for in USB devices.
_ST_VENDOR_ID = 0x0483
@@ -31,7 +32,7 @@
"""Information about a connected dev board."""
dev_name: str
- serial_number: str
+ serial_number: Optional[str]
def detect_boards() -> list:
@@ -67,7 +68,7 @@
for idx, board in enumerate(boards):
_LOG.info('Board %d:', idx)
_LOG.info(' - Port: %s', board.dev_name)
- _LOG.info(' - Serial #: %s', board.serial_number)
+ _LOG.info(' - Serial #: %s', board.serial_number or '<not set>')
if __name__ == '__main__':
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
index d79e470..b17d627 100755
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
@@ -23,7 +23,7 @@
from typing import List
import coloredlogs # type: ignore
-import serial # type: ignore
+import serial
from stm32f429i_disc1_utils import stm32f429i_detector
# Path used to access non-python resources in this python module.
diff --git a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
index 7b64e66..7922f29 100644
--- a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
+++ b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
@@ -23,26 +23,59 @@
licenses(["notice"])
pw_cc_library(
+ name = "freertos_config",
+ hdrs = [
+ "config/FreeRTOSConfig.h",
+ ],
+ includes = ["config/"],
+ target_compatible_with = [":freertos_config_cv"],
+ deps = ["//third_party/freertos:config_assert"],
+)
+
+# Constraint value corresponding to :freertos_config.
+#
+# If you include this in your platform definition, you will tell Bazel to use
+# the :freertos_config defined above when compiling FreeRTOS. (See
+# //third_party/freertos/BUILD.bazel.) If you include it in a target's
+# `target_compatible_with`, you will tell Bazel the target can only be built
+# for platforms that specify this FreeRTOS config.
+constraint_value(
+ name = "freertos_config_cv",
+ constraint_setting = "//third_party/freertos:freertos_config_setting",
+)
+
+# TODO(b/261506064): Additional constraint values for configuring stm32cube
+# need to be added here, once constraint settings for stm32cube are defined.
+platform(
+ name = "platform",
+ constraint_values = [
+ ":freertos_config_cv",
+ "//pw_build/constraints/rtos:freertos",
+ "@freertos//:port_ARM_CM4F",
+ ],
+ parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
+)
+
+pw_cc_library(
name = "pre_init",
srcs = [
"boot.cc",
"vector_table.c",
],
hdrs = [
- "config/FreeRTOSConfig.h",
"config/stm32f4xx_hal_conf.h",
],
- # TODO(b/261506064): Get this to build. Requires FreeRTOS.
- tags = ["manual"],
+ target_compatible_with = [":freertos_config_cv"],
deps = [
+ ":freertos_config",
"//pw_boot",
"//pw_boot_cortex_m",
"//pw_malloc",
"//pw_preprocessor",
"//pw_string",
"//pw_sys_io_stm32cube",
- "//third_party/freertos",
"//third_party/stm32cube",
+ "@freertos",
],
)
@@ -51,12 +84,11 @@
srcs = [
"main.cc",
],
- # TODO(b/261506064): Get this to build. Requires FreeRTOS.
- tags = ["manual"],
+ target_compatible_with = [":freertos_config_cv"],
deps = [
"//pw_thread:thread",
"//pw_thread:thread_core",
"//pw_thread_freertos:thread",
- "//third_party/freertos",
+ "@freertos",
],
)
diff --git a/third_party/emboss/BUILD.gn b/third_party/emboss/BUILD.gn
index 89df5c4..a0ef87d 100644
--- a/third_party/emboss/BUILD.gn
+++ b/third_party/emboss/BUILD.gn
@@ -18,7 +18,20 @@
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/emboss/emboss.gni")
+config("default_config") {
+ # EMBOSS_DCHECK is used as an assert() for logic embedded in Emboss, where
+ # EMBOSS_CHECK is used to check preconditions on application logic (e.g.
+ # Write() checks the [requires: ...] attribute).
+ defines = [
+ "EMBOSS_CHECK=PW_DCHECK",
+ "EMBOSS_CHECK_ABORTS",
+ "EMBOSS_DCHECK=PW_DCHECK",
+ "EMBOSS_DCHECK_ABORTS",
+ ]
+}
+
source_set("cpp_utils") {
+ public_configs = [ pw_third_party_emboss_CONFIG ]
sources = [
"$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic.h",
"$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic_all_known_generated.h",
diff --git a/third_party/emboss/docs.rst b/third_party/emboss/docs.rst
index 59ab754..1cabc8a 100644
--- a/third_party/emboss/docs.rst
+++ b/third_party/emboss/docs.rst
@@ -18,7 +18,7 @@
.. code-block:: sh
- git submodule add https://github.com/google/emboss.git third_party/emboss/src
+ git submodule add https://github.com/google/emboss.git third_party/emboss/src
Next, set the GN variable ``dir_pw_third_party_emboss`` to the path of your Emboss
installation. If using the submodule path from above, add the following to the
@@ -26,7 +26,19 @@
.. code-block::
- dir_pw_third_party_emboss = "//third_party/emboss/src"
+ dir_pw_third_party_emboss = "//third_party/emboss/src"
+
+..
+ inclusive-language: disable
+
+Optionally, configure the Emboss defines documented at
+`dir_pw_third_party_emboss/runtime/cpp/emboss_defines.h
+<https://github.com/google/emboss/blob/master/runtime/cpp/emboss_defines.h>`_
+by setting the ``pw_third_party_emboss_CONFIG`` variable to a config that
+overrides the defines. By default, checks will use PW_DCHECK.
+
+..
+ inclusive-language: enable
------------
Using Emboss
diff --git a/third_party/emboss/emboss.gni b/third_party/emboss/emboss.gni
index 4f90627..93d8396 100644
--- a/third_party/emboss/emboss.gni
+++ b/third_party/emboss/emboss.gni
@@ -12,8 +12,14 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pigweed.gni")
+
declare_args() {
# If compiling with Emboss, this variable is set to the path to the Emboss
# source code.
dir_pw_third_party_emboss = ""
+
+ # config target for overriding Emboss defines (e.g. EMBOSS_CHECK).
+ pw_third_party_emboss_CONFIG =
+ "$dir_pigweed/third_party/emboss:default_config"
}
diff --git a/third_party/freertos/BUILD.bazel b/third_party/freertos/BUILD.bazel
index f635a0f..01256d4 100644
--- a/third_party/freertos/BUILD.bazel
+++ b/third_party/freertos/BUILD.bazel
@@ -13,7 +13,7 @@
# the License.
load(
- "//pw_build:pigweed.bzl",
+ "@pigweed//pw_build:pigweed.bzl",
"pw_cc_library",
)
@@ -28,6 +28,154 @@
],
includes = ["public"],
deps = [
- "//pw_assert",
+ "@pigweed//pw_assert",
],
)
+
+constraint_setting(
+ name = "port",
+)
+
+constraint_value(
+ name = "port_ARM_CM7",
+ constraint_setting = ":port",
+)
+
+constraint_value(
+ name = "port_ARM_CM4F",
+ constraint_setting = ":port",
+)
+
+pw_cc_library(
+ name = "freertos",
+ srcs = [
+ "croutine.c",
+ "event_groups.c",
+ "list.c",
+ "queue.c",
+ "stream_buffer.c",
+ "timers.c",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/port.c"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/port.c"],
+ "//conditions:default": [],
+ }),
+ includes = ["include/"] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1"],
+ "//conditions:default": [],
+ }),
+ textual_hdrs = [
+ "include/FreeRTOS.h",
+ "include/StackMacros.h",
+ "include/croutine.h",
+ "include/deprecated_definitions.h",
+ "include/event_groups.h",
+ "include/list.h",
+ "include/message_buffer.h",
+ "include/mpu_wrappers.h",
+ "include/portable.h",
+ "include/projdefs.h",
+ "include/queue.h",
+ "include/semphr.h",
+ "include/stack_macros.h",
+ "include/stream_buffer.h",
+ "include/task.h",
+ "include/timers.h",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
+ "//conditions:default": [],
+ }),
+ deps = [
+ ":pigweed_tasks_c",
+ "@pigweed_config//:freertos_config",
+ ],
+ # Required because breaking out tasks_c results in the dependencies between
+ # the libraries not being quite correct: to link pigweed_tasks_c you
+ # actually need a bunch of the source files from here (e.g., list.c).
+ alwayslink = 1,
+)
+
+# Constraint setting used to determine if task statics should be disabled.
+constraint_setting(
+ name = "disable_tasks_statics_setting",
+ default_constraint_value = ":no_disable_task_statics",
+)
+
+constraint_value(
+ name = "disable_task_statics",
+ constraint_setting = ":disable_tasks_statics_setting",
+)
+
+constraint_value(
+ name = "no_disable_task_statics",
+ constraint_setting = ":disable_tasks_statics_setting",
+)
+
+pw_cc_library(
+ name = "pigweed_tasks_c",
+ srcs = ["tasks.c"],
+ defines = select({
+ ":disable_task_statics": [
+ "PW_THIRD_PARTY_FREERTOS_NO_STATICS=1",
+ ],
+ "//conditions:default": [],
+ }),
+ includes = [
+ "include/",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/"],
+ "//conditions:default": [],
+ }),
+ local_defines = select({
+ ":disable_task_statics": [
+ "static=",
+ ],
+ "//conditions:default": [],
+ }),
+ # tasks.c transitively includes all these headers :/
+ textual_hdrs = [
+ "include/FreeRTOS.h",
+ "include/portable.h",
+ "include/projdefs.h",
+ "include/list.h",
+ "include/deprecated_definitions.h",
+ "include/mpu_wrappers.h",
+ "include/stack_macros.h",
+ "include/task.h",
+ "include/timers.h",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
+ "//conditions:default": [],
+ }),
+ deps = ["@pigweed_config//:freertos_config"],
+)
+
+# Constraint setting used to select the FreeRTOSConfig version.
+constraint_setting(
+ name = "freertos_config_setting",
+)
+
+alias(
+ name = "freertos_config",
+ actual = select({
+ "@pigweed//targets/stm32f429i_disc1_stm32cube:freertos_config_cv": "@pigweed//targets/stm32f429i_disc1_stm32cube:freertos_config",
+ "//conditions:default": "default_freertos_config",
+ }),
+)
+
+pw_cc_library(
+ name = "default_freertos_config",
+ # The "default" config is not compatible with any configuration: you can't
+ # build FreeRTOS without choosing a config.
+ target_compatible_with = ["@platforms//:incompatible"],
+)
+
+# Exported for
+# pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
+exports_files(
+ ["tasks.c"],
+)
diff --git a/third_party/freertos/docs.rst b/third_party/freertos/docs.rst
index b43e71e..695dd7d 100644
--- a/third_party/freertos/docs.rst
+++ b/third_party/freertos/docs.rst
@@ -10,8 +10,8 @@
-------------
Build Support
-------------
-This module provides support to compile FreeRTOS with GN and CMake. This is
-required when compiling backends modules for FreeRTOS.
+This module provides support to compile FreeRTOS with GN, CMake, and Bazel.
+This is required when compiling backends modules for FreeRTOS.
GN
==
@@ -39,6 +39,25 @@
#. Set ``pw_third_party_freertos_PORT`` to a library target which provides
the FreeRTOS port specific includes and sources.
+Bazel
+=====
+In Bazel, the FreeRTOS build is configured through `constraint_settings
+<https://bazel.build/reference/be/platform#constraint_setting>`_. The `platform
+<https://bazel.build/extending/platforms>`_ you are building for must specify
+values for the following settings:
+
+* ``//third_party/freertos:port``, to set which FreeRTOS port to use. You can
+ select a value from those defined in ``third_party/freertos/BUILD.bazel``.
+* ``//third_party/freertos:disable_task_statics_setting``, to determine
+ whether statics should be disabled during compilation of the tasks.c source
+ file (see next section). This setting has only two possible values, also
+ defined in ``third_party/freertos/BUILD.bazel``.
+
+In addition, you need to set the ``@pigweed_config//:freertos_config`` label
+flag to point to the library target providing the FreeRTOS config header. See
+:ref:`docs-build_system-bazel_configuration` for a discussion of how to work
+with ``@pigweed_config``.
+
.. _third_party-freertos_disable_task_statics:
@@ -48,10 +67,12 @@
extern "C", statics can be optionally disabled for the tasks.c source file
to enable use of things like pw_thread_freertos/util.h's ``ForEachThread``.
-To facilitate this, Pigweed offers an opt-in option which can be enabled by
-configuring GN through
-``pw_third_party_freertos_DISABLE_TASKS_STATICS = true`` or CMake through
-``set(pw_third_party_freertos_DISABLE_TASKS_STATICS ON CACHE BOOL "" FORCE)``.
+To facilitate this, Pigweed offers an opt-in option which can be enabled,
+
+* in GN through ``pw_third_party_freertos_DISABLE_TASKS_STATICS = true``,
+* in CMake through ``set(pw_third_party_freertos_DISABLE_TASKS_STATICS ON CACHE BOOL "" FORCE)``,
+* in Bazel through ``//third_party/freertos:disable_task_statics``.
+
This redefines ``static`` to nothing for the ``Source/tasks.c`` FreeRTOS source
file when building through ``$dir_pw_third_party/freertos`` in GN and through
``pw_third_party.freertos`` in CMake.
diff --git a/third_party/micro_ecc/BUILD.gn b/third_party/micro_ecc/BUILD.gn
index 4ead9fd..39cfd09 100644
--- a/third_party/micro_ecc/BUILD.gn
+++ b/third_party/micro_ecc/BUILD.gn
@@ -29,8 +29,37 @@
defines = [ "uECC_SUPPORT_COMPRESSED_POINT=0" ]
}
+ # Endianess is a public configuration for uECC as it determines how large
+ # integers are interpreted in uECC public APIs.
+ #
+ # Big endian is a lot more common and thus is recommended unless you are
+ # really resource-constrained or another uECC client expects little
+ # endian.
+ config("big_endian_config") {
+ defines = [ "uECC_VLI_NATIVE_LITTLE_ENDIAN=0" ]
+ }
+
+ # Little endian can reduce call stack usage in native little endian
+ # execution environments (as determined by processor state, memory
+ # access config etc.)
+ config("little_endian_config") {
+ defines = [ "uECC_VLI_NATIVE_LITTLE_ENDIAN=1" ]
+ }
+
pw_source_set("micro_ecc") {
- public_configs = [ ":public_config" ]
+ public_configs = [
+ ":big_endian_config",
+ ":public_config",
+ ]
+ configs = [ ":internal_config" ]
+ sources = [ "$dir_pw_third_party_micro_ecc/uECC.c" ]
+ }
+
+ pw_source_set("micro_ecc_little_endian") {
+ public_configs = [
+ ":little_endian_config",
+ ":public_config",
+ ]
configs = [ ":internal_config" ]
sources = [ "$dir_pw_third_party_micro_ecc/uECC.c" ]
}