pw_hil: Add uart_blaster for testing dhalsim uart block

Change-Id: I2b0d34674cb925a9e2e0c318582d5e963dd11eea
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/sandbox/+/459065
diff --git a/pw_hil/BUILD.bazel b/pw_hil/BUILD.bazel
index 712bd71..0ce42f6 100644
--- a/pw_hil/BUILD.bazel
+++ b/pw_hil/BUILD.bazel
@@ -13,6 +13,7 @@
 # the License.
 
 load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library")
+load("@rules_platform//platform_data:defs.bzl", "platform_data")
 load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
@@ -57,50 +58,43 @@
     deps = [":hil_proto"],
 )
 
-cc_library(
-    name = "pw_hil",
-    srcs = ["hil.cc"],
-    hdrs = ["public/pw_hil/hil.h"],
-    strip_include_prefix = "public",
-    visibility = ["//visibility:public"],
-    deps = [
-        ":hil_nanopb_rpc",
-        "//pw_allocator",
-        "//pw_async2",
-        "//pw_async2:time_provider",
-        "//pw_chrono:system_clock",
-        "//pw_containers:vector",
-        "//pw_log",
-        "//pw_status",
-        "//pw_string:format",
-        "//pw_string:string",
-        "//pw_sync:mutex",
-        "@pico-sdk//src/rp2_common/hardware_gpio",
-    ],
-)
-
 pw_cc_test(
-    name = "hil_test",
-    srcs = ["hil_test.cc"],
-    visibility = ["//visibility:public"],
+    name = "uart_test",
+    srcs = ["uart_test.cc"],
     deps = [
-        ":pw_hil",
-        "//pw_async2",
-        "//pw_async2:simulated_time_provider",
-        "//pw_async2:testing",
+        "//pw_thread:sleep",
+        "@pico-sdk//src/rp2_common/hardware_gpio",
+        "@pico-sdk//src/rp2_common/hardware_uart",
     ],
 )
 
-rp2350_binary(
-    name = "hil_test.elf",
-    testonly = True,
-    binary = ":hil_test",
+cc_binary(
+    name = "uart_blaster",
+    srcs = [
+        "rp2xxx_main.cc",
+        "uart_blaster.cc",
+    ],
+    deps = [
+        "//pw_allocator:best_fit",
+        "//pw_async2:system_time_provider",
+        "//pw_channel:rp2_stdio_channel",
+        "//pw_multibuf",
+        "//pw_system:async",
+        "//pw_thread:sleep",
+        "@pico-sdk//src/rp2_common/hardware_gpio",
+        "@pico-sdk//src/rp2_common/hardware_uart",
+    ],
+)
+
+platform_data(
+    name = "pigweed_hil_board_uart_blaster",
+    platform = "//targets/rp2040:pigweed_hil_board",
+    target = ":uart_blaster",
 )
 
 flash_rp2350(
-    name = "flash_hil_test",
-    testonly = True,
-    rp2350_binary = ":hil_test.elf",
+    name = "flash_uart_blaster",
+    rp2350_binary = ":pigweed_hil_board_uart_blaster",
 )
 
 sphinx_docs_library(
diff --git a/pw_hil/dhalsim/BUILD.bazel b/pw_hil/dhalsim/BUILD.bazel
index 071769d..01433d8 100644
--- a/pw_hil/dhalsim/BUILD.bazel
+++ b/pw_hil/dhalsim/BUILD.bazel
@@ -89,6 +89,7 @@
         "-v",
         "-b",
         "115200",
+        "--no-device-tracing",
         "--token-databases",
         "$(rootpath :pigweed_hil_board_dhalsim)",
         "--config-file",
diff --git a/pw_hil/dhalsim/dhalsim_app.cc b/pw_hil/dhalsim/dhalsim_app.cc
index ac460af..d464512 100644
--- a/pw_hil/dhalsim/dhalsim_app.cc
+++ b/pw_hil/dhalsim/dhalsim_app.cc
@@ -33,21 +33,13 @@
     event_buffer, log_buffer, pw::chrono::VirtualSystemClock::RealClock());
 static DhalsimService dhalsim_service;
 
-// inline constexpr ThreadPriority kThreadPriorityBlinky = ThreadPriority();
-
-// inline constexpr pw::ThreadAttrs kBlinkyThread =
-//     pw::ThreadAttrs()
-//         .set_stack_size_bytes(1024)
-//         .set_name("BlinkyThread")
-//         .set_priority(kThreadPriorityBlinky);
-
 class BlinkyTask : public pw::async2::Task {
  public:
   BlinkyTask(pw::async2::TimeProvider<chrono::SystemClock>& time_provider)
       : time_provider_(time_provider) {
-    gpio_init(26);
-    gpio_set_dir(26, GPIO_OUT);
-    gpio_put(26, 0);
+    gpio_init(LED_PIN);
+    gpio_set_dir(LED_PIN, GPIO_OUT);
+    gpio_put(LED_PIN, 0);
   }
 
  protected:
@@ -59,11 +51,12 @@
       }
 
       PW_AWAIT(time_future_, cx);
-      gpio_put(26, !gpio_get(26));
+      gpio_put(LED_PIN, !gpio_get(LED_PIN));
     }
   }
 
  private:
+  static constexpr uint32_t LED_PIN = 26;
   chrono::SystemClock::duration period_ = std::chrono::milliseconds(500);
   async2::TimeProvider<chrono::SystemClock>& time_provider_;
   async2::TimeFuture<chrono::SystemClock> time_future_;
@@ -80,23 +73,12 @@
                                 pw::System().allocator(),
                                 pw::hil::event_allocator);
   pw::System().rpc_server().RegisterService(pw::hil::dhalsim_service);
+  auto service_id = pw::hil::dhalsim_service.service_id();
+  PW_LOG_WARN("DhalsimService: %08x",
+              *reinterpret_cast<uint32_t*>(&service_id));
 
   static pw::hil::BlinkyTask blinky(pw::async2::GetSystemTimeProvider());
   pw::System().dispatcher().Post(blinky);
-
-  // PW_CONSTINIT static ThreadContextFor<::pw::hil::kBlinkyThread>
-  // blinky_thread; Thread(blinky_thread, []() {
-  //   gpio_init(24);
-  //   gpio_set_dir(24, GPIO_OUT);
-  //   gpio_put(24, 0);
-  //   while (true) {
-  //     gpio_put(24, 1);
-  //     pw::this_thread::sleep_for(std::chrono::milliseconds(500));
-  //     gpio_put(24, 0);
-  //     pw::this_thread::sleep_for(std::chrono::milliseconds(500));
-  //     // PW_LOG_INFO("blink");
-  //   }
-  // }).detach();
 }
 
 }  // namespace pw::system
diff --git a/pw_hil/hil.cc b/pw_hil/hil.cc
deleted file mode 100644
index 82989fe..0000000
--- a/pw_hil/hil.cc
+++ /dev/null
@@ -1,274 +0,0 @@
-// Copyright 2026 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.
-
-#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
-
-#include "pw_hil/hil.h"
-
-#include <algorithm>
-#include <cstdio>
-#include <cstring>
-
-#include "public/pw_hil/hil.h"
-#include "pw_log/log.h"
-
-namespace pw::hil {
-
-static EventStream* g_event_stream = nullptr;
-
-void LogGlobalEvent(uint64_t signal_id, const Value& value) {
-  if (g_event_stream != nullptr) {
-    g_event_stream->Log(signal_id, value);
-  }
-}
-
-void HilTesterService::SetGlobalEventStream(EventStream* stream) {
-  g_event_stream = stream;
-}
-
-constexpr uint64_t Fnv1aHash(std::string_view str) {
-  uint64_t hash = 14695981039346656037ULL;
-  for (char c : str) {
-    hash ^= static_cast<uint64_t>(c);
-    hash *= 1099511628211ULL;
-  }
-  return hash;
-}
-
-uint64_t GetSlotOrSignalId(uint64_t owner_id, std::string_view name) {
-  pw::InlineString<64> str;
-  (void)pw::string::Format(
-      str, "%llu_%s", static_cast<unsigned long long>(owner_id), name.data());
-  return Fnv1aHash(str);
-}
-
-pw::Status HilTesterService::CreateBlocks(
-    const pw_hil_CreateBlocksRequest& request, pw_hil_Empty&) {
-  PW_LOG_INFO("CreateBlocks: count=%d", request.blocks_count);
-  for (size_t i = 0; i < request.blocks_count; ++i) {
-    const auto& b = request.blocks[i];
-    pw::UniquePtr<Block> block;
-
-    switch (b.type) {
-      case pw_hil_BlockType_BLOCK_TYPE_UNSPECIFIED:
-        PW_LOG_WARN("CreateBlocks: Invalid Block Type");
-        return pw::Status::InvalidArgument();
-      case pw_hil_BlockType_BLOCK_TYPE_TIMER: {
-        uint64_t tick_id = GetSlotOrSignalId(b.id, "tick");
-        block = allocator_.MakeUnique<TimerBlock>(
-            b.id,
-            b.name,
-            std::chrono::milliseconds(b.config.timer_period_ms),
-            time_provider_,
-            tick_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_COUNTER: {
-        PW_LOG_INFO("CreateBlocks: Counter");
-        uint64_t inc_id = GetSlotOrSignalId(b.id, "increment");
-        uint64_t dec_id = GetSlotOrSignalId(b.id, "decrement");
-        uint64_t change_id = GetSlotOrSignalId(b.id, "change");
-        block = allocator_.MakeUnique<CounterBlock>(
-            b.id, b.name, inc_id, dec_id, change_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_MODULO: {
-        PW_LOG_INFO("CreateBlocks: Modulo");
-        uint64_t input_id = GetSlotOrSignalId(b.id, "input");
-        uint64_t output_id = GetSlotOrSignalId(b.id, "output");
-        block = allocator_.MakeUnique<ModuloBlock>(
-            b.id, b.name, b.config.modulo_divisor, input_id, output_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_EQUAL: {
-        PW_LOG_INFO("CreateBlocks: Equal");
-        uint64_t input_id = GetSlotOrSignalId(b.id, "input");
-        uint64_t true_id = GetSlotOrSignalId(b.id, "true");
-        uint64_t false_id = GetSlotOrSignalId(b.id, "false");
-        uint64_t output_id = GetSlotOrSignalId(b.id, "output");
-        block = allocator_.MakeUnique<EqualBlock>(b.id,
-                                                  b.name,
-                                                  b.config.equal_value,
-                                                  input_id,
-                                                  true_id,
-                                                  false_id,
-                                                  output_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_AND: {
-        PW_LOG_INFO("CreateBlocks: And");
-        uint64_t output_id = GetSlotOrSignalId(b.id, "output");
-        block = allocator_.MakeUnique<AndBlock>(
-            b.id, b.name, output_id, allocator_);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_NOT: {
-        PW_LOG_INFO("CreateBlocks: Not");
-        uint64_t input_id = GetSlotOrSignalId(b.id, "input");
-        uint64_t output_id = GetSlotOrSignalId(b.id, "output");
-        block = allocator_.MakeUnique<NotBlock>(
-            b.id, b.name, input_id, output_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_STRING_CONSTANT: {
-        PW_LOG_INFO("CreateBlocks: StringConstant");
-        uint64_t input_id = GetSlotOrSignalId(b.id, "input");
-        uint64_t output_id = GetSlotOrSignalId(b.id, "output");
-        block = allocator_.MakeUnique<StringConstantBlock>(
-            b.id, b.name, b.config.string_constant, input_id, output_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_PRINT: {
-        PW_LOG_INFO("CreateBlocks: Print");
-        uint64_t print_id = GetSlotOrSignalId(b.id, "print");
-        uint64_t printed_id = GetSlotOrSignalId(b.id, "printed");
-        block = allocator_.MakeUnique<PrintBlock>(
-            b.id, b.name, print_id, printed_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_GPIO: {
-        PW_LOG_INFO("CreateBlocks: Gpio");
-        uint64_t toggle_id = GetSlotOrSignalId(b.id, "toggle");
-        uint64_t change_id = GetSlotOrSignalId(b.id, "change");
-        block = allocator_.MakeUnique<GpioBlock>(b.id,
-                                                 b.name,
-                                                 b.config.gpio.pin,
-                                                 b.config.gpio.dir,
-                                                 toggle_id,
-                                                 change_id);
-        break;
-      }
-      case pw_hil_BlockType_BLOCK_TYPE_END_TEST: {
-        PW_LOG_INFO("CreateBlocks: EndTest");
-        uint64_t input_id = GetSlotOrSignalId(b.id, "input");
-        block = allocator_.MakeUnique<EndTestBlock>(
-            b.id, b.name, input_id, [this]() {
-              for (auto& blk : blocks_) {
-                blk->Disable();
-              }
-            });
-        break;
-      }
-
-      default:
-        return pw::Status::InvalidArgument();
-    }
-
-    if (block == nullptr) {
-      PW_LOG_ERROR("CreateBlocks: Failed to allocate block");
-      return pw::Status::ResourceExhausted();
-    }
-
-    blocks_.push_back(std::move(block));
-    dispatcher_.Post(*blocks_.back());
-  }
-  return pw::OkStatus();
-}
-
-pw::Status HilTesterService::CreateConnections(
-    const pw_hil_CreateConnectionsRequest& request, pw_hil_Empty&) {
-  for (size_t i = 0; i < request.connections_count; ++i) {
-    const auto& conn = request.connections[i];
-    Signal* signal = nullptr;
-    Slot* slot = nullptr;
-
-    for (auto& block : blocks_) {
-      signal = block->GetSignal(conn.signal_id);
-      if (signal != nullptr) {
-        break;
-      }
-    }
-
-    for (auto& block : blocks_) {
-      slot = block->GetOrCreateSlot(conn.slot_id);
-      if (slot != nullptr) {
-        break;
-      }
-    }
-
-    if (signal == nullptr || slot == nullptr) {
-      return pw::Status::NotFound();
-    }
-
-    signal->Connect(slot);
-  }
-  return pw::OkStatus();
-}
-
-pw::Status HilTesterService::Start(const pw_hil_Empty&, pw_hil_Empty&) {
-  running_ = true;
-  for (auto& block : blocks_) {
-    block->Enable();
-    block->Wake();
-  }
-  return pw::OkStatus();
-}
-
-pw::Status HilTesterService::GetEvents(const pw_hil_Empty&,
-                                       pw_hil_EventsResponse& response) {
-  size_t i = 0;
-  response.events_count = 0;
-
-  event_stream_.ReadEvents([&response, &i](const Event& event) {
-    if (i >= 128) {
-      return;
-    }
-    auto& res_evt = response.events[i++];
-    res_evt.signal_id = event.signal_id;
-    std::visit(
-        [&res_evt](const auto& v) {
-          using T = std::decay_t<decltype(v)>;
-          if constexpr (std::is_same_v<T, std::monostate>) {
-            res_evt.which_value = pw_hil_Event_null_value_tag;
-            res_evt.value.null_value = true;
-          } else if constexpr (std::is_same_v<T, bool>) {
-            res_evt.which_value = pw_hil_Event_bool_value_tag;
-            res_evt.value.bool_value = v;
-          } else if constexpr (std::is_same_v<T, int64_t>) {
-            res_evt.which_value = pw_hil_Event_int_value_tag;
-            res_evt.value.int_value = v;
-          } else if constexpr (std::is_same_v<T, double>) {
-            res_evt.which_value = pw_hil_Event_float_value_tag;
-            res_evt.value.float_value = static_cast<float>(v);
-          } else if constexpr (std::is_same_v<T, pw::InlineString<64>>) {
-            res_evt.which_value = pw_hil_Event_string_value_tag;
-            snprintf(res_evt.value.string_value,
-                     sizeof(res_evt.value.string_value),
-                     "%s",
-                     v.c_str());
-          }
-        },
-        event.value);
-  });
-  response.events_count = static_cast<pb_size_t>(i);
-  return pw::OkStatus();
-}
-
-pw::Status HilTesterService::Reset(const pw_hil_Empty&, pw_hil_Empty&) {
-  ResetService();
-  return pw::OkStatus();
-}
-
-void HilTesterService::ResetService(bool clear_events) {
-  running_ = false;
-  for (auto& block : blocks_) {
-    block->Deregister();
-  }
-  blocks_.clear();
-  if (clear_events) {
-    event_stream_.Clear();
-  }
-}
-
-}  // namespace pw::hil
diff --git a/pw_hil/hil_test.cc b/pw_hil/hil_test.cc
deleted file mode 100644
index 1ef35e5..0000000
--- a/pw_hil/hil_test.cc
+++ /dev/null
@@ -1,380 +0,0 @@
-// Copyright 2026 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_hil/hil.h"
-
-#include <chrono>
-
-#include "public/pw_hil/hil.h"
-#include "pw_async2/dispatcher.h"
-#include "pw_async2/dispatcher_for_test.h"
-#include "pw_async2/simulated_time_provider.h"
-#include "pw_unit_test/framework.h"
-
-namespace pw::hil {
-namespace {
-
-using namespace std::chrono_literals;
-
-class TestSlot : public Slot {
- public:
-  TestSlot(uint64_t id, Block& owner) : Slot(id, owner), calls_(0) {}
-
-  void Trigger(const Value& value) override {
-    calls_++;
-    last_value_ = value;
-  }
-
-  int calls() const { return calls_; }
-  const Value& last_value() const { return last_value_; }
-
-  void Reset() { calls_ = 0; }
-
- private:
-  int calls_;
-  Value last_value_;
-};
-
-class DummyBlock : public Block {
- public:
-  DummyBlock() : Block(999, "dummy") {}
-  Slot* GetOrCreateSlot(uint64_t) override { return nullptr; }
-  Signal* GetSignal(uint64_t) override { return nullptr; }
-
- protected:
-  async2::Poll<> DoPend(async2::Context&) override { return async2::Pending(); }
-};
-
-TEST(TimerBlock, Ticks) {
-  async2::DispatcherForTest dispatcher;
-  async2::SimulatedTimeProvider<chrono::SystemClock> time_provider;
-
-  TimerBlock timer(1, "timer", 10ms, time_provider, 100);
-  DummyBlock dummy;
-  TestSlot slot(200, dummy);
-  timer.GetSignal(100)->Connect(&slot);
-
-  timer.Enable();
-  dispatcher.Post(timer);
-  dispatcher.RunUntilStalled();
-
-  EXPECT_EQ(slot.calls(), 0);
-
-  // Advance to first tick
-  EXPECT_TRUE(time_provider.AdvanceUntilNextExpiration());
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-
-  // Advance to second tick
-  EXPECT_TRUE(time_provider.AdvanceUntilNextExpiration());
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 2);
-
-  timer.Deregister();
-}
-
-TEST(CounterBlock, IncrementsAndDecrements) {
-  async2::DispatcherForTest dispatcher;
-  CounterBlock counter(1, "counter", 100, 200, 300);
-
-  DummyBlock dummy;
-  TestSlot slot(400, dummy);
-  counter.GetSignal(300)->Connect(&slot);
-
-  counter.Enable();
-  dispatcher.Post(counter);
-  dispatcher.RunUntilStalled();
-
-  EXPECT_EQ(slot.calls(), 0);
-
-  // Trigger increment
-  counter.GetOrCreateSlot(100)->Trigger(std::monostate{});
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<int64_t>(slot.last_value()), 1);
-
-  // Trigger decrement
-  counter.GetOrCreateSlot(200)->Trigger(std::monostate{});
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 2);
-  EXPECT_EQ(std::get<int64_t>(slot.last_value()), 0);
-
-  counter.Deregister();
-}
-
-TEST(ModuloBlock, ComputesModulo) {
-  async2::DispatcherForTest dispatcher;
-  ModuloBlock modulo(1, "modulo", 3, 100, 200);
-
-  DummyBlock dummy;
-  TestSlot slot(300, dummy);
-  modulo.GetSignal(200)->Connect(&slot);
-
-  modulo.Enable();
-  dispatcher.Post(modulo);
-
-  modulo.GetOrCreateSlot(100)->Trigger(int64_t(7));
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<int64_t>(slot.last_value()), 1);  // 7 % 3 = 1
-
-  modulo.GetOrCreateSlot(100)->Trigger(int64_t(9));
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 2);
-  EXPECT_EQ(std::get<int64_t>(slot.last_value()), 0);  // 9 % 3 = 0
-
-  modulo.Deregister();
-}
-
-TEST(EqualBlock, ComparesEquality) {
-  async2::DispatcherForTest dispatcher;
-  EqualBlock eq(1, "eq", 5, 100, 200, 300, 400);
-
-  DummyBlock dummy;
-  TestSlot slot_true(500, dummy);
-  TestSlot slot_false(600, dummy);
-  TestSlot slot_output(700, dummy);
-
-  eq.GetSignal(200)->Connect(&slot_true);
-  eq.GetSignal(300)->Connect(&slot_false);
-  eq.GetSignal(400)->Connect(&slot_output);
-
-  eq.Enable();
-  dispatcher.Post(eq);
-
-  // Trigger unequal
-  eq.GetOrCreateSlot(100)->Trigger(int64_t(4));
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot_true.calls(), 0);
-  EXPECT_EQ(slot_false.calls(), 1);
-  EXPECT_EQ(slot_output.calls(), 1);
-  EXPECT_EQ(std::get<bool>(slot_output.last_value()), false);
-
-  // Trigger equal
-  eq.GetOrCreateSlot(100)->Trigger(int64_t(5));
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot_true.calls(), 1);
-  EXPECT_EQ(slot_false.calls(), 1);
-  EXPECT_EQ(slot_output.calls(), 2);
-  EXPECT_EQ(std::get<bool>(slot_output.last_value()), true);
-
-  eq.Deregister();
-}
-
-TEST(AndBlock, LatchingLogic) {
-  async2::DispatcherForTest dispatcher;
-  AndBlock and_block(1, "and", 300);
-
-  DummyBlock dummy;
-  TestSlot slot(400, dummy);
-  and_block.GetSignal(300)->Connect(&slot);
-
-  // Connect 2 input slots
-  Slot* in0 = and_block.GetOrCreateSlot(100);
-  Slot* in1 = and_block.GetOrCreateSlot(200);
-
-  and_block.Enable();
-  dispatcher.Post(and_block);
-
-  // Trigger in0 true. Not all are true yet.
-  in0->Trigger(true);
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<bool>(slot.last_value()), false);
-
-  // Trigger in1 true. All are true.
-  in1->Trigger(true);
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 2);
-  EXPECT_EQ(std::get<bool>(slot.last_value()), true);
-
-  // Trigger in0 false. Not all true.
-  in0->Trigger(false);
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 3);
-  EXPECT_EQ(std::get<bool>(slot.last_value()), false);
-
-  and_block.Deregister();
-}
-
-TEST(NotBlock, InvertsInput) {
-  async2::DispatcherForTest dispatcher;
-  NotBlock not_block(1, "not", 100, 200);
-
-  DummyBlock dummy;
-  TestSlot slot(300, dummy);
-  not_block.GetSignal(200)->Connect(&slot);
-
-  not_block.Enable();
-  dispatcher.Post(not_block);
-
-  not_block.GetOrCreateSlot(100)->Trigger(true);
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<bool>(slot.last_value()), false);
-
-  not_block.GetOrCreateSlot(100)->Trigger(false);
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 2);
-  EXPECT_EQ(std::get<bool>(slot.last_value()), true);
-
-  not_block.Deregister();
-}
-
-TEST(StringConstantBlock, EmitsConstantString) {
-  async2::DispatcherForTest dispatcher;
-  StringConstantBlock sc(1, "sc", "hello", 100, 200);
-
-  DummyBlock dummy;
-  TestSlot slot(300, dummy);
-  sc.GetSignal(200)->Connect(&slot);
-
-  sc.Enable();
-  dispatcher.Post(sc);
-
-  sc.GetOrCreateSlot(100)->Trigger(std::monostate{});
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<pw::InlineString<64>>(slot.last_value()), "hello");
-
-  sc.Deregister();
-}
-
-TEST(PrintBlock, PrintsAndEmitsString) {
-  async2::DispatcherForTest dispatcher;
-  PrintBlock pb(1, "print", 100, 200);
-
-  DummyBlock dummy;
-  TestSlot slot(300, dummy);
-  pb.GetSignal(200)->Connect(&slot);
-
-  pb.Enable();
-  dispatcher.Post(pb);
-
-  pb.GetOrCreateSlot(100)->Trigger(pw::InlineString<64>("test-output"));
-  dispatcher.RunUntilStalled();
-  EXPECT_EQ(slot.calls(), 1);
-  EXPECT_EQ(std::get<pw::InlineString<64>>(slot.last_value()), "test-output");
-
-  pb.Deregister();
-}
-
-TEST(EndTestBlock, CallbackDisables) {
-  async2::DispatcherForTest dispatcher;
-  bool ended = false;
-  EndTestBlock end_test(1, "end", 100, [&ended]() { ended = true; });
-
-  end_test.Enable();
-  dispatcher.Post(end_test);
-
-  end_test.GetOrCreateSlot(100)->Trigger(std::monostate{});
-  dispatcher.RunUntilStalled();
-  EXPECT_TRUE(ended);
-
-  end_test.Deregister();
-}
-
-TEST(HilTesterService, CompleteRPCWorkflow) {
-  async2::DispatcherForTest dispatcher;
-  async2::SimulatedTimeProvider<chrono::SystemClock> time_provider;
-  HilTesterService service(dispatcher, time_provider);
-
-  // 1. Create Blocks
-  pw_hil_CreateBlocksRequest create_blocks_req = {};
-  create_blocks_req.blocks_count = 3;
-
-  // Timer Block (ID 1)
-  create_blocks_req.blocks[0].id = 1;
-  create_blocks_req.blocks[0].type = pw_hil_BlockType_BLOCK_TYPE_TIMER;
-  snprintf(create_blocks_req.blocks[0].name,
-           sizeof(create_blocks_req.blocks[0].name),
-           "timer");
-  create_blocks_req.blocks[0].which_config =
-      pw_hil_BlockConfig_timer_period_ms_tag;
-  create_blocks_req.blocks[0].config.timer_period_ms = 10;
-
-  // Counter Block (ID 2)
-  create_blocks_req.blocks[1].id = 2;
-  create_blocks_req.blocks[1].type = pw_hil_BlockType_BLOCK_TYPE_COUNTER;
-  snprintf(create_blocks_req.blocks[1].name,
-           sizeof(create_blocks_req.blocks[1].name),
-           "counter");
-
-  // EndTest Block (ID 3)
-  create_blocks_req.blocks[2].id = 3;
-  create_blocks_req.blocks[2].type = pw_hil_BlockType_BLOCK_TYPE_END_TEST;
-  snprintf(create_blocks_req.blocks[2].name,
-           sizeof(create_blocks_req.blocks[2].name),
-           "end");
-
-  pw_hil_Empty empty_resp = {};
-  EXPECT_EQ(service.CreateBlocks(create_blocks_req, empty_resp),
-            pw::OkStatus());
-  EXPECT_EQ(service.blocks().size(), 3u);
-
-  // Get standard generated IDs
-  uint64_t tick_id = GetSlotOrSignalId(1, "tick");
-  uint64_t inc_id = GetSlotOrSignalId(2, "increment");
-  uint64_t change_id = GetSlotOrSignalId(2, "change");
-  uint64_t end_slot_id = GetSlotOrSignalId(3, "input");
-
-  // 2. Connect: Timer.tick -> Counter.increment, Counter.change ->
-  // EndTest.input
-  pw_hil_CreateConnectionsRequest conn_req = {};
-  conn_req.connections_count = 2;
-  conn_req.connections[0].signal_id = tick_id;
-  conn_req.connections[0].slot_id = inc_id;
-  conn_req.connections[1].signal_id = change_id;
-  conn_req.connections[1].slot_id = end_slot_id;
-
-  EXPECT_EQ(service.CreateConnections(conn_req, empty_resp), pw::OkStatus());
-
-  // 3. Start
-  pw_hil_Empty empty_req = {};
-  EXPECT_EQ(service.Start(empty_req, empty_resp), pw::OkStatus());
-
-  // Run. Time = 0
-  dispatcher.RunUntilStalled();
-
-  // Advance time to first tick (10ms) -> triggers Timer tick -> triggers
-  // Counter inc -> triggers Counter change -> triggers EndTest
-  EXPECT_TRUE(time_provider.AdvanceUntilNextExpiration());
-  dispatcher.RunUntilStalled();
-
-  // EndTest disables all blocks. We then call ResetService(false) to safely
-  // clean up blocks but preserve events.
-  service.ResetService(false);
-
-  EXPECT_EQ(service.blocks().size(), 0u);
-
-  // 4. Retrieve logged events
-  pw_hil_EventsResponse events_resp = {};
-  EXPECT_EQ(service.GetEvents(empty_req, events_resp), pw::OkStatus());
-
-  // Expected events:
-  // 1. Timer tick (ID = tick_id)
-  // 2. Counter change (ID = change_id, value = 1)
-  EXPECT_EQ(events_resp.events_count, 2u);
-  EXPECT_EQ(events_resp.events[0].signal_id, tick_id);
-  EXPECT_EQ(events_resp.events[0].which_value, pw_hil_Event_null_value_tag);
-
-  EXPECT_EQ(events_resp.events[1].signal_id, change_id);
-  EXPECT_EQ(events_resp.events[1].which_value, pw_hil_Event_int_value_tag);
-  EXPECT_EQ(events_resp.events[1].value.int_value, 1);
-
-  service.ResetService();
-}
-
-}  // namespace
-}  // namespace pw::hil
diff --git a/pw_hil/public/pw_hil/hil.h b/pw_hil/public/pw_hil/hil.h
deleted file mode 100644
index 095caa4..0000000
--- a/pw_hil/public/pw_hil/hil.h
+++ /dev/null
@@ -1,790 +0,0 @@
-// Copyright 2026 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
-
-#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
-
-#include <array>
-#include <chrono>
-#include <functional>
-#include <mutex>
-#include <string_view>
-#include <variant>
-#include <vector>
-
-#include "hardware/gpio.h"
-#include "pw_allocator/allocator.h"
-#include "pw_allocator/unique_ptr.h"
-#include "pw_async2/dispatcher.h"
-#include "pw_async2/task.h"
-#include "pw_async2/time_provider.h"
-#include "pw_async2/waker.h"
-#include "pw_chrono/system_clock.h"
-#include "pw_containers/vector.h"
-#include "pw_hil/hil.rpc.pb.h"
-#include "pw_log/log.h"
-#include "pw_status/status.h"
-#include "pw_string/format.h"
-#include "pw_string/string.h"
-#include "pw_sync/mutex.h"
-
-namespace pw::hil {
-
-using Value =
-    std::variant<std::monostate, bool, int64_t, double, pw::InlineString<64>>;
-
-class Block;
-
-class Slot {
- public:
-  Slot(uint64_t id, Block& owner) : id_(id), owner_(owner) {}
-  virtual ~Slot() = default;
-
-  uint64_t id() const { return id_; }
-  Block& owner() { return owner_; }
-
-  virtual void Trigger(const Value& value) = 0;
-
- private:
-  uint64_t id_;
-  Block& owner_;
-};
-
-struct Event {
-  uint64_t signal_id;
-  Value value;
-};
-
-class EventStream {
- public:
-  void Log(uint64_t signal_id, const Value& value) {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    events_.push_back({signal_id, value});
-  }
-
-  template <typename Func>
-  void ReadEvents(Func&& func) const {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    for (const auto& event : events_) {
-      func(event);
-    }
-  }
-
-  void Clear() {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    events_.clear();
-  }
-
- private:
-  mutable pw::sync::Mutex mutex_;
-  pw::Vector<Event, 128> events_;
-};
-
-// Global event stream pointer or callback.
-void LogGlobalEvent(uint64_t signal_id, const Value& value);
-
-uint64_t GetSlotOrSignalId(uint64_t owner_id, std::string_view name);
-
-class Signal {
- public:
-  Signal(uint64_t id) : id_(id) {}
-
-  uint64_t id() const { return id_; }
-
-  void Connect(Slot* slot) { slots_.push_back(slot); }
-
-  void Emit(const Value& value) {
-    LogGlobalEvent(id_, value);
-    for (auto* slot : slots_) {
-      slot->Trigger(value);
-    }
-  }
-
- private:
-  uint64_t id_;
-  pw::Vector<Slot*, 32> slots_;
-};
-
-class Block : public pw::async2::Task {
- public:
-  Block(uint64_t id, std::string_view name)
-      : id_(id), name_(name), enabled_(false) {}
-  virtual ~Block() = default;
-
-  uint64_t id() const { return id_; }
-  std::string_view name() const { return name_; }
-
-  void Enable() { enabled_ = true; }
-  void Disable() { enabled_ = false; }
-  bool enabled() const { return enabled_; }
-
-  virtual Slot* GetOrCreateSlot(uint64_t id) = 0;
-  virtual Signal* GetSignal(uint64_t id) = 0;
-
-  void Wake() { waker_.Wake(); }
-
- protected:
-  void StoreWaker(pw::async2::Context& cx) {
-    PW_ASYNC_STORE_WAKER(cx, waker_, "Block Waker");
-  }
-
- private:
-  uint64_t id_;
-  std::string name_;
-  bool enabled_;
-  pw::async2::Waker waker_;
-};
-
-class InputSlot : public Slot {
- public:
-  InputSlot(uint64_t id, Block& owner) : Slot(id, owner) {}
-
-  void Trigger(const Value& value) override {
-    {
-      std::lock_guard<pw::sync::Mutex> lock(mutex_);
-      queue_.push_back(value);
-    }
-    owner().Wake();
-  }
-
-  bool PopTrigger(Value& out_value) {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    if (queue_.empty()) {
-      return false;
-    }
-    out_value = std::move(queue_.front());
-    queue_.erase(queue_.begin());
-    return true;
-  }
-
-  bool has_pending() {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    return !queue_.empty();
-  }
-
-  void Clear() {
-    std::lock_guard<pw::sync::Mutex> lock(mutex_);
-    queue_.clear();
-  }
-
- private:
-  pw::sync::Mutex mutex_;
-  pw::Vector<Value, 64> queue_;
-};
-
-class TimerBlock : public Block {
- public:
-  TimerBlock(uint64_t id,
-             std::string_view name,
-             chrono::SystemClock::duration period,
-             async2::TimeProvider<chrono::SystemClock>& time_provider,
-             uint64_t tick_signal_id)
-      : Block(id, name),
-        period_(period),
-        time_provider_(time_provider),
-        tick_signal_(tick_signal_id) {}
-  ~TimerBlock() { PW_LOG_INFO("TimerBlock: Dtor"); }
-
-  Slot* GetOrCreateSlot(uint64_t) override { return nullptr; }
-  Signal* GetSignal(uint64_t id) override {
-    if (id == tick_signal_.id()) {
-      return &tick_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    PW_LOG_INFO("TimerBlock: DoPend");
-    // if (!enabled()) {
-    //   return async2::Pending();
-    // }
-
-    // StoreWaker(cx);
-
-    while (true) {
-      if (!timer_active_) {
-        timer_future_ = time_provider_.WaitFor(period_);
-        timer_active_ = true;
-      }
-
-      if (timer_future_.Pend(cx).IsPending()) {
-        return async2::Pending();
-      }
-
-      timer_active_ = false;
-      tick_signal_.Emit(std::monostate{});
-    }
-  }
-
- private:
-  chrono::SystemClock::duration period_;
-  async2::TimeProvider<chrono::SystemClock>& time_provider_;
-  Signal tick_signal_;
-  async2::TimeFuture<chrono::SystemClock> timer_future_;
-  bool timer_active_ = false;
-};
-
-class CounterBlock : public Block {
- public:
-  CounterBlock(uint64_t id,
-               std::string_view name,
-               uint64_t increment_slot_id,
-               uint64_t decrement_slot_id,
-               uint64_t change_signal_id)
-      : Block(id, name),
-        increment_slot_(increment_slot_id, *this),
-        decrement_slot_(decrement_slot_id, *this),
-        change_signal_(change_signal_id),
-        value_(0) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == increment_slot_.id()) {
-      return &increment_slot_;
-    }
-    if (id == decrement_slot_.id()) {
-      return &decrement_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == change_signal_.id()) {
-      return &change_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    bool changed = false;
-    Value val;
-    while (increment_slot_.PopTrigger(val)) {
-      value_++;
-      changed = true;
-    }
-    while (decrement_slot_.PopTrigger(val)) {
-      value_--;
-      changed = true;
-    }
-
-    if (changed) {
-      change_signal_.Emit(value_);
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  InputSlot increment_slot_;
-  InputSlot decrement_slot_;
-  Signal change_signal_;
-  int64_t value_;
-};
-
-class GpioBlock : public Block {
- public:
-  GpioBlock(uint64_t id,
-            std::string_view name,
-            uint64_t pin,
-            bool is_out,
-            uint64_t toggle_slot_id,
-            uint64_t change_signal_id)
-      : Block(id, name),
-        toggle_slot_(toggle_slot_id, *this),
-        change_signal_(change_signal_id),
-        pin_(pin),
-        is_out_(is_out) {
-    gpio_init(pin_);
-    gpio_set_dir(pin_, is_out_);
-    if (is_out_) {
-      gpio_put(pin_, 0);
-    }
-  }
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == toggle_slot_.id()) {
-      return &toggle_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == change_signal_.id()) {
-      return &change_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    bool changed = false;
-    Value val;
-    while (toggle_slot_.PopTrigger(val)) {
-      gpio_put(pin_, !gpio_get_out_level(pin_));
-      changed = true;
-    }
-
-    if (changed) {
-      change_signal_.Emit(gpio_get_out_level(pin_));
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  InputSlot toggle_slot_;
-  Signal change_signal_;
-  int64_t pin_;
-  bool is_out_;
-};
-
-class ModuloBlock : public Block {
- public:
-  ModuloBlock(uint64_t id,
-              std::string_view name,
-              int64_t divisor,
-              uint64_t input_slot_id,
-              uint64_t output_signal_id)
-      : Block(id, name),
-        divisor_(divisor),
-        input_slot_(input_slot_id, *this),
-        output_signal_(output_signal_id) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == output_signal_.id()) {
-      return &output_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    while (input_slot_.PopTrigger(val)) {
-      if (auto* pval = std::get_if<int64_t>(&val)) {
-        int64_t result = (*pval) % divisor_;
-        output_signal_.Emit(result);
-      }
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  int64_t divisor_;
-  InputSlot input_slot_;
-  Signal output_signal_;
-};
-
-class EqualBlock : public Block {
- public:
-  EqualBlock(uint64_t id,
-             std::string_view name,
-             int64_t compare_value,
-             uint64_t input_slot_id,
-             uint64_t true_signal_id,
-             uint64_t false_signal_id,
-             uint64_t output_signal_id)
-      : Block(id, name),
-        compare_value_(compare_value),
-        input_slot_(input_slot_id, *this),
-        true_signal_(true_signal_id),
-        false_signal_(false_signal_id),
-        output_signal_(output_signal_id) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == true_signal_.id()) {
-      return &true_signal_;
-    }
-    if (id == false_signal_.id()) {
-      return &false_signal_;
-    }
-    if (id == output_signal_.id()) {
-      return &output_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    while (input_slot_.PopTrigger(val)) {
-      if (auto* pval = std::get_if<int64_t>(&val)) {
-        bool eq = (*pval == compare_value_);
-        output_signal_.Emit(eq);
-        if (eq) {
-          true_signal_.Emit(std::monostate{});
-        } else {
-          false_signal_.Emit(std::monostate{});
-        }
-      }
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  int64_t compare_value_;
-  InputSlot input_slot_;
-  Signal true_signal_;
-  Signal false_signal_;
-  Signal output_signal_;
-};
-
-class AndBlock : public Block {
- public:
-  AndBlock(uint64_t id,
-           std::string_view name,
-           uint64_t output_signal_id,
-           pw::Allocator& allocator)
-      : Block(id, name),
-        output_signal_(output_signal_id),
-        allocator_(allocator) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    for (size_t i = 0; i < slots_.size(); ++i) {
-      if (slots_[i]->id() == id) {
-        return slots_[i].get();
-      }
-    }
-    if (slots_.size() < 8) {
-      auto slot = allocator_.MakeUnique<InputSlot>(id, *this);
-      if (slot == nullptr) {
-        return nullptr;
-      }
-      slots_.push_back(std::move(slot));
-      latched_values_[slots_.size() - 1] = false;
-      return slots_.back().get();
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == output_signal_.id()) {
-      return &output_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    bool trigger_processed = false;
-    for (size_t i = 0; i < slots_.size(); ++i) {
-      Value val;
-      while (slots_[i]->PopTrigger(val)) {
-        if (auto* pval = std::get_if<bool>(&val)) {
-          latched_values_[i] = *pval;
-          trigger_processed = true;
-        }
-      }
-    }
-
-    if (trigger_processed) {
-      bool all_true = true;
-      for (size_t i = 0; i < slots_.size(); ++i) {
-        if (!latched_values_[i]) {
-          all_true = false;
-          break;
-        }
-      }
-      output_signal_.Emit(all_true);
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  pw::Vector<pw::UniquePtr<InputSlot>, 8> slots_;
-
-  Signal output_signal_;
-  pw::Allocator& allocator_;
-  std::array<bool, 8> latched_values_{};
-};
-
-class NotBlock : public Block {
- public:
-  NotBlock(uint64_t id,
-           std::string_view name,
-           uint64_t input_slot_id,
-           uint64_t output_signal_id)
-      : Block(id, name),
-        input_slot_(input_slot_id, *this),
-        output_signal_(output_signal_id) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == output_signal_.id()) {
-      return &output_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    while (input_slot_.PopTrigger(val)) {
-      if (auto* pval = std::get_if<bool>(&val)) {
-        output_signal_.Emit(!*pval);
-      }
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  InputSlot input_slot_;
-  Signal output_signal_;
-};
-
-class StringConstantBlock : public Block {
- public:
-  StringConstantBlock(uint64_t id,
-                      std::string_view name,
-                      std::string_view constant,
-                      uint64_t input_slot_id,
-                      uint64_t output_signal_id)
-      : Block(id, name),
-        constant_(constant),
-        input_slot_(input_slot_id, *this),
-        output_signal_(output_signal_id) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == output_signal_.id()) {
-      return &output_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    while (input_slot_.PopTrigger(val)) {
-      output_signal_.Emit(constant_);
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  pw::InlineString<64> constant_;
-  InputSlot input_slot_;
-  Signal output_signal_;
-};
-
-class PrintBlock : public Block {
- public:
-  PrintBlock(uint64_t id,
-             std::string_view name,
-             uint64_t input_slot_id,
-             uint64_t printed_signal_id)
-      : Block(id, name),
-        input_slot_(input_slot_id, *this),
-        printed_signal_(printed_signal_id) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t id) override {
-    if (id == printed_signal_.id()) {
-      return &printed_signal_;
-    }
-    return nullptr;
-  }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    while (input_slot_.PopTrigger(val)) {
-      pw::InlineString<64> str;
-      if (auto* pval = std::get_if<pw::InlineString<64>>(&val)) {
-        str = *pval;
-      } else if (auto* pb = std::get_if<bool>(&val)) {
-        str = *pb ? "true" : "false";
-      } else if (auto* pi = std::get_if<int64_t>(&val)) {
-        (void)pw::string::Format(str, "%lld", static_cast<long long>(*pi));
-      } else if (auto* pf = std::get_if<double>(&val)) {
-        (void)pw::string::Format(str, "%f", *pf);
-      } else {
-        str = "null";
-      }
-      printf("%s\n", str.c_str());
-      printed_signal_.Emit(str);
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  InputSlot input_slot_;
-  Signal printed_signal_;
-};
-
-class EndTestBlock : public Block {
- public:
-  EndTestBlock(uint64_t id,
-               std::string_view name,
-               uint64_t input_slot_id,
-               std::function<void()> on_end_test)
-      : Block(id, name),
-        input_slot_(input_slot_id, *this),
-        on_end_test_(std::move(on_end_test)) {}
-
-  Slot* GetOrCreateSlot(uint64_t id) override {
-    if (id == input_slot_.id()) {
-      return &input_slot_;
-    }
-    return nullptr;
-  }
-
-  Signal* GetSignal(uint64_t) override { return nullptr; }
-
- protected:
-  async2::Poll<> DoPend(async2::Context& cx) override {
-    if (!enabled()) {
-      return async2::Pending();
-    }
-
-    StoreWaker(cx);
-
-    Value val;
-    if (input_slot_.PopTrigger(val)) {
-      on_end_test_();
-    }
-
-    return async2::Pending();
-  }
-
- private:
-  InputSlot input_slot_;
-  std::function<void()> on_end_test_;
-};
-
-class HilTesterService final
-    : public pw::hil::pw_rpc::nanopb::HilTester::Service<HilTesterService> {
- public:
-  HilTesterService(async2::Dispatcher& dispatcher,
-                   async2::TimeProvider<chrono::SystemClock>& time_provider,
-                   pw::Allocator& allocator)
-      : dispatcher_(dispatcher),
-        time_provider_(time_provider),
-        allocator_(allocator) {
-    SetGlobalEventStream(&event_stream_);
-  }
-
-  ~HilTesterService() {
-    SetGlobalEventStream(nullptr);
-    ResetService();
-  }
-
-  pw::Status CreateBlocks(const pw_hil_CreateBlocksRequest& request,
-                          pw_hil_Empty& response);
-  pw::Status CreateConnections(const pw_hil_CreateConnectionsRequest& request,
-                               pw_hil_Empty& response);
-  pw::Status Start(const pw_hil_Empty& request, pw_hil_Empty& response);
-  pw::Status GetEvents(const pw_hil_Empty& request,
-                       pw_hil_EventsResponse& response);
-  pw::Status Reset(const pw_hil_Empty& request, pw_hil_Empty& response);
-
-  void ResetService(bool clear_events = true);
-
-  EventStream& event_stream() { return event_stream_; }
-  const pw::Vector<pw::UniquePtr<Block>>& blocks() const { return blocks_; }
-
-  static void SetGlobalEventStream(EventStream* stream);
-
- private:
-  async2::Dispatcher& dispatcher_;
-  async2::TimeProvider<chrono::SystemClock>& time_provider_;
-  pw::Allocator& allocator_;
-  EventStream event_stream_;
-  pw::Vector<pw::UniquePtr<Block>, 128> blocks_;
-
-  bool running_ = false;
-};
-
-}  // namespace pw::hil
diff --git a/pw_hil/py/BUILD.bazel b/pw_hil/py/BUILD.bazel
index 909e39a..33f53b5 100644
--- a/pw_hil/py/BUILD.bazel
+++ b/pw_hil/py/BUILD.bazel
@@ -34,6 +34,7 @@
     deps = [
         ":dhalsim",
         "//pw_cli/py:pw_cli",
+        "//pw_hil/debug_probe/py:debug_probe",
         "//pw_hil/protos:hil_py_pb2",
     ],
 )
diff --git a/pw_hil/py/dhalsim_test.py b/pw_hil/py/dhalsim_test.py
index f5f748f..efff7f4 100644
--- a/pw_hil/py/dhalsim_test.py
+++ b/pw_hil/py/dhalsim_test.py
@@ -27,46 +27,9 @@
 
 _LOG = logging.getLogger("DhalsimTest")
 
-class PrettyFormatter(logging.Formatter):
-    """A logging formatter that tunes logging for this script."""
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self._colors = color.colors()
-        self._color_enabled = color.is_enabled()
-
-    def _gray(self, msg: str) -> str:
-        if self._color_enabled:
-            return self._colors.gray(msg)
-        return msg
-
-    def format(self, record: logging.LogRecord) -> str:
-        """Formats the log record."""
-        message = record.getMessage()
-        if record.levelno >= logging.ERROR:
-            level_prefix = f"❌ [{record.name}] "
-            message = self._colors.red(message)
-        elif record.levelno >= logging.WARNING:
-            level_prefix = f"⚠️ [{record.name}] "
-            message = self._colors.yellow(message)
-        elif record.levelno == logging.DEBUG:
-            level_prefix = f"[{record.name}] "
-            message = self._gray(message)
-        else:
-            level_prefix = f"[{record.name}] "
-        return f"{level_prefix}{message}"
-
-
-def _setup_logging(log_level: int):
-    handler = logging.StreamHandler()
-    handler.setFormatter(PrettyFormatter())
-    _LOG.addHandler(handler)
-    _LOG.setLevel(log_level)
-
-
 class DhalsimTest(hiltest.TestCase):
     def __init__(self):
-        _setup_logging(logging.INFO)
+        pass
     
     def setUp(self):
         _LOG.debug("DhalsimTest.setUp")
@@ -75,12 +38,16 @@
         _LOG.debug("DhalsimTest.tearDown")
     
     def test_blinky(self):
-        _LOG.info("Hello World!")
+        _LOG.info("Testing Blinky")
         timer_block = dhalsim.create(timer.PeriodicTimer("timer", 1000))
-        gpio_block = dhalsim.create(gpio.Gpio("gpio", pin=25, direction=gpio.Direction.OUT))
-        uart_block = dhalsim.create(uart.Uart("uart", baud_rate=115200))
+        gpio_block = dhalsim.create(gpio.Gpio("gpio", pin=14, direction=gpio.Direction.OUT))
         
         dhalsim.connect(timer_block.tick(), gpio_block.toggle())
+    
+    def test_uart_loopback(self):
+        _LOG.info("Testing UART Loopback")
+        uart_block = dhalsim.create(uart.Uart("uart", baud_rate=115200))
+
         dhalsim.connect(uart_block.rxd_byte(), uart_block.tx_byte())
 
 
diff --git a/pw_hil/py/hil_framework/hiltest.py b/pw_hil/py/hil_framework/hiltest.py
index ea09cc9..8c7d363 100644
--- a/pw_hil/py/hil_framework/hiltest.py
+++ b/pw_hil/py/hil_framework/hiltest.py
@@ -37,7 +37,7 @@
 compiled_protos = [
     hil_pb2,
 ]
-_LOG = logging.getLogger()
+_LOG = logging.getLogger('hiltest')
 
 class TestCase(object):
     def __init__(self):
@@ -49,7 +49,7 @@
     def tearDown(self):
         pass
 
-def create_device_connection(args: argparse.Namespace) -> DeviceConnection:
+def create_dhalsim_connection(args: argparse.Namespace) -> DeviceConnection:
     return create_device_serial_or_socket_connection(
             device=args.device,
             baudrate=args.baudrate,
@@ -125,7 +125,7 @@
     parser = add_device_args(parser)
     parser.add_argument("--timeout", type=int, default=10, help="Test timeout in seconds (Default: 10 seconds)")
     args = parser.parse_args()
-    with create_device_connection(args) as dhalsim_device:
+    with create_dhalsim_connection(args) as dhalsim_device:
         dhalsim.set_device(dhalsim_device)
         suite = HilTestSuite(args.timeout)
         if not suite.run_tests():
diff --git a/pw_hil/rp2xxx_main.cc b/pw_hil/rp2xxx_main.cc
new file mode 100644
index 0000000..04d737c
--- /dev/null
+++ b/pw_hil/rp2xxx_main.cc
@@ -0,0 +1,45 @@
+// Copyright 2026 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.
+
+#define PW_LOG_MODULE_NAME "rp2xxx_main"
+
+#include "hardware/gpio.h"
+#include "pico/stdlib.h"
+#include "pw_allocator/best_fit.h"
+#include "pw_channel/rp2_stdio_channel.h"
+#include "pw_multibuf/simple_allocator.h"
+#include "pw_system/system.h"
+
+static std::array<std::byte, 4096> mb_data;
+static std::array<std::byte, 2048> meta_data;
+
+inline void pause(uint32_t delay_loops = 10) {
+  volatile uint32_t loop = 0;
+  for (loop = 0; loop < delay_loops;) {
+    loop = loop + 1;
+  }
+}
+
+int main() {
+  // PICO_SDK Inits
+  stdio_init_all();
+  setup_default_uart();
+  stdio_usb_init();
+
+  static pw::allocator::BestFitAllocator alloc{meta_data};
+  static pw::multibuf::SimpleAllocator mb_alloc{mb_data, alloc};
+  pw::system::StartAndClobberTheStack(
+      pw::channel::Rp2StdioChannelInit(mb_alloc, mb_alloc));
+  PW_UNREACHABLE;
+}
diff --git a/pw_hil/uart_blaster.cc b/pw_hil/uart_blaster.cc
new file mode 100644
index 0000000..1b4197f
--- /dev/null
+++ b/pw_hil/uart_blaster.cc
@@ -0,0 +1,135 @@
+// Copyright 2026 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 <chrono>
+
+#include "hardware/gpio.h"
+#include "hardware/uart.h"
+#include "pw_system/config.h"
+#include "pw_system/system.h"
+#include "pw_thread/attrs.h"
+#include "pw_thread/detached_thread.h"
+#include "pw_thread/sleep.h"
+
+namespace pw::hil {
+namespace {
+
+inline constexpr ThreadPriority kThreadPriorityUartBlaster = ThreadPriority();
+
+inline constexpr pw::ThreadAttrs kUartBlasterThread =
+    pw::ThreadAttrs()
+        .set_stack_size_bytes(4096)
+        .set_name("UartBlasterThread")
+        .set_priority(kThreadPriorityUartBlaster);
+
+static constexpr char ABC_LUT[26] = {
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
+
+}  // namespace
+}  // namespace pw::hil
+
+namespace pw {
+template <typename T>
+class Debouncer {
+ public:
+  Debouncer(const T& initial_state, size_t threshold)
+      : state_(initial_state),
+        last_state_(initial_state),
+        threshold_(threshold),
+        count_(0) {}
+
+  const T& state() const { return state_; }
+
+  const T& Update(const T& new_state) {
+    if (new_state == state_) {
+      count_ = 0;
+    } else if (new_state == last_state_) {
+      if (count_ >= threshold_) {
+        state_ = last_state_;
+        count_ = 0;
+      } else {
+        count_++;
+      }
+    } else {
+      last_state_ = new_state;
+      count_ = 1;
+    }
+    return state_;
+  }
+
+ private:
+  T state_;
+  T last_state_;
+  size_t threshold_;
+  size_t count_;
+};
+}  // namespace pw
+
+inline void pause(uint32_t delay_loops = 10) {
+  volatile uint32_t loop = 0;
+  for (loop = 0; loop < delay_loops;) {
+    loop = loop + 1;
+  }
+}
+
+namespace pw::system {
+
+void UserAppInit() {
+  PW_CONSTINIT static ThreadContextFor<::pw::hil::kUartBlasterThread>
+      uart_blaster_thread;
+
+  pw::Thread(uart_blaster_thread, []() {
+    static constexpr uint32_t TRIGGER_GPIO = 17;
+    static constexpr uint32_t LED_GPIO = 24;
+
+    static constexpr uint32_t UART1_RX_PIN = 5;
+    static constexpr uint32_t UART1_TX_PIN = 4;
+
+    static constexpr uint32_t LOOP_TIME_MS = 10;
+    static constexpr uint32_t LED_PERIOD_MS = 500;
+
+    gpio_init(TRIGGER_GPIO);
+    gpio_set_dir(TRIGGER_GPIO, GPIO_IN);
+    gpio_init(LED_GPIO);
+    gpio_set_dir(LED_GPIO, GPIO_OUT);
+
+    uart_init(uart1, 115200);
+    uart_set_format(uart1, 8, 1, UART_PARITY_NONE);
+    gpio_set_function(UART1_RX_PIN,
+                      UART_FUNCSEL_NUM(uart1, UART1_RX_PIN));  // TEST_UART_RX
+    gpio_set_function(UART1_TX_PIN,
+                      UART_FUNCSEL_NUM(uart1, UART1_TX_PIN));  // TEST_UART_TX
+
+    pw::Debouncer<bool> debouncer(false, 10);
+    size_t loop_counts = 0;
+    while (true) {
+      loop_counts++;
+      debouncer.Update(gpio_get(TRIGGER_GPIO));
+
+      // While GPIO is high, blast UART
+      if (debouncer.state() == true) {
+        uart_putc(uart1, pw::hil::ABC_LUT[loop_counts % 26]);
+      }
+
+      if (loop_counts % (LED_PERIOD_MS / LOOP_TIME_MS) == 0) {
+        gpio_put(LED_GPIO, !gpio_get(LED_GPIO));
+      }
+
+      pw::this_thread::sleep_for(std::chrono::milliseconds(LOOP_TIME_MS));
+    }
+  }).detach();
+}
+
+}  // namespace pw::system