From 82de9f6dacf95e24e0bbd72b454393996d59ef07 Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Wed, 14 Oct 2026 00:14:11 -0700 Subject: [PATCH 01/49] [Platform][Linux] Fix GLib main loop guards when disabled with WiFi enabled (#74753) --- src/platform/Linux/CHIPDevicePlatformConfig.h | 2 ++ src/platform/Linux/PlatformManagerImpl.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/platform/Linux/CHIPDevicePlatformConfig.h b/src/platform/Linux/CHIPDevicePlatformConfig.h index 6cca8b17d0..593cc49c54 100644 --- a/src/platform/Linux/CHIPDevicePlatformConfig.h +++ b/src/platform/Linux/CHIPDevicePlatformConfig.h @@ -41,6 +41,7 @@ #define CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE 0 #endif +#ifndef CHIP_DEVICE_CONFIG_WITH_GLIB_MAIN_LOOP // Start GLib main event loop if BLE, Thread or WiFi is enabled. This is needed // to handle D-Bus communication with BlueZ or wpa_supplicant. #if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE || CHIP_DEVICE_CONFIG_ENABLE_WIFI || \ @@ -49,6 +50,7 @@ #else #define CHIP_DEVICE_CONFIG_WITH_GLIB_MAIN_LOOP 0 #endif +#endif // CHIP_DEVICE_CONFIG_WITH_GLIB_MAIN_LOOP // ========== Platform-specific Configuration ========= diff --git a/src/platform/Linux/PlatformManagerImpl.cpp b/src/platform/Linux/PlatformManagerImpl.cpp index 4fc530a4aa..4f7c939321 100644 --- a/src/platform/Linux/PlatformManagerImpl.cpp +++ b/src/platform/Linux/PlatformManagerImpl.cpp @@ -65,8 +65,6 @@ void * GLibMainLoopThread(void * userData) return nullptr; } -#endif - #if CHIP_DEVICE_CONFIG_ENABLE_WIFI gboolean WiFiIPChangeListener(GIOChannel * ch, GIOCondition /* condition */, void * /* userData */) @@ -196,6 +194,8 @@ CHIP_ERROR RunWiFiIPChangeListener() #endif // #if CHIP_DEVICE_CONFIG_ENABLE_WIFI +#endif // #if CHIP_DEVICE_CONFIG_WITH_GLIB_MAIN_LOOP + } // namespace CHIP_ERROR PlatformManagerImpl::_InitChipStack() @@ -232,10 +232,10 @@ CHIP_ERROR PlatformManagerImpl::_InitChipStack() invokeData.mDoneCond.wait(lock, [&invokeData]() { return invokeData.mDone; }); } -#endif - #if CHIP_DEVICE_CONFIG_ENABLE_WIFI ReturnErrorOnFailure(RunWiFiIPChangeListener()); +#endif + #endif // Initialize the configuration system. From 344bf031d9fc20fab316e6de0573e1e196ccbbea Mon Sep 17 00:00:00 2001 From: fengming-ye <142978240+fengming-ye@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:18:03 +0900 Subject: [PATCH 02/49] [NXP] Add ThreadStackManagerImpl test helpers for unit test build (Auto-merged by platform-bot) (#74762) Add SetThreadEnabledForTest, SetThreadAttachedForTest, and ResetThreadStateForTest to NXP ThreadStackManagerImpl, matching the Linux platform interface. These are guarded by CONFIG_BUILD_FOR_HOST_UNIT_TEST and needed by TestICDManager.cpp which calls these methods when both CONFIG_BUILD_FOR_HOST_UNIT_TEST and CHIP_DEVICE_CONFIG_ENABLE_THREAD are defined. Without these methods, the NXP MCXW72 (Thread) unit test build fails: error: 'class ThreadStackManagerImpl' has no member named 'SetThreadEnabledForTest' --- .../nxp/common/ThreadStackManagerImpl.h | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/platform/nxp/common/ThreadStackManagerImpl.h b/src/platform/nxp/common/ThreadStackManagerImpl.h index 83990db215..511b349d36 100644 --- a/src/platform/nxp/common/ThreadStackManagerImpl.h +++ b/src/platform/nxp/common/ThreadStackManagerImpl.h @@ -77,6 +77,16 @@ class ThreadStackManagerImpl final : public ThreadStackManager, public: // ===== Platform-specific members that may be accessed directly by the application. +#if CONFIG_BUILD_FOR_HOST_UNIT_TEST + void SetThreadEnabledForTest(bool enabled) { mThreadEnabledForTest.SetValue(enabled); } + void SetThreadAttachedForTest(bool attached) { mAttached = attached; } + void ResetThreadStateForTest() + { + mThreadEnabledForTest.ClearValue(); + mAttached = false; + } +#endif + #if CHIP_DEVICE_CONFIG_PROCESS_BLE_IN_THREAD using ThreadStackManager::ProcessThreadActivity; void ProcessThreadActivity(); @@ -86,6 +96,23 @@ class ThreadStackManagerImpl final : public ThreadStackManager, // ===== Methods that implement the ThreadStackManager abstract interface. CHIP_ERROR _InitThreadStack(void); +#if CONFIG_BUILD_FOR_HOST_UNIT_TEST + bool _IsThreadEnabled() + { + if (mThreadEnabledForTest.HasValue()) + { + return mThreadEnabledForTest.Value(); + } +#if CHIP_SYSTEM_CONFIG_USE_LWIP + return Internal::GenericThreadStackManagerImpl_OpenThread_LwIP::_IsThreadEnabled(); +#else + return Internal::GenericThreadStackManagerImpl_OpenThread::_IsThreadEnabled(); +#endif + } + + bool _IsThreadAttached() { return mAttached; } +#endif + private: // ===== Members for internal use by the following friends. @@ -96,6 +123,11 @@ class ThreadStackManagerImpl final : public ThreadStackManager, static bool IsInitialized(); +#if CONFIG_BUILD_FOR_HOST_UNIT_TEST + bool mAttached = false; + chip::Optional mThreadEnabledForTest; +#endif + // ===== Private members for use by this class only. ThreadStackManagerImpl() = default; From 56b49408cf43a6f17a0c51e003ee89a594ee9396 Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 14 Oct 2026 10:04:37 -0400 Subject: [PATCH 03/49] [all-devices-app] Battery Power Source OOB Accessor (#74754) * [all-devices-app] Implement BatteryPowerSourceAccessor and support OOB cluster writes - Implement BatteryPowerSourceAccessor to handle SetAttribute actions for BatPercentRemaining and BatVoltage. - Unrecognized attributes return std::nullopt to allow downstream handlers and data model provider fall-through. - Register BatteryPowerSourceAccessor via OOBAccessors and OOBAccessorHook. - Add comprehensive unit test suite TestBatteryPowerSourceAccessor using typed BuildSetAttributeRequest helpers and InMemoryOOBAccessorRegistry dispatch. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../device/types/power-source/BUILD.gn | 5 + .../BatteryPowerSourceAccessor.cpp | 73 ++++++++ .../power-source/BatteryPowerSourceAccessor.h | 40 ++++ .../types/power-source/OOBAccessors.cpp | 30 +++ .../device/types/power-source/OOBAccessors.h | 26 +++ .../oob-accessors/OOBAccessorHook.h | 1 + .../oob-accessors/all_devices_config.cmake | 2 + .../oob-accessors/tests/BUILD.gn | 4 + .../tests/TestBatteryPowerSourceAccessor.cpp | 172 ++++++++++++++++++ 9 files changed, 353 insertions(+) create mode 100644 examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp create mode 100644 examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h create mode 100644 examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp create mode 100644 examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h create mode 100644 examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn index 20846b0756..d4a9cd9d6e 100644 --- a/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn @@ -19,12 +19,17 @@ source_set("power-source") { sources = [ "BatteryPowerSource.cpp", "BatteryPowerSource.h", + "BatteryPowerSourceAccessor.cpp", + "BatteryPowerSourceAccessor.h", + "OOBAccessors.cpp", + "OOBAccessors.h", "impl/DecreasingBatteryPowerSource.cpp", "impl/DecreasingBatteryPowerSource.h", ] public_deps = [ "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", + "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/src/app/clusters/power-source-server", "${chip_root}/src/data-model-providers/codedriven", "${chip_root}/src/lib/core:error", diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp new file mode 100644 index 0000000000..d04d29cc65 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +using namespace chip::app::Clusters; + +namespace chip::app { + +std::optional BatteryPowerSourceAccessor::HandleAction(CharSpan actionName, ByteSpan tlvBuffer) +{ + if (!actionName.data_equal(OOBDataSerializer::kSetAttributeAction)) + { + return std::nullopt; + } + + auto parseResult = OOBDataSerializer::ParseAttributeRequest(tlvBuffer); + if (std::holds_alternative(parseResult)) + { + CHIP_ERROR err = std::get(parseResult); + ChipLogError(Support, "Failed to parse attribute request: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + auto & request = std::get(parseResult); + if (request.path.mEndpointId != mEndpointId) + { + return std::nullopt; + } + + switch (request.path.mClusterId) + { + case PowerSource::Id: { + switch (request.path.mAttributeId) + { + case PowerSource::Attributes::BatPercentRemaining::Id: { + DataModel::Nullable batteryValue; + ReturnErrorOnFailure(DataModel::Decode(request.value, batteryValue)); + return mCluster.SetBatPercentRemaining(batteryValue); + } + case PowerSource::Attributes::BatVoltage::Id: { + DataModel::Nullable voltageValue; + ReturnErrorOnFailure(DataModel::Decode(request.value, voltageValue)); + mCluster.SetBatVoltage(voltageValue); + return CHIP_NO_ERROR; + } + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h new file mode 100644 index 0000000000..2a557c4526 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +class BatteryPowerSourceAccessor : public OOBAccessor +{ +public: + BatteryPowerSourceAccessor(BatteryPowerSource::SimpleBatteryPowerSourceCluster & cluster, EndpointId endpointId) : + mCluster(cluster), mEndpointId(endpointId) + {} + + ~BatteryPowerSourceAccessor() override = default; + + std::optional HandleAction(CharSpan actionName, ByteSpan tlvBuffer) override; + +private: + BatteryPowerSource::SimpleBatteryPowerSourceCluster & mCluster; + EndpointId mEndpointId; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp new file mode 100644 index 0000000000..022b80d7cc --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OOBAccessors.h" +#include "BatteryPowerSourceAccessor.h" + +#include + +namespace chip::app { + +void RegisterOOBAccessors(BatteryPowerSource & device, OOBAccessorRegistry & registry) +{ + LogErrorOnFailure(registry.Register( + std::make_unique(device.BatteryPowerSourceCluster(), device.GetEndpointId()))); +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h new file mode 100644 index 0000000000..d5fe681181 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(BatteryPowerSource & device, OOBAccessorRegistry & registry); + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h b/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h index 273b9d5ea9..e130f1bac0 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h +++ b/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake b/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake index fe7879e186..b48d570a75 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake +++ b/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake @@ -54,6 +54,8 @@ if(ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS) "${ALL_DEVICES_COMMON_DIR}/device/types/occupancy-sensor/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/on-off-light/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/on-off-plug-in-unit/OOBAccessors.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/power-source/BatteryPowerSourceAccessor.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/power-source/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/robotic-vacuum-cleaner/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/root-node/OOBAccessors.cpp" ) diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn b/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn index d4c169e59f..d7c0b3f296 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn @@ -22,6 +22,7 @@ if (chip_build_tests) { output_name = "TestOOBAccessors" test_sources = [ + "TestBatteryPowerSourceAccessor.cpp", "TestOOBAccessors.cpp", "TestOOBDataSerializer.cpp", ] @@ -29,6 +30,7 @@ if (chip_build_tests) { cflags = [ "-Wconversion" ] public_deps = [ + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/power-source", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:ambient-context", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:basic-information", @@ -38,7 +40,9 @@ if (chip_build_tests) { "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:occupancy", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:on-off", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:rvc", + "${chip_root}/src/app/persistence:default", "${chip_root}/src/app/server-cluster/testing", + "${chip_root}/src/data-model-providers/codedriven", "${chip_root}/src/lib/support:testing", "${chip_root}/src/platform", ] diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp new file mode 100644 index 0000000000..76b3609ba9 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp @@ -0,0 +1,172 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { + +class TestBatteryPowerSourceAccessor : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } + +protected: + void SetUp() override + { + EXPECT_EQ(chip::DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR); + EXPECT_EQ(mDevice.Register(1, mProvider), CHIP_NO_ERROR); + EXPECT_EQ(mRegistry.Register( + std::make_unique(mDevice.BatteryPowerSourceCluster(), mDevice.GetEndpointId())), + CHIP_NO_ERROR); + } + + void TearDown() override + { + mDevice.Unregister(mProvider); + chip::DeviceLayer::PlatformMgr().Shutdown(); + } + + DefaultTimerDelegate mTimerDelegate; + Testing::TestServerClusterContext mClusterContext; + CodeDrivenDataModelProvider mProvider{ mClusterContext.StorageDelegate(), mClusterContext.AttributePersistenceProvider() }; + BatteryPowerSource mDevice{ "Test Battery"_span, Clusters::PowerSource::BatReplaceabilityEnum::kUserReplaceable, + mTimerDelegate }; + InMemoryOOBAccessorRegistry mRegistry; +}; + +// Initial state: verifies non-null default battery percentage +TEST_F(TestBatteryPowerSourceAccessor, InitialAttributeValues) +{ + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + ASSERT_FALSE(battery.IsNull()); + EXPECT_EQ(battery.Value(), 200); +} + +// Test 1: Setting battery percentage (e.g. 170 = 85%) +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_ValidValue) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(170)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + ASSERT_FALSE(battery.IsNull()); + EXPECT_EQ(battery.Value(), 170); +} + +// Test 2: Setting null battery percentage +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_Null) +{ + ConcreteDataAttributePath path(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id); + + uint8_t buffer[64]; + TLV::TLVWriter writer; + writer.Init(buffer); + EXPECT_EQ(writer.PutNull(TLV::AnonymousTag()), CHIP_NO_ERROR); + EXPECT_EQ(writer.Finalize(), CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest(path, reader); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + EXPECT_TRUE(battery.IsNull()); +} + +// Test 3: Setting battery percentage > 200 returns ConstraintError +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_ConstraintError) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(201)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); +} + +// Test 4: Setting battery voltage (e.g. 3300 mV) +TEST_F(TestBatteryPowerSourceAccessor, SetBatVoltage_ValidValue) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatVoltage::Id), static_cast(3300)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto voltage = mDevice.BatteryPowerSourceCluster().GetBatVoltage(); + ASSERT_FALSE(voltage.IsNull()); + EXPECT_EQ(voltage.Value(), 3300U); +} + +// Test 5: Rejects unsupported attributes on PowerSource cluster (returns std::nullopt -> NOT_FOUND in registry) +TEST_F(TestBatteryPowerSourceAccessor, RejectUnsupportedAttribute) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::Order::Id), static_cast(1)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 6: Unhandled action, wrong cluster, or wrong endpoint +TEST_F(TestBatteryPowerSourceAccessor, UnhandledActionOrEndpoint) +{ + // Wrong action + uint8_t dummy[4] = { 0 }; + EXPECT_EQ(mRegistry.HandleAction("UnknownAction"_span, ByteSpan(dummy, sizeof(dummy))), CHIP_ERROR_NOT_FOUND); + + // Wrong cluster (OnOff) + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, Clusters::OnOff::Id, Clusters::OnOff::Attributes::OnOff::Id), true); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); + } + + // Wrong endpoint (endpoint 99) + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(99, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(50)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); + } +} + +} // namespace From 3a68e87b0680ab3ffe09f90ac2899e7d02937cfb Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Wed, 14 Oct 2026 07:14:16 -0700 Subject: [PATCH 04/49] [Android] Add Thread dataset fetch from system with optional GMS support (#74761) * [Android] Add Thread dataset fetch from system with optional GMS support - Add a "Fetch dataset from system Thread manager" button to EnterNetworkFragment for Thread provisioning. It populates the channel, PAN ID, extended PAN ID and network key fields from the active operational dataset of the phone's Thread network. - Fetch credentials via the GMS ThreadNetworkClient (with the preferred-credentials consent flow) when Google Play services is available, and otherwise fall back to the Android 14+ ThreadNetworkManager system service. The GMS path is only taken when GoogleApiAvailability reports SUCCESS; otherwise GoogleApiManager keeps the request pending behind a "Get Google Play services" resolution and the fetch would never complete or fall back. - The ThreadNetworkController APIs are system APIs guarded by THREAD_NETWORK_PRIVILEGED, so the system-service path only works when CHIPTool is installed as a privileged app. Check the permission up front and surface the underlying error instead of an opaque reflection failure. - Add the matterEnableGms Gradle property (default true). It selects the src/gms or src/nogms source set for GmsThreadDatasetHelper, so builds with matterEnableGms=false do not link play-services-threadnetwork. - Ignore results delivered after the fragment view is destroyed and unregister the pending dataset callback in onDestroyView. * [Android] CHIPTool: address review comments on Thread dataset fetch - Replace getAllCredentials() with getAllActiveCredentials(). getAllCredentials() returns only credentials added by the calling app, which CHIPTool never adds, so taking its first entry could only return nothing or an unrelated stored network. getAllActiveCredentials() returns every active Thread network reachable through nearby border routers after a single consent prompt; when more than one is shared the user picks one from a list (network name, extended PAN ID, channel). Fall back to getPreferredCredentials() when it is unavailable (e.g. the phone is not on Wi-Fi). - Bump play-services-threadnetwork to 16.2.1, the first version with getAllActiveCredentials(). - Skip launching the consent prompt when the fragment view no longer exists, since the result would be discarded. --- examples/android/CHIPTool/app/build.gradle | 11 + .../provisioning/GmsThreadDatasetHelper.kt | 161 +++++++++++ .../CHIPTool/app/src/main/AndroidManifest.xml | 1 + .../provisioning/EnterNetworkFragment.kt | 272 +++++++++++++++++- .../provisioning/ThreadDatasetCandidate.kt | 26 ++ .../layout/enter_thread_network_fragment.xml | 11 +- .../app/src/main/res/values/strings.xml | 11 + .../provisioning/GmsThreadDatasetHelper.kt | 38 +++ examples/android/CHIPTool/gradle.properties | 6 + 9 files changed, 532 insertions(+), 5 deletions(-) create mode 100644 examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt create mode 100644 examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/provisioning/ThreadDatasetCandidate.kt create mode 100644 examples/android/CHIPTool/app/src/nogms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt diff --git a/examples/android/CHIPTool/app/build.gradle b/examples/android/CHIPTool/app/build.gradle index b180778eb9..05ed525c46 100644 --- a/examples/android/CHIPTool/app/build.gradle +++ b/examples/android/CHIPTool/app/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' +def enableGms = !project.hasProperty('matterEnableGms') || matterEnableGms.toBoolean() + android { namespace 'com.google.chip.chiptool' compileSdkVersion 34 @@ -78,6 +80,12 @@ android { } } + sourceSets { + main { + java.srcDirs += enableGms ? ['src/gms/java'] : ['src/nogms/java'] + } + } + if (matterSdkSourceBuild.toBoolean() && !matterBuildSrcDir.isEmpty()) { externalNativeBuild { cmake { @@ -131,6 +139,9 @@ dependencies { implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" + if (enableGms) { + implementation 'com.google.android.gms:play-services-threadnetwork:16.2.1' + } implementation 'com.google.protobuf:protobuf-java:3.22.0' } diff --git a/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt b/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt new file mode 100644 index 0000000000..46a21b900f --- /dev/null +++ b/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.chip.chiptool.provisioning + +import android.app.Activity +import android.content.Intent +import android.content.IntentSender +import android.util.Log +import androidx.fragment.app.Fragment +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.threadnetwork.GetAllActiveCredentialsRequest +import com.google.android.gms.threadnetwork.ThreadNetwork +import com.google.android.gms.threadnetwork.ThreadNetworkClient +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials + +/** GMSCore ThreadNetwork implementation enabled when `matterEnableGms=true`. */ +object GmsThreadDatasetHelper { + private const val TAG = "GmsThreadDatasetHelper" + private const val REQUEST_CODE_ACTIVE_CREDENTIALS = 1002 + private const val REQUEST_CODE_PREFERRED_CREDENTIALS = 1003 + const val IS_AVAILABLE = true + + /** + * Requests Thread network credentials via GMS ThreadNetwork: all active networks reachable + * through nearby border routers, or the preferred network if that is unavailable. The credentials + * are delivered through the consent intent result (see [parseConsentResult]); [onFallback] runs + * when Play services or the credentials are unavailable. Callbacks run on the main thread and may + * arrive after the fragment view is destroyed. + */ + fun fetchFromGms(fragment: Fragment, onFallback: () -> Unit) { + try { + val context = fragment.requireContext() + // Without Play services, GoogleApiManager keeps the call pending while it offers a + // "Get Google Play services" resolution, so the Task would never complete or fail. + if ( + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != + ConnectionResult.SUCCESS + ) { + Log.d(TAG, "Google Play services unavailable, falling back to system service") + onFallback() + return + } + val client = ThreadNetwork.getClient(context) + client + .getAllActiveCredentials(GetAllActiveCredentialsRequest.newBuilder().build()) + .addOnSuccessListener { result -> + if (fragment.view == null) { + return@addOnSuccessListener + } + if (!launchConsent(fragment, result?.intentSender, REQUEST_CODE_ACTIVE_CREDENTIALS)) { + fetchPreferredCredentials(client, fragment, onFallback) + } + } + .addOnFailureListener { e -> + // For example LOCAL_NETWORK_NOT_CONNECTED when the phone is not on Wi-Fi. + Log.w(TAG, "GMS getAllActiveCredentials failed, trying getPreferredCredentials", e) + fetchPreferredCredentials(client, fragment, onFallback) + } + } catch (e: Exception) { + Log.d(TAG, "GMS ThreadNetwork API failed, falling back to system service", e) + onFallback() + } + } + + private fun fetchPreferredCredentials( + client: ThreadNetworkClient, + fragment: Fragment, + onFallback: () -> Unit, + ) { + try { + client.preferredCredentials + .addOnSuccessListener { result -> + if (fragment.view == null) { + return@addOnSuccessListener + } + if (!launchConsent(fragment, result?.intentSender, REQUEST_CODE_PREFERRED_CREDENTIALS)) { + onFallback() + } + } + .addOnFailureListener { e -> + Log.w(TAG, "GMS getPreferredCredentials failed, falling back to system service", e) + onFallback() + } + } catch (e: Exception) { + Log.e(TAG, "Error calling getPreferredCredentials", e) + onFallback() + } + } + + /** Launches the GMS consent prompt; returns false if there is nothing to launch or it failed. */ + private fun launchConsent(fragment: Fragment, sender: IntentSender?, requestCode: Int): Boolean { + if (sender == null) { + return false + } + return try { + @Suppress("DEPRECATION") + fragment.startIntentSenderForResult(sender, requestCode, null, 0, 0, 0, null) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to launch GMS Thread consent intent", e) + false + } + } + + /** + * Returns the networks shared through a consent prompt launched by [fetchFromGms], an empty list + * if the user declined or nothing usable was shared, or null if [requestCode] is not ours. + */ + fun parseConsentResult( + requestCode: Int, + resultCode: Int, + data: Intent?, + ): List? { + if ( + requestCode != REQUEST_CODE_ACTIVE_CREDENTIALS && + requestCode != REQUEST_CODE_PREFERRED_CREDENTIALS + ) { + return null + } + if (resultCode != Activity.RESULT_OK || data == null) { + return emptyList() + } + return try { + val credentials = + if (requestCode == REQUEST_CODE_ACTIVE_CREDENTIALS) { + ThreadNetworkCredentials.parseListFromIntentSenderResultData(data) + } else { + listOf(ThreadNetworkCredentials.fromIntentSenderResultData(data)) + } + credentials + .filter { it.activeOperationalDataset.isNotEmpty() } + .map { + ThreadDatasetCandidate( + it.networkName, + it.extendedPanId, + it.channel, + it.activeOperationalDataset, + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse ThreadNetworkCredentials from intent result", e) + emptyList() + } + } +} diff --git a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml index efd089817e..7d196d9899 100644 --- a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml +++ b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + Unit)? = null + private val networkType: ProvisionNetworkType get() = requireNotNull( @@ -46,7 +58,7 @@ class EnterNetworkFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { val layoutRes = when (networkType) { @@ -55,8 +67,18 @@ class EnterNetworkFragment : Fragment() { } return inflater.inflate(layoutRes, container, false).apply { + val fragmentView = this val saveNetworkBtn: Button = findViewById(R.id.saveNetworkBtn) - saveNetworkBtn.setOnClickListener { onSaveNetworkClicked(this) } + saveNetworkBtn.setOnClickListener { onSaveNetworkClicked(fragmentView) } + if (networkType == ProvisionNetworkType.THREAD) { + findViewById