bazel: Use Pigweed host_device_simulator and update console

- Switch to using Pigweed upstream host_device_simulator_binary
- :simulator_console targets start the simulator binary now.
- Add pw_console.yaml config with a default window layout.

Bug: 343326881
Change-Id: I7f38087cbb565199bd7943bfd2faf7bd85705075
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/examples/+/214872
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Reviewed-by: Taylor Cramer <cramertj@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/.bazelrc b/.bazelrc
index add8b43..f7097cb 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -38,13 +38,20 @@
   //... \
   //examples/01_blinky:blinky \
   //examples/01_blinky:rp2040_blinky.elf \
+  //examples/01_blinky:simulator_blinky \
+  //examples/01_blinky:simulator_console \
   //examples/01_blinky:stm32_blinky.elf \
-  //examples/02_unit_testing:test_runner_app \
   //examples/02_unit_testing:rp2040_test_runner_app.elf \
   //examples/02_unit_testing:stm32_test_runner_app.elf \
-  //examples/03_rpc:rpc_main \
+  //examples/02_unit_testing:test_runner_app \
   //examples/03_rpc:rp2040_rpc_main.elf \
-  //examples/03_rpc:stm32_rpc_main.elf
+  //examples/03_rpc:rpc_main \
+  //examples/03_rpc:simulator_console \
+  //examples/03_rpc:simulator_rpc \
+  //examples/03_rpc:stm32_rpc_main.elf \
+  //tools:console \
+  //tools:device_sim
+
 
 # Don't automatically create __init__.py files.
 #
diff --git a/.pw_console.yaml b/.pw_console.yaml
new file mode 100644
index 0000000..2d8840e
--- /dev/null
+++ b/.pw_console.yaml
@@ -0,0 +1,32 @@
+# This is a pw_console config file that defines a default window layout.
+# For more info on what can be added to this file see:
+#   https://pigweed.dev/pw_console/py/pw_console/docs/user_guide.html#configuration
+config_title: pw_console
+
+# Window layout
+windows:
+  # Left split with tabbed views.
+  Split 1 tabbed:
+    Python Repl:
+      hidden: False
+  # Right split with tabbed views.
+  Split 2 stacked:
+    Device Logs:
+      hidden: False
+    Host Logs:
+      hidden: False
+
+# Color options
+ui_theme: dark
+# ui_theme: nord
+# ui_theme: high-contrast-dark
+code_theme: pigweed-code
+# code_theme: pigweed-code-light
+# code_theme: gruvbox-dark
+swap_light_and_dark: False
+
+# Display options
+spaces_between_columns: 2
+window_column_split_method: vertical
+# window_column_split_method: horizontal
+# hide_date_from_log_time: True
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 0000000..55b0628
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1,22 @@
+# Copyright 2024 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.
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "pw_console_config",
+    srcs = [
+        ".pw_console.yaml",
+    ],
+)
diff --git a/examples/01_blinky/BUILD.bazel b/examples/01_blinky/BUILD.bazel
index 2af263c..2b5e36c 100644
--- a/examples/01_blinky/BUILD.bazel
+++ b/examples/01_blinky/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@bazel_skylib//rules:native_binary.bzl", "native_binary")
+load("@pigweed//pw_system/py:console.bzl", "device_simulator_console")
+load("@pigweed//targets/host_device_simulator:transition.bzl", "host_device_simulator_binary")
 load("//targets:transition.bzl", "rp2040_binary", "stm32_binary")
 
 package(default_visibility = ["//visibility:public"])
@@ -60,6 +62,26 @@
         "115200",
         "--token-databases",
         "$(rootpath //examples/01_blinky:stm32_blinky.elf)",
+        "--config-file",
+        "$(rootpath //:pw_console_config)",
     ],
-    data = [":stm32_blinky.elf"],
+    data = [
+        ":stm32_blinky.elf",
+        "//:pw_console_config",
+    ],
+)
+
+# Create a host binary using the Pigweed upstream pw_system host_device_simulator.
+host_device_simulator_binary(
+    name = "simulator_blinky",
+    binary = ":blinky",
+)
+
+# Start :simulator_blinky and connect to it with pw console by running:
+#   bazel run //examples/01_blinky:simulator_console
+device_simulator_console(
+    name = "simulator_console",
+    host_binary = ":simulator_blinky",
+    pw_console_config = "//:pw_console_config",
+    script = "//tools:device_sim",
 )
diff --git a/examples/01_blinky/docs.rst b/examples/01_blinky/docs.rst
index e7f97c7..1f5921c 100644
--- a/examples/01_blinky/docs.rst
+++ b/examples/01_blinky/docs.rst
@@ -89,9 +89,15 @@
 
 #. Launch ``blinky`` using the ``pw device-sim`` helper.
 
-   .. code-block::
+   .. code-block:: sh
 
-      pw device-sim ./out/gn/host_device_simulator.speed_optimized/obj/examples/01_blinky/bin/blinky
+      pw device-sim --sim-binary ./out/gn/host_device_simulator.speed_optimized/obj/examples/01_blinky/bin/blinky
+
+   If using Bazel launch the simulator with ``bazel run``:
+
+   .. code-block:: sh
+
+      bazel run //examples/01_blinky:simulator_console
 
 #. When you're finished, you can type ``quit`` in the ``Python Repl`` pane to
    exit.
@@ -99,8 +105,6 @@
 -------------------
 Building with Bazel
 -------------------
-In general, the sample project doesn't support Bazel yet. But this example does!
-
 To build and flash the firmware to the device run,
 
 .. code-block:: sh
diff --git a/examples/02_unit_testing/docs.rst b/examples/02_unit_testing/docs.rst
index b432d5e..33707a1 100644
--- a/examples/02_unit_testing/docs.rst
+++ b/examples/02_unit_testing/docs.rst
@@ -72,7 +72,7 @@
 
    .. code-block::
 
-      pw device-sim ./out/gn/host_device_simulator.speed_optimized/obj/examples/02_unit_testing/bin/test_runner_app
+      pw device-sim --sim-binary ./out/gn/host_device_simulator.speed_optimized/obj/examples/02_unit_testing/bin/test_runner_app
 
 #. In the ``Python Repl`` pane, run the tests.
 
diff --git a/examples/03_rpc/BUILD.bazel b/examples/03_rpc/BUILD.bazel
index e6cbe6c..9b30220 100644
--- a/examples/03_rpc/BUILD.bazel
+++ b/examples/03_rpc/BUILD.bazel
@@ -18,6 +18,8 @@
     "pw_proto_filegroup",
     "pw_proto_library",
 )
+load("@pigweed//pw_system/py:console.bzl", "device_simulator_console")
+load("@pigweed//targets/host_device_simulator:transition.bzl", "host_device_simulator_binary")
 load("@rules_python//python:proto.bzl", "py_proto_library")
 load("//targets:transition.bzl", "rp2040_binary", "stm32_binary")
 
@@ -100,6 +102,26 @@
         "115200",
         "--token-databases",
         "$(rootpath //examples/03_rpc:stm32_rpc_main.elf)",
+        "--config-file",
+        "$(rootpath //:pw_console_config)",
     ],
-    data = [":stm32_rpc_main.elf"],
+    data = [
+        ":stm32_rpc_main.elf",
+        "//:pw_console_config",
+    ],
+)
+
+# Create a host binary using the Pigweed upstream pw_system host_device_simulator.
+host_device_simulator_binary(
+    name = "simulator_rpc",
+    binary = ":rpc_main",
+)
+
+# Start :simulator_rpc and connect to it with pw console by running:
+#   bazel run //examples/03_rpc:simulator_console
+device_simulator_console(
+    name = "simulator_console",
+    host_binary = ":simulator_rpc",
+    pw_console_config = "//:pw_console_config",
+    script = "//tools:device_sim",
 )
diff --git a/examples/03_rpc/docs.rst b/examples/03_rpc/docs.rst
index 6ee746d..43b6876 100644
--- a/examples/03_rpc/docs.rst
+++ b/examples/03_rpc/docs.rst
@@ -59,7 +59,13 @@
 
    .. code-block:: sh
 
-      pw device-sim ./out/gn/host_device_simulator.speed_optimized/obj/examples/03_rpc/bin/rpc_main
+      pw device-sim --sim-binary ./out/gn/host_device_simulator.speed_optimized/obj/examples/03_rpc/bin/rpc_main
+
+   If using Bazel launch the simulator with ``bazel run``:
+
+   .. code-block:: sh
+
+      bazel run //examples/03_rpc:simulator_console
 
 #. In the ``Python Repl`` pane, use an RPC to request the device's UUID.
 
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
index 9e12b57..e4aac39 100644
--- a/tools/BUILD.bazel
+++ b/tools/BUILD.bazel
@@ -21,12 +21,30 @@
     srcs = ["sample_project_tools/flash_device.py"],
 )
 
+py_library(
+    name = "console_library",
+    srcs = ["sample_project_tools/console.py"],
+    imports = ["."],
+    deps = [
+        "//examples/03_rpc:rpc_example_protos_py_pb2",
+        "@pigweed//pw_protobuf:common_py_pb2",
+        "@pigweed//pw_rpc:echo_py_pb2",
+        "@pigweed//pw_system/py:pw_system_lib",
+    ],
+)
+
 py_binary(
     name = "console",
     srcs = ["sample_project_tools/console.py"],
     deps = [
-        "//examples/03_rpc:rpc_example_protos_py_pb2",
-        "@pigweed//pw_protobuf:common_py_pb2",
-        "@pigweed//pw_system/py:pw_system_lib",
+        ":console_library",
+    ],
+)
+
+py_binary(
+    name = "device_sim",
+    srcs = ["sample_project_tools/device_sim.py"],
+    deps = [
+        ":console_library",
     ],
 )
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index c3b0ca4..4361b56 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -34,6 +34,7 @@
     "$dir_pw_cli/py",
     "$dir_pw_presubmit/py",
     "$dir_pw_protobuf:common_protos.python",
+    "$dir_pw_rpc:protos.python",
     "$dir_pw_system/py",
     "//examples/03_rpc:rpc_example_protos.python",
   ]
diff --git a/tools/sample_project_tools/console.py b/tools/sample_project_tools/console.py
index c722c62..8780d6c 100644
--- a/tools/sample_project_tools/console.py
+++ b/tools/sample_project_tools/console.py
@@ -13,18 +13,17 @@
 # the License.
 """Wraps pw_system's console to inject additional RPC protos."""
 
-import argparse
 import sys
-from typing import Optional
 
 from pw_protobuf_protos import common_pb2
+from pw_rpc import echo_pb2
 import pw_system.console
 from rpc_example_protos import rpc_example_service_pb2
 
 
-def main(args: Optional[argparse.Namespace] = None) -> int:
-    compiled_protos = [common_pb2, rpc_example_service_pb2]
-    return pw_system.console.main_with_compiled_protos(compiled_protos, args)
+def main() -> int:
+    compiled_protos = [common_pb2, echo_pb2, rpc_example_service_pb2]
+    return pw_system.console.main_with_compiled_protos(compiled_protos)
 
 
 if __name__ == '__main__':
diff --git a/tools/sample_project_tools/device_sim.py b/tools/sample_project_tools/device_sim.py
index ad15aec..12e7025 100644
--- a/tools/sample_project_tools/device_sim.py
+++ b/tools/sample_project_tools/device_sim.py
@@ -1,4 +1,4 @@
-# Copyright 2023 The Pigweed Authors
+# Copyright 2024 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
@@ -11,55 +11,19 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-"""Launches a simulated device subprocess under pw_console."""
+"""Wraps pw_system's console to inject additional RPC protos."""
 
-import argparse
-from pathlib import Path
-import subprocess
 import sys
-import time
 
-from pw_system.console import get_parser
-from sample_project_tools import console
-
-
-def _parse_args() -> argparse.Namespace:
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        'sim_binary',
-        type=Path,
-        help='Path to a simulated device binary to run on the host',
-    )
-    return parser.parse_known_args()[0]
-
-
-def launch_sim(sim_binary: Path) -> int:
-    """Launches a host-device-sim binary, and attaches a console to it."""
-
-    sim_process = subprocess.Popen(
-        sim_binary,
-        stdin=subprocess.DEVNULL,
-        stdout=subprocess.DEVNULL,
-        stderr=subprocess.STDOUT,
-    )
-
-    # Give the sim a second to warm up and open the socket, otherwise the
-    # console will fail to connect and then abort.
-    time.sleep(0.5)
-
-    try:
-        retval = console.main(get_parser().parse_args(['-s', 'default']))
-    except:
-        sim_process.terminate()
-        raise
-
-    sim_process.terminate()
-
-    return retval
+from pw_protobuf_protos import common_pb2
+from pw_rpc import echo_pb2
+import pw_system.device_sim
+from rpc_example_protos import rpc_example_service_pb2
 
 
 def main() -> int:
-    return launch_sim(**vars(_parse_args()))
+    compiled_protos = [common_pb2, echo_pb2, rpc_example_service_pb2]
+    return pw_system.device_sim.main_with_compiled_protos(compiled_protos)
 
 
 if __name__ == '__main__':