pw_router: Add module and static router implementation
This adds a pw_router module for routing packets over network links.
Initially, this module contains a basic StaticRouter which uses a static
routing table.
Change-Id: I5534c7fec5c622e7af6548c48793ff7f7d6dd098
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/29440
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index e9aca66..e5f94c0 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -280,6 +280,7 @@
"$dir_pw_random:tests",
"$dir_pw_result:tests",
"$dir_pw_ring_buffer:tests",
+ "$dir_pw_router:tests",
"$dir_pw_rpc:tests",
"$dir_pw_span:tests",
"$dir_pw_status:tests",
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ff37967..20870ee 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -48,6 +48,7 @@
add_subdirectory(pw_preprocessor EXCLUDE_FROM_ALL)
add_subdirectory(pw_random EXCLUDE_FROM_ALL)
add_subdirectory(pw_result EXCLUDE_FROM_ALL)
+add_subdirectory(pw_router EXCLUDE_FROM_ALL)
add_subdirectory(pw_rpc EXCLUDE_FROM_ALL)
add_subdirectory(pw_span EXCLUDE_FROM_ALL)
add_subdirectory(pw_status EXCLUDE_FROM_ALL)
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index 7c2f2bf..644a32b 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -93,6 +93,7 @@
"$dir_pw_random:docs",
"$dir_pw_result:docs",
"$dir_pw_ring_buffer:docs",
+ "$dir_pw_router:docs",
"$dir_pw_rpc:docs",
"$dir_pw_span:docs",
"$dir_pw_status:docs",
diff --git a/modules.gni b/modules.gni
index 49679be..337a7fb 100644
--- a/modules.gni
+++ b/modules.gni
@@ -70,6 +70,7 @@
dir_pw_random = get_path_info("pw_random", "abspath")
dir_pw_result = get_path_info("pw_result", "abspath")
dir_pw_ring_buffer = get_path_info("pw_ring_buffer", "abspath")
+ dir_pw_router = get_path_info("pw_router", "abspath")
dir_pw_rpc = get_path_info("pw_rpc", "abspath")
dir_pw_span = get_path_info("pw_span", "abspath")
dir_pw_status = get_path_info("pw_status", "abspath")
diff --git a/pw_router/BUILD b/pw_router/BUILD
new file mode 100644
index 0000000..f6f33c6
--- /dev/null
+++ b/pw_router/BUILD
@@ -0,0 +1,63 @@
+# Copyright 2021 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",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_library(
+ name = "static_router",
+ hdrs = ["public/pw_router/static_router.h"],
+ srcs = ["static_router.cc"],
+ deps = [
+ ":egress",
+ ":packet_parser",
+ "//pw_log",
+ "//pw_metric",
+ "//pw_sync:mutex",
+ ],
+)
+
+pw_cc_library(
+ name = "egress",
+ hdrs = ["public/pw_router/egress.h"],
+ deps = ["//pw_bytes"],
+)
+
+pw_cc_library(
+ name = "packet_parser",
+ hdrs = ["public/pw_router/packet_parser.h"],
+ deps = ["//pw_bytes"],
+)
+
+pw_cc_library(
+ name = "egress_function",
+ hdrs = ["public/pw_router/egress_function.h"],
+ deps = [":egress"],
+)
+
+pw_cc_test(
+ name = "static_router_test",
+ srcs = ["static_router_test.cc"],
+ deps = [
+ ":egress_function",
+ ":static_router",
+ ],
+)
diff --git a/pw_router/BUILD.gn b/pw_router/BUILD.gn
new file mode 100644
index 0000000..867f871
--- /dev/null
+++ b/pw_router/BUILD.gn
@@ -0,0 +1,88 @@
+# Copyright 2021 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_bloat/bloat.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("static_router") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":egress",
+ ":packet_parser",
+ "$dir_pw_sync:mutex",
+ dir_pw_metric,
+ ]
+ public = [ "public/pw_router/static_router.h" ]
+ sources = [ "static_router.cc" ]
+ deps = [ dir_pw_log ]
+}
+
+pw_source_set("egress") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_router/egress.h" ]
+ public_deps = [ dir_pw_bytes ]
+}
+
+pw_source_set("packet_parser") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_router/packet_parser.h" ]
+ public_deps = [ dir_pw_bytes ]
+}
+
+pw_source_set("egress_function") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_router/egress_function.h" ]
+ public_deps = [ ":egress" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+ # TODO(frolv): This size report can't currently be built as the docs target
+ # does not have a mutex backend.
+ # report_deps = [ ":static_router_size" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":static_router_test" ]
+}
+
+pw_test("static_router_test") {
+ deps = [
+ ":egress_function",
+ ":static_router",
+ ]
+ sources = [ "static_router_test.cc" ]
+ enable_if = pw_sync_MUTEX_BACKEND != ""
+}
+
+pw_size_report("static_router_size") {
+ title = "pw::router::StaticRouter size report"
+ binaries = [
+ {
+ target = "size_report:static_router_with_one_route"
+ base = "size_report:base"
+ label = "Static router with a single route"
+ },
+ ]
+}
diff --git a/pw_router/CMakeLists.txt b/pw_router/CMakeLists.txt
new file mode 100644
index 0000000..cf80e79
--- /dev/null
+++ b/pw_router/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Copyright 2021 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_module_library(pw_router.static_router
+ SOURCES
+ static_router.cc
+ PUBLIC_DEPS
+ pw_metric
+ pw_router.egress
+ pw_router.packet_parser
+ pw_sync.mutex
+ PRIVATE_DEPS
+ pw_log
+)
+
+pw_add_module_library(pw_router.egress
+ PUBLIC_DEPS
+ pw_bytes
+)
+
+pw_add_module_library(pw_router.packet_parser
+ PUBLIC_DEPS
+ pw_bytes
+)
+
+pw_add_module_library(pw_router.egress_function
+ PUBLIC_DEPS
+ pw_rpc.egress
+)
+
+pw_auto_add_module_tests(pw_router
+ PRIVATE_DEPS
+ pw_router.static_router
+)
diff --git a/pw_router/docs.rst b/pw_router/docs.rst
new file mode 100644
index 0000000..76fec30
--- /dev/null
+++ b/pw_router/docs.rst
@@ -0,0 +1,65 @@
+.. _module-pw_router:
+
+---------
+pw_router
+---------
+The ``pw_router`` module provides transport-agnostic classes for routing packets
+over network links.
+
+Common router interfaces
+========================
+
+PacketParser
+------------
+To work with arbitrary packet formats, routers require a common interface for
+extracting relevant packet data, such as the destination. This interface is
+``pw::router::PacketParser``, defined in ``pw_router/packet_parser.h``, which
+must be implemented for the packet framing format used by the network.
+
+Egress
+------
+The Egress class is a virtual interface for sending packet data over a network
+link. Egress implementations provide a single ``SendPacket`` function, which
+takes the raw packet data and transmits it.
+
+Some common egress implementations are provided upstream in Pigweed.
+
+StaticRouter
+============
+``pw::router::StaticRouter`` is a router with a static table of address to
+egress mappings. Routes in a static router never change; packets with the same
+address are always sent through the same egress. If links are unavailable,
+packets will be dropped.
+
+Static routers are suitable for basic networks with persistent links.
+
+Usage example
+-------------
+
+.. code-block:: c++
+
+ namespace {
+
+ // Define packet parser and egresses.
+ HdlcFrameParser hdlc_parser;
+ UartEgress uart_egress;
+ BluetoothEgress ble_egress;
+
+ // Define the routing table.
+ constexpr pw::router::StaticRouter::Route routes[] = {{1, uart_egress},
+ {7, ble_egress}};
+ pw::router::StaticRouter router(hdlc_parser, routes);
+
+ } // namespace
+
+ void ProcessPacket(pw::ConstByteSpan packet) {
+ router.RoutePacket(packet);
+ }
+
+.. TODO(frolv): Re-enable this when the size report builds.
+.. Size report
+.. -----------
+.. The following size report shows the cost of a ``StaticRouter`` with a simple
+.. ``PacketParser`` implementation and a single route using an ``EgressFunction``.
+
+.. .. include:: static_router_size
diff --git a/pw_router/public/pw_router/egress.h b/pw_router/public/pw_router/egress.h
new file mode 100644
index 0000000..09097e0
--- /dev/null
+++ b/pw_router/public/pw_router/egress.h
@@ -0,0 +1,35 @@
+// Copyright 2021 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 <span>
+
+#include "pw_bytes/span.h"
+#include "pw_status/status.h"
+
+namespace pw::router {
+
+// Data egress for a router to send packets over some transport system.
+class Egress {
+ public:
+ virtual ~Egress() = default;
+
+ // Sends a complete packet/frame over the transport. Returns OK on success, or
+ // an error status on failure.
+ //
+ // TODO(frolv): Document possible return values.
+ virtual Status SendPacket(ConstByteSpan packet) = 0;
+};
+
+} // namespace pw::router
diff --git a/pw_router/public/pw_router/egress_function.h b/pw_router/public/pw_router/egress_function.h
new file mode 100644
index 0000000..e766766
--- /dev/null
+++ b/pw_router/public/pw_router/egress_function.h
@@ -0,0 +1,33 @@
+// Copyright 2021 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 <span>
+
+#include "pw_router/egress.h"
+
+namespace pw::router {
+
+// Router egress that dispatches to a free function.
+class EgressFunction final : public Egress {
+ public:
+ constexpr EgressFunction(Status (*func)(ConstByteSpan)) : func_(*func) {}
+
+ Status SendPacket(ConstByteSpan packet) final { return func_(packet); }
+
+ private:
+ Status (&func_)(ConstByteSpan);
+};
+
+} // namespace pw::router
diff --git a/pw_router/public/pw_router/packet_parser.h b/pw_router/public/pw_router/packet_parser.h
new file mode 100644
index 0000000..c4ecc8a
--- /dev/null
+++ b/pw_router/public/pw_router/packet_parser.h
@@ -0,0 +1,46 @@
+// Copyright 2021 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 <optional>
+#include <span>
+
+#include "pw_bytes/span.h"
+
+namespace pw::router {
+
+// A PacketParser is an abstract interface for extracting data from different
+// kinds of transport layer packets or frames. It is used by routers to examine
+// fields within packets to know how to route them.
+class PacketParser {
+ public:
+ virtual ~PacketParser() = default;
+
+ // Parses a packet, storing its data for subsequent calls to Get* functions.
+ // Any currently stored packet is cleared. Returns true if successful, or
+ // false if the packet is incomplete or corrupt.
+ //
+ // The raw binary data passed to this function is guaranteed to remain valid
+ // through all subsequent Get* calls made for the packet's information, so
+ // implementations may store and use it directly.
+ virtual bool Parse(ConstByteSpan packet) = 0;
+
+ // Extracts the destination address the last parsed packet, if it exists.
+ //
+ // Guaranteed to only be called if Parse() succeeded and while the data passed
+ // to Parse() is valid.
+ virtual std::optional<uint32_t> GetDestinationAddress() const = 0;
+};
+
+} // namespace pw::router
diff --git a/pw_router/public/pw_router/static_router.h b/pw_router/public/pw_router/static_router.h
new file mode 100644
index 0000000..6697456
--- /dev/null
+++ b/pw_router/public/pw_router/static_router.h
@@ -0,0 +1,77 @@
+// Copyright 2021 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 <span>
+
+#include "pw_bytes/span.h"
+#include "pw_metric/metric.h"
+#include "pw_router/egress.h"
+#include "pw_router/packet_parser.h"
+#include "pw_status/status.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::router {
+
+// A packet router with a static routing table.
+//
+// Thread-safety:
+// Internal packet parsing and calls to the provided PacketParser are
+// synchronized. Synchronization at the egress level must be implemented by
+// derived egresses.
+//
+class StaticRouter {
+ public:
+ struct Route {
+ // TODO(frolv): Consider making address size configurable.
+ uint32_t address;
+ Egress& egress;
+ };
+
+ StaticRouter(PacketParser& parser, std::span<const Route> routes)
+ : parser_(parser), routes_(routes) {}
+
+ StaticRouter(const StaticRouter&) = delete;
+ StaticRouter(StaticRouter&&) = delete;
+ StaticRouter& operator=(const StaticRouter&) = delete;
+ StaticRouter& operator=(StaticRouter&&) = delete;
+
+ uint32_t dropped_packets() const {
+ return parser_errors_.value() + route_errors_.value() +
+ egress_errors_.value();
+ }
+
+ const metric::Group& metrics() { return metrics_; }
+
+ // Routes a single packet through the appropriate egress.
+ // Returns one of the following to indicate a router-side error:
+ //
+ // OK - Packet sent successfully.
+ // DATA_LOSS - Packet corrupt or incomplete.
+ // NOT_FOUND - No registered route for the packet.
+ // UNAVAILABLE - Route egress did not accept packet.
+ //
+ Status RoutePacket(ConstByteSpan packet);
+
+ private:
+ PacketParser& parser_;
+ std::span<const Route> routes_;
+ sync::Mutex mutex_;
+ PW_METRIC_GROUP(metrics_, "static_router");
+ PW_METRIC(metrics_, parser_errors_, "parser_errors", 0u);
+ PW_METRIC(metrics_, route_errors_, "route_errors", 0u);
+ PW_METRIC(metrics_, egress_errors_, "egress_errors", 0u);
+};
+
+} // namespace pw::router
diff --git a/pw_router/size_report/BUILD b/pw_router/size_report/BUILD
new file mode 100644
index 0000000..f90c83b
--- /dev/null
+++ b/pw_router/size_report/BUILD
@@ -0,0 +1,45 @@
+# Copyright 2021 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_binary",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_binary(
+ name = "base",
+ srcs = ["base.cc"],
+ deps = [
+ "//pw_assert",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_log",
+ "//pw_sys_io",
+ ],
+)
+
+pw_cc_binary(
+ name = "static_router_with_one_route",
+ srcs = ["static_router_with_one_route.cc"],
+ deps = [
+ "//pw_assert",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_log",
+ "//pw_router:static_router",
+ "//pw_sys_io",
+ ],
+)
diff --git a/pw_router/size_report/BUILD.gn b/pw_router/size_report/BUILD.gn
new file mode 100644
index 0000000..e30430e
--- /dev/null
+++ b/pw_router/size_report/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2021 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")
+
+_common_deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ dir_pw_assert,
+ dir_pw_log,
+ dir_pw_sys_io,
+]
+
+pw_executable("base") {
+ sources = [ "base.cc" ]
+ deps = _common_deps
+}
+
+pw_executable("static_router_with_one_route") {
+ sources = [ "static_router_with_one_route.cc" ]
+ deps = _common_deps + [ "..:static_router" ]
+}
diff --git a/pw_router/size_report/base.cc b/pw_router/size_report/base.cc
new file mode 100644
index 0000000..8d63f6f
--- /dev/null
+++ b/pw_router/size_report/base.cc
@@ -0,0 +1,49 @@
+// Copyright 2021 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_assert/assert.h"
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_log/log.h"
+#include "pw_sys_io/sys_io.h"
+
+namespace {
+
+struct BasicPacket {
+ static constexpr uint32_t kMagic = 0x8badf00d;
+
+ constexpr BasicPacket(uint32_t addr, uint64_t data)
+ : magic(kMagic), address(addr), payload(data) {}
+
+ uint32_t magic;
+ uint32_t address;
+ uint64_t payload;
+};
+
+} // namespace
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ // Ensure we are paying the cost for log and assert.
+ BasicPacket packet(0x1, 0x2);
+ PW_CHECK_UINT_EQ(packet.magic, BasicPacket::kMagic, "Some CHECK logic");
+ PW_LOG_INFO("Packet has address %u", static_cast<unsigned>(packet.address));
+ PW_LOG_INFO("pw_StatusString %s", pw::OkStatus().str());
+
+ std::array<std::byte, sizeof(BasicPacket)> packet_buffer;
+ pw::sys_io::ReadBytes(packet_buffer);
+ pw::sys_io::WriteBytes(packet_buffer);
+
+ return static_cast<int>(packet.payload);
+}
diff --git a/pw_router/size_report/static_router_with_one_route.cc b/pw_router/size_report/static_router_with_one_route.cc
new file mode 100644
index 0000000..a285954
--- /dev/null
+++ b/pw_router/size_report/static_router_with_one_route.cc
@@ -0,0 +1,85 @@
+// Copyright 2021 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_assert/assert.h"
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_log/log.h"
+#include "pw_router/egress_function.h"
+#include "pw_router/static_router.h"
+#include "pw_sys_io/sys_io.h"
+
+namespace {
+
+struct BasicPacket {
+ static constexpr uint32_t kMagic = 0x8badf00d;
+
+ constexpr BasicPacket(uint32_t addr, uint64_t data)
+ : magic(kMagic), address(addr), payload(data) {}
+
+ uint32_t magic;
+ uint32_t address;
+ uint64_t payload;
+};
+
+} // namespace
+
+// All the new router-specific stuff.
+namespace {
+
+class BasicPacketParser : public pw::router::PacketParser {
+ public:
+ constexpr BasicPacketParser() : packet_(nullptr) {}
+
+ bool Parse(pw::ConstByteSpan packet) final {
+ packet_ = reinterpret_cast<const BasicPacket*>(packet.data());
+ return packet_->magic == BasicPacket::kMagic;
+ }
+
+ std::optional<uint32_t> GetDestinationAddress() const final {
+ return packet_->address;
+ }
+
+ private:
+ const BasicPacket* packet_;
+};
+
+BasicPacketParser parser;
+pw::router::EgressFunction sys_io_egress(+[](pw::ConstByteSpan packet) {
+ return pw::sys_io::WriteBytes(packet).status();
+});
+constexpr pw::router::StaticRouter::Route routes[] = {{1, sys_io_egress}};
+pw::router::StaticRouter router(parser, routes);
+
+} // namespace
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ // Ensure we are paying the cost for log and assert.
+ BasicPacket packet(0x1, 0x2);
+ PW_CHECK_UINT_EQ(packet.magic, BasicPacket::kMagic, "Some CHECK logic");
+ PW_LOG_INFO("Packet has address %u", static_cast<unsigned>(packet.address));
+ PW_LOG_INFO("pw_StatusString %s", pw::OkStatus().str());
+
+ std::array<std::byte, sizeof(BasicPacket)> packet_buffer;
+ pw::sys_io::ReadBytes(packet_buffer);
+ pw::sys_io::WriteBytes(packet_buffer);
+
+ while (true) {
+ pw::sys_io::ReadBytes(packet_buffer);
+ router.RoutePacket(packet_buffer);
+ }
+
+ return static_cast<int>(packet.payload);
+}
diff --git a/pw_router/static_router.cc b/pw_router/static_router.cc
new file mode 100644
index 0000000..72c7242
--- /dev/null
+++ b/pw_router/static_router.cc
@@ -0,0 +1,73 @@
+// Copyright 2021 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_router/static_router.h"
+
+#include <algorithm>
+#include <mutex>
+
+#include "pw_log/log.h"
+
+namespace pw::router {
+
+Status StaticRouter::RoutePacket(ConstByteSpan packet) {
+ uint32_t address;
+
+ {
+ // Only packet parsing is synchronized within the router; egresses must be
+ // synchronized externally.
+ std::lock_guard lock(mutex_);
+
+ if (!parser_.Parse(packet)) {
+ PW_LOG_ERROR("StaticRouter failed to parse packet; dropping");
+ parser_errors_.Increment();
+ return Status::DataLoss();
+ }
+
+ std::optional<uint32_t> result = parser_.GetDestinationAddress();
+ if (!result.has_value()) {
+ PW_LOG_ERROR("StaticRouter packet does not have address; dropping");
+ parser_errors_.Increment();
+ return Status::DataLoss();
+ }
+
+ address = result.value();
+ }
+
+ auto route = std::find_if(routes_.begin(), routes_.end(), [&](auto r) {
+ return r.address == address;
+ });
+ if (route == routes_.end()) {
+ PW_LOG_ERROR("StaticRouter no route for address %u; dropping packet",
+ static_cast<unsigned>(address));
+ route_errors_.Increment();
+ return Status::NotFound();
+ }
+
+ PW_LOG_DEBUG("StaticRouter routing %u-byte packet to address %u",
+ static_cast<unsigned>(packet.size()),
+ static_cast<unsigned>(address));
+
+ if (Status status = route->egress.SendPacket(packet); !status.ok()) {
+ PW_LOG_ERROR("StaticRouter egress error for address %u: %s",
+ static_cast<unsigned>(address),
+ status.str());
+ egress_errors_.Increment();
+ return Status::Unavailable();
+ }
+
+ return OkStatus();
+}
+
+} // namespace pw::router
diff --git a/pw_router/static_router_test.cc b/pw_router/static_router_test.cc
new file mode 100644
index 0000000..72ca116
--- /dev/null
+++ b/pw_router/static_router_test.cc
@@ -0,0 +1,116 @@
+// Copyright 2021 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_router/static_router.h"
+
+#include "gtest/gtest.h"
+#include "pw_router/egress_function.h"
+
+namespace pw::router {
+namespace {
+
+struct BasicPacket {
+ static constexpr uint32_t kMagic = 0x8badf00d;
+
+ constexpr BasicPacket(uint32_t addr, uint64_t data)
+ : magic(kMagic), address(addr), payload(data) {}
+
+ ConstByteSpan data() const { return std::as_bytes(std::span(this, 1)); }
+
+ uint32_t magic;
+ uint32_t address;
+ uint64_t payload;
+};
+
+class BasicPacketParser : public PacketParser {
+ public:
+ constexpr BasicPacketParser() : packet_(nullptr) {}
+
+ bool Parse(pw::ConstByteSpan packet) final {
+ packet_ = reinterpret_cast<const BasicPacket*>(packet.data());
+ return packet_->magic == BasicPacket::kMagic;
+ }
+
+ std::optional<uint32_t> GetDestinationAddress() const final {
+ PW_DCHECK_NOTNULL(packet_);
+ return packet_->address;
+ }
+
+ private:
+ const BasicPacket* packet_;
+};
+
+EgressFunction GoodEgress(+[](ConstByteSpan) { return OkStatus(); });
+EgressFunction BadEgress(+[](ConstByteSpan) {
+ return Status::ResourceExhausted();
+});
+
+TEST(StaticRouter, RoutePacket_RoutesToAnEgress) {
+ BasicPacketParser parser;
+ constexpr StaticRouter::Route routes[] = {{1, GoodEgress}, {2, BadEgress}};
+ StaticRouter router(parser, std::span(routes));
+
+ EXPECT_EQ(router.RoutePacket(BasicPacket(1, 0xdddd).data()), OkStatus());
+ EXPECT_EQ(router.RoutePacket(BasicPacket(2, 0xdddd).data()),
+ Status::Unavailable());
+}
+
+TEST(StaticRouter, RoutePacket_ReturnsParserError) {
+ BasicPacketParser parser;
+ constexpr StaticRouter::Route routes[] = {{1, GoodEgress}, {2, BadEgress}};
+ StaticRouter router(parser, std::span(routes));
+
+ BasicPacket bad_magic(1, 0xdddd);
+ bad_magic.magic = 0x1badda7a;
+ EXPECT_EQ(router.RoutePacket(bad_magic.data()), Status::DataLoss());
+}
+
+TEST(StaticRouter, RoutePacket_ReturnsNotFoundOnInvalidRoute) {
+ BasicPacketParser parser;
+ constexpr StaticRouter::Route routes[] = {{1, GoodEgress}, {2, BadEgress}};
+ StaticRouter router(parser, std::span(routes));
+
+ EXPECT_EQ(router.RoutePacket(BasicPacket(42, 0xdddd).data()),
+ Status::NotFound());
+}
+
+TEST(StaticRouter, RoutePacket_TracksNumberOfDrops) {
+ BasicPacketParser parser;
+ constexpr StaticRouter::Route routes[] = {{1, GoodEgress}, {2, BadEgress}};
+ StaticRouter router(parser, std::span(routes));
+
+ // Good
+ EXPECT_EQ(router.RoutePacket(BasicPacket(1, 0xdddd).data()), OkStatus());
+
+ // Egress error
+ EXPECT_EQ(router.RoutePacket(BasicPacket(2, 0xdddd).data()),
+ Status::Unavailable());
+
+ // Parser error
+ BasicPacket bad_magic(1, 0xdddd);
+ bad_magic.magic = 0x1badda7a;
+ EXPECT_EQ(router.RoutePacket(bad_magic.data()), Status::DataLoss());
+
+ // Good
+ EXPECT_EQ(router.RoutePacket(BasicPacket(1, 0xdddd).data()), OkStatus());
+
+ // Bad route
+ EXPECT_EQ(router.RoutePacket(BasicPacket(42, 0xdddd).data()),
+ Status::NotFound());
+
+ EXPECT_EQ(router.dropped_packets(), 3u);
+}
+
+} // namespace
+} // namespace pw::router