munge
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 69f28fe..a4bc21c 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -1,5 +1,5 @@
 {
-  "lockFileVersion": 18,
+  "lockFileVersion": 24,
   "registryFileHashes": {
     "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
     "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
@@ -19,7 +19,8 @@
     "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
     "https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b",
     "https://bcr.bazel.build/modules/apple_support/1.23.0/MODULE.bazel": "317d47e3f65b580e7fb4221c160797fda48e32f07d2dfff63d754ef2316dcd25",
-    "https://bcr.bazel.build/modules/apple_support/1.23.0/source.json": "cb90a670c368cd37b5a7021486fd3f9a3fb0fcb8f45af43399137938d32ced76",
+    "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442",
+    "https://bcr.bazel.build/modules/apple_support/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f",
     "https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
@@ -44,6 +45,7 @@
     "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
     "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
     "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
+    "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
     "https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc",
     "https://bcr.bazel.build/modules/bazel_features/1.32.0/source.json": "2546c766986a6541f0bacd3e8542a1f621e2b14a80ea9e88c6f89f7eedf64ae1",
     "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
@@ -165,6 +167,7 @@
     "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
     "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
     "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
+    "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
     "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
     "https://bcr.bazel.build/modules/rules_cc/0.2.4/source.json": "2bd87ef9b41d4753eadf65175745737135cba0e70b479bdc204ef0c67404d0c4",
     "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
@@ -193,11 +196,12 @@
     "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
     "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934",
     "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
+    "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615",
+    "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc",
     "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
     "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
     "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71",
     "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
-    "https://bcr.bazel.build/modules/rules_java/8.6.1/source.json": "f18d9ad3c4c54945bf422ad584fa6c5ca5b3116ff55a5b1bc77e5c1210be5960",
     "https://bcr.bazel.build/modules/rules_jni/0.11.0/MODULE.bazel": "f701405ceafc721bd6e1d6efee0094e941dc92e0652f73f431878a22aab0ba48",
     "https://bcr.bazel.build/modules/rules_jni/0.11.0/source.json": "368017b9548943ad80c741547c6912273e2d255b6d1c580a7afe60a1fef912bc",
     "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
@@ -293,8 +297,8 @@
     "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
     "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
     "https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5",
-    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
-    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
+    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
+    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
     "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198",
     "https://bcr.bazel.build/modules/zstd/1.5.6/MODULE.bazel": "471ebe7d3cdd8c6469390fcf623eb4779ff55fbee0a87f1dc57a1def468b96d4",
     "https://bcr.bazel.build/modules/zstd/1.5.6/source.json": "02010c3333fc89b44fe861db049968decb6e688411f7f9d4f6791d74f9adfb51"
@@ -317,36 +321,9 @@
         "recordedRepoMappingEntries": []
       }
     },
-    "//third_party/hsm:extensions.bzl%hsm": {
-      "general": {
-        "bzlTransitiveDigest": "FNKStqvrQ6Eikdj24+vIUXQQ9yp7jfSDhlpg6gpl4EE=",
-        "usagesDigest": "7eja32+CLusTLtPh6EwF/x8FVeysZDXvIY7eEXbQJ2c=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "cloud_kms_hsm": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "build_file": "@@//third_party/hsm:BUILD.cloud_kms_hsm.bazel",
-              "url": "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/pkcs11-v1.8/libkmsp11-1.8-linux-amd64.tar.gz",
-              "strip_prefix": "libkmsp11-1.8-linux-amd64",
-              "sha256": "3b932f22a8abb631442c3276e9c309554c81855526b74fbc9edaddcb57a557f7"
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
     "@@aspect_bazel_lib+//lib:extensions.bzl%toolchains": {
       "general": {
-        "bzlTransitiveDigest": "QYzNsxOEydJ+3EvC2QMD9H0UjNzWD4r3AZ3bgke1PYY=",
+        "bzlTransitiveDigest": "UW8xG1Kwu5Xqz58UquEM4xIcKekzW/gjuYTnU97IFUw=",
         "usagesDigest": "OSDiT17Iojc2nI9CxYoJwyt0eQLLaRhbxOSwSp/RGCc=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -753,128 +730,9 @@
         ]
       }
     },
-    "@@aspect_rules_js+//npm:extensions.bzl%pnpm": {
-      "general": {
-        "bzlTransitiveDigest": "tmLEs2ngwNrJq1OgblYDQTIjF8TjHPE1t98jMTDqD1g=",
-        "usagesDigest": "tSjL49zyVxawlaiGFcrp3a28a6CEbLJE2VWEMr+2Ii0=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "pnpm": {
-            "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule",
-            "attributes": {
-              "package": "pnpm",
-              "version": "8.6.7",
-              "root_package": "",
-              "link_workspace": "",
-              "link_packages": {},
-              "integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==",
-              "url": "",
-              "commit": "",
-              "patch_args": [
-                "-p0"
-              ],
-              "patches": [],
-              "custom_postinstall": "",
-              "npm_auth": "",
-              "npm_auth_basic": "",
-              "npm_auth_username": "",
-              "npm_auth_password": "",
-              "lifecycle_hooks": [],
-              "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])",
-              "generate_bzl_library_targets": false,
-              "extract_full_archive": true,
-              "system_tar": "auto"
-            }
-          },
-          "pnpm__links": {
-            "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links",
-            "attributes": {
-              "package": "pnpm",
-              "version": "8.6.7",
-              "dev": false,
-              "root_package": "",
-              "link_packages": {},
-              "deps": {},
-              "transitive_closure": {},
-              "lifecycle_build_target": false,
-              "lifecycle_hooks_env": [],
-              "lifecycle_hooks_execution_requirements": [
-                "no-sandbox"
-              ],
-              "lifecycle_hooks_use_default_shell_env": false,
-              "bins": {},
-              "package_visibility": [
-                "//visibility:public"
-              ],
-              "replace_package": ""
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "aspect_bazel_lib+",
-            "bazel_skylib",
-            "bazel_skylib+"
-          ],
-          [
-            "aspect_bazel_lib+",
-            "bazel_tools",
-            "bazel_tools"
-          ],
-          [
-            "aspect_rules_js+",
-            "aspect_bazel_lib",
-            "aspect_bazel_lib+"
-          ],
-          [
-            "aspect_rules_js+",
-            "bazel_features",
-            "bazel_features+"
-          ],
-          [
-            "aspect_rules_js+",
-            "bazel_skylib",
-            "bazel_skylib+"
-          ],
-          [
-            "aspect_rules_js+",
-            "bazel_tools",
-            "bazel_tools"
-          ],
-          [
-            "bazel_features+",
-            "bazel_features_globals",
-            "bazel_features++version_extension+bazel_features_globals"
-          ],
-          [
-            "bazel_features+",
-            "bazel_features_version",
-            "bazel_features++version_extension+bazel_features_version"
-          ]
-        ]
-      }
-    },
-    "@@pigweed+//pw_build:pw_rust_crates_extension.bzl%pw_rust_crates_extension": {
-      "general": {
-        "bzlTransitiveDigest": "xYEcMiyyO+NfbdLyYLvKjnmEp4mBibFLOLVw8oRoV6k=",
-        "usagesDigest": "z7CtbK0kUgRAfLkVT5iDYRSzuwSWhsI8W9REdmbChhw=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "rust_crates": {
-            "repoRuleId": "@@pigweed+//pw_build:pw_rust_crates_extension.bzl%_crates_io_hub",
-            "attributes": {}
-          }
-        },
-        "recordedRepoMappingEntries": []
-      }
-    },
     "@@pigweed+//pw_toolchain/cc:pw_cxx_toolchain.bzl%pw_cxx_toolchain": {
       "general": {
-        "bzlTransitiveDigest": "Ra/Vtw43gGL83DZW8FsxeeY8A98P3aHzSmg4dX/QNNk=",
+        "bzlTransitiveDigest": "hciO8VUJTXRWfoaBvBdqqwBlHLoS7lHSff+HOQIyMd0=",
         "usagesDigest": "Xbpc0tIAtYpAPrZ66Usv8vtKskTRFdwZ23gL9rU1kGU=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -916,7 +774,7 @@
     },
     "@@pigweed+//pw_toolchain/rust:extensions.bzl%pw_rust": {
       "general": {
-        "bzlTransitiveDigest": "pVUeogHygMYtC9swbEiXBrUm5winIz9hB4Th6ndT/Gk=",
+        "bzlTransitiveDigest": "xY4xQ8o+XnS/OvktxUguY5mBbB/1PfZuL8KGSvfF46A=",
         "usagesDigest": "SOxoNL6ar3yJlcO3rdvgKJ0EYI6s9GaDpJtfgBJeTNI=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1028,61 +886,6 @@
         ]
       }
     },
-    "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": {
-      "general": {
-        "bzlTransitiveDigest": "yA4GkX1zdUTOXU4qKsh/YkjZKHAWkG8Fd7adNeGwiRc=",
-        "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=",
-        "recordedFileInputs": {
-          "@@pybind11_bazel+//MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34"
-        },
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "pybind11": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel",
-              "strip_prefix": "pybind11-2.12.0",
-              "urls": [
-                "https://github.com/pybind/pybind11/archive/v2.12.0.zip"
-              ]
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "pybind11_bazel+",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
-    "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": {
-      "general": {
-        "bzlTransitiveDigest": "BjalZzJZCCeFUitHa3nPXYW0o22VinHOSCBGaN0y87Q=",
-        "usagesDigest": "e4ckeOnzg06Fr7DT0gu4mzeX8LSpZuX/wUu21TSbi94=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "apksig": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz",
-              "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD"
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "rules_android+",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
     "@@rules_android+//rules/android_sdk_repository:rule.bzl%android_sdk_repository_extension": {
       "general": {
         "bzlTransitiveDigest": "NAy+0M15JNVEBb8Tny6t7j3lKqTnsAMjoBB6LJ+C370=",
@@ -1099,114 +902,9 @@
         "recordedRepoMappingEntries": []
       }
     },
-    "@@rules_fuzzing+//fuzzing/private:extensions.bzl%non_module_dependencies": {
-      "general": {
-        "bzlTransitiveDigest": "mGiTB79hRNjmeDTQdzkpCHyzXhErMbufeAmySBt7s5s=",
-        "usagesDigest": "T13X7WXnVd863yIkg/0xDmftJ/CetmPbmrkGVQ99gkE=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "platforms": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "urls": [
-                "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
-                "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz"
-              ],
-              "sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74"
-            }
-          },
-          "rules_python": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8",
-              "strip_prefix": "rules_python-0.28.0",
-              "url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz"
-            }
-          },
-          "bazel_skylib": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94",
-              "urls": [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz",
-                "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz"
-              ]
-            }
-          },
-          "com_google_absl": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "urls": [
-                "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip"
-              ],
-              "strip_prefix": "abseil-cpp-20240116.1",
-              "integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk="
-            }
-          },
-          "rules_fuzzing_oss_fuzz": {
-            "repoRuleId": "@@rules_fuzzing+//fuzzing/private/oss_fuzz:repository.bzl%oss_fuzz_repository",
-            "attributes": {}
-          },
-          "honggfuzz": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "build_file": "@@rules_fuzzing+//:honggfuzz.BUILD",
-              "sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e",
-              "url": "https://github.com/google/honggfuzz/archive/2.5.zip",
-              "strip_prefix": "honggfuzz-2.5"
-            }
-          },
-          "rules_fuzzing_jazzer": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
-            "attributes": {
-              "sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2",
-              "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar"
-            }
-          },
-          "rules_fuzzing_jazzer_api": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
-            "attributes": {
-              "sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b",
-              "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar"
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "rules_fuzzing+",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
-    "@@rules_java+//java:rules_java_deps.bzl%compatibility_proxy": {
-      "general": {
-        "bzlTransitiveDigest": "84xJEZ1jnXXwo8BXMprvBm++rRt4jsTu9liBxz0ivps=",
-        "usagesDigest": "jTQDdLDxsS43zuRmg1faAjIEPWdLAbDAowI1pInQSoo=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "compatibility_proxy": {
-            "repoRuleId": "@@rules_java+//java:rules_java_deps.bzl%_compatibility_proxy_repo_rule",
-            "attributes": {}
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "rules_java+",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
     "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
       "general": {
-        "bzlTransitiveDigest": "sFhcgPbDQehmbD1EOXzX4H1q/CD5df8zwG4kp4jbvr8=",
+        "bzlTransitiveDigest": "rL/34P1aFDq2GqVC2zCFgQ8nTuOC6ziogocpvG50Qz8=",
         "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1268,35 +966,6 @@
         ]
       }
     },
-    "@@rules_libusb+//:extensions.bzl%libusb": {
-      "general": {
-        "bzlTransitiveDigest": "q8w6UeaTkAyBBb3iDZ6OxV87Iq/zovx8RrRjEpxxqao=",
-        "usagesDigest": "mS8IemUWNRRKkBagACH+EOS/mM9aBoJISPKaD2aTfbI=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "libusb": {
-            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
-            "attributes": {
-              "urls": [
-                "https://github.com/libusb/libusb/releases/download/v1.0.27/libusb-1.0.27.tar.bz2"
-              ],
-              "build_file": "@@rules_libusb+//:libusb.BUILD",
-              "strip_prefix": "libusb-1.0.27",
-              "sha256": "ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575"
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "rules_libusb+",
-            "bazel_tools",
-            "bazel_tools"
-          ]
-        ]
-      }
-    },
     "@@rules_nodejs+//nodejs:extensions.bzl%node": {
       "general": {
         "bzlTransitiveDigest": "SqbzUarOVzAfK28Ca5+NIU3LUwnW/b3h0xXBUS97oyI=",
@@ -1418,131 +1087,9 @@
         "recordedRepoMappingEntries": []
       }
     },
-    "@@rules_probe_rs+//probe_rs:extensions.bzl%probe_rs": {
-      "general": {
-        "bzlTransitiveDigest": "qACxDYMmZFnJ5z3DgFxaM5vcM6BluXK+tGm6QjKzbo0=",
-        "usagesDigest": "UCZMqqV7nRwSIgEQDhXaj9l7SOxiqzZuKi0lVFV0DE0=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "probe_rs": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "platforms": {
-                "probe_rs_tools_x86_64_unknown_linux_gnu": [
-                  "@platforms//os:linux",
-                  "@platforms//cpu:x86_64"
-                ],
-                "probe_rs_tools_aarch64_unknown_linux_gnu": [
-                  "@platforms//os:linux",
-                  "@platforms//cpu:arm64"
-                ],
-                "probe_rs_tools_x86_64_apple_darwin": [
-                  "@platforms//os:macos",
-                  "@platforms//cpu:x86_64"
-                ],
-                "probe_rs_tools_aarch64_apple_darwin": [
-                  "@platforms//os:macos",
-                  "@platforms//cpu:arm64"
-                ],
-                "probe_rs_tools_x86_64_pc_windows_msvc": [
-                  "@platforms//os:windows",
-                  "@platforms//cpu:x86_64"
-                ]
-              }
-            }
-          },
-          "probe_rs_tools_x86_64_unknown_linux_gnu": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_tools_archive_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "url": "https://github.com/probe-rs/probe-rs/releases/download/v0.27.0/probe-rs-tools-x86_64-unknown-linux-gnu.tar.xz",
-              "sha256": "314c40db7ded9ae5622c6af8fe5149bcf5c507d7d6f5b7a415d6acf8d2005cb0",
-              "strip_prefix": "probe-rs-tools-x86_64-unknown-linux-gnu",
-              "exec_compatible_with": [
-                "@platforms//os:linux",
-                "@platforms//cpu:x86_64"
-              ]
-            }
-          },
-          "probe_rs_tools_aarch64_unknown_linux_gnu": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_tools_archive_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "url": "https://github.com/probe-rs/probe-rs/releases/download/v0.27.0/probe-rs-tools-aarch64-unknown-linux-gnu.tar.xz",
-              "sha256": "7aed7af58c9e267b7145490de61d0147033fadb0a70313fe7dbd363bfd98a728",
-              "strip_prefix": "probe-rs-tools-aarch64-unknown-linux-gnu",
-              "exec_compatible_with": [
-                "@platforms//os:linux",
-                "@platforms//cpu:arm64"
-              ]
-            }
-          },
-          "probe_rs_tools_x86_64_apple_darwin": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_tools_archive_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "url": "https://github.com/probe-rs/probe-rs/releases/download/v0.27.0/probe-rs-tools-x86_64-apple-darwin.tar.xz",
-              "sha256": "c00a87173008b8279c8d2669e90ce878629abb95ceaa003099adb68d62f3fafb",
-              "strip_prefix": "probe-rs-tools-x86_64-apple-darwin",
-              "exec_compatible_with": [
-                "@platforms//os:macos",
-                "@platforms//cpu:x86_64"
-              ]
-            }
-          },
-          "probe_rs_tools_aarch64_apple_darwin": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_tools_archive_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "url": "https://github.com/probe-rs/probe-rs/releases/download/v0.27.0/probe-rs-tools-aarch64-apple-darwin.tar.xz",
-              "sha256": "7b1ad0d61f6355a9f095e4d06a5e6149ad9824f4725d4976104486941d652d5c",
-              "strip_prefix": "probe-rs-tools-aarch64-apple-darwin",
-              "exec_compatible_with": [
-                "@platforms//os:macos",
-                "@platforms//cpu:arm64"
-              ]
-            }
-          },
-          "probe_rs_tools_x86_64_pc_windows_msvc": {
-            "repoRuleId": "@@rules_probe_rs+//probe_rs/private:repositories.bzl%probe_rs_tools_archive_repo",
-            "attributes": {
-              "tools": [
-                "probe-rs"
-              ],
-              "url": "https://github.com/probe-rs/probe-rs/releases/download/v0.27.0/probe-rs-tools-x86_64-pc-windows-msvc.zip",
-              "sha256": "88a80e304caf706235c5fd2c75e8c2f6280ff86472348fc3bef19eadb9e47b8c",
-              "exec_compatible_with": [
-                "@platforms//os:windows",
-                "@platforms//cpu:x86_64"
-              ]
-            }
-          }
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "rules_probe_rs+",
-            "rules_probe_rs",
-            "rules_probe_rs+"
-          ]
-        ]
-      }
-    },
     "@@rules_python+//python/uv:uv.bzl%uv": {
       "general": {
-        "bzlTransitiveDigest": "477hS4MXeJ7LqPNLTqL+1ltraV5lqwOw3tEXWqnJRt8=",
+        "bzlTransitiveDigest": "8vT1ddXtljNxYD0tJkksqzeKE6xqx4Ix+tXthAppjTI=",
         "usagesDigest": "icnInV8HDGrRQf9x8RMfxWfBHgT3OgRlYovS/9POEJw=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
@@ -1583,12 +1130,12 @@
     },
     "@@rules_rust+//crate_universe:extension.bzl%crate": {
       "general": {
-        "bzlTransitiveDigest": "R3uUM8wZ6+Ue81AfVYJjzZJDkn6UdJu2gq59GrtQIY0=",
+        "bzlTransitiveDigest": "ip4D12Lz5jyA9AqXvt5BGvSeGwmBkz0xVNNhO0ywVSw=",
         "usagesDigest": "WfdfJaIbRmjr4fMHKZY9ihDeOMFR/bzh5E/2qr531WI=",
         "recordedFileInputs": {
           "@@//third_party/crates_io/Cargo.lock": "7b0c74ed1c866c9bc1a67b6809dac1b18014ad3907c0feab15b46bd4f8bc7d47",
           "@@//third_party/crates_io/Cargo.toml": "9a5623aec342fc0a155822290970b5051ffa281843a4f66c54ebc4fe91647c8f",
-          "@@//third_party/crates_io/crates_no_std/Cargo.lock": "f6378898fa08add3632613c54241edf68787789c59ea9ccccb051f6f47ab68bc",
+          "@@//third_party/crates_io/crates_no_std/Cargo.lock": "aa20e23f9aebaf59a30831fa5cb56bce46741b36a02e795b3cc09cff3cb5d372",
           "@@//third_party/crates_io/crates_no_std/Cargo.toml": "b7d1554800404f815e933bf28da4815b25ca6e86d5f79fcb11f5422ccdffc3a8",
           "@@pigweed+//third_party/crates_io/crates_no_std/Cargo.lock": "d38da5fa5f942b59a8017730285afe69aaf6f741b6171f361d66202de3473bc2",
           "@@pigweed+//third_party/crates_io/crates_no_std/Cargo.toml": "00e4a621a4a49d1b269bb2e688d8b5bbabb8700eb7b583ccaa20c36a60aebf98",
@@ -8982,10 +8529,6 @@
             }
           }
         },
-        "moduleExtensionMetadata": {
-          "useAllRepos": "NO",
-          "reproducible": false
-        },
         "recordedRepoMappingEntries": [
           [
             "bazel_features+",
@@ -9034,148 +8577,7 @@
           ]
         ]
       }
-    },
-    "@@rules_rust+//crate_universe/private:internal_extensions.bzl%cu_nr": {
-      "general": {
-        "bzlTransitiveDigest": "lIBKOfm2y3ftMR7jZfrHt+wWa5enJJaJMb8vfpIQV94=",
-        "usagesDigest": "h9wqdPgSnpDxdRNP6kO+DMoTuAWtxtek2SnJOOL321c=",
-        "recordedFileInputs": {},
-        "recordedDirentsInputs": {},
-        "envVariables": {},
-        "generatedRepoSpecs": {
-          "cargo_bazel_bootstrap": {
-            "repoRuleId": "@@rules_rust+//cargo/private:cargo_bootstrap.bzl%cargo_bootstrap_repository",
-            "attributes": {
-              "srcs": [
-                "@@rules_rust+//crate_universe:src/api.rs",
-                "@@rules_rust+//crate_universe:src/api/lockfile.rs",
-                "@@rules_rust+//crate_universe:src/cli.rs",
-                "@@rules_rust+//crate_universe:src/cli/generate.rs",
-                "@@rules_rust+//crate_universe:src/cli/query.rs",
-                "@@rules_rust+//crate_universe:src/cli/render.rs",
-                "@@rules_rust+//crate_universe:src/cli/splice.rs",
-                "@@rules_rust+//crate_universe:src/cli/vendor.rs",
-                "@@rules_rust+//crate_universe:src/config.rs",
-                "@@rules_rust+//crate_universe:src/context.rs",
-                "@@rules_rust+//crate_universe:src/context/crate_context.rs",
-                "@@rules_rust+//crate_universe:src/context/platforms.rs",
-                "@@rules_rust+//crate_universe:src/lib.rs",
-                "@@rules_rust+//crate_universe:src/lockfile.rs",
-                "@@rules_rust+//crate_universe:src/main.rs",
-                "@@rules_rust+//crate_universe:src/metadata.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_bin.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_resolver.rs",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.bat",
-                "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.sh",
-                "@@rules_rust+//crate_universe:src/metadata/dependency.rs",
-                "@@rules_rust+//crate_universe:src/metadata/metadata_annotation.rs",
-                "@@rules_rust+//crate_universe:src/rendering.rs",
-                "@@rules_rust+//crate_universe:src/rendering/template_engine.rs",
-                "@@rules_rust+//crate_universe:src/rendering/templates/module_bzl.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/header.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/aliases_map.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/deps_map.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_git.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_http.j2",
-                "@@rules_rust+//crate_universe:src/rendering/templates/vendor_module.j2",
-                "@@rules_rust+//crate_universe:src/rendering/verbatim/alias_rules.bzl",
-                "@@rules_rust+//crate_universe:src/select.rs",
-                "@@rules_rust+//crate_universe:src/splicing.rs",
-                "@@rules_rust+//crate_universe:src/splicing/cargo_config.rs",
-                "@@rules_rust+//crate_universe:src/splicing/crate_index_lookup.rs",
-                "@@rules_rust+//crate_universe:src/splicing/splicer.rs",
-                "@@rules_rust+//crate_universe:src/test.rs",
-                "@@rules_rust+//crate_universe:src/utils.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/glob.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/label.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_dict.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_list.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_scalar.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/select_set.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/serialize.rs",
-                "@@rules_rust+//crate_universe:src/utils/starlark/target_compatible_with.rs",
-                "@@rules_rust+//crate_universe:src/utils/symlink.rs",
-                "@@rules_rust+//crate_universe:src/utils/target_triple.rs"
-              ],
-              "binary": "cargo-bazel",
-              "cargo_lockfile": "@@rules_rust+//crate_universe:Cargo.lock",
-              "cargo_toml": "@@rules_rust+//crate_universe:Cargo.toml",
-              "version": "1.86.0",
-              "timeout": 900,
-              "rust_toolchain_cargo_template": "@rust_host_tools//:bin/{tool}",
-              "rust_toolchain_rustc_template": "@rust_host_tools//:bin/{tool}",
-              "compressed_windows_toolchain_names": false
-            }
-          }
-        },
-        "moduleExtensionMetadata": {
-          "explicitRootModuleDirectDeps": [
-            "cargo_bazel_bootstrap"
-          ],
-          "explicitRootModuleDirectDevDeps": [],
-          "useAllRepos": "NO",
-          "reproducible": false
-        },
-        "recordedRepoMappingEntries": [
-          [
-            "bazel_features+",
-            "bazel_features_globals",
-            "bazel_features++version_extension+bazel_features_globals"
-          ],
-          [
-            "bazel_features+",
-            "bazel_features_version",
-            "bazel_features++version_extension+bazel_features_version"
-          ],
-          [
-            "rules_cc+",
-            "bazel_tools",
-            "bazel_tools"
-          ],
-          [
-            "rules_cc+",
-            "rules_cc",
-            "rules_cc+"
-          ],
-          [
-            "rules_rust+",
-            "bazel_features",
-            "bazel_features+"
-          ],
-          [
-            "rules_rust+",
-            "bazel_skylib",
-            "bazel_skylib+"
-          ],
-          [
-            "rules_rust+",
-            "bazel_tools",
-            "bazel_tools"
-          ],
-          [
-            "rules_rust+",
-            "cui",
-            "rules_rust++cu+cui"
-          ],
-          [
-            "rules_rust+",
-            "rules_cc",
-            "rules_cc+"
-          ],
-          [
-            "rules_rust+",
-            "rules_rust",
-            "rules_rust+"
-          ],
-          [
-            "rules_rust+",
-            "rules_rust_ctve",
-            "rules_rust++i2+rules_rust_ctve"
-          ]
-        ]
-      }
     }
-  }
+  },
+  "facts": {}
 }
diff --git a/apps/i2c-slave-echo/BUILD.bazel b/apps/i2c-slave-echo/BUILD.bazel
new file mode 100644
index 0000000..7930ef1
--- /dev/null
+++ b/apps/i2c-slave-echo/BUILD.bazel
@@ -0,0 +1,28 @@
+# Licensed under the Apache-2.0 license
+
+load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package")
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+
+rust_binary(
+    name = "i2c_slave_echo",
+    srcs = ["src/main.rs"],
+    edition = "2024",
+    tags = ["kernel"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":app_i2c_slave_echo",
+        "//services/i2c/api:i2c_api",
+        "//services/i2c/client:i2c_client",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_log/rust:pw_log",
+        "@pigweed//pw_status/rust:pw_status",
+    ],
+)
+
+app_package(
+    name = "app_i2c_slave_echo",
+    app_name = "i2c_slave_echo",
+    edition = "2024",
+    system_config = "//target/ast1060-evb/i2c-slave:system_config",
+    tags = ["kernel"],
+)
diff --git a/apps/i2c-slave-echo/src/main.rs b/apps/i2c-slave-echo/src/main.rs
new file mode 100644
index 0000000..fd96ec1
--- /dev/null
+++ b/apps/i2c-slave-echo/src/main.rs
@@ -0,0 +1,137 @@
+// Licensed under the Apache-2.0 license
+
+//! I2C Slave Echo Application
+//!
+//! Sits on I2C2 at address 0x42 and implements a simple register model:
+//!
+//! - **Write** `[reg, value]` — stores `value` at `reg` and prints the access.
+//! - **Write** `[reg]` followed by **Read** — returns the stored value for `reg`
+//!   and prints the access.
+//!
+//! # Protocol
+//!
+//! ```text
+//! Master writes [reg, val]  →  slave stores reg_map[reg] = val
+//!                              prints: "WRITE reg=0xRR val=0xVV"
+//!
+//! Master writes [reg]       →  slave sets read pointer to reg
+//! Master reads N bytes      →  slave returns reg_map[reg..reg+N]
+//!                              prints: "READ  reg=0xRR val=0xVV"
+//! ```
+//!
+//! # Hardware
+//!
+//! The AST1060 I2C2 controller is used in simultaneous master+slave mode:
+//! master operations use the I2CM* register set, slave operations use I2CS*.
+
+#![no_main]
+#![no_std]
+
+use app_i2c_slave_echo::handle;
+use i2c_api::{BusIndex, I2cAddress, I2cTargetClient, SlaveEventKind};
+use i2c_client::IpcI2cClient;
+use pw_status::Result;
+use userspace::entry;
+
+/// I2C bus index (I2C2).
+const SLAVE_BUS: BusIndex = BusIndex::BUS_2;
+
+/// Slave address.
+const SLAVE_ADDR: u8 = 0x42;
+
+/// Number of registers in the echo register map.
+const REG_MAP_SIZE: usize = 256;
+
+fn slave_echo_loop() -> Result<()> {
+    let mut client = IpcI2cClient::new(handle::I2C);
+
+    let addr = I2cAddress::new(SLAVE_ADDR).map_err(|_| pw_status::Error::InvalidArgument)?;
+
+    // Configure and enable slave mode.
+    client
+        .configure_target_address(SLAVE_BUS, addr)
+        .map_err(|_| pw_status::Error::Internal)?;
+    client
+        .enable_receive(SLAVE_BUS)
+        .map_err(|_| pw_status::Error::Internal)?;
+
+    pw_log::info!(
+        "I2C slave echo running on bus {} at address 0x{:02x}",
+        SLAVE_BUS.value() as u32,
+        SLAVE_ADDR as u32,
+    );
+
+    // Register map — all bytes initialised to zero.
+    let mut reg_map = [0u8; REG_MAP_SIZE];
+    // Current register pointer (set by a 1-byte write, used for reads).
+    let mut reg_ptr: u8 = 0;
+
+    // Pre-load the initial read response.
+    let _ = client.slave_set_response(SLAVE_BUS, &[reg_map[reg_ptr as usize]]);
+
+    let mut rx_buf = [0u8; 32];
+
+    loop {
+        match client.slave_wait_event(SLAVE_BUS, &mut rx_buf) {
+            Ok((SlaveEventKind::DataReceived, n)) => {
+                match n {
+                    0 => {
+                        // Zero-length write — ignore (e.g. probe).
+                    }
+                    1 => {
+                        // Register pointer set; update read response.
+                        reg_ptr = rx_buf[0];
+                        let val = reg_map[reg_ptr as usize];
+                        // pw_log::info!(
+                        //     "READ  reg=0x{:02x} val=0x{:02x}",
+                        //     reg_ptr as u32,
+                        //     val as u32,
+                        // );
+                        let _ = client.slave_set_response(SLAVE_BUS, &[val]);
+                    }
+                    _ => {
+                        // Write: byte 0 = register address, byte 1 = value.
+                        reg_ptr = rx_buf[0];
+                        let val = rx_buf[1];
+                        reg_map[reg_ptr as usize] = val;
+                        // pw_log::info!(
+                        //     "WRITE reg=0x{:02x} val=0x{:02x}",
+                        //     reg_ptr as u32,
+                        //     val as u32,
+                        // );
+                        // Update read response in case master reads back immediately.
+                        let _ = client.slave_set_response(SLAVE_BUS, &[val]);
+                    }
+                }
+            }
+            Ok((SlaveEventKind::ReadRequest, _)) => {
+                // Master read our pre-loaded response; log it.
+                let val = reg_map[reg_ptr as usize];
+                // pw_log::info!(
+                //     "READ  reg=0x{:02x} val=0x{:02x}",
+                //     reg_ptr as u32,
+                //     val as u32,
+                // );
+                let _ = val;
+            }
+            Ok((SlaveEventKind::Stop, _)) => {
+                // Stop condition — nothing to do.
+            }
+            Err(_) => {
+                // Timeout or error — keep looping.
+            }
+        }
+    }
+}
+
+#[entry]
+fn entry() -> ! {
+    pw_log::info!("I2C slave echo starting");
+    let _ = slave_echo_loop();
+    loop {}
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
diff --git a/services/i2c/api/src/lib.rs b/services/i2c/api/src/lib.rs
index 62834c3..8164d11 100644
--- a/services/i2c/api/src/lib.rs
+++ b/services/i2c/api/src/lib.rs
@@ -98,4 +98,4 @@
 pub use operation::Operation;
 
 // Re-export target mode types
-pub use target::{I2cTargetClient, TargetMessage, TARGET_MESSAGE_MAX_LEN};
+pub use target::{I2cTargetClient, SlaveEventKind, TargetMessage, TARGET_MESSAGE_MAX_LEN};
diff --git a/services/i2c/api/src/target.rs b/services/i2c/api/src/target.rs
index e44b2d1..ef04777 100644
--- a/services/i2c/api/src/target.rs
+++ b/services/i2c/api/src/target.rs
@@ -122,6 +122,30 @@
     ) -> Result<usize, Self::Error>;
 }
 
+/// The type of event reported by a slave wait operation.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[repr(u8)]
+pub enum SlaveEventKind {
+    /// Master wrote data to us.
+    DataReceived = 0,
+    /// Master requested to read from us (TX data was sent).
+    ReadRequest = 1,
+    /// Stop condition received.
+    Stop = 2,
+}
+
+impl SlaveEventKind {
+    /// Decode from a raw byte.
+    pub fn from_u8(val: u8) -> Option<Self> {
+        match val {
+            0 => Some(Self::DataReceived),
+            1 => Some(Self::ReadRequest),
+            2 => Some(Self::Stop),
+            _ => None,
+        }
+    }
+}
+
 /// Maximum size of a target message payload.
 pub const TARGET_MESSAGE_MAX_LEN: usize = 255;
 
diff --git a/services/i2c/api/src/wire.rs b/services/i2c/api/src/wire.rs
index f993660..2e8161f 100644
--- a/services/i2c/api/src/wire.rs
+++ b/services/i2c/api/src/wire.rs
@@ -62,6 +62,18 @@
     ConfigureSpeed = 5,
     /// Bus recovery
     RecoverBus = 6,
+    /// Configure slave address on a bus
+    ConfigureSlave = 7,
+    /// Enable slave receive mode
+    EnableSlave = 8,
+    /// Disable slave receive mode
+    DisableSlave = 9,
+    /// Blocking poll for received slave data
+    SlaveReceive = 10,
+    /// Block until next slave event; returns event kind + any received data
+    SlaveWaitEvent = 11,
+    /// Pre-load TX buffer with data to send on next master read
+    SlaveSetResponse = 12,
 }
 
 impl I2cOp {
@@ -75,6 +87,12 @@
             4 => Some(Self::Probe),
             5 => Some(Self::ConfigureSpeed),
             6 => Some(Self::RecoverBus),
+            7 => Some(Self::ConfigureSlave),
+            8 => Some(Self::EnableSlave),
+            9 => Some(Self::DisableSlave),
+            10 => Some(Self::SlaveReceive),
+            11 => Some(Self::SlaveWaitEvent),
+            12 => Some(Self::SlaveSetResponse),
             _ => None,
         }
     }
@@ -160,6 +178,79 @@
         }
     }
 
+    /// Create a new ConfigureSlave request header
+    pub const fn configure_slave(bus: u8, address: u8) -> Self {
+        Self {
+            op: I2cOp::ConfigureSlave as u8,
+            bus,
+            address,
+            write_len: 0,
+            read_len: 0,
+        }
+    }
+
+    /// Create a new EnableSlave request header
+    pub const fn enable_slave(bus: u8) -> Self {
+        Self {
+            op: I2cOp::EnableSlave as u8,
+            bus,
+            address: 0,
+            write_len: 0,
+            read_len: 0,
+        }
+    }
+
+    /// Create a new DisableSlave request header
+    pub const fn disable_slave(bus: u8) -> Self {
+        Self {
+            op: I2cOp::DisableSlave as u8,
+            bus,
+            address: 0,
+            write_len: 0,
+            read_len: 0,
+        }
+    }
+
+    /// Create a new SlaveReceive request header
+    ///
+    /// `max_len` is the maximum number of bytes the caller can accept.
+    pub const fn slave_receive(bus: u8, max_len: u16) -> Self {
+        Self {
+            op: I2cOp::SlaveReceive as u8,
+            bus,
+            address: 0,
+            write_len: 0,
+            read_len: max_len,
+        }
+    }
+
+    /// Create a new SlaveWaitEvent request header
+    ///
+    /// `max_rx_len` is the maximum number of received data bytes the caller
+    /// can accept (not counting the leading event-kind byte).
+    pub const fn slave_wait_event(bus: u8, max_rx_len: u16) -> Self {
+        Self {
+            op: I2cOp::SlaveWaitEvent as u8,
+            bus,
+            address: 0,
+            write_len: 0,
+            read_len: max_rx_len,
+        }
+    }
+
+    /// Create a new SlaveSetResponse request header
+    ///
+    /// `data_len` is the number of bytes in the following payload.
+    pub const fn slave_set_response(bus: u8, data_len: u16) -> Self {
+        Self {
+            op: I2cOp::SlaveSetResponse as u8,
+            bus,
+            address: 0,
+            write_len: data_len,
+            read_len: 0,
+        }
+    }
+
     /// Encode header to bytes (little-endian)
     pub fn to_bytes(&self) -> [u8; Self::SIZE] {
         let write_le = self.write_len.to_le_bytes();
@@ -362,6 +453,106 @@
     Ok(I2cRequestHeader::SIZE)
 }
 
+/// Encode a configure-slave request into a buffer
+///
+/// # Errors
+/// - `WireError::BufferTooSmall` if buffer cannot hold header
+pub fn encode_configure_slave_request(buf: &mut [u8], bus: u8, address: u8) -> Result<usize, WireError> {
+    if buf.len() < I2cRequestHeader::SIZE {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::configure_slave(bus, address);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    Ok(I2cRequestHeader::SIZE)
+}
+
+/// Encode an enable-slave request into a buffer
+///
+/// # Errors
+/// - `WireError::BufferTooSmall` if buffer cannot hold header
+pub fn encode_enable_slave_request(buf: &mut [u8], bus: u8) -> Result<usize, WireError> {
+    if buf.len() < I2cRequestHeader::SIZE {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::enable_slave(bus);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    Ok(I2cRequestHeader::SIZE)
+}
+
+/// Encode a disable-slave request into a buffer
+///
+/// # Errors
+/// - `WireError::BufferTooSmall` if buffer cannot hold header
+pub fn encode_disable_slave_request(buf: &mut [u8], bus: u8) -> Result<usize, WireError> {
+    if buf.len() < I2cRequestHeader::SIZE {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::disable_slave(bus);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    Ok(I2cRequestHeader::SIZE)
+}
+
+/// Encode a slave-receive request into a buffer
+///
+/// `max_len` is the maximum number of bytes the caller can accept in the response.
+///
+/// # Errors
+/// - `WireError::PayloadTooLarge` if max_len exceeds MAX_PAYLOAD_SIZE
+/// - `WireError::BufferTooSmall` if buffer cannot hold header
+pub fn encode_slave_receive_request(buf: &mut [u8], bus: u8, max_len: u16) -> Result<usize, WireError> {
+    if max_len as usize > MAX_PAYLOAD_SIZE {
+        return Err(WireError::PayloadTooLarge);
+    }
+    if buf.len() < I2cRequestHeader::SIZE {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::slave_receive(bus, max_len);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    Ok(I2cRequestHeader::SIZE)
+}
+
+/// Encode a slave-wait-event request into a buffer.
+///
+/// `max_rx_len` is the maximum received data bytes the caller accepts (not
+/// counting the leading event-kind byte in the response payload).
+///
+/// # Errors
+/// - `WireError::PayloadTooLarge` if max_rx_len exceeds MAX_PAYLOAD_SIZE - 1
+/// - `WireError::BufferTooSmall` if buffer cannot hold header
+pub fn encode_slave_wait_event_request(buf: &mut [u8], bus: u8, max_rx_len: u16) -> Result<usize, WireError> {
+    if max_rx_len as usize >= MAX_PAYLOAD_SIZE {
+        return Err(WireError::PayloadTooLarge);
+    }
+    if buf.len() < I2cRequestHeader::SIZE {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::slave_wait_event(bus, max_rx_len);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    Ok(I2cRequestHeader::SIZE)
+}
+
+/// Encode a slave-set-response request into a buffer.
+///
+/// The `data` slice is the TX payload the slave will send on the next master read.
+///
+/// # Errors
+/// - `WireError::PayloadTooLarge` if data exceeds MAX_PAYLOAD_SIZE
+/// - `WireError::BufferTooSmall` if buffer cannot hold header + data
+pub fn encode_slave_set_response_request(buf: &mut [u8], bus: u8, data: &[u8]) -> Result<usize, WireError> {
+    if data.len() > MAX_PAYLOAD_SIZE {
+        return Err(WireError::PayloadTooLarge);
+    }
+    let data_len = u16::try_from(data.len()).map_err(|_| WireError::PayloadTooLarge)?;
+    let total_len = I2cRequestHeader::SIZE + data.len();
+    if buf.len() < total_len {
+        return Err(WireError::BufferTooSmall);
+    }
+    let header = I2cRequestHeader::slave_set_response(bus, data_len);
+    buf[..I2cRequestHeader::SIZE].copy_from_slice(&header.to_bytes());
+    buf[I2cRequestHeader::SIZE..total_len].copy_from_slice(data);
+    Ok(total_len)
+}
+
 // ============================================================================
 // Decoding Helpers
 // ============================================================================
diff --git a/services/i2c/backend-aspeed/src/lib.rs b/services/i2c/backend-aspeed/src/lib.rs
index 32587f7..86a4343 100644
--- a/services/i2c/backend-aspeed/src/lib.rs
+++ b/services/i2c/backend-aspeed/src/lib.rs
@@ -39,8 +39,8 @@
 
 #![no_std]
 
-use aspeed_ddk::i2c_core::{Ast1060I2c, Controller as DdkController, I2cConfig, I2cError};
-use i2c_api::ResponseCode;
+use aspeed_ddk::i2c_core::{Ast1060I2c, Controller as DdkController, I2cConfig, I2cError, SlaveConfig, SlaveEvent};
+use i2c_api::{ResponseCode, SlaveEventKind};
 
 use pw_log;
 
@@ -86,10 +86,19 @@
 /// The backend exclusively owns `ast1060_pac::Peripherals`, ensuring no
 /// aliased register access. The `unsafe` is confined to [`new()`] where
 /// `Peripherals::steal()` is called.
+/// Maximum TX buffer size per bus (matches hardware buffer size).
+const SLAVE_TX_BUF_SIZE: usize = 32;
+
 pub struct AspeedI2cBackend {
     peripherals: ast1060_pac::Peripherals,
     /// Tracks which buses have been initialized via `init_bus()`.
     initialized: u16,
+    /// Tracks which buses have slave mode configured via `configure_slave()`.
+    slave_configured: u16,
+    /// Pre-loaded TX data to send when master reads from us (one buffer per bus).
+    slave_tx_bufs: [[u8; SLAVE_TX_BUF_SIZE]; 14],
+    /// Valid byte count in each `slave_tx_bufs` slot.
+    slave_tx_lens: [usize; 14],
 }
 
 impl AspeedI2cBackend {
@@ -105,6 +114,9 @@
         Self {
             peripherals: unsafe { ast1060_pac::Peripherals::steal() },
             initialized: 0,
+            slave_configured: 0,
+            slave_tx_bufs: [[0u8; SLAVE_TX_BUF_SIZE]; 14],
+            slave_tx_lens: [0usize; 14],
         }
     }
 
@@ -258,4 +270,194 @@
         let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::with_static_clocks());
         i2c.recover_bus().map_err(map_i2c_error)
     }
+
+    // -----------------------------------------------------------------------
+    // Slave operations
+    // -----------------------------------------------------------------------
+
+    /// Configure a bus as an I2C slave at the given 7-bit address.
+    ///
+    /// The bus must have been initialized via [`init_bus()`] first.
+    /// After configuration, call [`enable_slave()`] to start receiving.
+    pub fn configure_slave(&mut self, bus: u8, addr: u8) -> Result<(), ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        let (regs, buffs) = self.controller_regs(bus)?;
+        let ctrl = aspeed_ddk::i2c_core::I2cController {
+            controller: DdkController(bus),
+            registers: regs,
+            buff_registers: buffs,
+        };
+        let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::default());
+        let config = SlaveConfig::new(addr).map_err(map_i2c_error)?;
+        i2c.configure_slave(&config).map_err(map_i2c_error)?;
+        self.slave_configured |= 1 << bus;
+        pw_log::info!("I2C bus {} slave configured at address 0x{:02x}", bus as u8, addr as u8);
+        Ok(())
+    }
+
+    /// Enable slave receive mode on a previously configured bus.
+    pub fn enable_slave(&mut self, bus: u8) -> Result<(), ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        if (self.slave_configured & (1 << bus)) == 0 {
+            return Err(ResponseCode::NotInitialized);
+        }
+        let (regs, buffs) = self.controller_regs(bus)?;
+        let ctrl = aspeed_ddk::i2c_core::I2cController {
+            controller: DdkController(bus),
+            registers: regs,
+            buff_registers: buffs,
+        };
+        let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::default());
+        i2c.enable_slave();
+        pw_log::info!("I2C bus {} slave enabled", bus as u8);
+        Ok(())
+    }
+
+    /// Disable slave receive mode on a bus.
+    pub fn disable_slave(&mut self, bus: u8) -> Result<(), ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        let (regs, buffs) = self.controller_regs(bus)?;
+        let ctrl = aspeed_ddk::i2c_core::I2cController {
+            controller: DdkController(bus),
+            registers: regs,
+            buff_registers: buffs,
+        };
+        let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::default());
+        i2c.disable_slave();
+        pw_log::info!("I2C bus {} slave disabled", bus as u8);
+        Ok(())
+    }
+
+    /// Pre-load the TX buffer for a bus.
+    ///
+    /// Data stored here will be sent to the master when it issues a read
+    /// transaction to our slave address, as part of `slave_wait_event`.
+    pub fn slave_set_response(&mut self, bus: u8, data: &[u8]) -> Result<(), ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        let len = data.len().min(SLAVE_TX_BUF_SIZE);
+        self.slave_tx_bufs[bus as usize][..len].copy_from_slice(&data[..len]);
+        self.slave_tx_lens[bus as usize] = len;
+        Ok(())
+    }
+
+    /// Block until the next slave event, handling reads automatically.
+    ///
+    /// On `DataReceived`, reads bytes into `rx_buf` and returns
+    /// `(SlaveEventKind::DataReceived, n)` where `n` is the byte count.
+    ///
+    /// On `ReadRequest`, sends the pre-loaded TX buffer (set via
+    /// [`slave_set_response`]) and returns `(SlaveEventKind::ReadRequest, 0)`.
+    ///
+    /// On `Stop`, returns `(SlaveEventKind::Stop, 0)`.
+    pub fn slave_wait_event(
+        &mut self,
+        bus: u8,
+        rx_buf: &mut [u8],
+    ) -> Result<(SlaveEventKind, usize), ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        if (self.slave_configured & (1 << bus)) == 0 {
+            return Err(ResponseCode::NotInitialized);
+        }
+
+        // Copy TX buffer to local stack before borrowing peripherals.
+        let tx_len = self.slave_tx_lens[bus as usize];
+        let mut tx_local = [0u8; SLAVE_TX_BUF_SIZE];
+        tx_local[..tx_len].copy_from_slice(&self.slave_tx_bufs[bus as usize][..tx_len]);
+
+        let (regs, buffs) = self.controller_regs(bus)?;
+        let ctrl = aspeed_ddk::i2c_core::I2cController {
+            controller: DdkController(bus),
+            registers: regs,
+            buff_registers: buffs,
+        };
+        let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::default());
+
+        const POLL_BUDGET: usize = 10_000;
+        for _ in 0..POLL_BUDGET {
+            match i2c.handle_slave_interrupt() {
+                Some(SlaveEvent::DataReceived { len: _ }) => {
+                    let n = i2c.slave_read(rx_buf).map_err(map_i2c_error)?;
+                    return Ok((SlaveEventKind::DataReceived, n));
+                }
+                Some(SlaveEvent::ReadRequest) => {
+                    let _ = i2c.slave_write(&tx_local[..tx_len]);
+                    return Ok((SlaveEventKind::ReadRequest, 0));
+                }
+                Some(SlaveEvent::DataSent { len: _ }) => {
+                    // Read transaction completed — treat same as ReadRequest
+                    // (data was already pre-loaded and sent by hardware).
+                    return Ok((SlaveEventKind::ReadRequest, 0));
+                }
+                Some(SlaveEvent::Stop) => {
+                    return Ok((SlaveEventKind::Stop, 0));
+                }
+                Some(SlaveEvent::WriteRequest) | None => {
+                    continue;
+                }
+            }
+        }
+
+        Err(ResponseCode::Timeout)
+    }
+
+    /// Poll for received slave data, blocking until data arrives or timeout.
+    ///
+    /// Returns the number of bytes written into `buf`, or 0 if a Stop was
+    /// received before any data (e.g. a probe). Returns `ResponseCode::Timeout`
+    /// if no activity is seen within the polling budget.
+    pub fn slave_receive(&mut self, bus: u8, buf: &mut [u8]) -> Result<usize, ResponseCode> {
+        if !self.is_bus_initialized(bus) {
+            return Err(ResponseCode::ServerError);
+        }
+        if (self.slave_configured & (1 << bus)) == 0 {
+            return Err(ResponseCode::NotInitialized);
+        }
+        let (regs, buffs) = self.controller_regs(bus)?;
+        let ctrl = aspeed_ddk::i2c_core::I2cController {
+            controller: DdkController(bus),
+            registers: regs,
+            buff_registers: buffs,
+        };
+        let mut i2c = Ast1060I2c::from_initialized(&ctrl, I2cConfig::default());
+
+        // Polling loop — spins until a relevant event or budget exhausted.
+        // At 200 MHz, ~10_000 iterations ≈ a few hundred microseconds.
+        const POLL_BUDGET: usize = 10_000;
+        for _ in 0..POLL_BUDGET {
+            match i2c.handle_slave_interrupt() {
+                Some(SlaveEvent::DataReceived { len: _ }) => {
+                    let n = i2c.slave_read(buf).map_err(map_i2c_error)?;
+                    return Ok(n);
+                }
+                Some(SlaveEvent::Stop) => {
+                    // Stop without data (e.g. probe or zero-length write).
+                    return Ok(0);
+                }
+                Some(SlaveEvent::WriteRequest) | Some(SlaveEvent::ReadRequest) => {
+                    // Transaction in progress — keep polling for data.
+                    continue;
+                }
+                Some(SlaveEvent::DataSent { len: _ }) => {
+                    // Master read from us; not relevant for receive path.
+                    continue;
+                }
+                None => {
+                    // No hardware event yet — keep polling.
+                    continue;
+                }
+            }
+        }
+
+        Err(ResponseCode::Timeout)
+    }
 }
diff --git a/services/i2c/client/src/lib.rs b/services/i2c/client/src/lib.rs
index 5f50a0c..6048c45 100644
--- a/services/i2c/client/src/lib.rs
+++ b/services/i2c/client/src/lib.rs
@@ -24,11 +24,14 @@
 
 use i2c_api::{
     wire::{
-        self, encode_probe_request, encode_read_request, encode_write_read_request,
-        encode_write_request, I2cResponseHeader, WireError,
+        self, encode_configure_slave_request, encode_disable_slave_request,
+        encode_enable_slave_request, encode_probe_request, encode_read_request,
+        encode_slave_receive_request, encode_slave_set_response_request,
+        encode_slave_wait_event_request, encode_write_read_request, encode_write_request,
+        I2cResponseHeader, WireError,
     },
-    BusIndex, I2cAddress, I2cClient, I2cError, I2cErrorKind, NoAcknowledgeSource, Operation,
-    ResponseCode,
+    BusIndex, I2cAddress, I2cClient, I2cError, I2cErrorKind, I2cTargetClient, NoAcknowledgeSource,
+    Operation, ResponseCode, SlaveEventKind, TargetMessage, TARGET_MESSAGE_MAX_LEN,
 };
 
 use userspace::syscall;
@@ -207,3 +210,134 @@
         Ok(())
     }
 }
+
+impl IpcI2cClient {
+    /// Pre-load data the slave will send when the master reads from us.
+    ///
+    /// Must be called before [`slave_wait_event`] if a read response is needed.
+    pub fn slave_set_response(&mut self, bus: BusIndex, data: &[u8]) -> Result<(), I2cError> {
+        let req_len =
+            encode_slave_set_response_request(&mut self.request_buf, bus.value(), data)
+                .map_err(wire_to_i2c_error)?;
+        let resp_len = self.send_recv(req_len)?;
+        let _ = self.decode_response(resp_len)?;
+        Ok(())
+    }
+
+    /// Block until the next slave event on `bus`.
+    ///
+    /// Returns the event kind and, for `DataReceived`, the received bytes
+    /// written into `rx_buf`. The returned `usize` is the number of bytes
+    /// written.
+    pub fn slave_wait_event(
+        &mut self,
+        bus: BusIndex,
+        rx_buf: &mut [u8],
+    ) -> Result<(SlaveEventKind, usize), I2cError> {
+        let max_rx = rx_buf.len().min(wire::MAX_PAYLOAD_SIZE - 1);
+        let req_len = encode_slave_wait_event_request(
+            &mut self.request_buf,
+            bus.value(),
+            max_rx as u16,
+        )
+        .map_err(wire_to_i2c_error)?;
+
+        let resp_len = self.send_recv(req_len)?;
+        let data = self.decode_response(resp_len)?;
+
+        if data.is_empty() {
+            return Err(I2cError::from_code(ResponseCode::ServerError));
+        }
+
+        let kind = SlaveEventKind::from_u8(data[0])
+            .ok_or_else(|| I2cError::from_code(ResponseCode::ServerError))?;
+
+        let rx_len = if kind == SlaveEventKind::DataReceived {
+            let n = (data.len() - 1).min(rx_buf.len());
+            rx_buf[..n].copy_from_slice(&data[1..1 + n]);
+            n
+        } else {
+            0
+        };
+
+        Ok((kind, rx_len))
+    }
+}
+
+impl I2cTargetClient for IpcI2cClient {
+    fn configure_target_address(
+        &mut self,
+        bus: BusIndex,
+        address: I2cAddress,
+    ) -> Result<(), Self::Error> {
+        let req_len =
+            encode_configure_slave_request(&mut self.request_buf, bus.value(), address.value())
+                .map_err(wire_to_i2c_error)?;
+        let resp_len = self.send_recv(req_len)?;
+        let _ = self.decode_response(resp_len)?;
+        Ok(())
+    }
+
+    fn enable_receive(&mut self, bus: BusIndex) -> Result<(), Self::Error> {
+        let req_len = encode_enable_slave_request(&mut self.request_buf, bus.value())
+            .map_err(wire_to_i2c_error)?;
+        let resp_len = self.send_recv(req_len)?;
+        let _ = self.decode_response(resp_len)?;
+        Ok(())
+    }
+
+    fn disable_receive(&mut self, bus: BusIndex) -> Result<(), Self::Error> {
+        let req_len = encode_disable_slave_request(&mut self.request_buf, bus.value())
+            .map_err(wire_to_i2c_error)?;
+        let resp_len = self.send_recv(req_len)?;
+        let _ = self.decode_response(resp_len)?;
+        Ok(())
+    }
+
+    fn wait_for_messages(
+        &mut self,
+        bus: BusIndex,
+        messages: &mut [TargetMessage],
+        _timeout: Option<core::time::Duration>,
+    ) -> Result<usize, Self::Error> {
+        let mut count = 0;
+        for msg in messages.iter_mut() {
+            let req_len = encode_slave_receive_request(
+                &mut self.request_buf,
+                bus.value(),
+                TARGET_MESSAGE_MAX_LEN as u16,
+            )
+            .map_err(wire_to_i2c_error)?;
+            let resp_len = self.send_recv(req_len)?;
+            let data = self.decode_response(resp_len)?;
+            if data.is_empty() {
+                // No data (Stop or timeout) — no more messages pending.
+                break;
+            }
+            let copy_len = core::cmp::min(data.len(), TARGET_MESSAGE_MAX_LEN);
+            msg.data_mut()[..copy_len].copy_from_slice(&data[..copy_len]);
+            msg.set_len(copy_len);
+            count += 1;
+        }
+        Ok(count)
+    }
+
+    fn register_notification(
+        &mut self,
+        _bus: BusIndex,
+        _notification_mask: u32,
+    ) -> Result<(), Self::Error> {
+        // Interrupt-driven notifications are not yet implemented.
+        Err(I2cError::from_code(ResponseCode::ServerError))
+    }
+
+    fn get_pending_messages(
+        &mut self,
+        bus: BusIndex,
+        messages: &mut [TargetMessage],
+    ) -> Result<usize, Self::Error> {
+        // Non-blocking variant: same as wait_for_messages (hardware poll returns
+        // immediately on timeout with 0 bytes).
+        self.wait_for_messages(bus, messages, None)
+    }
+}
diff --git a/services/i2c/server/src/main.rs b/services/i2c/server/src/main.rs
index 55c2066..9a5ae37 100644
--- a/services/i2c/server/src/main.rs
+++ b/services/i2c/server/src/main.rs
@@ -109,7 +109,6 @@
     response: &mut [u8],
     backend: &mut AspeedI2cBackend,
 ) -> usize {
-    pw_log::info!("I2C server dispatch");
     // Parse header
     let Some(header) = I2cRequestHeader::from_bytes(request) else {
         return encode_error(response, ResponseCode::ServerError);
@@ -203,6 +202,94 @@
         }
 
         // ------------------------------------------------------------------
+        // ConfigureSlave: set slave address on a bus
+        // ------------------------------------------------------------------
+        I2cOp::ConfigureSlave => {
+            pw_log::info!("I2C dispatch configure slave");
+            match backend.configure_slave(header.bus, header.address) {
+                Ok(()) => encode_success(response, 0),
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
+        // EnableSlave: activate slave receive mode
+        // ------------------------------------------------------------------
+        I2cOp::EnableSlave => {
+            pw_log::info!("I2C dispatch enable slave");
+            match backend.enable_slave(header.bus) {
+                Ok(()) => encode_success(response, 0),
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
+        // DisableSlave: deactivate slave receive mode
+        // ------------------------------------------------------------------
+        I2cOp::DisableSlave => {
+            pw_log::info!("I2C dispatch disable slave");
+            match backend.disable_slave(header.bus) {
+                Ok(()) => encode_success(response, 0),
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
+        // SlaveReceive: blocking poll for incoming data
+        // ------------------------------------------------------------------
+        I2cOp::SlaveReceive => {
+            pw_log::info!("I2C dispatch slave receive");
+            let rlen = header.read_len as usize;
+            let avail = response.len().saturating_sub(I2cResponseHeader::SIZE);
+            if rlen > avail {
+                return encode_error(response, ResponseCode::BufferTooLarge);
+            }
+            let buf = &mut response[I2cResponseHeader::SIZE..I2cResponseHeader::SIZE + rlen];
+            match backend.slave_receive(header.bus, buf) {
+                Ok(n) => encode_success(response, n),
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
+        // SlaveSetResponse: pre-load TX buffer for next master read
+        // ------------------------------------------------------------------
+        I2cOp::SlaveSetResponse => {
+            // pw_log::info!("I2C dispatch slave set response");
+            let wlen = header.write_len as usize;
+            if payload.len() < wlen {
+                return encode_error(response, ResponseCode::BufferTooSmall);
+            }
+            match backend.slave_set_response(header.bus, &payload[..wlen]) {
+                Ok(()) => encode_success(response, 0),
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
+        // SlaveWaitEvent: block until next slave event, return kind + data
+        //
+        // Response payload layout:
+        //   byte 0:    SlaveEventKind as u8
+        //   bytes 1..: received data (only for DataReceived events)
+        // ------------------------------------------------------------------
+        I2cOp::SlaveWaitEvent => {
+            let max_rx = header.read_len as usize;
+            // Reserve space for event-kind byte + rx data.
+            let avail = response.len().saturating_sub(I2cResponseHeader::SIZE + 1);
+            let rx_cap = max_rx.min(avail);
+            let rx_buf = &mut response[I2cResponseHeader::SIZE + 1..I2cResponseHeader::SIZE + 1 + rx_cap];
+            match backend.slave_wait_event(header.bus, rx_buf) {
+                Ok((kind, rx_len)) => {
+                    let total = 1 + rx_len;
+                    response[I2cResponseHeader::SIZE] = kind as u8;
+                    encode_success(response, total)
+                }
+                Err(code) => encode_error(response, code),
+            }
+        }
+
+        // ------------------------------------------------------------------
         // Not yet implemented
         // ------------------------------------------------------------------
         I2cOp::ConfigureSpeed | I2cOp::Transaction => {
diff --git a/target/ast1060-evb/harness/uart_test_exec.py b/target/ast1060-evb/harness/uart_test_exec.py
index 4f1fc1a..bfd942a 100644
--- a/target/ast1060-evb/harness/uart_test_exec.py
+++ b/target/ast1060-evb/harness/uart_test_exec.py
@@ -79,24 +79,35 @@
             self.log_file_handle.write(text)
             self.log_file_handle.flush()
 
-    def print_uart_data(self, data: str):
-        """Print UART data, with detokenized output on the following line in green.
+    def print_uart_data(self, raw: bytes):
+        """Print UART data, detokenizing base64-encoded pw_log tokens when possible.
 
-        If --notok is set, the raw tokenized data is suppressed and only the
-        detokenized output is printed. When --elf is supplied, the detokenized
-        string (not the raw token) is written to the log.
+        raw  -- the undecoded bytes straight from the serial port.
+
+        pw_tokenizer embeds base64-encoded token frames in otherwise plain text.
+        We let detokenize_base64 scan the byte string for those frames and
+        substitute decoded strings in-place; anything that is not a token frame
+        is left as-is.  The result is a plain str that we print in green when
+        detokenization changed anything.
+
+        If --notok is set the raw line is suppressed and only the decoded output
+        is shown.  When no ELF / detokenizer is available we just decode and
+        print the raw bytes.
         """
+        text = raw.decode("utf-8", errors="replace")
+
         if self.detokenizer:
-            detokenized = self.detokenizer.detokenize_text(data)
-            if detokenized != data:
+            detokenized = self.detokenizer.detokenize_text(raw).decode("utf-8", errors="replace")
+            if detokenized != text:
                 if not getattr(self.args, "notok", False):
-                    print(data, end="", flush=True)
-                    self._write_log(data)
+                    print(text, end="", flush=True)
+                    self._write_log(text)
                 print(f"\033[32m{detokenized}\033[0m", end="", flush=True)
                 self._write_log(detokenized)
                 return
-        print(data, end="", flush=True)
-        self._write_log(data)
+
+        print(text, end="", flush=True)
+        self._write_log(text)
 
     def run_command(self, cmd: list, check: bool = True) -> Tuple[int, str, str]:
         """Run command and return (returncode, stdout, stderr)."""
@@ -211,27 +222,22 @@
             self.log_file_handle.close()
             self.log_file_handle = None
 
-    def read_serial_data(self, timeout_seconds: float = 1.0) -> str:
-        """Read available data from serial port."""
+    def read_serial_data(self, timeout_seconds: float = 1.0) -> bytes:
+        """Read available data from serial port, returning raw bytes."""
         if not self.serial_port or self.args.skip_uart:
-            return ""
+            return b""
 
         if self.args.dry_run:
-            return ""
+            return b""
 
         try:
             self.serial_port.timeout = timeout_seconds
             data = self.serial_port.read(1024)
-
-            if data:
-                decoded = data.decode("utf-8", errors="ignore")
-                return decoded
-
-            return ""
+            return data if data else b""
 
         except Exception as e:
             self.log(f"Serial read error: {e}")
-            return ""
+            return b""
 
     def write_serial_data(self, data: bytes) -> bool:
         """Write data to serial port."""
@@ -264,7 +270,7 @@
             return True
 
         start_time = time.time()
-        buffer = ""
+        buffer = b""
 
         while time.time() - start_time < timeout:
             data = self.read_serial_data(0.1)
@@ -274,7 +280,7 @@
                     self.print_uart_data(data)
 
                 # Look for 'U' character
-                if "U" in buffer:
+                if b"U" in buffer:
                     self.log("\nUART bootloader ready detected!")
                     return True
 
@@ -346,18 +352,21 @@
             return True
 
         start_time = time.time()
-        buffer = ""
+        line_buffer = ""
         test_results = {"passed": 0, "failed": 0, "skipped": 0}
 
         while time.time() - start_time < actual_timeout:
             data = self.read_serial_data(0.5)
             if data:
-                buffer += data
                 if not self.args.quiet:
                     self.print_uart_data(data)
 
-                lines = buffer.split("\n")
-                for line in lines:
+                line_buffer += data.decode("utf-8", errors="replace")
+                lines = line_buffer.split("\n")
+                # Keep the last (possibly incomplete) line in the buffer
+                line_buffer = lines[-1]
+
+                for line in lines[:-1]:
                     if "PASS" in line:
                         test_results["passed"] += 1
                     elif "FAIL" in line:
@@ -378,8 +387,6 @@
                             self.log(f"\nFailure detected: {pattern}")
                             return False
 
-                buffer = "\n".join(lines[-10:])
-
         self.log(f"\nTest monitoring timeout. Results so far: {test_results}")
         return test_results["failed"] == 0
 
diff --git a/target/ast1060-evb/i2c-slave/BUILD.bazel b/target/ast1060-evb/i2c-slave/BUILD.bazel
new file mode 100644
index 0000000..98386ad
--- /dev/null
+++ b/target/ast1060-evb/i2c-slave/BUILD.bazel
@@ -0,0 +1,86 @@
+# Licensed under the Apache-2.0 license
+
+load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package")
+load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image")
+load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen")
+load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script")
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+load("//target/ast1060-evb:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target:uart_boot_image.bzl", "uart_boot_image")
+load("//target/ast1060-evb/harness:uart_upload_test.bzl", "uart_upload")
+
+# ---------------------------------------------------------------------------
+# System image: I2C server + slave echo app
+# ---------------------------------------------------------------------------
+
+system_image(
+    name = "i2c_slave",
+    apps = [
+        "//apps/i2c-slave-echo:i2c_slave_echo",
+        "//services/i2c/server:i2c_server",
+    ],
+    kernel = ":target",
+    platform = "//target/ast1060-evb",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    visibility = ["//visibility:public"],
+)
+
+# Generate UART bootable image for flashing to the physical board
+uart_boot_image(
+    name = "i2c_slave_uart",
+    src = ":i2c_slave",
+    out = "i2c_slave_uart.bin",
+)
+
+# Upload-only target for manual testing
+# Run with: bazel run :upload_i2c_slave --config=k_ast1060_evb
+uart_upload(
+    name = "upload_i2c_slave",
+    image = ":i2c_slave_uart",
+    skip_gpio = True,
+)
+
+filegroup(
+    name = "system_config",
+    srcs = ["system.json5"],
+    visibility = ["//visibility:public"],
+)
+
+target_codegen(
+    name = "codegen",
+    arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+    system_config = ":system_config",
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+)
+
+target_linker_script(
+    name = "linker_script",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    template = "//target/ast1060-evb:linker_script_template",
+)
+
+rust_binary(
+    name = "target",
+    srcs = [
+        "target.rs",
+    ],
+    edition = "2024",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    deps = [
+        ":codegen",
+        ":linker_script",
+        "//target/ast1060-evb:entry",
+        "//target/ast1060-evb:console_backend_uart",
+        "@rust_crates//:cortex-m-semihosting",
+        "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+        "@pigweed//pw_kernel/kernel",
+        "@pigweed//pw_kernel/target:target_common",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_log/rust:pw_log",
+    ],
+)
diff --git a/target/ast1060-evb/i2c-slave/system.json5 b/target/ast1060-evb/i2c-slave/system.json5
new file mode 100644
index 0000000..c3f99db
--- /dev/null
+++ b/target/ast1060-evb/i2c-slave/system.json5
@@ -0,0 +1,87 @@
+// Licensed under the Apache-2.0 license
+
+// AST1060-EVB I2C Slave Echo System Configuration
+//
+// Memory Layout:
+//   0x00000000 - 0x000004A0: Vector table + kernel annotations
+//   0x000004A0 - 0x00020000: Kernel code (~126KB)
+//   0x00020000 - 0x00040000: i2c_server flash (128KB)
+//   0x00040000 - 0x00050000: i2c_slave_echo flash (64KB)
+//   0x00060000 - 0x00080000: Kernel RAM (128KB)
+//   0x00080000 - 0x00090000: i2c_server RAM (64KB)
+//   0x00090000 - 0x00098000: i2c_slave_echo RAM (32KB)
+{
+    arch: {
+        type: "armv7m",
+        vector_table_start_address: 0x00000000,
+        vector_table_size_bytes: 1280,
+    },
+    kernel: {
+        flash_start_address: 0x00000500,
+        flash_size_bytes: 129792,
+        ram_start_address: 0x00060000,
+        ram_size_bytes: 131072,
+    },
+    apps: [
+        // ──── I2C Server ────
+        {
+            name: "i2c_server",
+            flash_size_bytes: 131072,
+            ram_size_bytes: 65536,
+            process: {
+                name: "i2c server process",
+                objects: [
+                    {
+                        name: "I2C",
+                        type: "channel_handler",
+                    },
+                ],
+                memory_mappings: [
+                    {
+                        // AST1060 I2C peripheral block: I2cglobal through I2cFilter
+                        // Base: 0x7e7b0000, covers all 14 controllers + buffers + filter
+                        name: "i2c_regs",
+                        type: "device",
+                        start_address: 0x7e7b0000,
+                        size_bytes: 0x4000,
+                    },
+                    {
+                        name: "scu",
+                        type: "device",
+                        start_address: 0x7e6e2000,
+                        size_bytes: 0x1000,  // SCU register block (scu310 at +0x310 read for APB clock)
+                    },
+                ],
+                threads: [
+                    {
+                        name: "i2c server thread",
+                        stack_size_bytes: 4096,
+                    },
+                ],
+            },
+        },
+        // ──── I2C Slave Echo ────
+        {
+            name: "i2c_slave_echo",
+            flash_size_bytes: 65536,
+            ram_size_bytes: 32768,
+            process: {
+                name: "i2c slave echo process",
+                objects: [
+                    {
+                        name: "I2C",
+                        type: "channel_initiator",
+                        handler_app: "i2c_server",
+                        handler_object_name: "I2C",
+                    },
+                ],
+                threads: [
+                    {
+                        name: "i2c slave echo thread",
+                        stack_size_bytes: 2048,
+                    },
+                ],
+            },
+        },
+    ],
+}
diff --git a/target/ast1060-evb/i2c-slave/target.rs b/target/ast1060-evb/i2c-slave/target.rs
new file mode 100644
index 0000000..e452ef8
--- /dev/null
+++ b/target/ast1060-evb/i2c-slave/target.rs
@@ -0,0 +1,29 @@
+// Licensed under the Apache-2.0 license
+
+//! AST1060-EVB I2C Slave Echo Target
+
+#![no_std]
+#![no_main]
+
+use target_common::{TargetInterface, declare_target};
+use {console_backend as _, entry as _};
+
+pub struct Target {}
+
+impl TargetInterface for Target {
+    const NAME: &'static str = "AST1060-EVB I2C Slave Echo";
+
+    fn main() -> ! {
+        codegen::start();
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+
+    fn shutdown(code: u32) -> ! {
+        pw_log::info!("Shutting down with code {}", code as u32);
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+}
+
+declare_target!(Target);
diff --git a/target/ast1060-evb/i2c/i2c_client_test.rs b/target/ast1060-evb/i2c/i2c_client_test.rs
index 5838ca1..d060f98 100644
--- a/target/ast1060-evb/i2c/i2c_client_test.rs
+++ b/target/ast1060-evb/i2c/i2c_client_test.rs
@@ -30,6 +30,8 @@
 
 use app_i2c_client::handle;
 use i2c_api::{BusIndex, I2cAddress, I2cClient, I2cClientBlocking};
+#[allow(unused_imports)]
+use i2c_api::I2cTargetClient;
 use i2c_client::IpcI2cClient;
 use pw_status::{Error, Result};
 use userspace::entry;
@@ -42,6 +44,19 @@
 /// I2C bus for master tests (I2C1 — connected to ADT7490 on the EVB)
 const I2C_BUS: BusIndex = BusIndex::BUS_2;
 
+/// I2C bus for slave tests (same bus as master — I2C2).
+///
+/// The AST1060 supports simultaneous master and slave operation on the same
+/// controller: master uses I2CM* registers, slave uses I2CS* registers.
+/// Loopback is achieved by writing to SLAVE_ADDR on I2C_BUS from the master;
+/// the slave controller on the same bus will receive the transaction.
+#[allow(dead_code)]
+const SLAVE_BUS: BusIndex = BusIndex::BUS_2;
+
+/// Slave address used for slave-mode tests.
+#[allow(dead_code)]
+const SLAVE_ADDR: u8 = 0x30;
+
 /// ADT7490 temperature sensor 7-bit address (on-board)
 const ADT7490_ADDR: u8 = 0x42;
 
@@ -129,22 +144,24 @@
     for &(reg, expected) in &ADT7490_REGS {
         // Set register pointer
         if let Err(_) = client.write(I2C_BUS, addr, &[reg]) {
-            pw_log::error!("[FAIL] reg write pointer");
+            pw_log::error!("[FAIL] write reg=0x%02X", reg as u32);
             results.fail();
             continue;
         }
+        pw_log::info!("write reg=0x%02X", reg as u32);
 
         // Read one byte
         let mut buf = [0u8; 1];
         match client.read(I2C_BUS, addr, &mut buf) {
             Ok(_) => {
+                pw_log::info!("read  reg=0x%02X val=0x%02X", reg as u32, buf[0] as u32);
                 if buf[0] == expected {
-                    pw_log::info!("[PASS] reg match");
+                    pw_log::info!("[PASS] reg=0x%02X match", reg as u32);
                     results.pass();
                 } else {
                     // Some registers are dynamic (e.g. temperature) — still pass
                     pw_log::info!(
-                        "reg 0x%02X: got 0x%02X, expected 0x%02X (dynamic OK)",
+                        "[PASS] reg=0x%02X got=0x%02X expected=0x%02X (dynamic OK)",
                         reg as u32,
                         buf[0] as u32,
                         expected as u32,
@@ -153,7 +170,7 @@
                 }
             }
             Err(_) => {
-                pw_log::error!("[FAIL] reg read");
+                pw_log::error!("[FAIL] read reg=0x%02X", reg as u32);
                 results.fail();
             }
         }
@@ -177,11 +194,11 @@
     let mut buf = [0u8; 1];
     match client.write_read(I2C_BUS, addr, &[0x3D], &mut buf) {
         Ok(_) => {
-            pw_log::info!("[PASS] write_read Device ID = 0x%02X", buf[0] as u32);
+            pw_log::info!("[PASS] write_read reg=0x3D val=0x%02X", buf[0] as u32);
             results.pass();
         }
         Err(_) => {
-            pw_log::error!("[FAIL] write_read Device ID");
+            pw_log::error!("[FAIL] write_read reg=0x3D");
             results.fail();
         }
     }
@@ -217,6 +234,136 @@
 }
 
 // ============================================================================
+// Slave tests (commented out pending hardware validation)
+// ============================================================================
+
+/*
+
+/// Verify the full IPC slave configuration path: configure → enable → disable.
+///
+/// Does not require external hardware — validates that the IPC plumbing and
+/// hardware register writes succeed without error.
+fn test_slave_configure(client: &mut IpcI2cClient, results: &mut TestResults) {
+    let addr = match I2cAddress::new(SLAVE_ADDR) {
+        Ok(a) => a,
+        Err(_) => {
+            pw_log::error!("[FAIL] slave configure: invalid address 0x{:02x}", SLAVE_ADDR as u32);
+            results.fail();
+            return;
+        }
+    };
+
+    match client.configure_target_address(SLAVE_BUS, addr) {
+        Ok(()) => {
+            pw_log::info!("[PASS] slave configure @ 0x{:02x} on bus {:?}", SLAVE_ADDR as u32, SLAVE_BUS.value() as u32);
+            results.pass();
+        }
+        Err(_) => {
+            pw_log::error!("[FAIL] slave configure");
+            results.fail();
+            return;
+        }
+    }
+
+    match client.enable_receive(SLAVE_BUS) {
+        Ok(()) => {
+            pw_log::info!("[PASS] slave enable_receive");
+            results.pass();
+        }
+        Err(_) => {
+            pw_log::error!("[FAIL] slave enable_receive");
+            results.fail();
+            return;
+        }
+    }
+
+    match client.disable_receive(SLAVE_BUS) {
+        Ok(()) => {
+            pw_log::info!("[PASS] slave disable_receive");
+            results.pass();
+        }
+        Err(_) => {
+            pw_log::error!("[FAIL] slave disable_receive");
+            results.fail();
+        }
+    }
+}
+
+/// Loopback test: master writes to slave address, slave receives the data.
+///
+/// # Hardware
+///
+/// Uses the AST1060's simultaneous master+slave capability on I2C2.
+/// The master (I2CM* registers) initiates a write to SLAVE_ADDR; the slave
+/// (I2CS* registers) on the same controller receives the transaction.
+/// No external wiring is required.
+fn test_slave_loopback(client: &mut IpcI2cClient, results: &mut TestResults) {
+    const WRITE_DATA: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
+
+    let slave_addr = match I2cAddress::new(SLAVE_ADDR) {
+        Ok(a) => a,
+        Err(_) => {
+            pw_log::error!("[FAIL] slave loopback: invalid address");
+            results.fail();
+            return;
+        }
+    };
+
+    // Re-enable slave (configure was called and disabled above).
+    if client.configure_target_address(SLAVE_BUS, slave_addr).is_err() {
+        pw_log::error!("[FAIL] slave loopback: configure");
+        results.fail();
+        return;
+    }
+    if client.enable_receive(SLAVE_BUS).is_err() {
+        pw_log::error!("[FAIL] slave loopback: enable");
+        results.fail();
+        return;
+    }
+
+    // Master write to slave address on the same bus.
+    match client.write(I2C_BUS, slave_addr, &WRITE_DATA) {
+        Ok(()) => {
+            pw_log::info!("[PASS] slave loopback: master write");
+            results.pass();
+        }
+        Err(_) => {
+            pw_log::error!("[FAIL] slave loopback: master write");
+            results.fail();
+            let _ = client.disable_receive(SLAVE_BUS);
+            return;
+        }
+    }
+
+    // Slave receive — should return the 4 bytes written above.
+    let mut messages = [i2c_api::TargetMessage::default(); 1];
+    match client.wait_for_messages(SLAVE_BUS, &mut messages, None) {
+        Ok(0) => {
+            pw_log::error!("[FAIL] slave loopback: no message received");
+            results.fail();
+        }
+        Ok(_) => {
+            let data = messages[0].data();
+            if data == &WRITE_DATA {
+                pw_log::info!("[PASS] slave loopback: received correct data");
+                results.pass();
+            } else {
+                pw_log::error!("[FAIL] slave loopback: data mismatch");
+                results.fail();
+            }
+        }
+        Err(_) => {
+            pw_log::error!("[FAIL] slave loopback: wait_for_messages error");
+            results.fail();
+        }
+    }
+
+    let _ = client.disable_receive(SLAVE_BUS);
+}
+
+*/ // end slave tests
+
+// ============================================================================
 // Entry point
 // ============================================================================
 
@@ -234,6 +381,14 @@
     test_write_read_device_id(&mut client, &mut results);
     test_probe_vacant(&mut client, &mut results);
 
+    // pw_log::info!("========================================");
+    // pw_log::info!("I2C Slave Tests (IPC slave path, I2C2)");
+    // pw_log::info!("Slave addr: 0x30");
+    // pw_log::info!("========================================");
+
+    // test_slave_configure(&mut client, &mut results);
+    // test_slave_loopback(&mut client, &mut results);
+
     pw_log::info!("========================================");
     pw_log::info!(
         "Results: %u passed, %u failed",