From 64b0b617266651f6c3982da83fdebc741eec23a9 Mon Sep 17 00:00:00 2001 From: sarthak shaha <130495524+Sarthak-Shaha@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:54:06 -0500 Subject: [PATCH 01/76] Modified the zap files to remove warnings, modified the xml files to reflect spec and generated the .matter files (#32444) --- .../light-switch-app.matter | 1 + .../light-switch-common/light-switch-app.zap | 26 +++++++++++++++++++ examples/lock-app/lock-common/lock-app.matter | 1 + examples/lock-app/lock-common/lock-app.zap | 8 ++++++ .../thermostat-common/thermostat.matter | 1 + .../thermostat-common/thermostat.zap | 8 ++++++ .../zcl/data-model/chip/matter-devices.xml | 6 ++--- 7 files changed, 47 insertions(+), 4 deletions(-) diff --git a/examples/light-switch-app/light-switch-common/light-switch-app.matter b/examples/light-switch-app/light-switch-common/light-switch-app.matter index 65bef372495d85..e51a772add14e2 100644 --- a/examples/light-switch-app/light-switch-common/light-switch-app.matter +++ b/examples/light-switch-app/light-switch-common/light-switch-app.matter @@ -2628,6 +2628,7 @@ endpoint 0 { ram attribute clusterRevision default = 1; handle command RetrieveLogsRequest; + handle command RetrieveLogsResponse; } server cluster GeneralDiagnostics { diff --git a/examples/light-switch-app/light-switch-common/light-switch-app.zap b/examples/light-switch-app/light-switch-common/light-switch-app.zap index 2c61c123c83437..ba56db35a2669b 100644 --- a/examples/light-switch-app/light-switch-common/light-switch-app.zap +++ b/examples/light-switch-app/light-switch-common/light-switch-app.zap @@ -1581,6 +1581,14 @@ "source": "client", "isIncoming": 1, "isEnabled": 1 + }, + { + "name": "RetrieveLogsResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 } ], "attributes": [ @@ -4605,6 +4613,24 @@ "define": "IDENTIFY_CLUSTER", "side": "client", "enabled": 1, + "commands": [ + { + "name": "Identify", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "TriggerEffect", + "code": 64, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + } + ], "attributes": [ { "name": "ClusterRevision", diff --git a/examples/lock-app/lock-common/lock-app.matter b/examples/lock-app/lock-common/lock-app.matter index b886f3e52d067a..8fee37c9493c67 100644 --- a/examples/lock-app/lock-common/lock-app.matter +++ b/examples/lock-app/lock-common/lock-app.matter @@ -2663,6 +2663,7 @@ endpoint 0 { ram attribute clusterRevision default = 1; handle command RetrieveLogsRequest; + handle command RetrieveLogsResponse; } server cluster GeneralDiagnostics { diff --git a/examples/lock-app/lock-common/lock-app.zap b/examples/lock-app/lock-common/lock-app.zap index 0c9ce257b3cf31..04689b5ca64a00 100644 --- a/examples/lock-app/lock-common/lock-app.zap +++ b/examples/lock-app/lock-common/lock-app.zap @@ -2126,6 +2126,14 @@ "source": "client", "isIncoming": 1, "isEnabled": 1 + }, + { + "name": "RetrieveLogsResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 } ], "attributes": [ diff --git a/examples/thermostat/thermostat-common/thermostat.matter b/examples/thermostat/thermostat-common/thermostat.matter index d821e6a6d609f6..a16168f55692b1 100644 --- a/examples/thermostat/thermostat-common/thermostat.matter +++ b/examples/thermostat/thermostat-common/thermostat.matter @@ -2240,6 +2240,7 @@ endpoint 0 { ram attribute clusterRevision default = 1; handle command RetrieveLogsRequest; + handle command RetrieveLogsResponse; } server cluster GeneralDiagnostics { diff --git a/examples/thermostat/thermostat-common/thermostat.zap b/examples/thermostat/thermostat-common/thermostat.zap index 79396ba5d313c4..e17e88b6578ad0 100644 --- a/examples/thermostat/thermostat-common/thermostat.zap +++ b/examples/thermostat/thermostat-common/thermostat.zap @@ -1707,6 +1707,14 @@ "source": "client", "isIncoming": 1, "isEnabled": 1 + }, + { + "name": "RetrieveLogsResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 } ], "attributes": [ diff --git a/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml b/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml index 259a59dae3ead5..96ed6fdc2da625 100644 --- a/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml +++ b/src/app/zap-templates/zcl/data-model/chip/matter-devices.xml @@ -74,10 +74,10 @@ limitations under the License. ACTIVE_LOCALE SUPPORTED_LOCALES - + HOUR_FORMAT - + UP_TIME @@ -1417,7 +1417,6 @@ limitations under the License. IDENTIFY_TYPE Identify IdentifyQuery - TriggerEffect DEVICE_TYPE_LIST @@ -1475,7 +1474,6 @@ limitations under the License. IDENTIFY_TYPE Identify IdentifyQuery - TriggerEffect DEVICE_TYPE_LIST From 807d03a71b1c3dd23930fde2904b86d91117e53b Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Thu, 7 Mar 2024 09:58:13 -0800 Subject: [PATCH 02/76] [Kotlin] Cleanup obsolete json package (#32475) --- src/controller/java/BUILD.gn | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 21e667716bbcb2..3b573daa93028c 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -396,7 +396,6 @@ kotlin_library("kotlin_matter_controller") { if (matter_enable_java_compilation) { deps += [ - "${chip_root}/third_party/java_deps:json", "${chip_root}/third_party/java_deps:kotlin-stdlib", "${chip_root}/third_party/java_deps:kotlinx-coroutines-core-jvm", "${chip_root}/third_party/java_deps/stub_src", From 3e36245ae8030c3fdd9dca74b27bed6ed5c652b3 Mon Sep 17 00:00:00 2001 From: Terence Hampson Date: Thu, 7 Mar 2024 13:19:46 -0500 Subject: [PATCH 03/76] Increase size to be more readable on 4k monitor (#32483) --- .../img/cluster_commands.png | Bin 114274 -> 394089 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/cluster_and_device_type_dev/img/cluster_commands.png b/docs/cluster_and_device_type_dev/img/cluster_commands.png index c04dbe7279b28b5f21e35241fe52df6b13061f3e..7df7a8a75412abe1e963b1c9d416bd7053edceef 100644 GIT binary patch literal 394089 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%0D$M1!xX#Y-y~qDr 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�o>q0GJwSfVjtb;@7WVWEzI_4;6> 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

s$N2E^(zp~;3Fue1uFypM&;!b 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

D|esJ93#q`=*U6x+(Y89|kB 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
M9Me0x;15(wd-)Uiex;NSbL)tNk_V*crC6%>Pf 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 Date: Thu, 7 Mar 2024 12:21:48 -0800 Subject: [PATCH 04/76] [UnitTests] Added UnitTests for MetricEvents in tracing (#32477) * [UnitTests] Added UnitTests for MetricEvents in tracing - Added unittests to validate behavior MetricEvent and related macros * Restyler fixes * Incorporated review feedback * Incorporated review feedback2 * Restyler fixes2 --- src/tracing/tests/BUILD.gn | 7 +- src/tracing/tests/TestMetricEvents.cpp | 348 +++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 src/tracing/tests/TestMetricEvents.cpp diff --git a/src/tracing/tests/BUILD.gn b/src/tracing/tests/BUILD.gn index 6f3ef63df9098d..8f28b56ecb890c 100644 --- a/src/tracing/tests/BUILD.gn +++ b/src/tracing/tests/BUILD.gn @@ -23,13 +23,16 @@ if (matter_enable_tracing_support && matter_trace_config == "multiplexed") { chip_test_suite_using_nltest("tests") { output_name = "libTracingTests" - test_sources = [ "TestTracing.cpp" ] - sources = [] + test_sources = [ + "TestMetricEvents.cpp", + "TestTracing.cpp", + ] public_deps = [ "${chip_root}/src/lib/support:testing_nlunit", "${chip_root}/src/platform", "${chip_root}/src/tracing", + "${chip_root}/src/tracing:macros", "${nlunit_test_root}:nlunit-test", ] } diff --git a/src/tracing/tests/TestMetricEvents.cpp b/src/tracing/tests/TestMetricEvents.cpp new file mode 100644 index 00000000000000..c52a5fc91b9e93 --- /dev/null +++ b/src/tracing/tests/TestMetricEvents.cpp @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2024 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 + +#include +#include +#include + +using namespace chip; +using namespace chip::Tracing; + +namespace chip { +namespace Tracing { + +static bool operator==(const MetricEvent & lhs, const MetricEvent & rhs) +{ + if (&lhs == &rhs) + { + return true; + } + + if (lhs.type() == rhs.type() && std::string(lhs.key()) == std::string(rhs.key()) && lhs.ValueType() == rhs.ValueType()) + { + switch (lhs.ValueType()) + { + case MetricEvent::Value::Type::kInt32: + return lhs.ValueInt32() == rhs.ValueInt32(); + + case MetricEvent::Value::Type::kUInt32: + return lhs.ValueUInt32() == rhs.ValueUInt32(); + + case MetricEvent::Value::Type::kChipErrorCode: + return lhs.ValueErrorCode() == rhs.ValueErrorCode(); + + case MetricEvent::Value::Type::kUndefined: + return true; + } + } + return false; +} +} // namespace Tracing +} // namespace chip + +namespace { + +// This keeps a log of all received trace items +class MetricEventBackend : public Backend +{ +public: + MetricEventBackend() {} + const std::vector & GetMetricEvents() const { return mMetricEvents; } + + // Implementation + virtual void LogMetricEvent(const MetricEvent & event) { mMetricEvents.push_back(event); } + +private: + std::vector mMetricEvents; +}; + +void TestBasicMetricEvent(nlTestSuite * inSuite, void * inContext) +{ + + { + MetricEvent event(MetricEvent::Type::kInstantEvent, "instant_event"); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kInstantEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("instant_event")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kUndefined); + } + + { + MetricEvent event(MetricEvent::Type::kBeginEvent, "begin_event"); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kBeginEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("begin_event")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kUndefined); + } + + { + MetricEvent event(MetricEvent::Type::kEndEvent, "end_event"); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kEndEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("end_event")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kUndefined); + } + + { + MetricEvent event(MetricEvent::Type::kEndEvent, "end_event_with_int32_value", int32_t(42)); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kEndEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("end_event_with_int32_value")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kInt32); + NL_TEST_ASSERT(inSuite, event.ValueInt32() == 42); + } + + { + MetricEvent event(MetricEvent::Type::kEndEvent, "end_event_with_uint32_value", uint32_t(42)); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kEndEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("end_event_with_uint32_value")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kUInt32); + NL_TEST_ASSERT(inSuite, event.ValueUInt32() == 42u); + } + + { + MetricEvent event(MetricEvent::Type::kEndEvent, "end_event_with_error_value", CHIP_ERROR_BUSY); + NL_TEST_ASSERT(inSuite, event.type() == MetricEvent::Type::kEndEvent); + NL_TEST_ASSERT(inSuite, std::string(event.key()) == std::string("end_event_with_error_value")); + NL_TEST_ASSERT(inSuite, event.ValueType() == MetricEvent::Value::Type::kChipErrorCode); + NL_TEST_ASSERT(inSuite, chip::ChipError(event.ValueErrorCode()) == CHIP_ERROR_BUSY); + } +} + +void TestInstantMetricEvent(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + + { + ScopedRegistration scope(backend); + + MATTER_LOG_METRIC("event1"); + MATTER_LOG_METRIC("event2"); + MATTER_LOG_METRIC("event3"); + } + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1"), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2"), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3"), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +void TestBeginEndMetricEvent(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend1; + MetricEventBackend backend2; + + { + ScopedRegistration scope1(backend1); + { + + MATTER_LOG_METRIC_BEGIN("event1"); + MATTER_LOG_METRIC_BEGIN("event2"); + MATTER_LOG_METRIC_END("event2", 53); + MATTER_LOG_METRIC_END("event1"); + } + + std::vector expected1 = { + MetricEvent(MetricEvent::Type::kBeginEvent, "event1"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event2"), + MetricEvent(MetricEvent::Type::kEndEvent, "event2", 53), + MetricEvent(MetricEvent::Type::kEndEvent, "event1"), + }; + + NL_TEST_ASSERT(inSuite, backend1.GetMetricEvents().size() == expected1.size()); + NL_TEST_ASSERT( + inSuite, + std::equal(backend1.GetMetricEvents().begin(), backend1.GetMetricEvents().end(), expected1.begin(), expected1.end())); + + { + ScopedRegistration scope2(backend2); + + MATTER_LOG_METRIC_BEGIN("event1"); + MATTER_LOG_METRIC_BEGIN("event2"); + MATTER_LOG_METRIC_BEGIN("event3"); + MATTER_LOG_METRIC_BEGIN("event4"); + MATTER_LOG_METRIC_END("event3", CHIP_ERROR_UNKNOWN_KEY_TYPE); + MATTER_LOG_METRIC_END("event1", 91u); + MATTER_LOG_METRIC_END("event2", 53); + MATTER_LOG_METRIC_END("event4"); + } + + std::vector expected2 = { + MetricEvent(MetricEvent::Type::kBeginEvent, "event1"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event2"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event3"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event4"), + MetricEvent(MetricEvent::Type::kEndEvent, "event3", CHIP_ERROR_UNKNOWN_KEY_TYPE), + MetricEvent(MetricEvent::Type::kEndEvent, "event1", 91u), + MetricEvent(MetricEvent::Type::kEndEvent, "event2", 53), + MetricEvent(MetricEvent::Type::kEndEvent, "event4"), + }; + + NL_TEST_ASSERT(inSuite, backend2.GetMetricEvents().size() == expected2.size()); + NL_TEST_ASSERT( + inSuite, + std::equal(backend2.GetMetricEvents().begin(), backend2.GetMetricEvents().end(), expected2.begin(), expected2.end())); + } +} + +void TestScopedMetricEvent(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend1; + MetricEventBackend backend2; + MetricEventBackend backend3; + chip::ChipError err1 = CHIP_NO_ERROR; + chip::ChipError err2 = CHIP_NO_ERROR; + chip::ChipError err3 = CHIP_NO_ERROR; + chip::ChipError err4 = CHIP_NO_ERROR; + + { + ScopedRegistration scope1(backend1); + { + MATTER_LOG_METRIC_SCOPE("event1", err1); + err1 = CHIP_ERROR_BUSY; + { + ScopedRegistration scope2(backend2); + MATTER_LOG_METRIC_SCOPE("event2", err2); + err2 = CHIP_ERROR_BAD_REQUEST; + + { + ScopedRegistration scope3(backend3); + MATTER_LOG_METRIC_SCOPE("event3", err3); + err3 = CHIP_ERROR_EVENT_ID_FOUND; + } + { + MATTER_LOG_METRIC_SCOPE("event4", err4); + err4 = CHIP_ERROR_BUFFER_TOO_SMALL; + } + } + } + + std::vector expected1 = { + MetricEvent(MetricEvent::Type::kBeginEvent, "event1"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event2"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event3"), + MetricEvent(MetricEvent::Type::kEndEvent, "event3", CHIP_ERROR_EVENT_ID_FOUND), + MetricEvent(MetricEvent::Type::kBeginEvent, "event4"), + MetricEvent(MetricEvent::Type::kEndEvent, "event4", CHIP_ERROR_BUFFER_TOO_SMALL), + MetricEvent(MetricEvent::Type::kEndEvent, "event2", CHIP_ERROR_BAD_REQUEST), + MetricEvent(MetricEvent::Type::kEndEvent, "event1", CHIP_ERROR_BUSY), + }; + + NL_TEST_ASSERT(inSuite, backend1.GetMetricEvents().size() == expected1.size()); + NL_TEST_ASSERT( + inSuite, + std::equal(backend1.GetMetricEvents().begin(), backend1.GetMetricEvents().end(), expected1.begin(), expected1.end())); + + std::vector expected2 = { + MetricEvent(MetricEvent::Type::kBeginEvent, "event2"), + MetricEvent(MetricEvent::Type::kBeginEvent, "event3"), + MetricEvent(MetricEvent::Type::kEndEvent, "event3", CHIP_ERROR_EVENT_ID_FOUND), + MetricEvent(MetricEvent::Type::kBeginEvent, "event4"), + MetricEvent(MetricEvent::Type::kEndEvent, "event4", CHIP_ERROR_BUFFER_TOO_SMALL), + MetricEvent(MetricEvent::Type::kEndEvent, "event2", CHIP_ERROR_BAD_REQUEST), + }; + + NL_TEST_ASSERT(inSuite, backend2.GetMetricEvents().size() == expected2.size()); + NL_TEST_ASSERT( + inSuite, + std::equal(backend2.GetMetricEvents().begin(), backend2.GetMetricEvents().end(), expected2.begin(), expected2.end())); + + std::vector expected3 = { + MetricEvent(MetricEvent::Type::kBeginEvent, "event3"), + MetricEvent(MetricEvent::Type::kEndEvent, "event3", CHIP_ERROR_EVENT_ID_FOUND), + }; + + NL_TEST_ASSERT(inSuite, backend3.GetMetricEvents().size() == expected3.size()); + NL_TEST_ASSERT( + inSuite, + std::equal(backend3.GetMetricEvents().begin(), backend3.GetMetricEvents().end(), expected3.begin(), expected3.end())); + } +} + +static int DoubleOf(int input) +{ + return input * 2; +} + +void TestVerifyOrExitWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + chip::ChipError err = CHIP_NO_ERROR; + + VerifyOrExitWithMetric("event0", DoubleOf(2) == 4, err = CHIP_ERROR_BAD_REQUEST); + VerifyOrExitWithMetric("event1", DoubleOf(3) == 9, err = CHIP_ERROR_INCORRECT_STATE); + +exit: + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_INCORRECT_STATE), + }; + + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +void TestSuccessOrExitWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + chip::ChipError err = CHIP_NO_ERROR; + + SuccessOrExitWithMetric("event1", err = CHIP_NO_ERROR); + SuccessOrExitWithMetric("event2", err = CHIP_ERROR_BUSY); + SuccessOrExitWithMetric("event3", err = CHIP_NO_ERROR); + +exit: + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", CHIP_ERROR_BUSY), + }; + + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_BUSY); + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static const nlTest sMetricTests[] = { + NL_TEST_DEF("BasicMetricEvent", TestBasicMetricEvent), // + NL_TEST_DEF("InstantMetricEvent", TestInstantMetricEvent), // + NL_TEST_DEF("BeginEndMetricEvent", TestBeginEndMetricEvent), // + NL_TEST_DEF("ScopedMetricEvent", TestScopedMetricEvent), // + NL_TEST_DEF("VerifyOrExitWithMetric", TestVerifyOrExitWithMetric), // + NL_TEST_DEF("SuccessOrExitWithMetric", TestSuccessOrExitWithMetric), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestMetricEvents() +{ + nlTestSuite theSuite = { "Metric event tests", &sMetricTests[0], nullptr, nullptr }; + + // Run test suite against one context. + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestMetricEvents) From bd42fb789d9d25423cdcb7acb403818072ca0edb Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 7 Mar 2024 15:26:12 -0500 Subject: [PATCH 05/76] Make it possible to dynamically set MRP parameters. (#32446) * Make it possible to dynamically set MRP parameters. Right now MRP parameters have to be compile-time constants, except for ICDs. But in some cases the same binary might be used on different types of hardware that have different MRP requirements. This change makes it possible to set the MRP parameters at run time, based on the actual hardware involved, not just compile time. The new functionality is gated by a new default-off compiler flag, so only systems that really need it pay the cost for it. * Address review comment. --- .../ReliableMessageProtocolConfig.cpp | 26 +++++++++++++++++++ src/messaging/ReliableMessageProtocolConfig.h | 23 ++++++++++++++++ src/platform/BUILD.gn | 6 +++++ 3 files changed, 55 insertions(+) diff --git a/src/messaging/ReliableMessageProtocolConfig.cpp b/src/messaging/ReliableMessageProtocolConfig.cpp index d2cda6123af5cb..e635e91940e0ef 100644 --- a/src/messaging/ReliableMessageProtocolConfig.cpp +++ b/src/messaging/ReliableMessageProtocolConfig.cpp @@ -57,18 +57,44 @@ void ClearLocalMRPConfigOverride() } #endif +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG +namespace { + +// This is not a static member of ReliableMessageProtocolConfig because the free +// function GetLocalMRPConfig() needs access to it. +Optional sDynamicLocalMPRConfig; + +} // anonymous namespace + +bool ReliableMessageProtocolConfig::SetLocalMRPConfig(const Optional & localMRPConfig) +{ + auto oldConfig = GetLocalMRPConfig(); + sDynamicLocalMPRConfig = localMRPConfig; + return oldConfig != GetLocalMRPConfig(); +} +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + ReliableMessageProtocolConfig GetDefaultMRPConfig() { // Default MRP intervals are defined in spec <4.12.8. Parameters and Constants> static constexpr const System::Clock::Milliseconds32 idleRetransTimeout = 500_ms32; static constexpr const System::Clock::Milliseconds32 activeRetransTimeout = 300_ms32; static constexpr const System::Clock::Milliseconds16 activeThresholdTime = 4000_ms16; + static_assert(activeThresholdTime == kDefaultActiveTime, "Different active defaults?"); return ReliableMessageProtocolConfig(idleRetransTimeout, activeRetransTimeout, activeThresholdTime); } Optional GetLocalMRPConfig() { ReliableMessageProtocolConfig config(CHIP_CONFIG_MRP_LOCAL_IDLE_RETRY_INTERVAL, CHIP_CONFIG_MRP_LOCAL_ACTIVE_RETRY_INTERVAL); + +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + if (sDynamicLocalMPRConfig.HasValue()) + { + config = sDynamicLocalMPRConfig.Value(); + } +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + #if CHIP_CONFIG_ENABLE_ICD_SERVER // TODO ICD LIT shall not advertise the SII key // Increase local MRP retry intervals by ICD polling intervals. That is, intervals for diff --git a/src/messaging/ReliableMessageProtocolConfig.h b/src/messaging/ReliableMessageProtocolConfig.h index 87864d1a67e922..2ab1c139657fc6 100644 --- a/src/messaging/ReliableMessageProtocolConfig.h +++ b/src/messaging/ReliableMessageProtocolConfig.h @@ -208,6 +208,29 @@ struct ReliableMessageProtocolConfig return mIdleRetransTimeout == that.mIdleRetransTimeout && mActiveRetransTimeout == that.mActiveRetransTimeout && mActiveThresholdTime == that.mActiveThresholdTime; } + +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + /** + * Set the local MRP configuration for the node. + * + * Passing a "no value" optional resets to the compiled-in settings + * (CHIP_CONFIG_MRP_LOCAL_IDLE_RETRY_INTERVAL and + * CHIP_CONFIG_MRP_LOCAL_ACTIVE_RETRY_INTERVAL). + * + * Otherwise the value set via this function is used instead of the + * compiled-in settings, but can still be overridden by ICD configuration + * and other things that would override the compiled-in settings. + * + * Changing the value via this function does not affect any existing + * sessions or exchanges, but does affect the values we communicate to our + * peer during future session establishments. + * + * @return whether the local MRP configuration actually changed as a result + * of this call. If it did, callers may need to reset DNS-SD + * advertising to advertise the updated values. + */ + static bool SetLocalMRPConfig(const Optional & localMRPConfig); +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG }; /// @brief The default MRP config. The value is defined by spec, and shall be same for all implementations, diff --git a/src/platform/BUILD.gn b/src/platform/BUILD.gn index 3c1f56da771786..4a0760334fb8cb 100644 --- a/src/platform/BUILD.gn +++ b/src/platform/BUILD.gn @@ -80,6 +80,11 @@ if (chip_device_platform != "none" && chip_device_platform != "external") { # Define the default number of ip addresses to discover chip_max_discovered_ip_addresses = 5 + + # Allows enabling dynamic setting of the local MRP configuration, for + # devices with multiple radios that have different sleep behavior for + # different radios. + chip_device_config_enable_dynamic_mrp_config = false } if (chip_stack_lock_tracking == "auto") { @@ -131,6 +136,7 @@ if (chip_device_platform != "none" && chip_device_platform != "external") { "CHIP_DISABLE_PLATFORM_KVS=${chip_disable_platform_kvs}", "CHIP_USE_TRANSITIONAL_COMMISSIONABLE_DATA_PROVIDER=${chip_use_transitional_commissionable_data_provider}", "CHIP_USE_TRANSITIONAL_DEVICE_INSTANCE_INFO_PROVIDER=${chip_use_transitional_device_instance_info_provider}", + "CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG=${chip_device_config_enable_dynamic_mrp_config}", ] if (chip_device_platform == "linux" || chip_device_platform == "darwin" || From c554f44a681e09e45724b604e27b6ecb69859590 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 7 Mar 2024 16:46:21 -0500 Subject: [PATCH 06/76] Stop treating BUSY response as fatal when CASE re-attempts are enabled. (#32490) 1. We now communicate the requested delay to the session establishment delegate when we get a BUSY response. 2. OperationalSessionSetup, when it has CASE re-attempts enabled, treats a BUSY response as a trigger to reattempt, not a fatal error. 3. In CASEServer, set the requested delay to a better value if we have sent Sigma2 and are waiting for a response to that. Fixes https://github.com/project-chip/connectedhomeip/issues/28288 --- src/app/OperationalSessionSetup.cpp | 43 ++++++++++++++++--- src/app/OperationalSessionSetup.h | 3 ++ src/protocols/secure_channel/CASEServer.cpp | 28 ++++++++++-- src/protocols/secure_channel/CASESession.cpp | 7 ++- src/protocols/secure_channel/CASESession.h | 3 +- src/protocols/secure_channel/PASESession.cpp | 3 +- src/protocols/secure_channel/PASESession.h | 3 +- src/protocols/secure_channel/PairingSession.h | 12 ++++-- .../SessionEstablishmentDelegate.h | 10 +++++ 9 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/app/OperationalSessionSetup.cpp b/src/app/OperationalSessionSetup.cpp index 179e2a3df5e120..6df07ca6b0568e 100644 --- a/src/app/OperationalSessionSetup.cpp +++ b/src/app/OperationalSessionSetup.cpp @@ -430,7 +430,7 @@ void OperationalSessionSetup::OnSessionEstablishmentError(CHIP_ERROR error, Sess // member instead of having a boolean // mTryingNextResultDueToSessionEstablishmentError, so we can recover the // error in UpdateDeviceData. - if (CHIP_ERROR_TIMEOUT == error) + if (CHIP_ERROR_TIMEOUT == error || CHIP_ERROR_BUSY == error) { #if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES // Make a copy of the ReliableMessageProtocolConfig, since our @@ -480,6 +480,15 @@ void OperationalSessionSetup::OnSessionEstablishmentError(CHIP_ERROR error, Sess // Do not touch `this` instance anymore; it has been destroyed in DequeueConnectionCallbacks. } +void OperationalSessionSetup::OnResponderBusy(System::Clock::Milliseconds16 requestedDelay) +{ +#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES + // Store the requested delay, so that we can use it for scheduling our + // retry. + mRequestedBusyDelay = requestedDelay; +#endif +} + void OperationalSessionSetup::OnSessionEstablished(const SessionHandle & session) { VerifyOrReturn(mState == State::Connecting, @@ -705,9 +714,22 @@ CHIP_ERROR OperationalSessionSetup::ScheduleSessionSetupReattempt(System::Clock: static_assert(UINT16_MAX / CHIP_DEVICE_CONFIG_AUTOMATIC_CASE_RETRY_INITIAL_DELAY_SECONDS >= (1 << CHIP_DEVICE_CONFIG_AUTOMATIC_CASE_RETRY_MAX_BACKOFF), "Our backoff calculation will overflow."); - timerDelay = System::Clock::Seconds16( + System::Clock::Timeout actualTimerDelay = System::Clock::Seconds16( static_cast(CHIP_DEVICE_CONFIG_AUTOMATIC_CASE_RETRY_INITIAL_DELAY_SECONDS << min((mAttemptsDone - 1), CHIP_DEVICE_CONFIG_AUTOMATIC_CASE_RETRY_MAX_BACKOFF))); + const bool responseWasBusy = mRequestedBusyDelay != System::Clock::kZero; + if (responseWasBusy) + { + if (mRequestedBusyDelay > actualTimerDelay) + { + actualTimerDelay = mRequestedBusyDelay; + } + + // Reset mRequestedBusyDelay now that we have consumed it, so it does + // not affect future reattempts not triggered by a busy response. + mRequestedBusyDelay = System::Clock::kZero; + } + if (mAttemptsDone % 2 == 0) { // It's possible that the other side received one of our Sigma1 messages @@ -716,11 +738,22 @@ CHIP_ERROR OperationalSessionSetup::ScheduleSessionSetupReattempt(System::Clock: // listening for Sigma1 messages again. // // To handle that, on every other retry, add the amount of time it would - // take the other side to time out. + // take the other side to time out. It would be nice if we could rely + // on the delay reported in a BUSY response to just tell us that value, + // but in practice for old devices BUSY often sends some hardcoded value + // that tells us nothing about when the other side will decide it has + // timed out. auto additionalTimeout = CASESession::ComputeSigma2ResponseTimeout(GetLocalMRPConfig().ValueOr(GetDefaultMRPConfig())); - timerDelay += std::chrono::duration_cast(additionalTimeout); + actualTimerDelay += additionalTimeout; } - CHIP_ERROR err = mInitParams.exchangeMgr->GetSessionManager()->SystemLayer()->StartTimer(timerDelay, TrySetupAgain, this); + timerDelay = std::chrono::duration_cast(actualTimerDelay); + + CHIP_ERROR err = mInitParams.exchangeMgr->GetSessionManager()->SystemLayer()->StartTimer(actualTimerDelay, TrySetupAgain, this); + + // TODO: If responseWasBusy, should we increment, mRemainingAttempts and + // mResolveAttemptsAllowed, since we were explicitly told to retry? Hard to + // tell what consumers expect out of a capped retry count here. + // The cast on count() is needed because the type count() returns might not // actually be uint16_t; on some platforms it's int. ChipLogProgress(Discovery, diff --git a/src/app/OperationalSessionSetup.h b/src/app/OperationalSessionSetup.h index 45b571a08aa633..9d24faad5efabb 100644 --- a/src/app/OperationalSessionSetup.h +++ b/src/app/OperationalSessionSetup.h @@ -227,6 +227,7 @@ class DLL_EXPORT OperationalSessionSetup : public SessionEstablishmentDelegate, //////////// SessionEstablishmentDelegate Implementation /////////////// void OnSessionEstablished(const SessionHandle & session) override; void OnSessionEstablishmentError(CHIP_ERROR error, SessionEstablishmentStage stage) override; + void OnResponderBusy(System::Clock::Milliseconds16 requestedDelay) override; ScopedNodeId GetPeerId() const { return mPeerId; } @@ -319,6 +320,8 @@ class DLL_EXPORT OperationalSessionSetup : public SessionEstablishmentDelegate, uint8_t mResolveAttemptsAllowed = 0; + System::Clock::Milliseconds16 mRequestedBusyDelay = System::Clock::kZero; + Callback::CallbackDeque mConnectionRetry; #endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES diff --git a/src/protocols/secure_channel/CASEServer.cpp b/src/protocols/secure_channel/CASEServer.cpp index 2ad196a31b9461..df0984d4d94eee 100644 --- a/src/protocols/secure_channel/CASEServer.cpp +++ b/src/protocols/secure_channel/CASEServer.cpp @@ -90,9 +90,31 @@ CHIP_ERROR CASEServer::OnMessageReceived(Messaging::ExchangeContext * ec, const // Handshake wasn't stuck, send the busy status report and let the existing handshake continue. // A successful CASE handshake can take several seconds and some may time out (30 seconds or more). - // TODO: Come up with better estimate: https://github.com/project-chip/connectedhomeip/issues/28288 - // For now, setting minimum wait time to 5000 milliseconds. - CHIP_ERROR err = SendBusyStatusReport(ec, System::Clock::Milliseconds16(5000)); + + System::Clock::Milliseconds16 delay = System::Clock::kZero; + if (GetSession().GetState() == CASESession::State::kSentSigma2) + { + // The delay should be however long we think it will take for + // that to time out. + auto sigma2Timeout = CASESession::ComputeSigma2ResponseTimeout(GetSession().GetRemoteMRPConfig()); + if (sigma2Timeout < System::Clock::Milliseconds16::max()) + { + delay = std::chrono::duration_cast(sigma2Timeout); + } + else + { + // Avoid overflow issues, just wait for as long as we can to + // get close to our expected Sigma2 timeout. + delay = System::Clock::Milliseconds16::max(); + } + } + else + { + // For now, setting minimum wait time to 5000 milliseconds if we + // have no other information. + delay = System::Clock::Milliseconds16(5000); + } + CHIP_ERROR err = SendBusyStatusReport(ec, delay); if (err != CHIP_NO_ERROR) { ChipLogError(Inet, "Failed to send the busy status report, err:%" CHIP_ERROR_FORMAT, err.Format()); diff --git a/src/protocols/secure_channel/CASESession.cpp b/src/protocols/secure_channel/CASESession.cpp index 30d1f71cb0ff64..a9b62489783794 100644 --- a/src/protocols/secure_channel/CASESession.cpp +++ b/src/protocols/secure_channel/CASESession.cpp @@ -1966,7 +1966,8 @@ void CASESession::OnSuccessStatusReport() Finish(); } -CHIP_ERROR CASESession::OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) +CHIP_ERROR CASESession::OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode, + Optional protocolData) { CHIP_ERROR err = CHIP_NO_ERROR; switch (protocolCode) @@ -1981,6 +1982,10 @@ CHIP_ERROR CASESession::OnFailureStatusReport(Protocols::SecureChannel::GeneralS case kProtocolCodeBusy: err = CHIP_ERROR_BUSY; + if (protocolData.HasValue()) + { + mDelegate->OnResponderBusy(System::Clock::Milliseconds16(static_cast(protocolData.Value()))); + } break; default: diff --git a/src/protocols/secure_channel/CASESession.h b/src/protocols/secure_channel/CASESession.h index cb7f3c7f13380d..9e41f6c69fbe84 100644 --- a/src/protocols/secure_channel/CASESession.h +++ b/src/protocols/secure_channel/CASESession.h @@ -272,7 +272,8 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, const ByteSpan & skInfo, const ByteSpan & nonce); void OnSuccessStatusReport() override; - CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) override; + CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode, + Optional protocolData) override; void AbortPendingEstablish(CHIP_ERROR err); diff --git a/src/protocols/secure_channel/PASESession.cpp b/src/protocols/secure_channel/PASESession.cpp index 50b186100841e0..40dc67793604e2 100644 --- a/src/protocols/secure_channel/PASESession.cpp +++ b/src/protocols/secure_channel/PASESession.cpp @@ -759,7 +759,8 @@ void PASESession::OnSuccessStatusReport() Finish(); } -CHIP_ERROR PASESession::OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) +CHIP_ERROR PASESession::OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode, + Optional protocolData) { CHIP_ERROR err = CHIP_NO_ERROR; switch (protocolCode) diff --git a/src/protocols/secure_channel/PASESession.h b/src/protocols/secure_channel/PASESession.h index 393d3a65fc958a..e270baf42e80f1 100644 --- a/src/protocols/secure_channel/PASESession.h +++ b/src/protocols/secure_channel/PASESession.h @@ -203,7 +203,8 @@ class DLL_EXPORT PASESession : public Messaging::UnsolicitedMessageHandler, CHIP_ERROR HandleMsg3(System::PacketBufferHandle && msg); void OnSuccessStatusReport() override; - CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) override; + CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode, + Optional protocolData) override; void Finish(); diff --git a/src/protocols/secure_channel/PairingSession.h b/src/protocols/secure_channel/PairingSession.h index 8ed9f269b9a32c..f49dbf7997f4e2 100644 --- a/src/protocols/secure_channel/PairingSession.h +++ b/src/protocols/secure_channel/PairingSession.h @@ -26,6 +26,7 @@ #pragma once #include +#include #include #include #include @@ -129,7 +130,11 @@ class DLL_EXPORT PairingSession : public SessionDelegate void SetPeerSessionId(uint16_t id) { mPeerSessionId.SetValue(id); } virtual void OnSuccessStatusReport() {} - virtual CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) + + // Handle a failure StatusReport message from the server. protocolData will + // depend on exactly what the generalCode/protocolCode are. + virtual CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode, + Optional protocolData) { return CHIP_ERROR_INTERNAL; } @@ -174,6 +179,7 @@ class DLL_EXPORT PairingSession : public SessionDelegate return CHIP_NO_ERROR; } + Optional protocolData; if (report.GetGeneralCode() == Protocols::SecureChannel::GeneralStatusCode::kBusy && report.GetProtocolCode() == Protocols::SecureChannel::kProtocolCodeBusy) { @@ -189,15 +195,15 @@ class DLL_EXPORT PairingSession : public SessionDelegate } else { - // TODO: CASE: Notify minimum wait time to clients on receiving busy status report #28290 ChipLogProgress(SecureChannel, "Received busy status report with minimum wait time: %u ms", minimumWaitTime); + protocolData.Emplace(minimumWaitTime); } } } // It's very important that we propagate the return value from // OnFailureStatusReport out to the caller. Make sure we return it directly. - return OnFailureStatusReport(report.GetGeneralCode(), report.GetProtocolCode()); + return OnFailureStatusReport(report.GetGeneralCode(), report.GetProtocolCode(), protocolData); } /** diff --git a/src/protocols/secure_channel/SessionEstablishmentDelegate.h b/src/protocols/secure_channel/SessionEstablishmentDelegate.h index a50949e6f6b2e8..c0ba56d9b01e09 100644 --- a/src/protocols/secure_channel/SessionEstablishmentDelegate.h +++ b/src/protocols/secure_channel/SessionEstablishmentDelegate.h @@ -25,6 +25,7 @@ #pragma once +#include #include #include #include @@ -80,6 +81,15 @@ class DLL_EXPORT SessionEstablishmentDelegate */ virtual void OnSessionEstablished(const SessionHandle & session) {} + /** + * Called when the responder has responded with a "busy" status code and + * provided a requested delay. + * + * This call will be followed by an OnSessionEstablishmentError with + * CHIP_ERROR_BUSY as the error. + */ + virtual void OnResponderBusy(System::Clock::Milliseconds16 requestedDelay) {} + virtual ~SessionEstablishmentDelegate() {} }; From 0aa24427aca9b66c2711c96fd4d3f13856a50566 Mon Sep 17 00:00:00 2001 From: Terence Hampson Date: Thu, 7 Mar 2024 18:58:41 -0500 Subject: [PATCH 07/76] Add OperationalState to chef examples (#32495) --- .../chef-operational-state-delegate-impl.cpp | 219 ++++++++++++++++++ .../chef-operational-state-delegate-impl.h | 147 ++++++++++++ examples/chef/linux/BUILD.gn | 1 + examples/chef/nrfconnect/CMakeLists.txt | 1 + 4 files changed, 368 insertions(+) create mode 100644 examples/chef/common/chef-operational-state-delegate-impl.cpp create mode 100644 examples/chef/common/chef-operational-state-delegate-impl.h diff --git a/examples/chef/common/chef-operational-state-delegate-impl.cpp b/examples/chef/common/chef-operational-state-delegate-impl.cpp new file mode 100644 index 00000000000000..48863b0cfa6c26 --- /dev/null +++ b/examples/chef/common/chef-operational-state-delegate-impl.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2023 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 + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::OperationalState; +using namespace chip::app::Clusters::RvcOperationalState; + +static void onOperationalStateTimerTick(System::Layer * systemLayer, void * data); + +DataModel::Nullable GenericOperationalStateDelegateImpl::GetCountdownTime() +{ + if (mCountDownTime.IsNull()) + return DataModel::NullNullable; + + return DataModel::MakeNullable((uint32_t) (mCountDownTime.Value() - mRunningTime)); +} + +CHIP_ERROR GenericOperationalStateDelegateImpl::GetOperationalStateAtIndex(size_t index, GenericOperationalState & operationalState) +{ + if (index >= mOperationalStateList.size()) + { + return CHIP_ERROR_NOT_FOUND; + } + operationalState = mOperationalStateList[index]; + return CHIP_NO_ERROR; +} + +CHIP_ERROR GenericOperationalStateDelegateImpl::GetOperationalPhaseAtIndex(size_t index, MutableCharSpan & operationalPhase) +{ + if (index >= mOperationalPhaseList.size()) + { + return CHIP_ERROR_NOT_FOUND; + } + return CopyCharSpanToMutableCharSpan(mOperationalPhaseList[index], operationalPhase); +} + +void GenericOperationalStateDelegateImpl::HandlePauseStateCallback(GenericOperationalError & err) +{ + OperationalState::OperationalStateEnum state = + static_cast(GetInstance()->GetCurrentOperationalState()); + + if (state == OperationalState::OperationalStateEnum::kStopped || state == OperationalState::OperationalStateEnum::kError) + { + err.Set(to_underlying(OperationalState::ErrorStateEnum::kCommandInvalidInState)); + return; + } + + // placeholder implementation + auto error = GetInstance()->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kPaused)); + if (error == CHIP_NO_ERROR) + { + err.Set(to_underlying(ErrorStateEnum::kNoError)); + } + else + { + err.Set(to_underlying(ErrorStateEnum::kUnableToCompleteOperation)); + } +} + +void GenericOperationalStateDelegateImpl::HandleResumeStateCallback(GenericOperationalError & err) +{ + OperationalState::OperationalStateEnum state = + static_cast(GetInstance()->GetCurrentOperationalState()); + + if (state == OperationalState::OperationalStateEnum::kStopped || state == OperationalState::OperationalStateEnum::kError) + { + err.Set(to_underlying(OperationalState::ErrorStateEnum::kCommandInvalidInState)); + return; + } + + // placeholder implementation + auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning)); + if (error == CHIP_NO_ERROR) + { + err.Set(to_underlying(ErrorStateEnum::kNoError)); + } + else + { + err.Set(to_underlying(ErrorStateEnum::kUnableToCompleteOperation)); + } +} + +void GenericOperationalStateDelegateImpl::HandleStartStateCallback(GenericOperationalError & err) +{ + OperationalState::GenericOperationalError current_err(to_underlying(OperationalState::ErrorStateEnum::kNoError)); + GetInstance()->GetCurrentOperationalError(current_err); + + if (current_err.errorStateID != to_underlying(OperationalState::ErrorStateEnum::kNoError)) + { + err.Set(to_underlying(OperationalState::ErrorStateEnum::kUnableToStartOrResume)); + return; + } + + // placeholder implementation + auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning)); + if (error == CHIP_NO_ERROR) + { + (void) DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds16(1), onOperationalStateTimerTick, this); + err.Set(to_underlying(ErrorStateEnum::kNoError)); + } + else + { + err.Set(to_underlying(ErrorStateEnum::kUnableToCompleteOperation)); + } +} + +void GenericOperationalStateDelegateImpl::HandleStopStateCallback(GenericOperationalError & err) +{ + // placeholder implementation + auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kStopped)); + if (error == CHIP_NO_ERROR) + { + (void) DeviceLayer::SystemLayer().CancelTimer(onOperationalStateTimerTick, this); + + OperationalState::GenericOperationalError current_err(to_underlying(OperationalState::ErrorStateEnum::kNoError)); + GetInstance()->GetCurrentOperationalError(current_err); + + Optional> totalTime((DataModel::Nullable(mRunningTime + mPausedTime))); + Optional> pausedTime((DataModel::Nullable(mPausedTime))); + + GetInstance()->OnOperationCompletionDetected(static_cast(current_err.errorStateID), totalTime, pausedTime); + + mRunningTime = 0; + mPausedTime = 0; + err.Set(to_underlying(ErrorStateEnum::kNoError)); + } + else + { + err.Set(to_underlying(ErrorStateEnum::kUnableToCompleteOperation)); + } +} + +static void onOperationalStateTimerTick(System::Layer * systemLayer, void * data) +{ + GenericOperationalStateDelegateImpl * delegate = reinterpret_cast(data); + + OperationalState::Instance * instance = OperationalState::GetOperationalStateInstance(); + OperationalState::OperationalStateEnum state = + static_cast(instance->GetCurrentOperationalState()); + + auto countdown_time = delegate->GetCountdownTime(); + + if (countdown_time.IsNull() || (!countdown_time.IsNull() && countdown_time.Value() > 0)) + { + if (state == OperationalState::OperationalStateEnum::kRunning) + { + delegate->mRunningTime++; + } + else if (state == OperationalState::OperationalStateEnum::kPaused) + { + delegate->mPausedTime++; + } + } + + if (state == OperationalState::OperationalStateEnum::kRunning || state == OperationalState::OperationalStateEnum::kPaused) + { + (void) DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds16(1), onOperationalStateTimerTick, delegate); + } + else + { + (void) DeviceLayer::SystemLayer().CancelTimer(onOperationalStateTimerTick, delegate); + } +} + +// Init Operational State cluster + +static OperationalState::Instance * gOperationalStateInstance = nullptr; +static OperationalStateDelegate * gOperationalStateDelegate = nullptr; + +OperationalState::Instance * OperationalState::GetOperationalStateInstance() +{ + return gOperationalStateInstance; +} + +void OperationalState::Shutdown() +{ + if (gOperationalStateInstance != nullptr) + { + delete gOperationalStateInstance; + gOperationalStateInstance = nullptr; + } + if (gOperationalStateDelegate != nullptr) + { + delete gOperationalStateDelegate; + gOperationalStateDelegate = nullptr; + } +} + +void emberAfOperationalStateClusterInitCallback(chip::EndpointId endpointId) +{ + VerifyOrDie(endpointId == 1); // this cluster is only enabled for endpoint 1. + VerifyOrDie(gOperationalStateInstance == nullptr && gOperationalStateDelegate == nullptr); + + gOperationalStateDelegate = new OperationalStateDelegate; + EndpointId operationalStateEndpoint = 0x01; + gOperationalStateInstance = new OperationalState::Instance(gOperationalStateDelegate, operationalStateEndpoint); + + gOperationalStateInstance->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kStopped)); + + gOperationalStateInstance->Init(); +} diff --git a/examples/chef/common/chef-operational-state-delegate-impl.h b/examples/chef/common/chef-operational-state-delegate-impl.h new file mode 100644 index 00000000000000..60b6b09e9b6511 --- /dev/null +++ b/examples/chef/common/chef-operational-state-delegate-impl.h @@ -0,0 +1,147 @@ +/* + * + * Copyright (c) 2023 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. + */ + +#pragma once + +#include +#include + +#include + +namespace chip { +namespace app { +namespace Clusters { + +namespace OperationalState { + +// This is an application level delegate to handle operational state commands according to the specific business logic. +class GenericOperationalStateDelegateImpl : public Delegate +{ +public: + uint32_t mRunningTime = 0; + uint32_t mPausedTime = 0; + app::DataModel::Nullable mCountDownTime; + + /** + * Get the countdown time. This attribute is not used in this application. + * @return The current countdown time. + */ + app::DataModel::Nullable GetCountdownTime() override; + + /** + * Fills in the provided GenericOperationalState with the state at index `index` if there is one, + * or returns CHIP_ERROR_NOT_FOUND if the index is out of range for the list of states. + * Note: This is used by the SDK to populate the operational state list attribute. If the contents of this list changes, + * the device SHALL call the Instance's ReportOperationalStateListChange method to report that this attribute has changed. + * @param index The index of the state, with 0 representing the first state. + * @param operationalState The GenericOperationalState is filled. + */ + CHIP_ERROR GetOperationalStateAtIndex(size_t index, GenericOperationalState & operationalState) override; + + /** + * Fills in the provided MutableCharSpan with the phase at index `index` if there is one, + * or returns CHIP_ERROR_NOT_FOUND if the index is out of range for the list of phases. + * + * If CHIP_ERROR_NOT_FOUND is returned for index 0, that indicates that the PhaseList attribute is null + * (there are no phases defined at all). + * + * Note: This is used by the SDK to populate the phase list attribute. If the contents of this list changes, the + * device SHALL call the Instance's ReportPhaseListChange method to report that this attribute has changed. + * @param index The index of the phase, with 0 representing the first phase. + * @param operationalPhase The MutableCharSpan is filled. + */ + CHIP_ERROR GetOperationalPhaseAtIndex(size_t index, MutableCharSpan & operationalPhase) override; + + // command callback + /** + * Handle Command Callback in application: Pause + * @param[out] get operational error after callback. + */ + void HandlePauseStateCallback(GenericOperationalError & err) override; + + /** + * Handle Command Callback in application: Resume + * @param[out] get operational error after callback. + */ + void HandleResumeStateCallback(GenericOperationalError & err) override; + + /** + * Handle Command Callback in application: Start + * @param[out] get operational error after callback. + */ + void HandleStartStateCallback(GenericOperationalError & err) override; + + /** + * Handle Command Callback in application: Stop + * @param[out] get operational error after callback. + */ + void HandleStopStateCallback(GenericOperationalError & err) override; + +protected: + Span mOperationalStateList; + Span mOperationalPhaseList; +}; + +// This is an application level delegate to handle operational state commands according to the specific business logic. +class OperationalStateDelegate : public GenericOperationalStateDelegateImpl +{ +private: + const GenericOperationalState opStateList[4] = { + GenericOperationalState(to_underlying(OperationalStateEnum::kStopped)), + GenericOperationalState(to_underlying(OperationalStateEnum::kRunning)), + GenericOperationalState(to_underlying(OperationalStateEnum::kPaused)), + GenericOperationalState(to_underlying(OperationalStateEnum::kError)), + }; + + const uint32_t kExampleCountDown = 30; + +public: + OperationalStateDelegate() + { + GenericOperationalStateDelegateImpl::mOperationalStateList = Span(opStateList); + } + + /** + * Handle Command Callback in application: Start + * @param[out] get operational error after callback. + */ + void HandleStartStateCallback(GenericOperationalError & err) override + { + mCountDownTime.SetNonNull(static_cast(kExampleCountDown)); + GenericOperationalStateDelegateImpl::HandleStartStateCallback(err); + } + + /** + * Handle Command Callback in application: Stop + * @param[out] get operational error after callback. + */ + void HandleStopStateCallback(GenericOperationalError & err) override + { + GenericOperationalStateDelegateImpl::HandleStopStateCallback(err); + mCountDownTime.SetNull(); + } +}; + +Instance * GetOperationalStateInstance(); + +void Shutdown(); + +} // namespace OperationalState +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/examples/chef/linux/BUILD.gn b/examples/chef/linux/BUILD.gn index 02fa77dac864e5..0a4e2385f28dda 100644 --- a/examples/chef/linux/BUILD.gn +++ b/examples/chef/linux/BUILD.gn @@ -43,6 +43,7 @@ executable("${sample_name}") { "${project_dir}/common/chef-air-quality.cpp", "${project_dir}/common/chef-concentration-measurement.cpp", "${project_dir}/common/chef-fan-control-manager.cpp", + "${project_dir}/common/chef-operational-state-delegate-impl.cpp", "${project_dir}/common/chef-resource-monitoring-delegates.cpp", "${project_dir}/common/chef-rvc-mode-delegate.cpp", "${project_dir}/common/chef-rvc-operational-state-delegate.cpp", diff --git a/examples/chef/nrfconnect/CMakeLists.txt b/examples/chef/nrfconnect/CMakeLists.txt index 0a408e829dd04f..25d663211b9f15 100644 --- a/examples/chef/nrfconnect/CMakeLists.txt +++ b/examples/chef/nrfconnect/CMakeLists.txt @@ -84,6 +84,7 @@ target_sources(app PRIVATE ${CHEF}/common/chef-air-quality.cpp ${CHEF}/common/chef-concentration-measurement.cpp ${CHEF}/common/chef-fan-control-manager.cpp + ${CHEF}/common/chef-operational-state-delegate-impl.cpp ${CHEF}/common/chef-resource-monitoring-delegates.cpp ${CHEF}/common/chef-rvc-mode-delegate.cpp ${CHEF}/common/chef-rvc-operational-state-delegate.cpp From 3dc23203d0a43ddbf4acd0deae18431cc91b5418 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:51:47 +0900 Subject: [PATCH 08/76] [Android] Add Discovered Device information (#32472) * Add Android Discovered Device infor * Restyled by whitespace * Restyled by google-java-format * Restyled by clang-format --------- Co-authored-by: Restyled.io --- src/controller/java/BUILD.gn | 2 + .../java/CHIPDeviceController-JNI.cpp | 36 +++++++++++- .../CommissioningWindowStatus.java | 39 +++++++++++++ .../devicecontroller/DiscoveredDevice.java | 58 +++++++++++++++++++ .../devicecontroller/PairingHintBitmap.java | 57 ++++++++++++++++++ 5 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/controller/java/src/chip/devicecontroller/CommissioningWindowStatus.java create mode 100644 src/controller/java/src/chip/devicecontroller/PairingHintBitmap.java diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 3b573daa93028c..ff42d1b43a42f4 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -450,6 +450,7 @@ android_library("java") { "src/chip/devicecontroller/ChipCommandType.java", "src/chip/devicecontroller/ChipDeviceController.java", "src/chip/devicecontroller/ChipDeviceControllerException.java", + "src/chip/devicecontroller/CommissioningWindowStatus.java", "src/chip/devicecontroller/ConnectionFailureException.java", "src/chip/devicecontroller/ControllerParams.java", "src/chip/devicecontroller/DeviceAttestationDelegate.java", @@ -469,6 +470,7 @@ android_library("java") { "src/chip/devicecontroller/OTAProviderDelegate.java", "src/chip/devicecontroller/OpenCommissioningCallback.java", "src/chip/devicecontroller/OperationalKeyConfig.java", + "src/chip/devicecontroller/PairingHintBitmap.java", "src/chip/devicecontroller/PaseVerifierParams.java", "src/chip/devicecontroller/ReportCallback.java", "src/chip/devicecontroller/ReportCallbackJni.java", diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index fc2c0694b29849..99520d1a32c9a0 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -1897,9 +1897,19 @@ JNI_METHOD(jobject, getDiscoveredDevice)(JNIEnv * env, jobject self, jlong handl jclass discoveredDeviceCls = env->FindClass("chip/devicecontroller/DiscoveredDevice"); jmethodID constructor = env->GetMethodID(discoveredDeviceCls, "", "()V"); - jfieldID discrminatorID = env->GetFieldID(discoveredDeviceCls, "discriminator", "J"); - jfieldID ipAddressID = env->GetFieldID(discoveredDeviceCls, "ipAddress", "Ljava/lang/String;"); - jfieldID portID = env->GetFieldID(discoveredDeviceCls, "port", "I"); + jfieldID discrminatorID = env->GetFieldID(discoveredDeviceCls, "discriminator", "J"); + jfieldID ipAddressID = env->GetFieldID(discoveredDeviceCls, "ipAddress", "Ljava/lang/String;"); + jfieldID portID = env->GetFieldID(discoveredDeviceCls, "port", "I"); + jfieldID deviceTypeID = env->GetFieldID(discoveredDeviceCls, "deviceType", "J"); + jfieldID vendorIdID = env->GetFieldID(discoveredDeviceCls, "vendorId", "I"); + jfieldID productIdID = env->GetFieldID(discoveredDeviceCls, "productId", "I"); + jfieldID rotatingIdID = env->GetFieldID(discoveredDeviceCls, "rotatingId", "[B"); + jfieldID instanceNameID = env->GetFieldID(discoveredDeviceCls, "instanceName", "Ljava/lang/String;"); + jfieldID deviceNameID = env->GetFieldID(discoveredDeviceCls, "deviceName", "Ljava/lang/String;"); + jfieldID pairingInstructionID = env->GetFieldID(discoveredDeviceCls, "pairingInstruction", "Ljava/lang/String;"); + + jmethodID setCommissioningModeID = env->GetMethodID(discoveredDeviceCls, "setCommissioningMode", "(I)V"); + jmethodID setPairingHintID = env->GetMethodID(discoveredDeviceCls, "setPairingHint", "(I)V"); jobject discoveredObj = env->NewObject(discoveredDeviceCls, constructor); @@ -1911,6 +1921,26 @@ JNI_METHOD(jobject, getDiscoveredDevice)(JNIEnv * env, jobject self, jlong handl env->SetObjectField(discoveredObj, ipAddressID, jniipAdress); env->SetIntField(discoveredObj, portID, static_cast(data->resolutionData.port)); + env->SetLongField(discoveredObj, deviceTypeID, static_cast(data->commissionData.deviceType)); + env->SetIntField(discoveredObj, vendorIdID, static_cast(data->commissionData.vendorId)); + env->SetIntField(discoveredObj, productIdID, static_cast(data->commissionData.productId)); + + jbyteArray jRotatingId; + CHIP_ERROR err = JniReferences::GetInstance().N2J_ByteArray( + env, data->commissionData.rotatingId, static_cast(data->commissionData.rotatingIdLen), jRotatingId); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "jRotatingId N2J_ByteArray error : %" CHIP_ERROR_FORMAT, err.Format()); + return nullptr; + } + env->SetObjectField(discoveredObj, rotatingIdID, static_cast(jRotatingId)); + env->SetObjectField(discoveredObj, instanceNameID, env->NewStringUTF(data->commissionData.instanceName)); + env->SetObjectField(discoveredObj, deviceNameID, env->NewStringUTF(data->commissionData.deviceName)); + env->SetObjectField(discoveredObj, pairingInstructionID, env->NewStringUTF(data->commissionData.pairingInstruction)); + + env->CallVoidMethod(discoveredObj, setCommissioningModeID, static_cast(data->commissionData.commissioningMode)); + env->CallVoidMethod(discoveredObj, setPairingHintID, static_cast(data->commissionData.pairingHint)); return discoveredObj; } diff --git a/src/controller/java/src/chip/devicecontroller/CommissioningWindowStatus.java b/src/controller/java/src/chip/devicecontroller/CommissioningWindowStatus.java new file mode 100644 index 00000000000000..bfced35f778b78 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/CommissioningWindowStatus.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 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 chip.devicecontroller; + +public enum CommissioningWindowStatus { + WindowNotOpen(0), + EnhancedWindowOpen(1), + BasicWindowOpen(2); + + private final int value; + + CommissioningWindowStatus(int value) { + this.value = value; + } + + public static CommissioningWindowStatus value(int value) { + for (CommissioningWindowStatus status : CommissioningWindowStatus.values()) { + if (status.value == value) { + return status; + } + } + throw new IllegalArgumentException("Invalid value: " + value); + } +} diff --git a/src/controller/java/src/chip/devicecontroller/DiscoveredDevice.java b/src/controller/java/src/chip/devicecontroller/DiscoveredDevice.java index 01d6ecc28cce42..2fb8bcf4e79842 100644 --- a/src/controller/java/src/chip/devicecontroller/DiscoveredDevice.java +++ b/src/controller/java/src/chip/devicecontroller/DiscoveredDevice.java @@ -17,8 +17,66 @@ */ package chip.devicecontroller; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + public class DiscoveredDevice { public long discriminator; public String ipAddress; public int port; + public long deviceType; + public int vendorId; + public int productId; + public Set pairingHint; + public CommissioningWindowStatus commissioningMode; + public byte[] rotatingId; + public String instanceName; + public String deviceName; + public String pairingInstruction; + + // For use in JNI. + private void setCommissioningMode(int value) { + this.commissioningMode = CommissioningWindowStatus.value(value); + } + + private void setPairingHint(int value) { + this.pairingHint = new HashSet<>(); + for (PairingHintBitmap mode : PairingHintBitmap.values()) { + int bitmask = 1 << mode.getBitIndex(); + if ((value & bitmask) != 0) { + pairingHint.add(mode); + } + } + } + + @Override + public String toString() { + return "DiscoveredDevice : {" + + "\n\tdiscriminator : " + + discriminator + + "\n\tipAddress : " + + ipAddress + + "\n\tport : " + + port + + "\n\tdeviceType : " + + deviceType + + "\n\tvendorId : " + + vendorId + + "\n\tproductId : " + + productId + + "\n\tpairingHint : " + + pairingHint + + "\n\tcommissioningMode : " + + commissioningMode + + "\n\trotatingId : " + + (rotatingId != null ? Arrays.toString(rotatingId) : "null") + + "\n\tinstanceName : " + + instanceName + + "\n\tdeviceName : " + + deviceName + + "\n\tpairingInstruction : " + + pairingInstruction + + "\n}"; + } } diff --git a/src/controller/java/src/chip/devicecontroller/PairingHintBitmap.java b/src/controller/java/src/chip/devicecontroller/PairingHintBitmap.java new file mode 100644 index 00000000000000..d13b710c9c1b98 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/PairingHintBitmap.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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 chip.devicecontroller; + +public enum PairingHintBitmap { + PowerCycle(0, false), + DeviceManufacturerURL(1, false), + Administrator(2, false), + SettingsMenuOnTheNode(3, false), + CustomInstruction(4, true), + DeviceManual(5, false), + PressResetButton(6, false), + PressResetButtonWithApplicationOfPower(7, false), + PressResetButtonForNseconds(8, true), + PressResetButtonUntilLightBlinks(9, true), + PressResetButtonForNsecondsWithApplicationOfPower(10, true), + PressResetButtonUntilLightBlinksWithApplicationOfPower(11, true), + PressResetButtonNTimes(12, true), + PressSetupButton(13, false), + PressSetupButtonWithApplicationOfPower(14, false), + PressSetupButtonForNseconds(15, true), + PressSetupButtonUntilLightBlinks(16, true), + PressSetupButtonForNsecondsWithApplicationOfPower(17, true), + PressSetupButtonUntilLightBlinksWithApplicationOfPower(18, true), + PressSetupButtonNtimes(19, true); + + private final int bitIndex; + private final boolean isRequirePairingInstruction; + + PairingHintBitmap(int bitIndex, boolean isRequirePairingInstruction) { + this.bitIndex = bitIndex; + this.isRequirePairingInstruction = isRequirePairingInstruction; + } + + public int getBitIndex() { + return bitIndex; + } + + public boolean getRequirePairingInstruction() { + return isRequirePairingInstruction; + } +} From 968cea2949ca48298f23c2dd3f3d514515eb4f24 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:21:39 +0900 Subject: [PATCH 09/76] Remove unset threadNetwork Scan (#31704) * divide android commissioning parameter * Fix countrycode copy issue * Restyled by clang-format * Update commissioningParameter --------- Co-authored-by: Restyled.io --- src/controller/CommissioningDelegate.h | 3 ++ .../java/AndroidDeviceControllerWrapper.cpp | 24 ++++++++++- .../java/AndroidDeviceControllerWrapper.h | 32 ++++++++------- .../java/CHIPDeviceController-JNI.cpp | 41 +++++-------------- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/controller/CommissioningDelegate.h b/src/controller/CommissioningDelegate.h index 175c246e908f9d..2a78663541dd49 100644 --- a/src/controller/CommissioningDelegate.h +++ b/src/controller/CommissioningDelegate.h @@ -370,9 +370,12 @@ class CommissioningParameters mAttestationNonce.SetValue(attestationNonce); return *this; } + + // If a WiFiCredentials is provided, then the WiFiNetworkScan will not be attempted CommissioningParameters & SetWiFiCredentials(WiFiCredentials wifiCreds) { mWiFiCreds.SetValue(wifiCreds); + mAttemptWiFiNetworkScan.SetValue(false); return *this; } diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp index 74e1187ef2a03b..c965bb0f17584b 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.cpp +++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp @@ -104,7 +104,7 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, bool skipCommissioningComplete, - bool skipAttestationCertificateValidation, CHIP_ERROR * errInfoOnFailure) + bool skipAttestationCertificateValidation, jstring countryCode, CHIP_ERROR * errInfoOnFailure) { if (errInfoOnFailure == nullptr) { @@ -207,11 +207,30 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( wrapper->mGroupDataProvider.SetStorageDelegate(wrapperStorage); wrapper->mGroupDataProvider.SetSessionKeystore(initParams.sessionKeystore); - CommissioningParameters params = wrapper->mAutoCommissioner.GetCommissioningParameters(); + CommissioningParameters params = wrapper->GetCommissioningParameters(); params.SetFailsafeTimerSeconds(failsafeTimerSeconds); params.SetAttemptWiFiNetworkScan(attemptNetworkScanWiFi); params.SetAttemptThreadNetworkScan(attemptNetworkScanThread); params.SetSkipCommissioningComplete(skipCommissioningComplete); + + if (countryCode != nullptr) + { + JniUtfString countryCodeJniString(env, countryCode); + if (countryCodeJniString.size() != kCountryCodeBufferLen) + { + *errInfoOnFailure = CHIP_ERROR_INVALID_ARGUMENT; + return nullptr; + } + + MutableCharSpan copiedCode(wrapper->mCountryCode); + if (CopyCharSpanToMutableCharSpan(countryCodeJniString.charSpan(), copiedCode) != CHIP_NO_ERROR) + { + *errInfoOnFailure = CHIP_ERROR_INVALID_ARGUMENT; + return nullptr; + } + params.SetCountryCode(copiedCode); + } + wrapper->UpdateCommissioningParameters(params); CHIP_ERROR err = wrapper->mGroupDataProvider.Init(); @@ -526,6 +545,7 @@ CHIP_ERROR AndroidDeviceControllerWrapper::UpdateCommissioningParameters(const c { // this will wipe out any custom attestationNonce and csrNonce that was being used. // however, Android APIs don't allow these to be set to custom values today. + mCommissioningParameter = params; return mAutoCommissioner.SetCommissioningParameters(params); } diff --git a/src/controller/java/AndroidDeviceControllerWrapper.h b/src/controller/java/AndroidDeviceControllerWrapper.h index ac279370f62c4a..5ccb2edb3b2050 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.h +++ b/src/controller/java/AndroidDeviceControllerWrapper.h @@ -50,6 +50,8 @@ constexpr uint8_t kUserActiveModeTriggerInstructionBufferLen = 128 + 1; // 128bytes is max UserActiveModeTriggerInstruction size and 1 byte is for escape sequence. + +constexpr uint8_t kCountryCodeBufferLen = 2; /** * This class contains all relevant information for the JNI view of CHIPDeviceController * to handle all controller-related processing. @@ -123,10 +125,7 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::Credentials::PartialDACVerifier * GetPartialDACVerifier() { return &mPartialDACVerifier; } - const chip::Controller::CommissioningParameters & GetCommissioningParameters() const - { - return mAutoCommissioner.GetCommissioningParameters(); - } + const chip::Controller::CommissioningParameters & GetCommissioningParameters() const { return mCommissioningParameter; } static AndroidDeviceControllerWrapper * FromJNIHandle(jlong handle) { @@ -171,20 +170,19 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel * @param[in] skipCommissioningComplete whether to skip the CASE commissioningComplete command during commissioning * @param[out] errInfoOnFailure a pointer to a CHIP_ERROR that will be populated if this method returns nullptr */ - static AndroidDeviceControllerWrapper * - AllocateNew(JavaVM * vm, jobject deviceControllerObj, chip::NodeId nodeId, chip::FabricId fabricId, - const chip::CATValues & cats, chip::System::Layer * systemLayer, - chip::Inet::EndPointManager * tcpEndPointManager, - chip::Inet::EndPointManager * udpEndPointManager, + static AndroidDeviceControllerWrapper * AllocateNew( + JavaVM * vm, jobject deviceControllerObj, chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats, + chip::System::Layer * systemLayer, chip::Inet::EndPointManager * tcpEndPointManager, + chip::Inet::EndPointManager * udpEndPointManager, #ifdef JAVA_MATTER_CONTROLLER_TEST - ExampleOperationalCredentialsIssuerPtr opCredsIssuer, + ExampleOperationalCredentialsIssuerPtr opCredsIssuer, #else - AndroidOperationalCredentialsIssuerPtr opCredsIssuer, + AndroidOperationalCredentialsIssuerPtr opCredsIssuer, #endif - jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, - jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, - uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, - bool skipCommissioningComplete, bool skipAttestationCertificateValidation, CHIP_ERROR * errInfoOnFailure); + jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, + jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, + uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, bool skipCommissioningComplete, + bool skipAttestationCertificateValidation, jstring countryCode, CHIP_ERROR * errInfoOnFailure); void Shutdown(); @@ -246,6 +244,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel std::vector mIcacCertificate; std::vector mRcacCertificate; + char mCountryCode[kCountryCodeBufferLen]; + chip::Controller::AutoCommissioner mAutoCommissioner; chip::Credentials::PartialDACVerifier mPartialDACVerifier; @@ -262,6 +262,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::MutableCharSpan mUserActiveModeTriggerInstruction = chip::MutableCharSpan(mUserActiveModeTriggerInstructionBuffer); chip::BitMask mUserActiveModeTriggerHint; + chip::Controller::CommissioningParameters mCommissioningParameter; + AndroidDeviceControllerWrapper(ChipDeviceControllerPtr controller, #ifdef JAVA_MATTER_CONTROLLER_TEST ExampleOperationalCredentialsIssuerPtr opCredsIssuer diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 99520d1a32c9a0..f1c7487e4e3f87 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -371,6 +371,10 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr jobject countryCodeOptional = env->CallObjectMethod(controllerParams, getCountryCode); jobject regulatoryLocationOptional = env->CallObjectMethod(controllerParams, getRegulatoryLocation); + jobject countryCode; + err = chip::JniReferences::GetInstance().GetOptionalValue(countryCodeOptional, countryCode); + SuccessOrExit(err); + #ifdef JAVA_MATTER_CONTROLLER_TEST std::unique_ptr opCredsIssuer( new chip::Controller::ExampleOperationalCredentialsIssuer()); @@ -383,7 +387,7 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr DeviceLayer::TCPEndPointManager(), DeviceLayer::UDPEndPointManager(), std::move(opCredsIssuer), keypairDelegate, rootCertificate, intermediateCertificate, operationalCertificate, ipk, listenPort, controllerVendorId, failsafeTimerSeconds, attemptNetworkScanWiFi, attemptNetworkScanThread, skipCommissioningComplete, - skipAttestationCertificateValidation, &err); + skipAttestationCertificateValidation, static_cast(countryCode), &err); SuccessOrExit(err); if (caseFailsafeTimerSeconds > 0) @@ -411,29 +415,6 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr } } - jobject countryCode; - err = chip::JniReferences::GetInstance().GetOptionalValue(countryCodeOptional, countryCode); - SuccessOrExit(err); - - if (countryCode != nullptr) - { - jstring countryCodeStr = static_cast(countryCode); - JniUtfString countryCodeJniString(env, countryCodeStr); - - VerifyOrExit(countryCodeJniString.size() == 2, err = CHIP_ERROR_INVALID_ARGUMENT); - - chip::Controller::CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); - commissioningParams.SetCountryCode(countryCodeJniString.charSpan()); - - // The wrapper internally has reserved storage for the country code and will copy the value. - err = wrapper->UpdateCommissioningParameters(commissioningParams); - if (err != CHIP_NO_ERROR) - { - ChipLogError(Controller, "UpdateCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); - SuccessOrExit(err); - } - } - jobject regulatoryLocation; err = chip::JniReferences::GetInstance().GetOptionalValue(regulatoryLocationOptional, regulatoryLocation); SuccessOrExit(err); @@ -876,7 +857,7 @@ JNI_METHOD(void, updateCommissioningNetworkCredentials) chip::DeviceLayer::StackLock lock; AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); - CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + CommissioningParameters commissioningParams = wrapper->GetAutoCommissioner()->GetCommissioningParameters(); CHIP_ERROR err = wrapper->ApplyNetworkCredentials(commissioningParams, networkCredentials); if (err != CHIP_NO_ERROR) { @@ -884,10 +865,10 @@ JNI_METHOD(void, updateCommissioningNetworkCredentials) JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); return; } - err = wrapper->UpdateCommissioningParameters(commissioningParams); + err = wrapper->GetAutoCommissioner()->SetCommissioningParameters(commissioningParams); if (err != CHIP_NO_ERROR) { - ChipLogError(Controller, "UpdateCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); + ChipLogError(Controller, "SetCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); return; } @@ -911,7 +892,7 @@ JNI_METHOD(void, updateCommissioningICDRegistrationInfo) chip::DeviceLayer::StackLock lock; AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); - CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + CommissioningParameters commissioningParams = wrapper->GetAutoCommissioner()->GetCommissioningParameters(); CHIP_ERROR err = wrapper->ApplyICDRegistrationInfo(commissioningParams, icdRegistrationInfo); if (err != CHIP_NO_ERROR) { @@ -919,10 +900,10 @@ JNI_METHOD(void, updateCommissioningICDRegistrationInfo) JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); return; } - err = wrapper->UpdateCommissioningParameters(commissioningParams); + err = wrapper->GetAutoCommissioner()->SetCommissioningParameters(commissioningParams); if (err != CHIP_NO_ERROR) { - ChipLogError(Controller, "UpdateCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); + ChipLogError(Controller, "SetCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); return; } From 610435a8968eac51b3d2658f554e65ab14c088fe Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 8 Mar 2024 06:26:19 +0100 Subject: [PATCH 10/76] [Linux] Use reinterpret_cast<> for trivial glib casting (#32376) * Use reinterpret_cast for trivial glib casting * Replace BLUEZ_OBJECT with reinterpret_cast * More cast fixes * Restyled by clang-format --------- Co-authored-by: Restyled.io --- .../Linux/bluez/BluezAdvertisement.cpp | 8 ++--- src/platform/Linux/bluez/BluezConnection.cpp | 31 ++++++++++--------- src/platform/Linux/bluez/BluezEndpoint.cpp | 9 +++--- .../Linux/bluez/BluezObjectIterator.h | 4 +-- .../Linux/bluez/ChipDeviceScanner.cpp | 4 +-- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/platform/Linux/bluez/BluezAdvertisement.cpp b/src/platform/Linux/bluez/BluezAdvertisement.cpp index 06f831bff8b58f..0fed99478cdaf9 100644 --- a/src/platform/Linux/bluez/BluezAdvertisement.cpp +++ b/src/platform/Linux/bluez/BluezAdvertisement.cpp @@ -224,7 +224,7 @@ void BluezAdvertisement::Shutdown() void BluezAdvertisement::StartDone(GObject * aObject, GAsyncResult * aResult) { - BluezLEAdvertisingManager1 * advMgr = BLUEZ_LEADVERTISING_MANAGER1(aObject); + auto * advMgr = reinterpret_cast(aObject); GAutoPtr error; gboolean success = FALSE; @@ -252,7 +252,7 @@ CHIP_ERROR BluezAdvertisement::StartImpl() adapterObject = g_dbus_interface_get_object(G_DBUS_INTERFACE(mAdapter.get())); VerifyOrExit(adapterObject != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL adapterObject in %s", __func__)); - advMgr.reset(bluez_object_get_leadvertising_manager1(BLUEZ_OBJECT(adapterObject))); + advMgr.reset(bluez_object_get_leadvertising_manager1(reinterpret_cast(adapterObject))); VerifyOrExit(advMgr.get() != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL advMgr in %s", __func__)); g_variant_builder_init(&optionsBuilder, G_VARIANT_TYPE("a{sv}")); @@ -282,7 +282,7 @@ CHIP_ERROR BluezAdvertisement::Start() void BluezAdvertisement::StopDone(GObject * aObject, GAsyncResult * aResult) { - BluezLEAdvertisingManager1 * advMgr = BLUEZ_LEADVERTISING_MANAGER1(aObject); + auto * advMgr = reinterpret_cast(aObject); GAutoPtr error; gboolean success = FALSE; @@ -308,7 +308,7 @@ CHIP_ERROR BluezAdvertisement::StopImpl() adapterObject = g_dbus_interface_get_object(G_DBUS_INTERFACE(mAdapter.get())); VerifyOrExit(adapterObject != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL adapterObject in %s", __func__)); - advMgr.reset(bluez_object_get_leadvertising_manager1(BLUEZ_OBJECT(adapterObject))); + advMgr.reset(bluez_object_get_leadvertising_manager1(reinterpret_cast(adapterObject))); VerifyOrExit(advMgr.get() != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL advMgr in %s", __func__)); bluez_leadvertising_manager1_call_unregister_advertisement( diff --git a/src/platform/Linux/bluez/BluezConnection.cpp b/src/platform/Linux/bluez/BluezConnection.cpp index d2db5edc005eba..88595bd86c720c 100644 --- a/src/platform/Linux/bluez/BluezConnection.cpp +++ b/src/platform/Linux/bluez/BluezConnection.cpp @@ -305,9 +305,10 @@ CHIP_ERROR BluezConnection::SendIndication(chip::System::PacketBufferHandle apBu void BluezConnection::SendWriteRequestDone(GObject * aObject, GAsyncResult * aResult, gpointer apConnection) { - BluezGattCharacteristic1 * c1 = BLUEZ_GATT_CHARACTERISTIC1(aObject); + auto * pC1 = reinterpret_cast(aObject); + GAutoPtr error; - gboolean success = bluez_gatt_characteristic1_call_write_value_finish(c1, aResult, &error.GetReceiver()); + gboolean success = bluez_gatt_characteristic1_call_write_value_finish(pC1, aResult, &error.GetReceiver()); VerifyOrReturn(success == TRUE, ChipLogError(DeviceLayer, "FAIL: SendWriteRequest : %s", error->message)); BLEManagerImpl::HandleWriteComplete(static_cast(apConnection)); @@ -354,9 +355,10 @@ void BluezConnection::OnCharacteristicChanged(GDBusProxy * aInterface, GVariant void BluezConnection::SubscribeCharacteristicDone(GObject * aObject, GAsyncResult * aResult, gpointer apConnection) { - BluezGattCharacteristic1 * c2 = BLUEZ_GATT_CHARACTERISTIC1(aObject); + auto * pC2 = reinterpret_cast(aObject); + GAutoPtr error; - gboolean success = bluez_gatt_characteristic1_call_write_value_finish(c2, aResult, &error.GetReceiver()); + gboolean success = bluez_gatt_characteristic1_call_write_value_finish(pC2, aResult, &error.GetReceiver()); VerifyOrReturn(success == TRUE, ChipLogError(DeviceLayer, "FAIL: SubscribeCharacteristic : %s", error->message)); @@ -365,13 +367,12 @@ void BluezConnection::SubscribeCharacteristicDone(GObject * aObject, GAsyncResul CHIP_ERROR BluezConnection::SubscribeCharacteristicImpl(BluezConnection * connection) { - BluezGattCharacteristic1 * c2 = nullptr; - VerifyOrExit(connection->mpC2 != nullptr, ChipLogError(DeviceLayer, "C2 is NULL in %s", __func__)); - c2 = BLUEZ_GATT_CHARACTERISTIC1(connection->mpC2); + BluezGattCharacteristic1 * pC2 = connection->mpC2; + VerifyOrExit(pC2 != nullptr, ChipLogError(DeviceLayer, "C2 is NULL in %s", __func__)); // Get notifications on the TX characteristic change (e.g. indication is received) - g_signal_connect(c2, "g-properties-changed", G_CALLBACK(OnCharacteristicChanged), connection); - bluez_gatt_characteristic1_call_start_notify(connection->mpC2, nullptr, SubscribeCharacteristicDone, connection); + g_signal_connect(pC2, "g-properties-changed", G_CALLBACK(OnCharacteristicChanged), connection); + bluez_gatt_characteristic1_call_start_notify(pC2, nullptr, SubscribeCharacteristicDone, connection); exit: return CHIP_NO_ERROR; @@ -386,22 +387,24 @@ CHIP_ERROR BluezConnection::SubscribeCharacteristic() void BluezConnection::UnsubscribeCharacteristicDone(GObject * aObject, GAsyncResult * aResult, gpointer apConnection) { - BluezGattCharacteristic1 * c2 = BLUEZ_GATT_CHARACTERISTIC1(aObject); + auto * pC2 = reinterpret_cast(aObject); + GAutoPtr error; - gboolean success = bluez_gatt_characteristic1_call_write_value_finish(c2, aResult, &error.GetReceiver()); + gboolean success = bluez_gatt_characteristic1_call_write_value_finish(pC2, aResult, &error.GetReceiver()); VerifyOrReturn(success == TRUE, ChipLogError(DeviceLayer, "FAIL: UnsubscribeCharacteristic : %s", error->message)); // Stop listening to the TX characteristic changes - g_signal_handlers_disconnect_by_data(c2, apConnection); + g_signal_handlers_disconnect_by_data(pC2, apConnection); BLEManagerImpl::HandleSubscribeOpComplete(static_cast(apConnection), false); } CHIP_ERROR BluezConnection::UnsubscribeCharacteristicImpl(BluezConnection * connection) { - VerifyOrExit(connection->mpC2 != nullptr, ChipLogError(DeviceLayer, "C2 is NULL in %s", __func__)); + BluezGattCharacteristic1 * pC2 = connection->mpC2; + VerifyOrExit(pC2 != nullptr, ChipLogError(DeviceLayer, "C2 is NULL in %s", __func__)); - bluez_gatt_characteristic1_call_stop_notify(connection->mpC2, nullptr, UnsubscribeCharacteristicDone, connection); + bluez_gatt_characteristic1_call_stop_notify(pC2, nullptr, UnsubscribeCharacteristicDone, connection); exit: return CHIP_NO_ERROR; diff --git a/src/platform/Linux/bluez/BluezEndpoint.cpp b/src/platform/Linux/bluez/BluezEndpoint.cpp index 2fdd1e1788af6b..8d37b1363e84ad 100644 --- a/src/platform/Linux/bluez/BluezEndpoint.cpp +++ b/src/platform/Linux/bluez/BluezEndpoint.cpp @@ -262,9 +262,8 @@ BluezGattCharacteristic1 * BluezEndpoint::CreateGattCharacteristic(BluezGattServ void BluezEndpoint::RegisterGattApplicationDone(GObject * aObject, GAsyncResult * aResult) { GAutoPtr error; - BluezGattManager1 * gattMgr = BLUEZ_GATT_MANAGER1(aObject); - - gboolean success = bluez_gatt_manager1_call_register_application_finish(gattMgr, aResult, &error.GetReceiver()); + gboolean success = bluez_gatt_manager1_call_register_application_finish(reinterpret_cast(aObject), aResult, + &error.GetReceiver()); VerifyOrReturn(success == TRUE, { ChipLogError(DeviceLayer, "FAIL: RegisterApplication : %s", error->message); @@ -287,7 +286,7 @@ CHIP_ERROR BluezEndpoint::RegisterGattApplicationImpl() adapterObject = g_dbus_interface_get_object(G_DBUS_INTERFACE(mAdapter.get())); VerifyOrExit(adapterObject != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL adapterObject in %s", __func__)); - gattMgr.reset(bluez_object_get_gatt_manager1(BLUEZ_OBJECT(adapterObject))); + gattMgr.reset(bluez_object_get_gatt_manager1(reinterpret_cast(adapterObject))); VerifyOrExit(gattMgr.get() != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL gattMgr in %s", __func__)); g_variant_builder_init(&optionsBuilder, G_VARIANT_TYPE("a{sv}")); @@ -383,7 +382,7 @@ void BluezEndpoint::BluezSignalOnObjectAdded(GDBusObjectManager * aManager, GDBu { // TODO: right now we do not handle addition/removal of adapters // Primary focus here is to handle addition of a device - GAutoPtr device(bluez_object_get_device1(BLUEZ_OBJECT(aObject))); + GAutoPtr device(bluez_object_get_device1(reinterpret_cast(aObject))); VerifyOrReturn(device.get() != nullptr); if (BluezIsDeviceOnAdapter(device.get(), mAdapter.get()) == TRUE) diff --git a/src/platform/Linux/bluez/BluezObjectIterator.h b/src/platform/Linux/bluez/BluezObjectIterator.h index ae5e01c6b9e34d..0e09cbcf217fc0 100644 --- a/src/platform/Linux/bluez/BluezObjectIterator.h +++ b/src/platform/Linux/bluez/BluezObjectIterator.h @@ -41,8 +41,8 @@ class BluezObjectIterator BluezObjectIterator() = default; explicit BluezObjectIterator(GList * position) : mPosition(position) {} - reference operator*() const { return *BLUEZ_OBJECT(mPosition->data); } - pointer operator->() const { return BLUEZ_OBJECT(mPosition->data); } + reference operator*() const { return *reinterpret_cast(mPosition->data); } + pointer operator->() const { return reinterpret_cast(mPosition->data); } bool operator==(const BluezObjectIterator & other) const { return mPosition == other.mPosition; } bool operator!=(const BluezObjectIterator & other) const { return mPosition != other.mPosition; } diff --git a/src/platform/Linux/bluez/ChipDeviceScanner.cpp b/src/platform/Linux/bluez/ChipDeviceScanner.cpp index 0d248058c3d1a0..5523bf87dfdf66 100644 --- a/src/platform/Linux/bluez/ChipDeviceScanner.cpp +++ b/src/platform/Linux/bluez/ChipDeviceScanner.cpp @@ -215,7 +215,7 @@ CHIP_ERROR ChipDeviceScanner::MainLoopStopScan(ChipDeviceScanner * self) void ChipDeviceScanner::SignalObjectAdded(GDBusObjectManager * manager, GDBusObject * object, ChipDeviceScanner * self) { - GAutoPtr device(bluez_object_get_device1(BLUEZ_OBJECT(object))); + GAutoPtr device(bluez_object_get_device1(reinterpret_cast(object))); VerifyOrReturn(device.get() != nullptr); self->ReportDevice(*device.get()); @@ -225,7 +225,7 @@ void ChipDeviceScanner::SignalInterfaceChanged(GDBusObjectManagerClient * manage GDBusProxy * aInterface, GVariant * aChangedProperties, const gchar * const * aInvalidatedProps, ChipDeviceScanner * self) { - GAutoPtr device(bluez_object_get_device1(BLUEZ_OBJECT(object))); + GAutoPtr device(bluez_object_get_device1(reinterpret_cast(object))); VerifyOrReturn(device.get() != nullptr); self->ReportDevice(*device.get()); From b278978aa3fd92e2f6aec39b7f007fdf79ad8a5c Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 8 Mar 2024 06:28:51 +0100 Subject: [PATCH 11/76] [Linux] Reuse AdapterIterator in all places where listing managed objects (#32372) * Use dedicated iterator for listing BlueZ objects * Use GAutoPtr to manage GDBusObjectManager instance * Wrap static function with lambda * Fix iterator initialization for AdapterIterator --- src/platform/GLibTypeDeleter.h | 6 ++ src/platform/Linux/bluez/AdapterIterator.cpp | 64 +++++-------------- src/platform/Linux/bluez/AdapterIterator.h | 14 ++-- src/platform/Linux/bluez/BluezConnection.cpp | 17 ++--- src/platform/Linux/bluez/BluezEndpoint.cpp | 13 ++-- src/platform/Linux/bluez/BluezEndpoint.h | 2 +- .../Linux/bluez/BluezObjectIterator.h | 2 + src/platform/Linux/bluez/BluezObjectList.h | 29 ++++----- 8 files changed, 58 insertions(+), 89 deletions(-) diff --git a/src/platform/GLibTypeDeleter.h b/src/platform/GLibTypeDeleter.h index f083a6c5e460d5..b1cec176ad4715 100644 --- a/src/platform/GLibTypeDeleter.h +++ b/src/platform/GLibTypeDeleter.h @@ -120,6 +120,12 @@ struct GAutoPtrDeleter using deleter = GObjectDeleter; }; +template <> +struct GAutoPtrDeleter +{ + using deleter = GObjectDeleter; +}; + template <> struct GAutoPtrDeleter { diff --git a/src/platform/Linux/bluez/AdapterIterator.cpp b/src/platform/Linux/bluez/AdapterIterator.cpp index 5510ceae996ce2..ced172446882b8 100644 --- a/src/platform/Linux/bluez/AdapterIterator.cpp +++ b/src/platform/Linux/bluez/AdapterIterator.cpp @@ -26,69 +26,38 @@ namespace chip { namespace DeviceLayer { namespace Internal { -AdapterIterator::~AdapterIterator() -{ - if (mManager != nullptr) - { - g_object_unref(mManager); - } - - if (mObjectList != nullptr) - { - g_list_free_full(mObjectList, g_object_unref); - } -} - -CHIP_ERROR AdapterIterator::Initialize(AdapterIterator * self) +CHIP_ERROR AdapterIterator::Initialize() { // When creating D-Bus proxy object, the thread default context must be initialized. Otherwise, // all D-Bus signals will be delivered to the GLib global default main context. VerifyOrDie(g_main_context_get_thread_default() != nullptr); - CHIP_ERROR err = CHIP_NO_ERROR; GAutoPtr error; - - self->mManager = g_dbus_object_manager_client_new_for_bus_sync( + mManager.reset(g_dbus_object_manager_client_new_for_bus_sync( G_BUS_TYPE_SYSTEM, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, BLUEZ_INTERFACE, "/", bluez_object_manager_client_get_proxy_type, nullptr /* unused user data in the Proxy Type Func */, - nullptr /* destroy notify */, nullptr /* cancellable */, &error.GetReceiver()); + nullptr /* destroy notify */, nullptr /* cancellable */, &error.GetReceiver())); - VerifyOrExit(self->mManager != nullptr, ChipLogError(DeviceLayer, "Failed to get DBUS object manager for listing adapters."); - err = CHIP_ERROR_INTERNAL); + VerifyOrReturnError(mManager, CHIP_ERROR_INTERNAL, + ChipLogError(DeviceLayer, "Failed to get D-Bus object manager for listing adapters: %s", error->message)); - self->mObjectList = g_dbus_object_manager_get_objects(self->mManager); - self->mCurrentListItem = self->mObjectList; + mObjectList.Init(mManager.get()); + mIterator = mObjectList.begin(); -exit: - if (error != nullptr) - { - ChipLogError(DeviceLayer, "DBus error: %s", error->message); - } - - return err; + return CHIP_NO_ERROR; } bool AdapterIterator::Advance() { - if (mCurrentListItem == nullptr) + for (; mIterator != BluezObjectList::end(); ++mIterator) { - return false; - } - - while (mCurrentListItem != nullptr) - { - BluezAdapter1 * adapter = bluez_object_get_adapter1(BLUEZ_OBJECT(mCurrentListItem->data)); - if (adapter == nullptr) + BluezAdapter1 * adapter = bluez_object_get_adapter1(&(*mIterator)); + if (adapter != nullptr) { - mCurrentListItem = mCurrentListItem->next; - continue; + mCurrentAdapter.reset(adapter); + ++mIterator; + return true; } - - mCurrentAdapter.reset(adapter); - - mCurrentListItem = mCurrentListItem->next; - - return true; } return false; @@ -112,9 +81,10 @@ uint32_t AdapterIterator::GetIndex() const bool AdapterIterator::Next() { - if (mManager == nullptr) + if (!mManager) { - CHIP_ERROR err = PlatformMgrImpl().GLibMatterContextInvokeSync(Initialize, this); + CHIP_ERROR err = PlatformMgrImpl().GLibMatterContextInvokeSync( + +[](AdapterIterator * self) { return self->Initialize(); }, this); VerifyOrReturnError(err == CHIP_NO_ERROR, false, ChipLogError(DeviceLayer, "Failed to initialize adapter iterator")); } diff --git a/src/platform/Linux/bluez/AdapterIterator.h b/src/platform/Linux/bluez/AdapterIterator.h index 0d44074889773b..38af64f7ecc21c 100644 --- a/src/platform/Linux/bluez/AdapterIterator.h +++ b/src/platform/Linux/bluez/AdapterIterator.h @@ -17,13 +17,15 @@ #pragma once -#include +#include #include #include +#include #include +#include "BluezObjectList.h" #include "Types.h" namespace chip { @@ -46,8 +48,6 @@ namespace Internal { class AdapterIterator { public: - ~AdapterIterator(); - /// Moves to the next DBUS interface. /// /// MUST be called before any of the 'current value' methods are @@ -65,7 +65,7 @@ class AdapterIterator private: /// Sets up the DBUS manager and loads the list - static CHIP_ERROR Initialize(AdapterIterator * self); + CHIP_ERROR Initialize(); /// Loads the next value in the list. /// @@ -73,9 +73,9 @@ class AdapterIterator /// iterate through. bool Advance(); - GDBusObjectManager * mManager = nullptr; // DBus connection - GList * mObjectList = nullptr; // listing of objects on the bus - GList * mCurrentListItem = nullptr; // current item viewed in the list + GAutoPtr mManager; + BluezObjectList mObjectList; + BluezObjectIterator mIterator; // Data valid only if Next() returns true GAutoPtr mCurrentAdapter; }; diff --git a/src/platform/Linux/bluez/BluezConnection.cpp b/src/platform/Linux/bluez/BluezConnection.cpp index 88595bd86c720c..dd4f9b108e2eb8 100644 --- a/src/platform/Linux/bluez/BluezConnection.cpp +++ b/src/platform/Linux/bluez/BluezConnection.cpp @@ -34,6 +34,7 @@ #include #include "BluezEndpoint.h" +#include "BluezObjectList.h" #include "Types.h" namespace chip { @@ -95,10 +96,6 @@ BluezConnection::ConnectionDataBundle::ConnectionDataBundle(const BluezConnectio CHIP_ERROR BluezConnection::Init(const BluezEndpoint & aEndpoint) { - // populate the service and the characteristics - GList * objects = nullptr; - GList * l; - if (!aEndpoint.mIsCentral) { mpService = reinterpret_cast(g_object_ref(aEndpoint.mpService)); @@ -107,11 +104,9 @@ CHIP_ERROR BluezConnection::Init(const BluezEndpoint & aEndpoint) } else { - objects = g_dbus_object_manager_get_objects(aEndpoint.mpObjMgr); - - for (l = objects; l != nullptr; l = l->next) + for (BluezObject & object : BluezObjectList(aEndpoint.mpObjMgr)) { - BluezGattService1 * service = bluez_object_get_gatt_service1(BLUEZ_OBJECT(l->data)); + BluezGattService1 * service = bluez_object_get_gatt_service1(&object); if (service != nullptr) { if ((BluezIsServiceOnDevice(service, mpDevice)) == TRUE && @@ -126,9 +121,9 @@ CHIP_ERROR BluezConnection::Init(const BluezEndpoint & aEndpoint) VerifyOrExit(mpService != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL service in %s", __func__)); - for (l = objects; l != nullptr; l = l->next) + for (BluezObject & object : BluezObjectList(aEndpoint.mpObjMgr)) { - BluezGattCharacteristic1 * char1 = bluez_object_get_gatt_characteristic1(BLUEZ_OBJECT(l->data)); + BluezGattCharacteristic1 * char1 = bluez_object_get_gatt_characteristic1(&object); if (char1 != nullptr) { if ((BluezIsCharOnService(char1, mpService) == TRUE) && @@ -164,8 +159,6 @@ CHIP_ERROR BluezConnection::Init(const BluezEndpoint & aEndpoint) } exit: - if (objects != nullptr) - g_list_free_full(objects, g_object_unref); return CHIP_NO_ERROR; } diff --git a/src/platform/Linux/bluez/BluezEndpoint.cpp b/src/platform/Linux/bluez/BluezEndpoint.cpp index 8d37b1363e84ad..fe7a5a25171f73 100644 --- a/src/platform/Linux/bluez/BluezEndpoint.cpp +++ b/src/platform/Linux/bluez/BluezEndpoint.cpp @@ -72,6 +72,7 @@ #include #include "BluezConnection.h" +#include "BluezObjectList.h" #include "Types.h" namespace chip { @@ -420,15 +421,14 @@ BluezGattService1 * BluezEndpoint::CreateGattService(const char * aUUID) return service; } -void BluezEndpoint::SetupAdapter() +CHIP_ERROR BluezEndpoint::SetupAdapter() { char expectedPath[32]; snprintf(expectedPath, sizeof(expectedPath), BLUEZ_PATH "/hci%u", mAdapterId); - GList * objects = g_dbus_object_manager_get_objects(mpObjMgr); - for (auto l = objects; l != nullptr && mAdapter.get() == nullptr; l = l->next) + for (BluezObject & object : BluezObjectList(mpObjMgr)) { - GAutoPtr adapter(bluez_object_get_adapter1(BLUEZ_OBJECT(l->data))); + GAutoPtr adapter(bluez_object_get_adapter1(&object)); if (adapter.get() != nullptr) { if (mpAdapterAddr == nullptr) // no adapter address provided, bind to the hci indicated by nodeid @@ -450,7 +450,7 @@ void BluezEndpoint::SetupAdapter() } } - VerifyOrExit(mAdapter.get() != nullptr, ChipLogError(DeviceLayer, "FAIL: NULL mAdapter in %s", __func__)); + VerifyOrReturnError(mAdapter, CHIP_ERROR_INTERNAL, ChipLogError(DeviceLayer, "FAIL: NULL mAdapter in %s", __func__)); bluez_adapter1_set_powered(mAdapter.get(), TRUE); @@ -459,8 +459,7 @@ void BluezEndpoint::SetupAdapter() // and the flag is necessary to force using LE transport. bluez_adapter1_set_discoverable(mAdapter.get(), FALSE); -exit: - g_list_free_full(objects, g_object_unref); + return CHIP_NO_ERROR; } BluezConnection * BluezEndpoint::GetBluezConnection(const char * aPath) diff --git a/src/platform/Linux/bluez/BluezEndpoint.h b/src/platform/Linux/bluez/BluezEndpoint.h index d35cebdf6a6cfe..687f0002fe64b0 100644 --- a/src/platform/Linux/bluez/BluezEndpoint.h +++ b/src/platform/Linux/bluez/BluezEndpoint.h @@ -85,7 +85,7 @@ class BluezEndpoint private: CHIP_ERROR StartupEndpointBindings(); - void SetupAdapter(); + CHIP_ERROR SetupAdapter(); void SetupGattServer(GDBusConnection * aConn); void SetupGattService(); diff --git a/src/platform/Linux/bluez/BluezObjectIterator.h b/src/platform/Linux/bluez/BluezObjectIterator.h index 0e09cbcf217fc0..6b177acd03027b 100644 --- a/src/platform/Linux/bluez/BluezObjectIterator.h +++ b/src/platform/Linux/bluez/BluezObjectIterator.h @@ -17,6 +17,8 @@ #pragma once +#include + #include #include diff --git a/src/platform/Linux/bluez/BluezObjectList.h b/src/platform/Linux/bluez/BluezObjectList.h index ee7f6c001514e6..5831f07a3c6c85 100644 --- a/src/platform/Linux/bluez/BluezObjectList.h +++ b/src/platform/Linux/bluez/BluezObjectList.h @@ -35,27 +35,26 @@ namespace Internal { class BluezObjectList { public: - explicit BluezObjectList(GDBusObjectManager * manager) { Initialize(manager); } + BluezObjectList() = default; + explicit BluezObjectList(GDBusObjectManager * manager) { Init(manager); } - ~BluezObjectList() { g_list_free_full(mObjectList, g_object_unref); } - - BluezObjectIterator begin() const { return BluezObjectIterator(mObjectList); } - BluezObjectIterator end() const { return BluezObjectIterator(); } - -protected: - BluezObjectList() {} - - void Initialize(GDBusObjectManager * manager) + ~BluezObjectList() { - if (manager == nullptr) - { - ChipLogError(DeviceLayer, "Manager is NULL in %s", __func__); - return; - } + if (mObjectList != nullptr) + g_list_free_full(mObjectList, g_object_unref); + } + CHIP_ERROR Init(GDBusObjectManager * manager) + { + VerifyOrReturnError(manager != nullptr, CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(DeviceLayer, "Manager is NULL in %s", __func__)); mObjectList = g_dbus_object_manager_get_objects(manager); + return CHIP_NO_ERROR; } + BluezObjectIterator begin() const { return BluezObjectIterator(mObjectList); } + static BluezObjectIterator end() { return BluezObjectIterator(); } + private: GList * mObjectList = nullptr; }; From 4601714eac222d0ef63944f194ce2164be230e6b Mon Sep 17 00:00:00 2001 From: Wang Qixiang <43193572+wqx6@users.noreply.github.com> Date: Fri, 8 Mar 2024 22:57:46 +0800 Subject: [PATCH 12/76] Add checks for mOTInst in GenericThreadStackManagerImpl_OpenThread (#32482) * Add checks for mOTInst in GenericThreadStackManagerImpl_OpenThread * review changes --- ...nericThreadStackManagerImpl_OpenThread.hpp | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp index 21c44dd00279e3..707e1f710743ec 100644 --- a/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp +++ b/src/platform/OpenThread/GenericThreadStackManagerImpl_OpenThread.hpp @@ -149,6 +149,7 @@ void GenericThreadStackManagerImpl_OpenThread::_ProcessThreadActivity template bool GenericThreadStackManagerImpl_OpenThread::_HaveRouteToAddress(const Inet::IPAddress & destAddr) { + VerifyOrReturnValue(mOTInst, false); bool res = false; // Lock OpenThread @@ -233,6 +234,7 @@ void GenericThreadStackManagerImpl_OpenThread::_OnPlatformEvent(const template bool GenericThreadStackManagerImpl_OpenThread::_IsThreadEnabled(void) { + VerifyOrReturnValue(mOTInst, false); otDeviceRole curRole; Impl()->LockThreadStack(); @@ -245,6 +247,7 @@ bool GenericThreadStackManagerImpl_OpenThread::_IsThreadEnabled(void) template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetThreadEnabled(bool val) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); otError otErr = OT_ERROR_NONE; Impl()->LockThreadStack(); @@ -279,6 +282,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetThreadEnable template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetThreadProvision(ByteSpan netInfo) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); otError otErr = OT_ERROR_FAILED; otOperationalDatasetTlvs tlvs; @@ -305,6 +309,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetThreadProvis template bool GenericThreadStackManagerImpl_OpenThread::_IsThreadProvisioned(void) { + VerifyOrReturnValue(mOTInst, false); bool provisioned; Impl()->LockThreadStack(); @@ -317,6 +322,7 @@ bool GenericThreadStackManagerImpl_OpenThread::_IsThreadProvisioned(v template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetThreadProvision(Thread::OperationalDataset & dataset) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); VerifyOrReturnError(Impl()->IsThreadProvisioned(), CHIP_ERROR_INCORRECT_STATE); otOperationalDatasetTlvs datasetTlv; @@ -336,6 +342,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetThreadProvis template bool GenericThreadStackManagerImpl_OpenThread::_IsThreadAttached(void) { + VerifyOrReturnValue(mOTInst, false); otDeviceRole curRole; Impl()->LockThreadStack(); @@ -380,6 +387,7 @@ template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_StartThreadScan(NetworkCommissioning::ThreadDriver::ScanCallback * callback) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; #if CHIP_CONFIG_ENABLE_ICD_SERVER otLinkModeConfig linkMode; @@ -488,6 +496,7 @@ void GenericThreadStackManagerImpl_OpenThread::_OnNetworkScanFinished template ConnectivityManager::ThreadDeviceType GenericThreadStackManagerImpl_OpenThread::_GetThreadDeviceType(void) { + VerifyOrReturnValue(mOTInst, ConnectivityManager::kThreadDeviceType_NotSupported); ConnectivityManager::ThreadDeviceType deviceType; Impl()->LockThreadStack(); @@ -524,6 +533,7 @@ template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetThreadDeviceType(ConnectivityManager::ThreadDeviceType deviceType) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR err = CHIP_NO_ERROR; otLinkModeConfig linkMode; @@ -612,6 +622,7 @@ GenericThreadStackManagerImpl_OpenThread::_SetThreadDeviceType(Connec template bool GenericThreadStackManagerImpl_OpenThread::_HaveMeshConnectivity(void) { + VerifyOrReturnValue(mOTInst, false); bool res; otDeviceRole curRole; @@ -660,6 +671,7 @@ bool GenericThreadStackManagerImpl_OpenThread::_HaveMeshConnectivity( template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThreadStatsCounters(void) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR err = CHIP_NO_ERROR; otError otErr; otOperationalDataset activeDataset; @@ -754,6 +766,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThread template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThreadTopologyMinimal(void) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR err = CHIP_NO_ERROR; #if CHIP_PROGRESS_LOGGING @@ -822,6 +835,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThread template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThreadTopologyFull() { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR err = CHIP_NO_ERROR; #if CHIP_PROGRESS_LOGGING @@ -991,6 +1005,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetAndLogThread template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetPrimary802154MACAddress(uint8_t * buf) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); const otExtAddress * extendedAddr = otLinkGetExtendedAddress(mOTInst); memcpy(buf, extendedAddr, sizeof(otExtAddress)); return CHIP_NO_ERROR; @@ -999,6 +1014,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetPrimary80215 template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetExternalIPv6Address(chip::Inet::IPAddress & addr) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); const otNetifAddress * otAddresses = otIp6GetUnicastAddresses(mOTInst); // Look only for the global unicast addresses, not internally assigned by Thread. @@ -1034,6 +1050,7 @@ void GenericThreadStackManagerImpl_OpenThread::_ResetThreadNetworkDia template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_GetPollPeriod(uint32_t & buf) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); Impl()->LockThreadStack(); buf = otLinkGetPollPeriod(mOTInst); Impl()->UnlockThreadStack(); @@ -1121,6 +1138,7 @@ bool GenericThreadStackManagerImpl_OpenThread::IsThreadInterfaceUpNoL template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetPollingInterval(System::Clock::Milliseconds32 pollingInterval) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR err = CHIP_NO_ERROR; Impl()->LockThreadStack(); @@ -1173,6 +1191,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetPollingInter template void GenericThreadStackManagerImpl_OpenThread::_ErasePersistentInfo(void) { + VerifyOrReturn(mOTInst); ChipLogProgress(DeviceLayer, "Erasing Thread persistent info..."); Impl()->LockThreadStack(); otThreadSetEnabled(mOTInst, false); @@ -1205,6 +1224,7 @@ void GenericThreadStackManagerImpl_OpenThread::OnJoinerComplete(otErr template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_JoinerStart(void) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; Impl()->LockThreadStack(); @@ -1254,6 +1274,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_JoinerStart(voi template void GenericThreadStackManagerImpl_OpenThread::_UpdateNetworkStatus() { + VerifyOrReturn(mOTInst); // Thread is not enabled, then we are not trying to connect to the network. VerifyOrReturn(ThreadStackMgrImpl().IsThreadEnabled() && mpStatusChangeCallback != nullptr); @@ -1636,6 +1657,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_RemoveInvalidSr template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_ClearAllSrpHostAndServices() { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; Impl()->LockThreadStack(); if (!mIsSrpClearAllRequested) @@ -1684,6 +1706,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_SetupSrpHost(co template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_ClearSrpHost(const char * aHostName) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; Impl()->LockThreadStack(); @@ -1798,6 +1821,7 @@ CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::FromOtDnsRespons template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::ResolveAddress(intptr_t context, otDnsAddressCallback callback) { + VerifyOrReturnError(ThreadStackMgrImpl().OTInstance(), CHIP_ERROR_INCORRECT_STATE); DnsResult * dnsResult = reinterpret_cast(context); ThreadStackMgrImpl().LockThreadStack(); @@ -1952,6 +1976,7 @@ template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_DnsBrowse(const char * aServiceName, DnsBrowseCallback aCallback, void * aContext) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; Impl()->LockThreadStack(); @@ -2062,6 +2087,7 @@ template CHIP_ERROR GenericThreadStackManagerImpl_OpenThread::_DnsResolve(const char * aServiceName, const char * aInstanceName, DnsResolveCallback aCallback, void * aContext) { + VerifyOrReturnError(mOTInst, CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR error = CHIP_NO_ERROR; Impl()->LockThreadStack(); From b5705ea7457cf6efd4a8776299b52d72b0f803ab Mon Sep 17 00:00:00 2001 From: sarthak shaha <130495524+Sarthak-Shaha@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:58:11 -0500 Subject: [PATCH 13/76] enabled required identify type attribute in thermostat.zap (#32492) --- .../thermostat-common/thermostat.matter | 1 + .../thermostat/thermostat-common/thermostat.zap | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/thermostat/thermostat-common/thermostat.matter b/examples/thermostat/thermostat-common/thermostat.matter index a16168f55692b1..60427f9ac0f854 100644 --- a/examples/thermostat/thermostat-common/thermostat.matter +++ b/examples/thermostat/thermostat-common/thermostat.matter @@ -2097,6 +2097,7 @@ endpoint 0 { server cluster Identify { ram attribute identifyTime default = 0x0000; + ram attribute identifyType default = 0x00; ram attribute featureMap default = 0; ram attribute clusterRevision default = 4; diff --git a/examples/thermostat/thermostat-common/thermostat.zap b/examples/thermostat/thermostat-common/thermostat.zap index e17e88b6578ad0..64c87bce7e15c6 100644 --- a/examples/thermostat/thermostat-common/thermostat.zap +++ b/examples/thermostat/thermostat-common/thermostat.zap @@ -94,6 +94,22 @@ "maxInterval": 65344, "reportableChange": 0 }, + { + "name": "IdentifyType", + "code": 1, + "mfgCode": null, + "side": "server", + "type": "IdentifyTypeEnum", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0x00", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "FeatureMap", "code": 65532, From e6e1c3a8379c95503684477af5cbf1327d49a5a4 Mon Sep 17 00:00:00 2001 From: jamesharrow <93921463+jamesharrow@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:17:55 -0800 Subject: [PATCH 14/76] Fixes #32429 Added PICS guard to steps in TC_EEM_2_1 (#32464) * Fixes #32429 Added PICS guard to steps in TC_EEM_2_1 * Changed to use a fixed string for the PICS EEM.S * Corrected top level script PICS feature names (removed bracketed human helpfulness) * Corrected description in EEVSE 2.5 and added PICS condition for StartDiagnostics command support * Added PICS guard to TC_EPM_2_2.py for ActivePower and Voltage attribute readings --- src/python_testing/TC_EEM_2_1.py | 25 +++++++++++++++---------- src/python_testing/TC_EEM_2_2.py | 2 +- src/python_testing/TC_EEM_2_3.py | 2 +- src/python_testing/TC_EEM_2_4.py | 2 +- src/python_testing/TC_EEM_2_5.py | 2 +- src/python_testing/TC_EEVSE_2_4.py | 2 +- src/python_testing/TC_EEVSE_2_5.py | 6 +++--- src/python_testing/TC_EPM_2_2.py | 20 +++++++++++++------- 8 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/python_testing/TC_EEM_2_1.py b/src/python_testing/TC_EEM_2_1.py index 909380401e49d6..27246b26c272a2 100644 --- a/src/python_testing/TC_EEM_2_1.py +++ b/src/python_testing/TC_EEM_2_1.py @@ -62,24 +62,29 @@ async def test_TC_EEM_2_1(self): "Accuracy measurementType must be ElectricalEnergy") self.step("3") - cumulativeEnergyImported = await self.read_eem_attribute_expect_success("CumulativeEnergyImported") - logger.info(f"Rx'd CumulativeEnergyImported: {cumulativeEnergyImported}") + if self.pics_guard(self.check_pics("EEM.S.A0001")): + cumulativeEnergyImported = await self.read_eem_attribute_expect_success("CumulativeEnergyImported") + logger.info(f"Rx'd CumulativeEnergyImported: {cumulativeEnergyImported}") self.step("4") - cumulativeEnergyExported = await self.read_eem_attribute_expect_success("CumulativeEnergyExported") - logger.info(f"Rx'd CumulativeEnergyExported: {cumulativeEnergyExported}") + if self.pics_guard(self.check_pics("EEM.S.A0002")): + cumulativeEnergyExported = await self.read_eem_attribute_expect_success("CumulativeEnergyExported") + logger.info(f"Rx'd CumulativeEnergyExported: {cumulativeEnergyExported}") self.step("5") - periodicEnergyImported = await self.read_eem_attribute_expect_success("PeriodicEnergyImported") - logger.info(f"Rx'd PeriodicEnergyImported: {periodicEnergyImported}") + if self.pics_guard(self.check_pics("EEM.S.A0003")): + periodicEnergyImported = await self.read_eem_attribute_expect_success("PeriodicEnergyImported") + logger.info(f"Rx'd PeriodicEnergyImported: {periodicEnergyImported}") self.step("6") - periodicEnergyExported = await self.read_eem_attribute_expect_success("PeriodicEnergyExported") - logger.info(f"Rx'd PeriodicEnergyExported: {periodicEnergyExported}") + if self.pics_guard(self.check_pics("EEM.S.A0004")): + periodicEnergyExported = await self.read_eem_attribute_expect_success("PeriodicEnergyExported") + logger.info(f"Rx'd PeriodicEnergyExported: {periodicEnergyExported}") self.step("7") - cumulativeEnergyReset = await self.read_eem_attribute_expect_success("CumulativeEnergyReset") - logger.info(f"Rx'd CumulativeEnergyReset: {cumulativeEnergyReset}") + if self.pics_guard(self.check_pics("EEM.S.A0005")): + cumulativeEnergyReset = await self.read_eem_attribute_expect_success("CumulativeEnergyReset") + logger.info(f"Rx'd CumulativeEnergyReset: {cumulativeEnergyReset}") if __name__ == "__main__": diff --git a/src/python_testing/TC_EEM_2_2.py b/src/python_testing/TC_EEM_2_2.py index 58e651de9425eb..3f3cb5b67cad8b 100644 --- a/src/python_testing/TC_EEM_2_2.py +++ b/src/python_testing/TC_EEM_2_2.py @@ -30,7 +30,7 @@ def desc_TC_EEM_2_2(self) -> str: def pics_TC_EEM_2_2(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" - return ["EEM.S", "EEM.S.F02(CUME)", "EEM.S.F00(IMPE)"] + return ["EEM.S", "EEM.S.F02", "EEM.S.F00"] def steps_TC_EEM_2_2(self) -> list[TestStep]: steps = [ diff --git a/src/python_testing/TC_EEM_2_3.py b/src/python_testing/TC_EEM_2_3.py index 1183d9591bcd5e..4691043502dfee 100644 --- a/src/python_testing/TC_EEM_2_3.py +++ b/src/python_testing/TC_EEM_2_3.py @@ -30,7 +30,7 @@ def desc_TC_EEM_2_3(self) -> str: def pics_TC_EEM_2_3(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" - return ["EEM.S", "EEM.S.F02(CUME)", "EEM.S.F01(EXPE)"] + return ["EEM.S", "EEM.S.F02", "EEM.S.F01"] def steps_TC_EEM_2_3(self) -> list[TestStep]: steps = [ diff --git a/src/python_testing/TC_EEM_2_4.py b/src/python_testing/TC_EEM_2_4.py index b219e3c4e770a5..3b8dd346d2a36c 100644 --- a/src/python_testing/TC_EEM_2_4.py +++ b/src/python_testing/TC_EEM_2_4.py @@ -30,7 +30,7 @@ def desc_TC_EEM_2_4(self) -> str: def pics_TC_EEM_2_4(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" - return ["EEM.S", "EEM.S.F03(PERE)", "EEM.S.F00(IMPE)"] + return ["EEM.S", "EEM.S.F03", "EEM.S.F00"] def steps_TC_EEM_2_4(self) -> list[TestStep]: steps = [ diff --git a/src/python_testing/TC_EEM_2_5.py b/src/python_testing/TC_EEM_2_5.py index 945a97f89da8aa..871fb8e5536743 100644 --- a/src/python_testing/TC_EEM_2_5.py +++ b/src/python_testing/TC_EEM_2_5.py @@ -30,7 +30,7 @@ def desc_TC_EEM_2_5(self) -> str: def pics_TC_EEM_2_5(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" - return ["EEM.S", "EEM.S.F03(PERE)", "EEM.S.F01(EXPE)"] + return ["EEM.S", "EEM.S.F03", "EEM.S.F01"] def steps_TC_EEM_2_5(self) -> list[TestStep]: steps = [ diff --git a/src/python_testing/TC_EEVSE_2_4.py b/src/python_testing/TC_EEVSE_2_4.py index 9ccad2fdb692db..959932da1fb6cb 100644 --- a/src/python_testing/TC_EEVSE_2_4.py +++ b/src/python_testing/TC_EEVSE_2_4.py @@ -30,7 +30,7 @@ class TC_EEVSE_2_4(MatterBaseTest, EEVSEBaseTestHelper): def desc_TC_EEVSE_2_4(self) -> str: """Returns a description of this test""" - return "5.1.XXX. [TC-EEVSE-2.4] Fault test functionality with DUT as Server" + return "5.1.5. [TC-EEVSE-2.4] Fault test functionality with DUT as Server" def pics_TC_EEVSE_2_4(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" diff --git a/src/python_testing/TC_EEVSE_2_5.py b/src/python_testing/TC_EEVSE_2_5.py index 00150f263c7576..7ab6efddc40366 100644 --- a/src/python_testing/TC_EEVSE_2_5.py +++ b/src/python_testing/TC_EEVSE_2_5.py @@ -30,12 +30,12 @@ class TC_EEVSE_2_5(MatterBaseTest, EEVSEBaseTestHelper): def desc_TC_EEVSE_2_5(self) -> str: """Returns a description of this test""" - return "5.1.XXX. [TC-EEVSE-2.4] Fault test functionality with DUT as Server" + return "5.1.6. [TC-EEVSE-2.5] Optional diagnostics functionality with DUT as Server" def pics_TC_EEVSE_2_5(self): """ This function returns a list of PICS for this test case that must be True for the test to be run""" - # In this case - there is no feature flags needed to run this test case - return ["EEVSE.S"] + # In this case - we need the EVSE to support the StartDiagnostics command + return ["EEVSE.S", "EEVSE.S.C04.Rsp"] def steps_TC_EEVSE_2_5(self) -> list[TestStep]: steps = [ diff --git a/src/python_testing/TC_EPM_2_2.py b/src/python_testing/TC_EPM_2_2.py index eb6f6081f6d690..756f62f626a0b2 100644 --- a/src/python_testing/TC_EPM_2_2.py +++ b/src/python_testing/TC_EPM_2_2.py @@ -71,31 +71,37 @@ async def test_TC_EPM_2_2(self): time.sleep(3) self.step("4a") + # Active power is Mandatory active_power = await self.check_epm_attribute_in_range("ActivePower", 980000, 1020000) # 1kW +/- 20W self.step("4b") - active_current = await self.check_epm_attribute_in_range("ActiveCurrent", 3848, 4848) # 4.348 A +/- 500mA + if self.pics_guard(self.check_pics("EPM.S.A0005")): + active_current = await self.check_epm_attribute_in_range("ActiveCurrent", 3848, 4848) # 4.348 A +/- 500mA self.step("4c") - voltage = await self.check_epm_attribute_in_range("Voltage", 229000, 231000) # 230V +/- 1V + if self.pics_guard(self.check_pics("EPM.S.A0004")): + voltage = await self.check_epm_attribute_in_range("Voltage", 229000, 231000) # 230V +/- 1V self.step("5") # After 3 seconds... time.sleep(3) self.step("5a") + # Active power is Mandatory active_power2 = await self.check_epm_attribute_in_range("ActivePower", 980000, 1020000) # 1kW +/- 20W asserts.assert_not_equal(active_power, active_power2, f"Expected ActivePower readings to have changed {active_power}, {active_power2}") self.step("5b") - active_current2 = await self.check_epm_attribute_in_range("ActiveCurrent", 3848, 4848) # 4.348 A +/- 500mA - asserts.assert_not_equal(active_current, active_current2, - f"Expected ActiveCurrent readings to have changed {active_current}, {active_current2}") + if self.pics_guard(self.check_pics("EPM.S.A0005")): + active_current2 = await self.check_epm_attribute_in_range("ActiveCurrent", 3848, 4848) # 4.348 A +/- 500mA + asserts.assert_not_equal(active_current, active_current2, + f"Expected ActiveCurrent readings to have changed {active_current}, {active_current2}") self.step("5c") - voltage2 = await self.check_epm_attribute_in_range("Voltage", 229000, 231000) # 230V +/- 1V - asserts.assert_not_equal(voltage, voltage2, f"Expected Voltage readings to have changed {voltage}, {voltage2}") + if self.pics_guard(self.check_pics("EPM.S.A0004")): + voltage2 = await self.check_epm_attribute_in_range("Voltage", 229000, 231000) # 230V +/- 1V + asserts.assert_not_equal(voltage, voltage2, f"Expected Voltage readings to have changed {voltage}, {voltage2}") self.step("6") await self.send_test_event_trigger_stop_fake_readings() From d87e006519109f130a6be6bf3f3b1e0fd7947de6 Mon Sep 17 00:00:00 2001 From: Jakub Latusek Date: Fri, 8 Mar 2024 20:08:29 +0100 Subject: [PATCH 15/76] Add pigweed support for tizen (#32503) --- src/test_driver/tizen/.gn | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test_driver/tizen/.gn b/src/test_driver/tizen/.gn index fa6b2fc9621e28..65992f8720b0c5 100644 --- a/src/test_driver/tizen/.gn +++ b/src/test_driver/tizen/.gn @@ -13,6 +13,8 @@ # limitations under the License. import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") +import("//build_overrides/pigweed.gni") # The location of the build configuration file. buildconfig = "${build_root}/config/BUILDCONFIG.gn" @@ -22,4 +24,16 @@ check_system_includes = true default_args = { target_os = "tizen" + + pw_sys_io_BACKEND = "$dir_pw_sys_io_stdio" + pw_assert_BACKEND = "$dir_pw_assert_log" + pw_log_BACKEND = "$dir_pw_log_basic" + + pw_unit_test_BACKEND = "$dir_pw_unit_test:light" + + # TODO: Make sure only unit tests link against this + pw_build_LINK_DEPS = [ + "$dir_pw_assert:impl", + "$dir_pw_log:impl", + ] } From f7a9b5931bb733fd6f07bd5ae491ec0b841be292 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Fri, 8 Mar 2024 17:46:57 -0500 Subject: [PATCH 16/76] Remove provisional markings on Darwin from Fan Control bits that shipped in 1.2. (#32514) --- .../CHIP/templates/availability.yaml | 46 ++++++++++++------- .../CHIP/zap-generated/MTRBaseClusters.h | 28 +++++------ .../CHIP/zap-generated/MTRClusterConstants.h | 4 +- .../CHIP/zap-generated/MTRClusters.h | 8 ++-- .../zap-generated/MTRCommandPayloadsObjc.h | 8 ++-- .../zap-generated/cluster/Commands.h | 23 ---------- 6 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/darwin/Framework/CHIP/templates/availability.yaml b/src/darwin/Framework/CHIP/templates/availability.yaml index 91191285701e33..b745709fa434cf 100644 --- a/src/darwin/Framework/CHIP/templates/availability.yaml +++ b/src/darwin/Framework/CHIP/templates/availability.yaml @@ -7488,9 +7488,6 @@ Scenes: # New scenes bits not stable yet. - SceneTableSize - FanControl: - # New Fan Control bits not stable yet. - - AirflowDirection RVCCleanMode: # People are trying to deprecate this one - OnMode @@ -7498,9 +7495,6 @@ # People are trying to deprecate this one - OnMode commands: - FanControl: - # Not stable yet - - Step DoorLock: # Not stable yet - UnboltDoor @@ -7508,11 +7502,6 @@ # Disallowed in the spec, but present in our XML? - Start - Stop - enums: - FanControl: - # Not stable yet. - - StepDirectionEnum - - AirflowDirectionEnum enum values: DoorLock: # Not stable yet @@ -7544,11 +7533,6 @@ # here. Feature: - Unbolt - FanControl: - # Not stable yet - Feature: - - Step - - AirflowDirection RVCRunMode: Feature: # People are trying to deprecate this one @@ -8543,6 +8527,36 @@ - release: "Future" versions: "future" + introduced: + attributes: + FanControl: + - AirflowDirection + commands: + FanControl: + - Step + command fields: + FanControl: + Step: + - direction + - wrap + - lowestOff + enums: + FanControl: + - StepDirectionEnum + - AirflowDirectionEnum + enum values: + FanControl: + StepDirectionEnum: + - Increase + - Decrease + AirflowDirectionEnum: + - Forward + - Reverse + bitmap values: + FanControl: + Feature: + - Step + - AirflowDirection provisional: clusters: # Targeting Spring 2024 Matter release diff --git a/src/darwin/Framework/CHIP/zap-generated/MTRBaseClusters.h b/src/darwin/Framework/CHIP/zap-generated/MTRBaseClusters.h index 6232b6424b8eb1..d28558722a280f 100644 --- a/src/darwin/Framework/CHIP/zap-generated/MTRBaseClusters.h +++ b/src/darwin/Framework/CHIP/zap-generated/MTRBaseClusters.h @@ -10202,7 +10202,7 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) * * The Step command speeds up or slows down the fan, in steps. */ -- (void)stepWithParams:(MTRFanControlClusterStepParams *)params completion:(MTRStatusCompletion)completion MTR_PROVISIONALLY_AVAILABLE; +- (void)stepWithParams:(MTRFanControlClusterStepParams *)params completion:(MTRStatusCompletion)completion MTR_NEWLY_AVAILABLE; - (void)readAttributeFanModeWithCompletion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); - (void)writeAttributeFanModeWithValue:(NSNumber * _Nonnull)value completion:(MTRStatusCompletion)completion MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); @@ -10282,13 +10282,13 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) reportHandler:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))reportHandler MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); + (void)readAttributeWindSettingWithClusterStateCache:(MTRClusterStateCacheContainer *)clusterStateCacheContainer endpoint:(NSNumber *)endpoint queue:(dispatch_queue_t)queue completion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); -- (void)readAttributeAirflowDirectionWithCompletion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_PROVISIONALLY_AVAILABLE; -- (void)writeAttributeAirflowDirectionWithValue:(NSNumber * _Nonnull)value completion:(MTRStatusCompletion)completion MTR_PROVISIONALLY_AVAILABLE; -- (void)writeAttributeAirflowDirectionWithValue:(NSNumber * _Nonnull)value params:(MTRWriteParams * _Nullable)params completion:(MTRStatusCompletion)completion MTR_PROVISIONALLY_AVAILABLE; +- (void)readAttributeAirflowDirectionWithCompletion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_NEWLY_AVAILABLE; +- (void)writeAttributeAirflowDirectionWithValue:(NSNumber * _Nonnull)value completion:(MTRStatusCompletion)completion MTR_NEWLY_AVAILABLE; +- (void)writeAttributeAirflowDirectionWithValue:(NSNumber * _Nonnull)value params:(MTRWriteParams * _Nullable)params completion:(MTRStatusCompletion)completion MTR_NEWLY_AVAILABLE; - (void)subscribeAttributeAirflowDirectionWithParams:(MTRSubscribeParams *)params subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished - reportHandler:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))reportHandler MTR_PROVISIONALLY_AVAILABLE; -+ (void)readAttributeAirflowDirectionWithClusterStateCache:(MTRClusterStateCacheContainer *)clusterStateCacheContainer endpoint:(NSNumber *)endpoint queue:(dispatch_queue_t)queue completion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_PROVISIONALLY_AVAILABLE; + reportHandler:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))reportHandler MTR_NEWLY_AVAILABLE; ++ (void)readAttributeAirflowDirectionWithClusterStateCache:(MTRClusterStateCacheContainer *)clusterStateCacheContainer endpoint:(NSNumber *)endpoint queue:(dispatch_queue_t)queue completion:(void (^)(NSNumber * _Nullable value, NSError * _Nullable error))completion MTR_NEWLY_AVAILABLE; - (void)readAttributeGeneratedCommandListWithCompletion:(void (^)(NSArray * _Nullable value, NSError * _Nullable error))completion MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); - (void)subscribeAttributeGeneratedCommandListWithParams:(MTRSubscribeParams *)params @@ -18935,9 +18935,9 @@ typedef NS_OPTIONS(uint8_t, MTRThermostatTemperatureSetpointHoldPolicyBitmap) { } MTR_PROVISIONALLY_AVAILABLE; typedef NS_ENUM(uint8_t, MTRFanControlAirflowDirection) { - MTRFanControlAirflowDirectionForward MTR_PROVISIONALLY_AVAILABLE = 0x00, - MTRFanControlAirflowDirectionReverse MTR_PROVISIONALLY_AVAILABLE = 0x01, -} MTR_PROVISIONALLY_AVAILABLE; + MTRFanControlAirflowDirectionForward MTR_NEWLY_AVAILABLE = 0x00, + MTRFanControlAirflowDirectionReverse MTR_NEWLY_AVAILABLE = 0x01, +} MTR_NEWLY_AVAILABLE; typedef NS_ENUM(uint8_t, MTRFanControlFanMode) { MTRFanControlFanModeOff MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) = 0x00, @@ -18980,17 +18980,17 @@ typedef NS_ENUM(uint8_t, MTRFanControlFanModeSequenceType) { } MTR_DEPRECATED("Please use MTRFanControlFanModeSequence", ios(16.1, 17.0), macos(13.0, 14.0), watchos(9.1, 10.0), tvos(16.1, 17.0)); typedef NS_ENUM(uint8_t, MTRFanControlStepDirection) { - MTRFanControlStepDirectionIncrease MTR_PROVISIONALLY_AVAILABLE = 0x00, - MTRFanControlStepDirectionDecrease MTR_PROVISIONALLY_AVAILABLE = 0x01, -} MTR_PROVISIONALLY_AVAILABLE; + MTRFanControlStepDirectionIncrease MTR_NEWLY_AVAILABLE = 0x00, + MTRFanControlStepDirectionDecrease MTR_NEWLY_AVAILABLE = 0x01, +} MTR_NEWLY_AVAILABLE; typedef NS_OPTIONS(uint32_t, MTRFanControlFeature) { MTRFanControlFeatureMultiSpeed MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) = 0x1, MTRFanControlFeatureAuto MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) = 0x2, MTRFanControlFeatureRocking MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) = 0x4, MTRFanControlFeatureWind MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) = 0x8, - MTRFanControlFeatureStep MTR_PROVISIONALLY_AVAILABLE = 0x10, - MTRFanControlFeatureAirflowDirection MTR_PROVISIONALLY_AVAILABLE = 0x20, + MTRFanControlFeatureStep MTR_NEWLY_AVAILABLE = 0x10, + MTRFanControlFeatureAirflowDirection MTR_NEWLY_AVAILABLE = 0x20, } MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); typedef NS_OPTIONS(uint8_t, MTRFanControlRockBitmap) { diff --git a/src/darwin/Framework/CHIP/zap-generated/MTRClusterConstants.h b/src/darwin/Framework/CHIP/zap-generated/MTRClusterConstants.h index a3351bc74d12d1..00d597ee594fd1 100644 --- a/src/darwin/Framework/CHIP/zap-generated/MTRClusterConstants.h +++ b/src/darwin/Framework/CHIP/zap-generated/MTRClusterConstants.h @@ -3510,7 +3510,7 @@ typedef NS_ENUM(uint32_t, MTRAttributeIDType) { MTRAttributeIDTypeClusterFanControlAttributeRockSettingID MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) = 0x00000008, MTRAttributeIDTypeClusterFanControlAttributeWindSupportID MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) = 0x00000009, MTRAttributeIDTypeClusterFanControlAttributeWindSettingID MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) = 0x0000000A, - MTRAttributeIDTypeClusterFanControlAttributeAirflowDirectionID MTR_PROVISIONALLY_AVAILABLE = 0x0000000B, + MTRAttributeIDTypeClusterFanControlAttributeAirflowDirectionID MTR_NEWLY_AVAILABLE = 0x0000000B, MTRAttributeIDTypeClusterFanControlAttributeGeneratedCommandListID MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) = MTRAttributeIDTypeGlobalAttributeGeneratedCommandListID, MTRAttributeIDTypeClusterFanControlAttributeAcceptedCommandListID MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) = MTRAttributeIDTypeGlobalAttributeAcceptedCommandListID, MTRAttributeIDTypeClusterFanControlAttributeEventListID MTR_PROVISIONALLY_AVAILABLE = MTRAttributeIDTypeGlobalAttributeEventListID, @@ -6493,7 +6493,7 @@ typedef NS_ENUM(uint32_t, MTRCommandIDType) { // Cluster FanControl deprecated command id names // Cluster FanControl commands - MTRCommandIDTypeClusterFanControlCommandStepID MTR_PROVISIONALLY_AVAILABLE = 0x00000000, + MTRCommandIDTypeClusterFanControlCommandStepID MTR_NEWLY_AVAILABLE = 0x00000000, // Cluster ColorControl deprecated command id names MTRClusterColorControlCommandMoveToHueID diff --git a/src/darwin/Framework/CHIP/zap-generated/MTRClusters.h b/src/darwin/Framework/CHIP/zap-generated/MTRClusters.h index 7ce97cfe09442f..0805d773d372df 100644 --- a/src/darwin/Framework/CHIP/zap-generated/MTRClusters.h +++ b/src/darwin/Framework/CHIP/zap-generated/MTRClusters.h @@ -4716,7 +4716,7 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) @interface MTRClusterFanControl : MTRGenericCluster -- (void)stepWithParams:(MTRFanControlClusterStepParams *)params expectedValues:(NSArray *> * _Nullable)expectedDataValueDictionaries expectedValueInterval:(NSNumber * _Nullable)expectedValueIntervalMs completion:(MTRStatusCompletion)completion MTR_PROVISIONALLY_AVAILABLE; +- (void)stepWithParams:(MTRFanControlClusterStepParams *)params expectedValues:(NSArray *> * _Nullable)expectedDataValueDictionaries expectedValueInterval:(NSNumber * _Nullable)expectedValueIntervalMs completion:(MTRStatusCompletion)completion MTR_NEWLY_AVAILABLE; - (NSDictionary * _Nullable)readAttributeFanModeWithParams:(MTRReadParams * _Nullable)params MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); - (void)writeAttributeFanModeWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); @@ -4752,9 +4752,9 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) - (void)writeAttributeWindSettingWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); - (void)writeAttributeWindSettingWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs params:(MTRWriteParams * _Nullable)params MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); -- (NSDictionary * _Nullable)readAttributeAirflowDirectionWithParams:(MTRReadParams * _Nullable)params MTR_PROVISIONALLY_AVAILABLE; -- (void)writeAttributeAirflowDirectionWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs MTR_PROVISIONALLY_AVAILABLE; -- (void)writeAttributeAirflowDirectionWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs params:(MTRWriteParams * _Nullable)params MTR_PROVISIONALLY_AVAILABLE; +- (NSDictionary * _Nullable)readAttributeAirflowDirectionWithParams:(MTRReadParams * _Nullable)params MTR_NEWLY_AVAILABLE; +- (void)writeAttributeAirflowDirectionWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs MTR_NEWLY_AVAILABLE; +- (void)writeAttributeAirflowDirectionWithValue:(NSDictionary *)dataValueDictionary expectedValueInterval:(NSNumber *)expectedValueIntervalMs params:(MTRWriteParams * _Nullable)params MTR_NEWLY_AVAILABLE; - (NSDictionary * _Nullable)readAttributeGeneratedCommandListWithParams:(MTRReadParams * _Nullable)params MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)); diff --git a/src/darwin/Framework/CHIP/zap-generated/MTRCommandPayloadsObjc.h b/src/darwin/Framework/CHIP/zap-generated/MTRCommandPayloadsObjc.h index ccd23b30f0aceb..2d00e6618716e5 100644 --- a/src/darwin/Framework/CHIP/zap-generated/MTRCommandPayloadsObjc.h +++ b/src/darwin/Framework/CHIP/zap-generated/MTRCommandPayloadsObjc.h @@ -7759,14 +7759,14 @@ MTR_PROVISIONALLY_AVAILABLE @property (nonatomic, copy, nullable) NSNumber * serverSideProcessingTimeout; @end -MTR_PROVISIONALLY_AVAILABLE +MTR_NEWLY_AVAILABLE @interface MTRFanControlClusterStepParams : NSObject -@property (nonatomic, copy) NSNumber * _Nonnull direction MTR_PROVISIONALLY_AVAILABLE; +@property (nonatomic, copy) NSNumber * _Nonnull direction MTR_NEWLY_AVAILABLE; -@property (nonatomic, copy) NSNumber * _Nullable wrap MTR_PROVISIONALLY_AVAILABLE; +@property (nonatomic, copy) NSNumber * _Nullable wrap MTR_NEWLY_AVAILABLE; -@property (nonatomic, copy) NSNumber * _Nullable lowestOff MTR_PROVISIONALLY_AVAILABLE; +@property (nonatomic, copy) NSNumber * _Nullable lowestOff MTR_NEWLY_AVAILABLE; /** * Controls whether the command is a timed command (using Timed Invoke). * diff --git a/zzz_generated/darwin-framework-tool/zap-generated/cluster/Commands.h b/zzz_generated/darwin-framework-tool/zap-generated/cluster/Commands.h index be046cb6155993..117f2b78066392 100644 --- a/zzz_generated/darwin-framework-tool/zap-generated/cluster/Commands.h +++ b/zzz_generated/darwin-framework-tool/zap-generated/cluster/Commands.h @@ -111365,7 +111365,6 @@ class SubscribeAttributeThermostatClusterRevision : public SubscribeAttribute { | Events: | | \*----------------------------------------------------------------------------*/ -#if MTR_ENABLE_PROVISIONAL /* * Command Step */ @@ -111374,15 +111373,9 @@ class FanControlStep : public ClusterCommand { FanControlStep() : ClusterCommand("step") { -#if MTR_ENABLE_PROVISIONAL AddArgument("Direction", 0, UINT8_MAX, &mRequest.direction); -#endif // MTR_ENABLE_PROVISIONAL -#if MTR_ENABLE_PROVISIONAL AddArgument("Wrap", 0, 1, &mRequest.wrap); -#endif // MTR_ENABLE_PROVISIONAL -#if MTR_ENABLE_PROVISIONAL AddArgument("LowestOff", 0, 1, &mRequest.lowestOff); -#endif // MTR_ENABLE_PROVISIONAL ClusterCommand::AddArguments(); } @@ -111397,23 +111390,17 @@ class FanControlStep : public ClusterCommand { __auto_type * cluster = [[MTRBaseClusterFanControl alloc] initWithDevice:device endpointID:@(endpointId) queue:callbackQueue]; __auto_type * params = [[MTRFanControlClusterStepParams alloc] init]; params.timedInvokeTimeoutMs = mTimedInteractionTimeoutMs.HasValue() ? [NSNumber numberWithUnsignedShort:mTimedInteractionTimeoutMs.Value()] : nil; -#if MTR_ENABLE_PROVISIONAL params.direction = [NSNumber numberWithUnsignedChar:chip::to_underlying(mRequest.direction)]; -#endif // MTR_ENABLE_PROVISIONAL -#if MTR_ENABLE_PROVISIONAL if (mRequest.wrap.HasValue()) { params.wrap = [NSNumber numberWithBool:mRequest.wrap.Value()]; } else { params.wrap = nil; } -#endif // MTR_ENABLE_PROVISIONAL -#if MTR_ENABLE_PROVISIONAL if (mRequest.lowestOff.HasValue()) { params.lowestOff = [NSNumber numberWithBool:mRequest.lowestOff.Value()]; } else { params.lowestOff = nil; } -#endif // MTR_ENABLE_PROVISIONAL uint16_t repeatCount = mRepeatCount.ValueOr(1); uint16_t __block responsesNeeded = repeatCount; while (repeatCount--) { @@ -111437,8 +111424,6 @@ class FanControlStep : public ClusterCommand { chip::app::Clusters::FanControl::Commands::Step::Type mRequest; }; -#endif // MTR_ENABLE_PROVISIONAL - /* * Attribute FanMode */ @@ -112552,8 +112537,6 @@ class SubscribeAttributeFanControlWindSetting : public SubscribeAttribute { } }; -#if MTR_ENABLE_PROVISIONAL - /* * Attribute AirflowDirection */ @@ -112677,8 +112660,6 @@ class SubscribeAttributeFanControlAirflowDirection : public SubscribeAttribute { } }; -#endif // MTR_ENABLE_PROVISIONAL - /* * Attribute GeneratedCommandList */ @@ -187932,9 +187913,7 @@ void registerClusterFanControl(Commands & commands) commands_list clusterCommands = { make_unique(Id), // -#if MTR_ENABLE_PROVISIONAL make_unique(), // -#endif // MTR_ENABLE_PROVISIONAL make_unique(Id), // make_unique(Id), // make_unique(Id), // @@ -187965,11 +187944,9 @@ void registerClusterFanControl(Commands & commands) make_unique(), // make_unique(), // make_unique(), // -#if MTR_ENABLE_PROVISIONAL make_unique(), // make_unique(), // make_unique(), // -#endif // MTR_ENABLE_PROVISIONAL make_unique(), // make_unique(), // make_unique(), // From 68c66e3cd0cf1df2e905ec639b89825cf55a654d Mon Sep 17 00:00:00 2001 From: chrisdecenzo <61757564+chrisdecenzo@users.noreply.github.com> Date: Mon, 11 Mar 2024 01:00:02 -0700 Subject: [PATCH 17/76] SVE fixes (#32519) --- examples/tv-app/android/java/MediaPlaybackManager.cpp | 2 +- .../com/matter/tv/server/tvapp/MediaPlaybackManagerStub.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tv-app/android/java/MediaPlaybackManager.cpp b/examples/tv-app/android/java/MediaPlaybackManager.cpp index 667ab2ec41b382..99afaaa02c942b 100644 --- a/examples/tv-app/android/java/MediaPlaybackManager.cpp +++ b/examples/tv-app/android/java/MediaPlaybackManager.cpp @@ -152,7 +152,7 @@ CHIP_ERROR MediaPlaybackManager::HandleGetActiveTrack(bool audio, AttributeValue } else { - err = aEncoder.EncodeNull(); + return aEncoder.EncodeNull(); } exit: diff --git a/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MediaPlaybackManagerStub.java b/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MediaPlaybackManagerStub.java index a6b9afcc603338..e72d544f153f21 100755 --- a/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MediaPlaybackManagerStub.java +++ b/examples/tv-app/android/java/src/com/matter/tv/server/tvapp/MediaPlaybackManagerStub.java @@ -35,11 +35,11 @@ public class MediaPlaybackManagerStub implements MediaPlaybackManager { private static int playbackMaxForwardSpeed = 10; private static int playbackMaxRewindSpeed = -10; - private static MediaTrack activeAudioTrack = null; private static MediaTrack[] audioTracks = { new MediaTrack("activeAudioTrackId_0", "languageCode1", "displayName1"), new MediaTrack("activeAudioTrackId_1", "languageCode2", "displayName2") }; + private static MediaTrack activeAudioTrack = audioTracks[0]; private static MediaTrack activeTextTrack = null; private static MediaTrack[] textTracks = { From 000edb450b7c975ff0ab0f60060f0605b75d8a44 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Mon, 11 Mar 2024 09:04:50 -0400 Subject: [PATCH 18/76] Prioritize DNS-SD results from the network on tvOS. (#32513) * Prioritize DNS-SD results from the network on tvOS. Since we can only deliver results from one interface given the current state of Matter SDK APIs (see https://github.com/project-chip/connectedhomeip/issues/32512), prioritize the interfaces that are more likely to have up-to-date results, not stale caches. * Address review comments, fix CI to use right SDK name. --- .github/workflows/darwin.yaml | 5 ++ src/platform/Darwin/DnssdContexts.cpp | 86 ++++++++++++++++++++------- src/platform/Darwin/DnssdImpl.h | 8 +++ 3 files changed, 79 insertions(+), 20 deletions(-) diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml index 088c541495665b..19afaa6da98c40 100644 --- a/.github/workflows/darwin.yaml +++ b/.github/workflows/darwin.yaml @@ -54,6 +54,11 @@ jobs: # Disable availability annotations, since we are not building a system # Matter.framework. run: xcodebuild -target "Matter" -sdk watchos -configuration Debug GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1' + - name: Run tvOS Build Debug + working-directory: src/darwin/Framework + # Disable availability annotations, since we are not building a system + # Matter.framework. + run: xcodebuild -target "Matter" -sdk appletvos -configuration Debug GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1' - name: Run iOS Build Debug working-directory: src/darwin/Framework # Disable availability annotations, since we are not building a system diff --git a/src/platform/Darwin/DnssdContexts.cpp b/src/platform/Darwin/DnssdContexts.cpp index cf4c35336edbf1..f1335641d67dbf 100644 --- a/src/platform/Darwin/DnssdContexts.cpp +++ b/src/platform/Darwin/DnssdContexts.cpp @@ -21,6 +21,8 @@ #include #include +#include + using namespace chip::Dnssd; using namespace chip::Dnssd::Internal; @@ -516,32 +518,40 @@ void ResolveContext::DispatchSuccess() // ChipDnssdResolveNoLongerNeeded don't find us and try to also remove us. bool needDelete = MdnsContexts::GetInstance().RemoveWithoutDeleting(this); - for (auto & interface : interfaces) - { - auto & ips = interface.second.addresses; +#if TARGET_OS_TV + // On tvOS, prioritize results from en0, en1, ir0 in that order, if those + // interfaces are present, since those will generally have more up-to-date + // information. + static const unsigned int priorityInterfaceIndices[] = { + if_nametoindex("en0"), + if_nametoindex("en1"), + if_nametoindex("ir0"), + }; +#else + // Elsewhere prioritize "lo0" over other interfaces. + static const unsigned int priorityInterfaceIndices[] = { + if_nametoindex("lo0"), + }; +#endif // TARGET_OS_TV - // Some interface may not have any ips, just ignore them. - if (ips.size() == 0) + for (auto interfaceIndex : priorityInterfaceIndices) + { + if (TryReportingResultsForInterfaceIndex(static_cast(interfaceIndex))) { - continue; + if (needDelete) + { + MdnsContexts::GetInstance().Delete(this); + } + return; } + } - ChipLogProgress(Discovery, "Mdns: Resolve success on interface %" PRIu32, interface.first); - - auto & service = interface.second.service; - auto addresses = Span(ips.data(), ips.size()); - if (nullptr == callback) - { - auto delegate = static_cast(context); - DiscoveredNodeData nodeData; - service.ToDiscoveredNodeData(addresses, nodeData); - delegate->OnNodeDiscovered(nodeData); - } - else + for (auto & interface : interfaces) + { + if (TryReportingResultsForInterfaceIndex(interface.first)) { - callback(context, &service, addresses, CHIP_NO_ERROR); + break; } - break; } if (needDelete) @@ -550,6 +560,42 @@ void ResolveContext::DispatchSuccess() } } +bool ResolveContext::TryReportingResultsForInterfaceIndex(uint32_t interfaceIndex) +{ + if (interfaceIndex == 0) + { + // Not actually an interface we have. + return false; + } + + auto & interface = interfaces[interfaceIndex]; + auto & ips = interface.addresses; + + // Some interface may not have any ips, just ignore them. + if (ips.size() == 0) + { + return false; + } + + ChipLogProgress(Discovery, "Mdns: Resolve success on interface %" PRIu32, interfaceIndex); + + auto & service = interface.service; + auto addresses = Span(ips.data(), ips.size()); + if (nullptr == callback) + { + auto delegate = static_cast(context); + DiscoveredNodeData nodeData; + service.ToDiscoveredNodeData(addresses, nodeData); + delegate->OnNodeDiscovered(nodeData); + } + else + { + callback(context, &service, addresses, CHIP_NO_ERROR); + } + + return true; +} + CHIP_ERROR ResolveContext::OnNewAddress(uint32_t interfaceId, const struct sockaddr * address) { // If we don't have any information about this interfaceId, just ignore the diff --git a/src/platform/Darwin/DnssdImpl.h b/src/platform/Darwin/DnssdImpl.h index 69ed0859b01f38..18f1d93baaab27 100644 --- a/src/platform/Darwin/DnssdImpl.h +++ b/src/platform/Darwin/DnssdImpl.h @@ -251,6 +251,14 @@ struct ResolveContext : public GenericContext const unsigned char * txtRecord); bool HasInterface(); bool Matches(const char * otherInstanceName) const { return instanceName == otherInstanceName; } + +private: + /** + * Try reporting the results we got on the provided interface index. + * Returns true if information was reported, false if not (e.g. if there + * were no IP addresses, etc). + */ + bool TryReportingResultsForInterfaceIndex(uint32_t interfaceIndex); }; } // namespace Dnssd From 3cbdb52a26cbe16dee36eb74a1cdef9831347107 Mon Sep 17 00:00:00 2001 From: Michael Rupp <95718139+mykrupp@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:30:07 -0400 Subject: [PATCH 19/76] [SILABS] Multi-chip OTA work (#32349) * Inital multi OTA work WIP * fix wifi build * cleanup and code review comments * Restyled by clang-format * Restyled by gn * Restyled by gn * code review comments * Restyled by clang-format --------- Co-authored-by: Restyled.io --- examples/platform/silabs/OTAConfig.cpp | 21 +- examples/platform/silabs/OTAConfig.h | 5 + examples/platform/silabs/efr32/BUILD.gn | 4 + scripts/tools/silabs/ota/README.md | 71 +++ scripts/tools/silabs/ota/crypto_utils.py | 487 ++++++++++++++++++ scripts/tools/silabs/ota/ota_image_tool.py | 390 ++++++++++++++ scripts/tools/silabs/ota/ota_payload.schema | 67 +++ src/platform/silabs/OTAImageProcessorImpl.h | 4 +- .../silabs/SiWx917/OTAImageProcessorImpl.cpp | 15 + src/platform/silabs/efr32/BUILD.gn | 13 +- .../silabs/efr32/OTAImageProcessorImpl.cpp | 16 + .../multi-ota/OTAMultiImageProcessorImpl.cpp | 457 ++++++++++++++++ .../multi-ota/OTAMultiImageProcessorImpl.h | 99 ++++ .../silabs/multi-ota/OTATlvProcessor.cpp | 171 ++++++ .../silabs/multi-ota/OTATlvProcessor.h | 159 ++++++ .../multi-ota/efr32/OTAFirmwareProcessor.cpp | 224 ++++++++ .../multi-ota/efr32/OTAFirmwareProcessor.h | 59 +++ .../silabs/multi-ota/efr32/OTAHooks.cpp | 44 ++ third_party/silabs/efr32_sdk.gni | 3 + 19 files changed, 2301 insertions(+), 8 deletions(-) create mode 100644 scripts/tools/silabs/ota/README.md create mode 100755 scripts/tools/silabs/ota/crypto_utils.py create mode 100755 scripts/tools/silabs/ota/ota_image_tool.py create mode 100644 scripts/tools/silabs/ota/ota_payload.schema create mode 100644 src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp create mode 100644 src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h create mode 100644 src/platform/silabs/multi-ota/OTATlvProcessor.cpp create mode 100644 src/platform/silabs/multi-ota/OTATlvProcessor.h create mode 100644 src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp create mode 100644 src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h create mode 100644 src/platform/silabs/multi-ota/efr32/OTAHooks.cpp diff --git a/examples/platform/silabs/OTAConfig.cpp b/examples/platform/silabs/OTAConfig.cpp index adc6f23abb4606..6305af08e628b5 100644 --- a/examples/platform/silabs/OTAConfig.cpp +++ b/examples/platform/silabs/OTAConfig.cpp @@ -17,6 +17,7 @@ */ #include "OTAConfig.h" +#include "silabs_utils.h" #include #ifndef SIWX_917 @@ -81,7 +82,6 @@ chip::DefaultOTARequestor gRequestorCore; chip::DefaultOTARequestorStorage gRequestorStorage; chip::DeviceLayer::DefaultOTARequestorDriver gRequestorUser; chip::BDXDownloader gDownloader; -chip::OTAImageProcessorImpl gImageProcessor; void OTAConfig::Init() { @@ -93,12 +93,23 @@ void OTAConfig::Init() // Periodic query timeout must be set prior to requestor being initialized gRequestorUser.SetPeriodicQueryTimeout(OTA_PERIODIC_TIMEOUT); - gRequestorUser.Init(&gRequestorCore, &gImageProcessor); - gImageProcessor.SetOTAImageFile("test.txt"); - gImageProcessor.SetOTADownloader(&gDownloader); +#if CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR + auto & imageProcessor = chip::OTAMultiImageProcessorImpl::GetDefaultInstance(); +#else + auto & imageProcessor = chip::OTAImageProcessorImpl::GetDefaultInstance(); +#endif + + gRequestorUser.Init(&gRequestorCore, &imageProcessor); + + CHIP_ERROR err = imageProcessor.Init(&gDownloader); + if (err != CHIP_NO_ERROR) + { + SILABS_LOG("Image processor init failed"); + assert(err == CHIP_NO_ERROR); + } // Connect the Downloader and Image Processor objects - gDownloader.SetImageProcessorDelegate(&gImageProcessor); + gDownloader.SetImageProcessorDelegate(&imageProcessor); // Initialize and interconnect the Requestor and Image Processor objects -- END } diff --git a/examples/platform/silabs/OTAConfig.h b/examples/platform/silabs/OTAConfig.h index 2d5dbcf3635223..2b7ed9a45fa257 100644 --- a/examples/platform/silabs/OTAConfig.h +++ b/examples/platform/silabs/OTAConfig.h @@ -22,7 +22,12 @@ #include #include #include + +#if CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR +#include +#else #include +#endif class OTAConfig { diff --git a/examples/platform/silabs/efr32/BUILD.gn b/examples/platform/silabs/efr32/BUILD.gn index 02f1317f55b643..8b7b1ebf86e264 100644 --- a/examples/platform/silabs/efr32/BUILD.gn +++ b/examples/platform/silabs/efr32/BUILD.gn @@ -180,6 +180,10 @@ config("efr32-common-config") { defines += [ "HEAP_MONITORING" ] } + if (chip_enable_multi_ota_requestor) { + defines += [ "CHIP_DEVICE_CONFIG_ENABLE_MULTI_OTA_REQUESTOR=1" ] + } + ldflags = [ "-Wl,--no-warn-rwx-segment" ] } diff --git a/scripts/tools/silabs/ota/README.md b/scripts/tools/silabs/ota/README.md new file mode 100644 index 00000000000000..d30dd458f10e1c --- /dev/null +++ b/scripts/tools/silabs/ota/README.md @@ -0,0 +1,71 @@ +--- +orphan: true +--- + +# Silabs OTA image tool + +## Overview + +This tool can generate an OTA image in the `|OTA standard header|TLV1|...|TLVn|` +format. The payload contains data in standard TLV format (not Matter TLV +format). During OTA transfer, these TLV can span across multiple BDX blocks, +thus the `OTAImageProcessorImpl` instance should take this into account. + +## Supported platforms + +- EFR32 - + +## Usage + +This is a wrapper over standard `ota_image_tool.py`, so the options for `create` +are also available here: + +``` +python3 ./scripts/tools/silabs/ota/ota_image_tool.py create -v 0xDEAD -p 0xBEEF -vn 50000 -vs "1.0" -da sha256 +``` + +followed by \*_custom options_- and a positional argument (should be last) that +specifies the output file. Please see the `create_ota_images.sh` for some +reference commands. + +The list of **custom options**: + +``` +# Application options +--app-input-file --> Path to the application binary. +--app-version --> Application version. It's part of the descriptor and + can be different than the OTA image header version: -vn. +--app-version-str --> Application version string. Same as above. +--app-build-date --> Application build date. Same as above. + +# SSBL options +--bl-input-file --> Path to the SSBL binary. +--bl-version --> SSBL version. +--bl-version-str --> SSBL version string. +--bl-build-date --> SSBL build date. + +# Factory data options +--factory-data --> If set, enables the generation of factory data. +--cert_declaration --> Certification Declaration. +--dac_cert --> DAC certificate. +--dac_key --> DAC private key. +--pai_cert --> PAI certificate. + +# Custom TLV options +--json --> Path to a JSON file following ota_payload.schema +``` + +Please note that the options above are separated into four categories: +application, bootloader, factory data and custom TLV (`--json` option). If no +descriptor options are specified for app/SSBL, the script will use the default +values (`50000`, `"50000-default"`, `"2023-01-01"`). The descriptor feature is +optional, TLV processors having the option to register a callback for descriptor +processing. + +## Custom payload + +When defining a custom processor, a user is able to also specify the custom +format of the TLV by creating a JSON file based on the `ota_payload.schema`. The +tool offers support for describing multiple TLV in the same JSON file. Please +see the `examples/ota_max_entries_example.json` for a multi-app + SSBL example. +Option `--json` must be used to specify the path to the JSON file. diff --git a/scripts/tools/silabs/ota/crypto_utils.py b/scripts/tools/silabs/ota/crypto_utils.py new file mode 100755 index 00000000000000..dbab140cf57cf9 --- /dev/null +++ b/scripts/tools/silabs/ota/crypto_utils.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2023 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. +# +""" +A pure python (slow) implementation of rijndael with a decent interface + +To include - + +from rijndael import rijndael + +To do a key setup - + +r = rijndael(key, block_size = 16) + +key must be a string of length 16, 24, or 32 +blocksize must be 16, 24, or 32. Default is 16 + +To use - + +ciphertext = r.encrypt(plaintext) +plaintext = r.decrypt(ciphertext) + +If any strings are of the wrong length a ValueError is thrown +""" +# ported from the Java reference code by Bram Cohen, April 2001 +# this code is public domain, unless someone makes +# an intellectual property claim against the reference +# code, in which case it can be made public domain by +# deleting all the comments and renaming all the variables + +import copy +import logging +import struct + +shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], + [[0, 0], [1, 5], [2, 4], [3, 3]], + [[0, 0], [1, 7], [3, 5], [4, 4]]] + +# [keysize][block_size] +num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} + +A = [[1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1]] + +# produce log and alog tables, needed for multiplying in the +# field GF(2^m) (generator = 3) +alog = [1] +for i in range(255): + j = (alog[-1] << 1) ^ alog[-1] + if j & 0x100 != 0: + j ^= 0x11B + alog.append(j) + +log = [0] * 256 +for i in range(1, 255): + log[alog[i]] = i + + +# multiply two elements of GF(2^m) +def mul(a, b): + if a == 0 or b == 0: + return 0 + return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] # noqa: F821 + + +# substitution box based on F^{-1}(x) +box = [[0] * 8 for i in range(256)] +box[1][7] = 1 +for i in range(2, 256): + j = alog[255 - log[i]] + for t in range(8): + box[i][t] = (j >> (7 - t)) & 0x01 + +B = [0, 1, 1, 0, 0, 0, 1, 1] + +# affine transform: box[i] <- B + A*box[i] +cox = [[0] * 8 for i in range(256)] +for i in range(256): + for t in range(8): + cox[i][t] = B[t] + for j in range(8): + cox[i][t] ^= A[t][j] * box[i][j] + +# S-boxes and inverse S-boxes +S = [0] * 256 +Si = [0] * 256 +for i in range(256): + S[i] = cox[i][0] << 7 + for t in range(1, 8): + S[i] ^= cox[i][t] << (7-t) + Si[S[i] & 0xFF] = i + +# T-boxes +G = [[2, 1, 1, 3], + [3, 2, 1, 1], + [1, 3, 2, 1], + [1, 1, 3, 2]] + +AA = [[0] * 8 for i in range(4)] + +for i in range(4): + for j in range(4): + AA[i][j] = G[i][j] + AA[i][i+4] = 1 + +for i in range(4): + pivot = AA[i][i] + if pivot == 0: + t = i + 1 + while AA[t][i] == 0 and t < 4: + t += 1 + assert t != 4, 'G matrix must be invertible' + for j in range(8): + AA[i][j], AA[t][j] = AA[t][j], AA[i][j] + pivot = AA[i][i] + for j in range(8): + if AA[i][j] != 0: + AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255] + for t in range(4): + if i != t: + for j in range(i+1, 8): + AA[t][j] ^= mul(AA[i][j], AA[t][i]) + AA[t][i] = 0 + +iG = [[0] * 4 for i in range(4)] + +for i in range(4): + for j in range(4): + iG[i][j] = AA[i][j + 4] + + +def mul4(a, bs): + if a == 0: + return 0 + r = 0 + for b in bs: + r <<= 8 + if b != 0: + r = r | mul(a, b) # noqa: F821 + return r + + +T1 = [] +T2 = [] +T3 = [] +T4 = [] +T5 = [] +T6 = [] +T7 = [] +T8 = [] +U1 = [] +U2 = [] +U3 = [] +U4 = [] + +for t in range(256): + s = S[t] + T1.append(mul4(s, G[0])) + T2.append(mul4(s, G[1])) + T3.append(mul4(s, G[2])) + T4.append(mul4(s, G[3])) + + s = Si[t] + T5.append(mul4(s, iG[0])) + T6.append(mul4(s, iG[1])) + T7.append(mul4(s, iG[2])) + T8.append(mul4(s, iG[3])) + + U1.append(mul4(t, iG[0])) + U2.append(mul4(t, iG[1])) + U3.append(mul4(t, iG[2])) + U4.append(mul4(t, iG[3])) + +# round constants +rcon = [1] +r = 1 +for t in range(1, 30): + r = mul(2, r) + rcon.append(r) + +del A +del AA +del pivot +del B +del G +del box +del log +del alog +del i +del j +del r +del s +del t +del mul +del mul4 +del cox +del iG + + +class rijndael: + def __init__(self, key, block_size=16): + if block_size != 16 and block_size != 24 and block_size != 32: + raise ValueError('Invalid block size: ' + str(block_size)) + if len(key) != 16 and len(key) != 24 and len(key) != 32: + raise ValueError('Invalid key size: ' + str(len(key))) + self.block_size = block_size + + ROUNDS = num_rounds[len(key)][block_size] + BC = int(block_size / 4) + + # encryption round keys + Ke = [[0] * BC for i in range(ROUNDS + 1)] + # decryption round keys + Kd = [[0] * BC for i in range(ROUNDS + 1)] + ROUND_KEY_COUNT = (ROUNDS + 1) * BC + KC = int(len(key) / 4) + + # copy user material bytes into temporary ints + tk = [] + for i in range(0, KC): + tk.append((key[i * 4] << 24) | (key[i * 4 + 1] << 16) | + (key[i * 4 + 2] << 8) | key[i * 4 + 3]) + + # copy values into round key arrays + t = 0 + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[int(t / BC)][t % BC] = tk[j] + Kd[ROUNDS - int(t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + tt = 0 + rconpointer = 0 + while t < ROUND_KEY_COUNT: + # extrapolate using phi (the round key evolution function) + tt = tk[KC - 1] + tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ + (S[tt & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) ^ \ + (rcon[rconpointer] & 0xFF) << 24 + rconpointer += 1 + if KC != 8: + for i in range(1, KC): + tk[i] ^= tk[i-1] + else: + for i in range(1, KC / 2): + tk[i] ^= tk[i-1] + tt = tk[KC / 2 - 1] + tk[KC / 2] ^= (S[tt & 0xFF] & 0xFF) ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) << 24 + for i in range(KC / 2 + 1, KC): + tk[i] ^= tk[i-1] + # copy values into round key arrays + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[int(t / BC)][t % BC] = tk[j] + Kd[ROUNDS - int(t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + # inverse MixColumn where needed + for r in range(1, ROUNDS): + for j in range(BC): + tt = Kd[r][j] + Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \ + U2[(tt >> 16) & 0xFF] ^ \ + U3[(tt >> 8) & 0xFF] ^ \ + U4[tt & 0xFF] + self.Ke = Ke + self.Kd = Kd + + def encrypt(self, plaintext): + if len(plaintext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Ke = self.Ke + + BC = int(self.block_size / 4) + ROUNDS = len(Ke) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][0] + s2 = shifts[SC][2][0] + s3 = shifts[SC][3][0] + a = [0] * BC + # temporary work array + t = [] + # plaintext to ints + key + for i in range(BC): + t.append((ord(plaintext[i * 4]) << 24 | + ord(plaintext[i * 4 + 1]) << 16 | + ord(plaintext[i * 4 + 2]) << 8 | + ord(plaintext[i * 4 + 3])) ^ Ke[0][i]) + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T1[(t[i] >> 24) & 0xFF] ^ + T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T4[t[(i + s3) % BC] & 0xFF]) ^ Ke[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Ke[ROUNDS][i] + result.append((S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((S[t[(i + s3) % BC] & 0xFF] ^ tt) & 0xFF) + return ''.join(list(map(chr, result))) + + def decrypt(self, ciphertext): + if len(ciphertext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext))) + Kd = self.Kd + + BC = int(self.block_size / 4) + ROUNDS = len(Kd) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][1] + s2 = shifts[SC][2][1] + s3 = shifts[SC][3][1] + a = [0] * BC + # temporary work array + t = [0] * BC + # ciphertext to ints + key + for i in range(BC): + t[i] = (ord(ciphertext[i * 4]) << 24 | + ord(ciphertext[i * 4 + 1]) << 16 | + ord(ciphertext[i * 4 + 2]) << 8 | + ord(ciphertext[i * 4 + 3])) ^ Kd[0][i] + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T5[(t[i] >> 24) & 0xFF] ^ + T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T8[t[(i + s3) % BC] & 0xFF]) ^ Kd[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Kd[ROUNDS][i] + result.append((Si[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((Si[t[(i + s3) % BC] & 0xFF] ^ tt) & 0xFF) + return ''.join(map(chr, result)) + + +def encryptFlashData(nonce, key, data, imageLen): + encyptedBlock = '' + if (imageLen % 16) != 0: + for x in range(16 - (imageLen % 16)): + data = data + bytes([255]) + imageLen = len(data) + + r = rijndael(key, block_size=16) + + for x in range(int(imageLen / 16)): + # use nonce value to create encrypted chunk + encryptNonce = '' + for i in nonce: + tempString = "%08x" % i + y = 0 + while y < 8: + encryptNonce = encryptNonce + chr(int(tempString[y:y+2], 16)) + y = y + 2 + encChunk = r.encrypt(encryptNonce) + + # increment the nonce value + if (nonce[3] == 0xffffffff): + nonce[3] = 0 + else: + nonce[3] += 1 + + # xor encypted junk with data chunk + chunk = data[x*16:(x+1)*16] # Read 16 byte chucks. 128 bits + + lchunk = chunk + lencChunk = list(map(ord, encChunk)) + + loutChunk = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + for i in range(16): + loutChunk[i] = lchunk[i] ^ lencChunk[i] + encyptedBlock = encyptedBlock + chr(lchunk[i] ^ lencChunk[i]) + + return (encyptedBlock) + + +def aParsePassKeyString(sPassKey): + lstu32Passkey = [0, 0, 0, 0] + + try: + lstStrPassKey = sPassKey.split(",") + + except Exception: + sPassKey = "0x00000000, 0x00000000, 0x00000000, 0x00000000" + lstStrPassKey = sPassKey.split(",") + + if len(lstStrPassKey) == 4: + for i in range(4): + if "0x" in lstStrPassKey[i]: + lstu32Passkey[i] = int(lstStrPassKey[i], 16) + else: + lstu32Passkey[i] = int(lstStrPassKey[i], 10) + + logging.info(f"\t-key: {lstu32Passkey[0]}, {lstu32Passkey[1]}, {lstu32Passkey[2]}, {lstu32Passkey[3]}") + abEncryptKey = struct.pack(">LLLL", lstu32Passkey[0], + lstu32Passkey[1], + lstu32Passkey[2], + lstu32Passkey[3]) + return abEncryptKey + + +def aParseNonce(sNonceValue): + lstu32Nonce = [0, 0, 0, 0] + + try: + lstStrNonce = sNonceValue.split(",") + + except Exception: + sNonceValue = "0x00000000, 0x00000000, 0x00000000, 0x00000000" + lstStrNonce = sNonceValue.split(",") + + if len(lstStrNonce) == 4: + for i in range(4): + if "0x" in lstStrNonce[i]: + lstu32Nonce[i] = int(lstStrNonce[i], 16) + else: + lstu32Nonce[i] = int(lstStrNonce[i], 10) + + logging.info(f"Nonce : {lstu32Nonce[0]}, {lstu32Nonce[1]}, {lstu32Nonce[2]}, {lstu32Nonce[3]}") + + return lstu32Nonce + + +def encryptData(sSrcData, sPassKey, aPassIv): + + sKeyString = sPassKey.strip() + assert len(sKeyString) == 32, 'the length of encryption key should be equal to 32' + sPassString = "0x" + sKeyString[:8] + ',' + "0x" + sKeyString[8:16] + \ + ',' + "0x" + sKeyString[16:24] + ',' + "0x" + sKeyString[24:32] + aPassKey = aParsePassKeyString(sPassString) + + sIvString = aPassIv.strip() + sPassString = "0x" + sIvString[:8] + ',' + "0x" + sIvString[8:16] + \ + ',' + "0x" + sIvString[16:24] + ',' + "0x" + sIvString[24:32] + aNonce = aParseNonce(sPassString) + + logging.info("Started Encrypting with key[{}] ......".format(sPassKey)) + + encryptedData = encryptFlashData(aNonce, aPassKey, sSrcData, len(sSrcData)) + + logging.info("Done") + + return encryptedData diff --git a/scripts/tools/silabs/ota/ota_image_tool.py b/scripts/tools/silabs/ota/ota_image_tool.py new file mode 100755 index 00000000000000..64715d784aeecd --- /dev/null +++ b/scripts/tools/silabs/ota/ota_image_tool.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2023 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. +# + +'''This file should contain a way to generate custom OTA payloads. + +The format of the custom payload is the following: +| Total size of TLVs | TLV1 | ... | TLVn | + +The OTA payload can then be used to generate an OTA image file, which +will be parsed by the OTA image processor. The total size of TLVs is +needed as input for a TLVReader. + +Currently, this script only supports Certification Declaration update, +but it could be modified to support all factory data fields. +''' + +import argparse +import glob +import json +import logging +import os +import sys + +import crypto_utils +import jsonschema + +sys.path.insert(0, os.path.join( + os.path.dirname(__file__), '../factory_data_generator')) +sys.path.insert(0, os.path.join( + os.path.dirname(__file__), '../../../../src/controller/python')) +sys.path.insert(0, os.path.join( + os.path.dirname(__file__), '../../../../src/app/')) + +import ota_image_tool # noqa: E402 isort:skip +from chip.tlv import TLVWriter # noqa: E402 isort:skip +from custom import CertDeclaration, DacCert, DacPKey, PaiCert # noqa: E402 isort:skip +from default import InputArgument # noqa: E402 isort:skip +from generate import set_logger # noqa: E402 isort:skip + +OTA_APP_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_app_tlv.bin") +OTA_BOOTLOADER_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_ssbl_tlv.bin") +OTA_FACTORY_TLV_TEMP = os.path.join(os.path.dirname(__file__), "ota_temp_factory_tlv.bin") + +INITIALIZATION_VECTOR = "00000010111213141516171800000000" + + +class TAG: + APPLICATION = 1 + BOOTLOADER = 2 + FACTORY_DATA = 3 + + +def write_to_temp(path: str, payload: bytearray): + with open(path, "wb") as _handle: + _handle.write(payload) + + logging.info(f"Data payload size for {path.split('/')[-1]}: {len(payload)}") + + +def generate_header(tag: int, length: int): + header = bytearray(tag.to_bytes(4, "little")) + header += bytearray(length.to_bytes(4, "little")) + return header + + +def generate_factory_data(args: object): + """ + Generate custom OTA payload from InputArgument derived objects. The payload is + written in a temporary file that will be appended to args.input_files. + """ + fields = dict() + + if args.dac_key is not None: + args.dac_key.generate_private_key(args.dac_key_password) + + data = [obj for key, obj in vars(args).items() if isinstance(obj, InputArgument)] + for arg in sorted(data, key=lambda x: x.key()): + fields.update({arg.key(): arg.encode()}) + + if fields: + writer = TLVWriter() + writer.put(None, fields) + logging.info(f"factory data encryption enable: {args.enc_enable}") + if args.enc_enable: + enc_factory_data = crypto_utils.encryptData(writer.encoding, args.input_ota_key, INITIALIZATION_VECTOR) + enc_factory_data1 = bytes([ord(x) for x in enc_factory_data]) + payload = generate_header(TAG.FACTORY_DATA, len(enc_factory_data1)) + payload += enc_factory_data1 + else: + payload = generate_header(TAG.FACTORY_DATA, len(writer.encoding)) + payload += writer.encoding + + write_to_temp(OTA_FACTORY_TLV_TEMP, payload) + + return [OTA_FACTORY_TLV_TEMP] + + +def generate_descriptor(version: int, versionStr: str, buildDate: str): + """ + Generate descriptor as bytearray for app/SSBL payload. + """ + v = version if version is not None else 50000 + vs = versionStr if versionStr is not None else "50000-default" + bd = buildDate if buildDate is not None else "2023-01-01" + + logging.info(f"\t-version: {v}") + logging.info(f"\t-version str: {vs}") + logging.info(f"\t-build date: {bd}") + + v = v.to_bytes(4, "little") + vs = bytearray(vs, "ascii") + bytearray(64 - len(vs)) + bd = bytearray(bd, "ascii") + bytearray(64 - len(bd)) + + return v + vs + bd + + +def generate_app(args: object): + """ + Generate app payload with descriptor. If a certain option is not specified, use the default values. + """ + logging.info("App descriptor information:") + + descriptor = generate_descriptor(args.app_version, args.app_version_str, args.app_build_date) + logging.info(f"App encryption enable: {args.enc_enable}") + if args.enc_enable: + inputFile = open(args.app_input_file, "rb") + enc_file = crypto_utils.encryptData(inputFile.read(), args.input_ota_key, INITIALIZATION_VECTOR) + enc_file1 = bytes([ord(x) for x in enc_file]) + file_size = len(enc_file1) + payload = generate_header(TAG.APPLICATION, len(descriptor) + file_size) + descriptor + enc_file1 + else: + file_size = os.path.getsize(args.app_input_file) + logging.info(f"file size: {file_size}") + payload = generate_header(TAG.APPLICATION, len(descriptor) + file_size) + descriptor + + write_to_temp(OTA_APP_TLV_TEMP, payload) + if args.enc_enable: + return [OTA_APP_TLV_TEMP] + else: + return [OTA_APP_TLV_TEMP, args.app_input_file] + + +def generate_bootloader(args: object): + """ + Generate SSBL payload with descriptor. If a certain option is not specified, use the default values. + """ + logging.info("SSBL descriptor information:") + + descriptor = generate_descriptor(args.bl_version, args.bl_version_str, args.bl_build_date) + logging.info(f"Bootloader encryption enable: {args.enc_enable}") + if args.enc_enable: + inputFile = open(args.bl_input_file, "rb") + enc_file = crypto_utils.encryptData(inputFile.read(), args.input_ota_key, INITIALIZATION_VECTOR) + enc_file1 = bytes([ord(x) for x in enc_file]) + file_size = len(enc_file1) + payload = generate_header(TAG.BOOTLOADER, len(descriptor) + file_size) + descriptor + enc_file1 + else: + file_size = os.path.getsize(args.bl_input_file) + logging.info(f"file size: {file_size}") + payload = generate_header(TAG.BOOTLOADER, len(descriptor) + file_size) + descriptor + + write_to_temp(OTA_BOOTLOADER_TLV_TEMP, payload) + if args.enc_enable: + return [OTA_BOOTLOADER_TLV_TEMP] + else: + return [OTA_BOOTLOADER_TLV_TEMP, args.bl_input_file] + + +def validate_json(data: str): + with open(os.path.join(os.path.dirname(__file__), 'ota_payload.schema'), 'r') as fd: + payload_schema = json.load(fd) + + try: + jsonschema.validate(instance=data, schema=payload_schema) + logging.info("JSON data is valid") + except jsonschema.exceptions.ValidationError as err: + logging.error(f"JSON data is invalid: {err}") + sys.exit(1) + + +def generate_custom_tlvs(data): + """ + Generate custom OTA payload from a JSON object following a predefined schema. + The payload is written in a temporary file that will be appended to args.input_files. + """ + input_files = [] + + payload = bytearray() + descriptor = bytearray() + iteration = 0 + for entry in data["inputs"]: + if "descriptor" in entry: + for field in entry["descriptor"]: + if isinstance(field["value"], str): + descriptor += bytearray(field["value"], "ascii") + bytearray(field["length"] - len(field["value"])) + elif isinstance(field["value"], int): + descriptor += bytearray(field["value"].to_bytes(field["length"], "little")) + file_size = os.path.getsize(entry["path"]) + payload = generate_header(entry["tag"], len(descriptor) + file_size) + descriptor + + temp_output = os.path.join(os.path.dirname(__file__), "ota_temp_custom_tlv_" + str(iteration) + ".bin") + write_to_temp(temp_output, payload) + + input_files += [temp_output, entry["path"]] + iteration += 1 + descriptor = bytearray() + + return input_files + + +def show_payload(args: object): + """ + Parse and present OTA custom payload in human-readable form. + """ + # TODO: implement to show current TLVs + pass + + +def create_image(args: object): + ota_image_tool.validate_header_attributes(args) + + input_files = list() + + if args.json: + with open(args.json, 'r') as fd: + data = json.load(fd) + validate_json(data) + input_files += generate_custom_tlvs(data) + + if args.factory_data: + input_files += generate_factory_data(args) + + if args.bl_input_file: + input_files += generate_bootloader(args) + + if args.app_input_file: + input_files += generate_app(args) + + if len(input_files) == 0: + print("Please specify an input option.") + sys.exit(1) + + logging.info("Input files used:") + [logging.info(f"\t- {_file}") for _file in input_files] + + args.input_files = input_files + ota_image_tool.generate_image(args) + + for filename in glob.glob(os.path.dirname(__file__) + "/ota_temp_*"): + os.remove(filename) + if args.enc_enable: + for filename in glob.glob(os.path.dirname(__file__) + "/enc_ota_temp_*"): + os.remove(filename) + + +def main(): + """ + This function is a modified version of ota_image_tool.py main function. + + The wrapper version defines a new set of args, which are used to generate + TLV data that will be embedded in the final OTA payload. + """ + + def any_base_int(s): return int(s, 0) + + set_logger() + parser = argparse.ArgumentParser( + description='Matter OTA (Over-the-air update) image utility', fromfile_prefix_chars='@') + subcommands = parser.add_subparsers( + dest='subcommand', title='valid subcommands', required=True) + + create_parser = subcommands.add_parser('create', help='Create OTA image') + create_parser.add_argument('-v', '--vendor-id', type=any_base_int, + required=True, help='Vendor ID') + create_parser.add_argument('-p', '--product-id', type=any_base_int, + required=True, help='Product ID') + create_parser.add_argument('-vn', '--version', type=any_base_int, + required=True, help='Software version (numeric)') + create_parser.add_argument('-vs', '--version-str', required=True, + help='Software version (string)') + create_parser.add_argument('-da', '--digest-algorithm', choices=ota_image_tool.DIGEST_ALL_ALGORITHMS, + required=True, help='Digest algorithm') + create_parser.add_argument('-mi', '--min-version', type=any_base_int, + help='Minimum software version that can be updated to this image') + create_parser.add_argument('-ma', '--max-version', type=any_base_int, + help='Maximum software version that can be updated to this image') + create_parser.add_argument('-rn', '--release-notes', + help='Release note URL') + + create_parser.add_argument('-app', "--app-input-file", + help='Path to application input file') + create_parser.add_argument('--app-version', type=any_base_int, + help='Application Software version (numeric)') + create_parser.add_argument('--app-version-str', type=str, + help='Application Software version (string)') + create_parser.add_argument('--app-build-date', type=str, + help='Application build date (string)') + + create_parser.add_argument('-bl', '--bl-input-file', + help='Path to input bootloader image payload file') + create_parser.add_argument('--bl-version', type=any_base_int, + help='Bootloader Software version (numeric)') + create_parser.add_argument('--bl-version-str', type=str, + help='Bootloader Software version (string)') + create_parser.add_argument('--bl-build-date', type=str, + help='Bootloader build date (string)') + + # Factory data specific arguments. Will be used to generate the TLV payload. + create_parser.add_argument('-fd', '--factory-data', action='store_true', + help='If found, enable factory data payload generation.') + create_parser.add_argument("--cert_declaration", type=CertDeclaration, + help="[path] Path to Certification Declaration in DER format") + create_parser.add_argument("--dac_cert", type=DacCert, + help="[path] Path to DAC certificate in DER format") + create_parser.add_argument("--dac_key", type=DacPKey, + help="[path] Path to DAC key in DER format") + create_parser.add_argument("--dac_key_password", type=str, + help="[path] Password to decode DAC Key if available") + create_parser.add_argument("--pai_cert", type=PaiCert, + help="[path] Path to PAI certificate in DER format") + + # Path to input JSON file which describes custom TLVs. + create_parser.add_argument('--json', help="[path] Path to the JSON describing custom TLVs") + + create_parser.add_argument('--enc_enable', action="store_true", help='enable ota encryption') + create_parser.add_argument('--input_ota_key', type=str, default="1234567890ABCDEFA1B2C3D4E5F6F1B4", + help='Input OTA Encryption KEY (string:16Bytes)') + + create_parser.add_argument('-i', '--input_files', default=list(), + help='Path to input image payload file') + create_parser.add_argument('output_file', help='Path to output image file') + + show_parser = subcommands.add_parser('show', help='Show OTA image info') + show_parser.add_argument('image_file', help='Path to OTA image file') + + extract_tool = subcommands.add_parser('extract', help='Remove the OTA header from an image file') + extract_tool.add_argument('image_file', help='Path to OTA image file with header') + extract_tool.add_argument('output_file', help='Path to put the output file (no header)') + + change_tool = subcommands.add_parser('change_header', help='Change the specified values in the header') + change_tool.add_argument('-v', '--vendor-id', type=any_base_int, + help='Vendor ID') + change_tool.add_argument('-p', '--product-id', type=any_base_int, + help='Product ID') + change_tool.add_argument('-vn', '--version', type=any_base_int, + help='Software version (numeric)') + change_tool.add_argument('-vs', '--version-str', + help='Software version (string)') + change_tool.add_argument('-da', '--digest-algorithm', choices=ota_image_tool.DIGEST_ALL_ALGORITHMS, + help='Digest algorithm') + change_tool.add_argument('-mi', '--min-version', type=any_base_int, + help='Minimum software version that can be updated to this image') + change_tool.add_argument('-ma', '--max-version', type=any_base_int, + help='Maximum software version that can be updated to this image') + change_tool.add_argument( + '-rn', '--release-notes', help='Release note URL') + change_tool.add_argument('image_file', + help='Path to input OTA file') + change_tool.add_argument('output_file', help='Path to output OTA file') + + args = parser.parse_args() + + if args.subcommand == 'create': + create_image(args) + elif args.subcommand == 'show': + ota_image_tool.show_header(args) + show_payload(args) + elif args.subcommand == 'extract': + ota_image_tool.remove_header(args) + elif args.subcommand == 'change_header': + ota_image_tool.update_header_args(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/tools/silabs/ota/ota_payload.schema b/scripts/tools/silabs/ota/ota_payload.schema new file mode 100644 index 00000000000000..bceacf75f029b9 --- /dev/null +++ b/scripts/tools/silabs/ota/ota_payload.schema @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "Custom_OTA_TLV_schema", + "description": "A representation of custom OTA payload with variable number of TLVs", + "type": "object", + "required": [ + "inputs" + ], + "properties": { + "inputs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "tag", + "path" + ], + "properties": { + "tag": { + "type": "integer", + "description": "TLV's tag value used to select a parser" + }, + "descriptor": { + "type": "array", + "description": "Metadata of the TLV value field (C struct)", + "items": { + "$ref": "#/$defs/field" + } + }, + "path": { + "type": "string", + "description": "System path to the binary" + } + } + } + } + }, + "$defs": { + "field": { + "type": "object", + "required": [ + "name", + "length", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "length": { + "type": "integer", + "description": "Number of bytes occupied in memory" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/src/platform/silabs/OTAImageProcessorImpl.h b/src/platform/silabs/OTAImageProcessorImpl.h index 46b78b063f2172..5598b67d4ba758 100644 --- a/src/platform/silabs/OTAImageProcessorImpl.h +++ b/src/platform/silabs/OTAImageProcessorImpl.h @@ -40,7 +40,8 @@ class OTAImageProcessorImpl : public OTAImageProcessorInterface CHIP_ERROR ConfirmCurrentImage() override; void SetOTADownloader(OTADownloader * downloader) { mDownloader = downloader; } - void SetOTAImageFile(const char * imageFile) { mImageFile = imageFile; } + CHIP_ERROR Init(OTADownloader * downloader); + static OTAImageProcessorImpl & GetDefaultInstance(); private: //////////// Actual handlers for the OTAImageProcessorInterface /////////////// @@ -68,7 +69,6 @@ class OTAImageProcessorImpl : public OTAImageProcessorInterface MutableByteSpan mBlock; OTADownloader * mDownloader; OTAImageHeaderParser mHeaderParser; - const char * mImageFile = nullptr; static constexpr size_t kAlignmentBytes = 64; // Intermediate, word-aligned buffer for writing to the bootloader storage. // Bootloader storage API requires the buffer size to be a multiple of 4. diff --git a/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp b/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp index 7e581135fde8bd..d641bfa61f9888 100644 --- a/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp +++ b/src/platform/silabs/SiWx917/OTAImageProcessorImpl.cpp @@ -38,6 +38,7 @@ extern "C" { #define SL_STATUS_FW_UPDATE_DONE SL_STATUS_SI91X_NO_AP_FOUND uint8_t flag = RPS_HEADER; +static chip::OTAImageProcessorImpl gImageProcessor; namespace chip { @@ -48,6 +49,15 @@ uint32_t OTAImageProcessorImpl::mWriteOffset uint16_t OTAImageProcessorImpl::writeBufOffset = 0; uint8_t OTAImageProcessorImpl::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 }; +CHIP_ERROR OTAImageProcessorImpl::Init(OTADownloader * downloader) +{ + ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + gImageProcessor.SetOTADownloader(downloader); + + return CHIP_NO_ERROR; +} + CHIP_ERROR OTAImageProcessorImpl::PrepareDownload() { DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast(this)); @@ -386,4 +396,9 @@ CHIP_ERROR OTAImageProcessorImpl::ReleaseBlock() return CHIP_NO_ERROR; } +OTAImageProcessorImpl & OTAImageProcessorImpl::GetDefaultInstance() +{ + return gImageProcessor; +} + } // namespace chip diff --git a/src/platform/silabs/efr32/BUILD.gn b/src/platform/silabs/efr32/BUILD.gn index 83198d1436ebdc..25b8e9f4690555 100644 --- a/src/platform/silabs/efr32/BUILD.gn +++ b/src/platform/silabs/efr32/BUILD.gn @@ -17,6 +17,7 @@ import("//build_overrides/chip.gni") import("${chip_root}/build/chip/buildconfig_header.gni") import("${chip_root}/src/crypto/crypto.gni") import("${chip_root}/src/platform/device.gni") +import("${chip_root}/third_party/silabs/efr32_sdk.gni") import("${chip_root}/third_party/silabs/silabs_board.gni") silabs_platform_dir = "${chip_root}/src/platform/silabs" @@ -77,7 +78,17 @@ static_library("efr32") { sources += [ "BLEManagerImpl.cpp" ] } - if (chip_enable_ota_requestor) { + if (chip_enable_multi_ota_requestor) { + sources += [ + "${silabs_platform_dir}/multi-ota/OTAMultiImageProcessorImpl.cpp", + "${silabs_platform_dir}/multi-ota/OTAMultiImageProcessorImpl.h", + "${silabs_platform_dir}/multi-ota/OTATlvProcessor.cpp", + "${silabs_platform_dir}/multi-ota/OTATlvProcessor.h", + "${silabs_platform_dir}/multi-ota/efr32/OTAFirmwareProcessor.cpp", + "${silabs_platform_dir}/multi-ota/efr32/OTAFirmwareProcessor.h", + "${silabs_platform_dir}/multi-ota/efr32/OTAHooks.cpp", + ] + } else if (chip_enable_ota_requestor) { sources += [ "${silabs_platform_dir}/OTAImageProcessorImpl.h", "OTAImageProcessorImpl.cpp", diff --git a/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp b/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp index a2544f879a8ff5..598ee5a2093605 100644 --- a/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp +++ b/src/platform/silabs/efr32/OTAImageProcessorImpl.cpp @@ -33,6 +33,8 @@ extern "C" { /// No error, operation OK #define SL_BOOTLOADER_OK 0L +static chip::OTAImageProcessorImpl gImageProcessor; + namespace chip { // Define static memebers @@ -41,6 +43,15 @@ uint32_t OTAImageProcessorImpl::mWriteOffset uint16_t OTAImageProcessorImpl::writeBufOffset = 0; uint8_t OTAImageProcessorImpl::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 }; +CHIP_ERROR OTAImageProcessorImpl::Init(OTADownloader * downloader) +{ + ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + gImageProcessor.SetOTADownloader(downloader); + + return CHIP_NO_ERROR; +} + CHIP_ERROR OTAImageProcessorImpl::PrepareDownload() { DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast(this)); @@ -400,4 +411,9 @@ CHIP_ERROR OTAImageProcessorImpl::ReleaseBlock() return CHIP_NO_ERROR; } +OTAImageProcessorImpl & OTAImageProcessorImpl::GetDefaultInstance() +{ + return gImageProcessor; +} + } // namespace chip diff --git a/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp new file mode 100644 index 00000000000000..147dcdaf8317ee --- /dev/null +++ b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.cpp @@ -0,0 +1,457 @@ +/* + * + * Copyright (c) 2021-2023 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 + +using namespace chip::DeviceLayer; +using namespace ::chip::DeviceLayer::Internal; + +static chip::OTAMultiImageProcessorImpl gImageProcessor; + +extern "C" { +#include "btl_interface.h" +#include "em_bus.h" // For CORE_CRITICAL_SECTION +#if SL_WIFI +#include "spi_multiplex.h" +#endif // SL_WIFI +} + +namespace chip { + +CHIP_ERROR OTAMultiImageProcessorImpl::Init(OTADownloader * downloader) +{ + ReturnErrorCodeIf(downloader == nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + gImageProcessor.SetOTADownloader(downloader); + + OtaHookInit(); + + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::Clear() +{ + mHeaderParser.Clear(); + mAccumulator.Clear(); + mParams.totalFileBytes = 0; + mParams.downloadedBytes = 0; + mCurrentProcessor = nullptr; + + ReleaseBlock(); +} + +CHIP_ERROR OTAMultiImageProcessorImpl::PrepareDownload() +{ + DeviceLayer::PlatformMgr().ScheduleWork(HandlePrepareDownload, reinterpret_cast(this)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::Finalize() +{ + DeviceLayer::PlatformMgr().ScheduleWork(HandleFinalize, reinterpret_cast(this)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::Apply() +{ + DeviceLayer::PlatformMgr().ScheduleWork(HandleApply, reinterpret_cast(this)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::Abort() +{ + DeviceLayer::PlatformMgr().ScheduleWork(HandleAbort, reinterpret_cast(this)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ProcessBlock(ByteSpan & block) +{ + if ((block.data() == nullptr) || block.empty()) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + // Store block data for HandleProcessBlock to access + CHIP_ERROR err = SetBlock(block); + if (err != CHIP_NO_ERROR) + { + ChipLogError(SoftwareUpdate, "Cannot set block data: %" CHIP_ERROR_FORMAT, err.Format()); + } + + DeviceLayer::PlatformMgr().ScheduleWork(HandleProcessBlock, reinterpret_cast(this)); + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::HandlePrepareDownload(intptr_t context) +{ + auto * imageProcessor = reinterpret_cast(context); + + VerifyOrReturn(imageProcessor != nullptr, ChipLogError(SoftwareUpdate, "ImageProcessor context is null")); + + VerifyOrReturn(imageProcessor->mDownloader != nullptr, ChipLogError(SoftwareUpdate, "mDownloader is null")); + + ChipLogProgress(SoftwareUpdate, "HandlePrepareDownload: started"); + + CORE_CRITICAL_SECTION(bootloader_init();) + + imageProcessor->mParams.downloadedBytes = 0; + + imageProcessor->mHeaderParser.Init(); + imageProcessor->mAccumulator.Init(sizeof(OTATlvHeader)); + imageProcessor->mDownloader->OnPreparedForDownload(CHIP_NO_ERROR); +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ProcessHeader(ByteSpan & block) +{ + OTAImageHeader header; + ReturnErrorOnFailure(mHeaderParser.AccumulateAndDecode(block, header)); + + mParams.totalFileBytes = header.mPayloadSize; + mHeaderParser.Clear(); + ChipLogError(SoftwareUpdate, "Processed header successfully"); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ProcessPayload(ByteSpan & block) +{ + CHIP_ERROR status = CHIP_NO_ERROR; + + while (true) + { + if (!mCurrentProcessor) + { + ReturnErrorOnFailure(mAccumulator.Accumulate(block)); + ByteSpan tlvHeader{ mAccumulator.data(), sizeof(OTATlvHeader) }; + ReturnErrorOnFailure(SelectProcessor(tlvHeader)); + ReturnErrorOnFailure(mCurrentProcessor->Init()); + } + + status = mCurrentProcessor->Process(block); + if (status == CHIP_OTA_CHANGE_PROCESSOR) + { + mAccumulator.Clear(); + mAccumulator.Init(sizeof(OTATlvHeader)); + + mCurrentProcessor = nullptr; + + // If the block size is 0, it means that the processed data was a multiple of + // received BDX block size (e.g. 8 blocks of 1024 bytes were transferred). + // After state for selecting next processor is reset, a request for fetching next + // data must be sent. + if (block.size() == 0) + { + status = CHIP_NO_ERROR; + break; + } + } + else + { + break; + } + } + + return status; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::SelectProcessor(ByteSpan & block) +{ + OTATlvHeader header; + Encoding::LittleEndian::Reader reader(block.data(), sizeof(header)); + + ReturnErrorOnFailure(reader.Read32(&header.tag).StatusCode()); + ReturnErrorOnFailure(reader.Read32(&header.length).StatusCode()); + + auto pair = mProcessorMap.find(header.tag); + if (pair == mProcessorMap.end()) + { + ChipLogError(SoftwareUpdate, "There is no registered processor for tag: %lu", header.tag); + return CHIP_OTA_PROCESSOR_NOT_REGISTERED; + } + + ChipLogDetail(SoftwareUpdate, "Selected processor with tag: %lu", pair->first); + mCurrentProcessor = pair->second; + mCurrentProcessor->SetLength(header.length); + mCurrentProcessor->SetWasSelected(true); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::RegisterProcessor(uint32_t tag, OTATlvProcessor * processor) +{ + auto pair = mProcessorMap.find(tag); + if (pair != mProcessorMap.end()) + { + ChipLogError(SoftwareUpdate, "A processor for tag %lu is already registered.", tag); + return CHIP_OTA_PROCESSOR_ALREADY_REGISTERED; + } + + mProcessorMap.insert({ tag, processor }); + + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::HandleAbort(intptr_t context) +{ + ChipLogError(SoftwareUpdate, "OTA was aborted"); + auto * imageProcessor = reinterpret_cast(context); + if (imageProcessor != nullptr) + { + imageProcessor->AbortAllProcessors(); + } + imageProcessor->Clear(); +} + +void OTAMultiImageProcessorImpl::HandleProcessBlock(intptr_t context) +{ + auto * imageProcessor = reinterpret_cast(context); + + VerifyOrReturn(imageProcessor != nullptr, ChipLogError(SoftwareUpdate, "ImageProcessor context is null")); + + VerifyOrReturn(imageProcessor->mDownloader != nullptr, ChipLogError(SoftwareUpdate, "mDownloader is null")); + + CHIP_ERROR status; + auto block = ByteSpan(imageProcessor->mBlock.data(), imageProcessor->mBlock.size()); + + if (imageProcessor->mHeaderParser.IsInitialized()) + { + status = imageProcessor->ProcessHeader(block); + if (status != CHIP_NO_ERROR) + { + imageProcessor->HandleStatus(status); + } + } + + status = imageProcessor->ProcessPayload(block); + imageProcessor->HandleStatus(status); +} + +void OTAMultiImageProcessorImpl::HandleStatus(CHIP_ERROR status) +{ + if (status == CHIP_NO_ERROR || status == CHIP_ERROR_BUFFER_TOO_SMALL) + { + mParams.downloadedBytes += mBlock.size(); + FetchNextData(0); + } + else if (status == CHIP_OTA_FETCH_ALREADY_SCHEDULED) + { + mParams.downloadedBytes += mBlock.size(); + } + else + { + ChipLogError(SoftwareUpdate, "Image update canceled. Failed to process OTA block: %s", ErrorStr(status)); + GetRequestorInstance()->CancelImageUpdate(); + } +} + +void OTAMultiImageProcessorImpl::AbortAllProcessors() +{ + ChipLogError(SoftwareUpdate, "All selected processors will call abort action"); + + for (auto const & pair : mProcessorMap) + { + if (pair.second->WasSelected()) + { + pair.second->Clear(); + pair.second->SetWasSelected(false); + } + } +} + +bool OTAMultiImageProcessorImpl::IsFirstImageRun() +{ + OTARequestorInterface * requestor = chip::GetRequestorInstance(); + if (requestor == nullptr) + { + return false; + } + + return requestor->GetCurrentUpdateState() == OTARequestorInterface::OTAUpdateStateEnum::kApplying; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ConfirmCurrentImage() +{ + uint32_t currentVersion; + uint32_t targetVersion; + + OTARequestorInterface * requestor = chip::GetRequestorInstance(); + ReturnErrorCodeIf(requestor == nullptr, CHIP_ERROR_INTERNAL); + + targetVersion = requestor->GetTargetVersion(); + ReturnErrorOnFailure(DeviceLayer::ConfigurationMgr().GetSoftwareVersion(currentVersion)); + if (currentVersion != targetVersion) + { + ChipLogError(SoftwareUpdate, "Current sw version %lu is different than the expected sw version = %lu", currentVersion, + targetVersion); + return CHIP_ERROR_INCORRECT_STATE; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAMultiImageProcessorImpl::SetBlock(ByteSpan & block) +{ + if (block.empty()) + { + return CHIP_NO_ERROR; + } + + if (mBlock.size() < block.size()) + { + if (!mBlock.empty()) + { + ReleaseBlock(); + } + uint8_t * mBlock_ptr = static_cast(chip::Platform::MemoryAlloc(block.size())); + if (mBlock_ptr == nullptr) + { + return CHIP_ERROR_NO_MEMORY; + } + mBlock = MutableByteSpan(mBlock_ptr, block.size()); + } + + CHIP_ERROR err = CopySpanToMutableSpan(block, mBlock); + if (err != CHIP_NO_ERROR) + { + ChipLogError(SoftwareUpdate, "Cannot copy block data: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::HandleFinalize(intptr_t context) +{ + ChipLogError(SoftwareUpdate, "HandleFinalize begin"); + CHIP_ERROR error = CHIP_NO_ERROR; + auto * imageProcessor = reinterpret_cast(context); + if (imageProcessor == nullptr) + { + return; + } + + error = imageProcessor->ProcessFinalize(); + + imageProcessor->mParams.downloadedBytes += imageProcessor->mBlock.size(); + + imageProcessor->ReleaseBlock(); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(SoftwareUpdate, "ProcessFinalize() error"); + imageProcessor->mDownloader->EndDownload(CHIP_ERROR_WRITE_FAILED); + return; + } + + ChipLogProgress(SoftwareUpdate, "OTA image downloaded successfully"); +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ProcessFinalize() +{ + for (auto const & pair : this->mProcessorMap) + { + pair.second->FinalizeAction(); + } + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::HandleApply(intptr_t context) +{ + CHIP_ERROR error = CHIP_NO_ERROR; + auto * imageProcessor = reinterpret_cast(context); + + ChipLogProgress(SoftwareUpdate, "HandleApply: started"); + + // Force KVS to store pending keys such as data from StoreCurrentUpdateInfo() + chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl().ForceKeyMapSave(); + + if (imageProcessor == nullptr) + { + return; + } + + for (auto const & pair : imageProcessor->mProcessorMap) + { + if (pair.second->WasSelected()) + { + error = pair.second->ApplyAction(); + if (error != CHIP_NO_ERROR) + { + ChipLogError(SoftwareUpdate, "Apply action for tag %d processor failed.", (uint8_t) pair.first); + // Revert all previously applied actions if current apply action fails. + // Reset image processor and requestor states. + imageProcessor->AbortAllProcessors(); + imageProcessor->Clear(); + GetRequestorInstance()->Reset(); + + return; + } + } + } + + for (auto const & pair : imageProcessor->mProcessorMap) + { + pair.second->Clear(); + pair.second->SetWasSelected(false); + } + + imageProcessor->mAccumulator.Clear(); + + ChipLogProgress(SoftwareUpdate, "HandleApply: Finished"); + + // TODO: check where to put this + // ConfigurationManagerImpl().StoreSoftwareUpdateCompleted(); + + // This reboots the device + CORE_CRITICAL_SECTION(bootloader_rebootAndInstall();) +} + +CHIP_ERROR OTAMultiImageProcessorImpl::ReleaseBlock() +{ + if (mBlock.data() != nullptr) + { + chip::Platform::MemoryFree(mBlock.data()); + } + + mBlock = MutableByteSpan(); + return CHIP_NO_ERROR; +} + +void OTAMultiImageProcessorImpl::FetchNextData(uint32_t context) +{ + auto * imageProcessor = &OTAMultiImageProcessorImpl::GetDefaultInstance(); + SystemLayer().ScheduleLambda([imageProcessor] { + if (imageProcessor->mDownloader) + { + imageProcessor->mDownloader->FetchNextData(); + } + }); +} + +OTAMultiImageProcessorImpl & OTAMultiImageProcessorImpl::GetDefaultInstance() +{ + return gImageProcessor; +} + +} // namespace chip diff --git a/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h new file mode 100644 index 00000000000000..3e9cf0dbb96c3f --- /dev/null +++ b/src/platform/silabs/multi-ota/OTAMultiImageProcessorImpl.h @@ -0,0 +1,99 @@ +/* + * + * Copyright (c) 2021-2023 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/* + * This hook is called at the end of OTAMultiImageProcessorImpl::Init. + * It should generally register the OTATlvProcessor instances. + */ + +namespace chip { + +class OTAMultiImageProcessorImpl : public OTAImageProcessorInterface +{ +public: + using ProviderLocation = chip::OTARequestorInterface::ProviderLocationType; + + CHIP_ERROR Init(OTADownloader * downloader); + CHIP_ERROR OtaHookInit(); + static CHIP_ERROR ProcessDescriptor(void * descriptor); + void Clear(); + + //////////// OTAImageProcessorInterface Implementation /////////////// + CHIP_ERROR PrepareDownload() override; + CHIP_ERROR Finalize() override; + CHIP_ERROR Apply() override; + CHIP_ERROR Abort() override; + CHIP_ERROR ProcessBlock(ByteSpan & block) override; + bool IsFirstImageRun() override; + CHIP_ERROR ConfirmCurrentImage() override; + + void SetOTADownloader(OTADownloader * downloader) { mDownloader = downloader; } + + CHIP_ERROR ProcessHeader(ByteSpan & block); + CHIP_ERROR ProcessPayload(ByteSpan & block); + CHIP_ERROR ProcessFinalize(); + CHIP_ERROR SelectProcessor(ByteSpan & block); + CHIP_ERROR RegisterProcessor(uint32_t tag, OTATlvProcessor * processor); + + static void FetchNextData(uint32_t context); + static OTAMultiImageProcessorImpl & GetDefaultInstance(); + +private: + //////////// Actual handlers for the OTAImageProcessorInterface /////////////// + static void HandlePrepareDownload(intptr_t context); + static void HandleFinalize(intptr_t context); + static void HandleApply(intptr_t context); + static void HandleAbort(intptr_t context); + static void HandleProcessBlock(intptr_t context); + + void HandleStatus(CHIP_ERROR status); + + /** + * Called to allocate memory for mBlock if necessary and set it to block + */ + CHIP_ERROR SetBlock(ByteSpan & block); + + /** + * Called to release allocated memory for mBlock + */ + CHIP_ERROR ReleaseBlock(); + + /** + * Call AbortAction for all processors that were used + */ + void AbortAllProcessors(); + + MutableByteSpan mBlock; + OTADownloader * mDownloader; + OTAImageHeaderParser mHeaderParser; + OTATlvProcessor * mCurrentProcessor = nullptr; + OTADataAccumulator mAccumulator; + std::map mProcessorMap; +}; + +} // namespace chip diff --git a/src/platform/silabs/multi-ota/OTATlvProcessor.cpp b/src/platform/silabs/multi-ota/OTATlvProcessor.cpp new file mode 100644 index 00000000000000..a5da7eaba00c10 --- /dev/null +++ b/src/platform/silabs/multi-ota/OTATlvProcessor.cpp @@ -0,0 +1,171 @@ +/* + * + * Copyright (c) 2023 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 +#if OTA_ENCRYPTION_ENABLE +#include "OtaUtils.h" +#include "rom_aes.h" +#endif +namespace chip { + +#if OTA_ENCRYPTION_ENABLE +constexpr uint8_t au8Iv[] = { 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00 }; +#endif +CHIP_ERROR OTATlvProcessor::Process(ByteSpan & block) +{ + CHIP_ERROR status = CHIP_NO_ERROR; + uint32_t bytes = chip::min(mLength - mProcessedLength, static_cast(block.size())); + ByteSpan relevantData = block.SubSpan(0, bytes); + + status = ProcessInternal(relevantData); + if (!IsError(status)) + { + mProcessedLength += bytes; + block = block.SubSpan(bytes); + if (mProcessedLength == mLength) + { + status = ExitAction(); + if (!IsError(status)) + { + // If current block was processed fully and the block still contains data, it + // means that the block contains another TLV's data and the current processor + // should be changed by OTAMultiImageProcessorImpl. + return CHIP_OTA_CHANGE_PROCESSOR; + } + } + } + + return status; +} + +void OTATlvProcessor::ClearInternal() +{ + mLength = 0; + mProcessedLength = 0; + mWasSelected = false; +#if OTA_ENCRYPTION_ENABLE + mIVOffset = 0; +#endif +} + +bool OTATlvProcessor::IsError(CHIP_ERROR & status) +{ + return status != CHIP_NO_ERROR && status != CHIP_ERROR_BUFFER_TOO_SMALL && status != CHIP_OTA_FETCH_ALREADY_SCHEDULED; +} + +void OTADataAccumulator::Init(uint32_t threshold) +{ + mThreshold = threshold; + mBufferOffset = 0; + mBuffer.Alloc(mThreshold); +} + +void OTADataAccumulator::Clear() +{ + mThreshold = 0; + mBufferOffset = 0; + mBuffer.Free(); +} + +CHIP_ERROR OTADataAccumulator::Accumulate(ByteSpan & block) +{ + uint32_t numBytes = chip::min(mThreshold - mBufferOffset, static_cast(block.size())); + memcpy(&mBuffer[mBufferOffset], block.data(), numBytes); + mBufferOffset += numBytes; + block = block.SubSpan(numBytes); + + if (mBufferOffset < mThreshold) + { + return CHIP_ERROR_BUFFER_TOO_SMALL; + } + + return CHIP_NO_ERROR; +} + +#if OTA_ENCRYPTION_ENABLE +CHIP_ERROR OTATlvProcessor::vOtaProcessInternalEncryption(MutableByteSpan & block) +{ + uint8_t iv[16]; + uint8_t key[kOTAEncryptionKeyLength]; + uint8_t dataOut[16] = { 0 }; + uint32_t u32IVCount; + uint32_t Offset = 0; + uint8_t data; + tsReg128 sKey; + aesContext_t Context; + + memcpy(iv, au8Iv, sizeof(au8Iv)); + + u32IVCount = (((uint32_t) iv[12]) << 24) | (((uint32_t) iv[13]) << 16) | (((uint32_t) iv[14]) << 8) | (iv[15]); + u32IVCount += (mIVOffset >> 4); + + iv[12] = (uint8_t) ((u32IVCount >> 24) & 0xff); + iv[13] = (uint8_t) ((u32IVCount >> 16) & 0xff); + iv[14] = (uint8_t) ((u32IVCount >> 8) & 0xff); + iv[15] = (uint8_t) (u32IVCount & 0xff); + + if (Encoding::HexToBytes(OTA_ENCRYPTION_KEY, strlen(OTA_ENCRYPTION_KEY), key, kOTAEncryptionKeyLength) != + kOTAEncryptionKeyLength) + { + // Failed to convert the OTAEncryptionKey string to octstr type value + return CHIP_ERROR_INVALID_STRING_LENGTH; + } + + ByteSpan KEY = ByteSpan(key); + Encoding::LittleEndian::Reader reader_key(KEY.data(), KEY.size()); + ReturnErrorOnFailure(reader_key.Read32(&sKey.u32register0) + .Read32(&sKey.u32register1) + .Read32(&sKey.u32register2) + .Read32(&sKey.u32register3) + .StatusCode()); + + while (Offset + 16 <= block.size()) + { + /*Encrypt the IV*/ + Context.mode = AES_MODE_ECB_ENCRYPT; + Context.pSoftwareKey = (uint32_t *) &sKey; + AES_128_ProcessBlocks(&Context, (uint32_t *) &iv[0], (uint32_t *) &dataOut[0], 1); + + /* Decrypt a block of the buffer */ + for (uint8_t i = 0; i < 16; i++) + { + data = block[Offset + i] ^ dataOut[i]; + memcpy(&block[Offset + i], &data, sizeof(uint8_t)); + } + + /* increment the IV for the next block */ + u32IVCount++; + + iv[12] = (uint8_t) ((u32IVCount >> 24) & 0xff); + iv[13] = (uint8_t) ((u32IVCount >> 16) & 0xff); + iv[14] = (uint8_t) ((u32IVCount >> 8) & 0xff); + iv[15] = (uint8_t) (u32IVCount & 0xff); + + Offset += 16; /* increment the buffer offset */ + mIVOffset += 16; + } + + return CHIP_NO_ERROR; +} +#endif +} // namespace chip diff --git a/src/platform/silabs/multi-ota/OTATlvProcessor.h b/src/platform/silabs/multi-ota/OTATlvProcessor.h new file mode 100644 index 00000000000000..9e8e56a2b35825 --- /dev/null +++ b/src/platform/silabs/multi-ota/OTATlvProcessor.h @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2023 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. + */ + +#pragma once + +#include +#include +#include + +namespace chip { + +#define CHIP_ERROR_TLV_PROCESSOR(e) \ + ChipError(ChipError::Range::kLastRange, ((uint8_t) ChipError::Range::kLastRange << 3) | e, __FILE__, __LINE__) + +#define CHIP_OTA_TLV_CONTINUE_PROCESSING CHIP_ERROR_TLV_PROCESSOR(0x01) +#define CHIP_OTA_CHANGE_PROCESSOR CHIP_ERROR_TLV_PROCESSOR(0x02) +#define CHIP_OTA_PROCESSOR_NOT_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x03) +#define CHIP_OTA_PROCESSOR_ALREADY_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x04) +#define CHIP_OTA_PROCESSOR_CLIENT_INIT CHIP_ERROR_TLV_PROCESSOR(0x05) +#define CHIP_OTA_PROCESSOR_MAKE_ROOM CHIP_ERROR_TLV_PROCESSOR(0x06) +#define CHIP_OTA_PROCESSOR_PUSH_CHUNK CHIP_ERROR_TLV_PROCESSOR(0x07) +#define CHIP_OTA_PROCESSOR_IMG_AUTH CHIP_ERROR_TLV_PROCESSOR(0x08) +#define CHIP_OTA_FETCH_ALREADY_SCHEDULED CHIP_ERROR_TLV_PROCESSOR(0x09) +#define CHIP_OTA_PROCESSOR_IMG_COMMIT CHIP_ERROR_TLV_PROCESSOR(0x0A) +#define CHIP_OTA_PROCESSOR_CB_NOT_REGISTERED CHIP_ERROR_TLV_PROCESSOR(0x0B) +#define CHIP_OTA_PROCESSOR_EEPROM_OFFSET CHIP_ERROR_TLV_PROCESSOR(0x0C) +#define CHIP_OTA_PROCESSOR_EXTERNAL_STORAGE CHIP_ERROR_TLV_PROCESSOR(0x0D) +#define CHIP_OTA_PROCESSOR_START_IMAGE CHIP_ERROR_TLV_PROCESSOR(0x0E) +#define SL_GENERIC_OTA_ERROR CHIP_ERROR_TLV_PROCESSOR(0x0E) + +// Descriptor constants +inline constexpr size_t kVersionStringSize = 64; +inline constexpr size_t kBuildDateSize = 64; + +/** + * Used alongside RegisterDescriptorCallback to register + * a custom descriptor processing function with a certain + * TLV processor. + */ +typedef CHIP_ERROR (*ProcessDescriptor)(void * descriptor); + +struct OTATlvHeader +{ + uint32_t tag; + uint32_t length; +}; + +/** + * This class defines an interface for a Matter TLV processor. + * Instances of derived classes can be registered as processors + * in OTAMultiImageProcessorImpl. Based on the TLV type, a certain + * processor is used to process subsequent blocks until the number + * of bytes found in the metadata is processed. In case a block contains + * data from two different TLVs, the processor should ensure the remaining + * data is returned in the block passed as input. + * The default processors: application, SSBL and factory data are registered + * in OTAMultiImageProcessorImpl::Init through OtaHookInit. + * Applications should use OTAMultiImageProcessorImpl::RegisterProcessor + * to register additional processors. + */ +class OTATlvProcessor +{ +public: + virtual ~OTATlvProcessor() {} + + virtual CHIP_ERROR Init() = 0; + virtual CHIP_ERROR Clear() = 0; + virtual CHIP_ERROR ApplyAction() = 0; + virtual CHIP_ERROR FinalizeAction() = 0; + virtual CHIP_ERROR ExitAction() { return CHIP_NO_ERROR; } + + CHIP_ERROR Process(ByteSpan & block); + void RegisterDescriptorCallback(ProcessDescriptor callback) { mCallbackProcessDescriptor = callback; } + void SetLength(uint32_t length) { mLength = length; } + void SetWasSelected(bool selected) { mWasSelected = selected; } + bool WasSelected() { return mWasSelected; } +#if OTA_ENCRYPTION_ENABLE + CHIP_ERROR vOtaProcessInternalEncryption(MutableByteSpan & block); +#endif + +protected: + /** + * @brief Process custom TLV payload + * + * The method takes subsequent chunks of the Matter OTA image file and processes them. + * If more image chunks are needed, CHIP_ERROR_BUFFER_TOO_SMALL error is returned. + * Other error codes indicate that an error occurred during processing. Fetching + * next data is scheduled automatically by OTAMultiImageProcessorImpl if the return value + * is neither an error code, nor CHIP_OTA_FETCH_ALREADY_SCHEDULED (which implies the + * scheduling is done inside ProcessInternal or will be done in the future, through a + * callback). + * + * @param block Byte span containing a subsequent Matter OTA image chunk. When the method + * returns CHIP_NO_ERROR, the byte span is used to return a remaining part + * of the chunk, not used by current TLV processor. + * + * @retval CHIP_NO_ERROR Block was processed successfully. + * @retval CHIP_ERROR_BUFFER_TOO_SMALL Provided buffers are insufficient to decode some + * metadata (e.g. a descriptor). + * @retval CHIP_OTA_FETCH_ALREADY_SCHEDULED Should be returned if ProcessInternal schedules + * fetching next data (e.g. through a callback). + * @retval Error code Something went wrong. Current OTA process will be + * canceled. + */ + virtual CHIP_ERROR ProcessInternal(ByteSpan & block) = 0; + + void ClearInternal(); + + bool IsError(CHIP_ERROR & status); + +#if OTA_ENCRYPTION_ENABLE + /*ota decryption*/ + uint32_t mIVOffset = 0; + /* Expected byte size of the OTAEncryptionKeyLength */ + static constexpr size_t kOTAEncryptionKeyLength = 16; +#endif + uint32_t mLength = 0; + uint32_t mProcessedLength = 0; + bool mWasSelected = false; + ProcessDescriptor mCallbackProcessDescriptor = nullptr; +}; + +/** + * This class can be used to accumulate data until a given threshold. + * Should be used by OTATlvProcessor derived classes if they need + * metadata accumulation (e.g. for custom header decoding). + */ +class OTADataAccumulator +{ +public: + void Init(uint32_t threshold); + void Clear(); + CHIP_ERROR Accumulate(ByteSpan & block); + + inline uint8_t * data() { return mBuffer.Get(); } + inline uint32_t GetThreshold() { return mThreshold; } + +private: + uint32_t mThreshold; + uint32_t mBufferOffset; + Platform::ScopedMemoryBuffer mBuffer; +}; + +} // namespace chip diff --git a/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp new file mode 100644 index 00000000000000..fdf4c3d0321262 --- /dev/null +++ b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.cpp @@ -0,0 +1,224 @@ +/* + * + * Copyright (c) 2023 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 + +extern "C" { +#include "btl_interface.h" +#include "em_bus.h" // For CORE_CRITICAL_SECTION +#if SL_WIFI +#include "spi_multiplex.h" +#endif // SL_WIFI +} + +/// No error, operation OK +#define SL_BOOTLOADER_OK 0L +// TODO: more descriptive error codes +#define SL_OTA_ERROR 1L + +namespace chip { + +// Define static memebers +uint8_t OTAFirmwareProcessor::mSlotId = 0; +uint32_t OTAFirmwareProcessor::mWriteOffset = 0; +uint16_t OTAFirmwareProcessor::writeBufOffset = 0; +uint8_t OTAFirmwareProcessor::writeBuffer[kAlignmentBytes] __attribute__((aligned(4))) = { 0 }; + +CHIP_ERROR OTAFirmwareProcessor::Init() +{ + ReturnErrorCodeIf(mCallbackProcessDescriptor == nullptr, CHIP_OTA_PROCESSOR_CB_NOT_REGISTERED); + mAccumulator.Init(sizeof(Descriptor)); +#if OTA_ENCRYPTION_ENABLE + mUnalignmentNum = 0; +#endif + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAFirmwareProcessor::Clear() +{ + OTATlvProcessor::ClearInternal(); + mAccumulator.Clear(); + mDescriptorProcessed = false; +#if OTA_ENCRYPTION_ENABLE + mUnalignmentNum = 0; +#endif + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAFirmwareProcessor::ProcessInternal(ByteSpan & block) +{ + uint32_t err = SL_BOOTLOADER_OK; + if (!mDescriptorProcessed) + { + ReturnErrorOnFailure(ProcessDescriptor(block)); + } + + uint32_t blockReadOffset = 0; + while (blockReadOffset < block.size()) + { + writeBuffer[writeBufOffset] = *((block.data()) + blockReadOffset); + writeBufOffset++; + blockReadOffset++; + if (writeBufOffset == kAlignmentBytes) + { + writeBufOffset = 0; +#if SL_BTLCTRL_MUX + err = sl_wfx_host_pre_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err); + return; + } +#endif // SL_BTLCTRL_MUX + CORE_CRITICAL_SECTION(err = bootloader_eraseWriteStorage(mSlotId, mWriteOffset, writeBuffer, kAlignmentBytes);) +#if SL_BTLCTRL_MUX + err = sl_wfx_host_post_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err); + return; + } +#endif // SL_BTLCTRL_MUX + if (err) + { + ChipLogError(SoftwareUpdate, "bootloader_eraseWriteStorage() error: %ld", err); + // TODO: add this somewhere + // imageProcessor->mDownloader->EndDownload(CHIP_ERROR_WRITE_FAILED); + // TODO: Replace CHIP_ERROR_CANCELLED with new error statement + return CHIP_ERROR_CANCELLED; + } + mWriteOffset += kAlignmentBytes; + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAFirmwareProcessor::ProcessDescriptor(ByteSpan & block) +{ + ReturnErrorOnFailure(mAccumulator.Accumulate(block)); + ReturnErrorOnFailure(mCallbackProcessDescriptor(static_cast(mAccumulator.data()))); + + mDescriptorProcessed = true; + mAccumulator.Clear(); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAFirmwareProcessor::ApplyAction() +{ + uint32_t err = SL_BOOTLOADER_OK; + +#if SL_BTLCTRL_MUX + err = sl_wfx_host_pre_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + CORE_CRITICAL_SECTION(err = bootloader_verifyImage(mSlotId, NULL);) + if (err != SL_BOOTLOADER_OK) + { + ChipLogError(SoftwareUpdate, "bootloader_verifyImage() error: %ld", err); + // Call the OTARequestor API to reset the state + GetRequestorInstance()->CancelImageUpdate(); +#if SL_BTLCTRL_MUX + err = sl_wfx_host_post_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + return SL_GENERIC_OTA_ERROR; + } + + CORE_CRITICAL_SECTION(err = bootloader_setImageToBootload(mSlotId);) + if (err != SL_BOOTLOADER_OK) + { + ChipLogError(SoftwareUpdate, "bootloader_setImageToBootload() error: %ld", err); + // Call the OTARequestor API to reset the state + GetRequestorInstance()->CancelImageUpdate(); +#if SL_BTLCTRL_MUX + err = sl_wfx_host_post_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + return SL_GENERIC_OTA_ERROR; + } + +#if SL_BTLCTRL_MUX + err = sl_wfx_host_post_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + // This reboots the device + // CORE_CRITICAL_SECTION(bootloader_rebootAndInstall();) + + return CHIP_NO_ERROR; +} + +CHIP_ERROR OTAFirmwareProcessor::FinalizeAction() +{ + uint32_t err = SL_BOOTLOADER_OK; + + // Pad the remainder of the write buffer with zeros and write it to bootloader storage + if (writeBufOffset != 0) + { + + while (writeBufOffset != kAlignmentBytes) + { + writeBuffer[writeBufOffset] = 0; + writeBufOffset++; + } +#if SL_BTLCTRL_MUX + err = sl_wfx_host_pre_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_pre_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + CORE_CRITICAL_SECTION(err = bootloader_eraseWriteStorage(mSlotId, mWriteOffset, writeBuffer, kAlignmentBytes);) +#if SL_BTLCTRL_MUX + err = sl_wfx_host_post_bootloader_spi_transfer(); + if (err != SL_STATUS_OK) + { + ChipLogError(SoftwareUpdate, "sl_wfx_host_post_bootloader_spi_transfer() error: %ld", err); + return SL_GENERIC_OTA_ERROR; + } +#endif // SL_BTLCTRL_MUX + } + + return err ? CHIP_ERROR_WRITE_FAILED : CHIP_NO_ERROR; +} + +} // namespace chip diff --git a/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h new file mode 100644 index 00000000000000..c383e7497b8b1a --- /dev/null +++ b/src/platform/silabs/multi-ota/efr32/OTAFirmwareProcessor.h @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2023 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. + */ + +#pragma once + +#include +#include + +namespace chip { + +class OTAFirmwareProcessor : public OTATlvProcessor +{ +public: + struct Descriptor + { + uint32_t version; + char versionString[kVersionStringSize]; + char buildDate[kBuildDateSize]; + }; + + CHIP_ERROR Init() override; + CHIP_ERROR Clear() override; + CHIP_ERROR ApplyAction() override; + CHIP_ERROR FinalizeAction() override; + +private: + CHIP_ERROR ProcessInternal(ByteSpan & block) override; + CHIP_ERROR ProcessDescriptor(ByteSpan & block); + + OTADataAccumulator mAccumulator; + bool mDescriptorProcessed = false; +#if OTA_ENCRYPTION_ENABLE + uint32_t mUnalignmentNum; +#endif + static constexpr size_t kAlignmentBytes = 64; + static uint32_t mWriteOffset; // End of last written block + static uint8_t mSlotId; // Bootloader storage slot + // Bootloader storage API requires the buffer size to be a multiple of 4. + static uint8_t writeBuffer[kAlignmentBytes] __attribute__((aligned(4))); + // Offset indicates how far the write buffer has been filled + static uint16_t writeBufOffset; +}; + +} // namespace chip diff --git a/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp b/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp new file mode 100644 index 00000000000000..cddb66980c27f9 --- /dev/null +++ b/src/platform/silabs/multi-ota/efr32/OTAHooks.cpp @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2023 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 + +CHIP_ERROR chip::OTAMultiImageProcessorImpl::ProcessDescriptor(void * descriptor) +{ + auto desc = static_cast(descriptor); + ChipLogDetail(SoftwareUpdate, "Descriptor: %ld, %s, %s", desc->version, desc->versionString, desc->buildDate); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::OTAMultiImageProcessorImpl::OtaHookInit() +{ + static chip::OTAFirmwareProcessor sApplicationProcessor; + + sApplicationProcessor.RegisterDescriptorCallback(ProcessDescriptor); + + auto & imageProcessor = chip::OTAMultiImageProcessorImpl::GetDefaultInstance(); + ReturnErrorOnFailure(imageProcessor.RegisterProcessor(1, &sApplicationProcessor)); + + return CHIP_NO_ERROR; +} diff --git a/third_party/silabs/efr32_sdk.gni b/third_party/silabs/efr32_sdk.gni index 55934ae5128b0f..9a2a1ec8153376 100644 --- a/third_party/silabs/efr32_sdk.gni +++ b/third_party/silabs/efr32_sdk.gni @@ -77,6 +77,9 @@ declare_args() { # Use SLC generated files slc_reuse_files = false + + # Multi-chip OTA + chip_enable_multi_ota_requestor = false } examples_plat_dir = "${chip_root}/examples/platform/silabs/efr32" silabs_plat_efr32_wifi_dir = "${chip_root}/src/platform/silabs/efr32/wifi" From 3802bda585b56ef04198aa8c9c94c35ea0c4ad97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 11 Mar 2024 15:50:07 +0100 Subject: [PATCH 20/76] Add TVOC in Linux Air Quality Example README (#32517) * Add TVOC in Linux Air Quality Example README Add TVOC in Linux Air Quality Example README * Update .wordlist.txt to add 'TVOC' --- .github/.wordlist.txt | 1 + examples/air-quality-sensor-app/linux/README.md | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 21708845b4ccfb..018ed267925642 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -1440,6 +1440,7 @@ ttymxc ttyUSB TurbidityConcentrationMeasurement TvCasting +TVOC tvOS TXD txt diff --git a/examples/air-quality-sensor-app/linux/README.md b/examples/air-quality-sensor-app/linux/README.md index 38a0adfb2fe7bf..651afa4c6297ee 100644 --- a/examples/air-quality-sensor-app/linux/README.md +++ b/examples/air-quality-sensor-app/linux/README.md @@ -183,3 +183,10 @@ Generate event `Pm10ConcentrationMeasurement`, to change the PM10 value. ``` echo '{"Name":"Pm10ConcentrationMeasurement","NewValue":10}' > /tmp/chip_air_quality_fifo_ ``` + +Generate event `TotalVolatileOrganicCompoundsConcentrationMeasurement`, to +change the TVOC value. + +``` +$ echo '{"Name":"TotalVolatileOrganicCompoundsConcentrationMeasurement","NewValue":100}' > /tmp/chip_air_quality_fifo_ +``` From bdb88bd9295b830fc2920cb40a71486702708852 Mon Sep 17 00:00:00 2001 From: Pradip De Date: Mon, 11 Mar 2024 08:46:28 -0700 Subject: [PATCH 21/76] Use Optional variable instead of ExchangeContext in PairingSession. (#32499) This ensures that the ExchangeContext for the session is automatically reference counted without explicit manual Retain() and Release() calls. As a result, the ExchangeContext is held until PairingSession::Clear() gets called that, in turn, calls ClearValue() on the Optional to internally call Release() on the underlying target. Fixes Issue #32498. --- src/protocols/secure_channel/CASESession.cpp | 50 +++++++++++-------- src/protocols/secure_channel/PASESession.cpp | 45 +++++++++-------- .../secure_channel/PairingSession.cpp | 28 +++++------ src/protocols/secure_channel/PairingSession.h | 12 ++--- 4 files changed, 73 insertions(+), 62 deletions(-) diff --git a/src/protocols/secure_channel/CASESession.cpp b/src/protocols/secure_channel/CASESession.cpp index a9b62489783794..5fda1968ef2d74 100644 --- a/src/protocols/secure_channel/CASESession.cpp +++ b/src/protocols/secure_channel/CASESession.cpp @@ -514,7 +514,7 @@ CHIP_ERROR CASESession::EstablishSession(SessionManager & sessionManager, Fabric // We are setting the exchange context specifically before checking for error. // This is to make sure the exchange will get closed if Init() returned an error. - mExchangeCtxt = exchangeCtxt; + mExchangeCtxt.Emplace(*exchangeCtxt); // From here onwards, let's go to exit on error, as some state might have already // been initialized @@ -527,7 +527,7 @@ CHIP_ERROR CASESession::EstablishSession(SessionManager & sessionManager, Fabric mSessionResumptionStorage = sessionResumptionStorage; mLocalMRPConfig = mrpLocalConfig.ValueOr(GetDefaultMRPConfig()); - mExchangeCtxt->UseSuggestedResponseTimeout(kExpectedSigma1ProcessingTime); + mExchangeCtxt.Value()->UseSuggestedResponseTimeout(kExpectedSigma1ProcessingTime); mPeerNodeId = peerScopedNodeId.GetNodeId(); mLocalNodeId = fabricInfo->GetNodeId(); @@ -549,7 +549,8 @@ void CASESession::OnResponseTimeout(ExchangeContext * ec) { MATTER_TRACE_SCOPE("OnResponseTimeout", "CASESession"); VerifyOrReturn(ec != nullptr, ChipLogError(SecureChannel, "CASESession::OnResponseTimeout was called by null exchange")); - VerifyOrReturn(mExchangeCtxt == ec, ChipLogError(SecureChannel, "CASESession::OnResponseTimeout exchange doesn't match")); + VerifyOrReturn(mExchangeCtxt.HasValue() && (&mExchangeCtxt.Value().Get() == ec), + ChipLogError(SecureChannel, "CASESession::OnResponseTimeout exchange doesn't match")); ChipLogError(SecureChannel, "CASESession timed out while waiting for a response from the peer. Current state was %u", to_underlying(mState)); MATTER_TRACE_COUNTER("CASETimeout"); @@ -735,8 +736,8 @@ CHIP_ERROR CASESession::SendSigma1() ReturnErrorOnFailure(mCommissioningHash.AddData(ByteSpan{ msg_R1->Start(), msg_R1->DataLength() })); // Call delegate to send the msg to peer - ReturnErrorOnFailure(mExchangeCtxt->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma1, std::move(msg_R1), - SendFlags(SendMessageFlags::kExpectResponse))); + ReturnErrorOnFailure(mExchangeCtxt.Value()->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma1, std::move(msg_R1), + SendFlags(SendMessageFlags::kExpectResponse))); mState = resuming ? State::kSentSigma1Resume : State::kSentSigma1; @@ -959,8 +960,9 @@ CHIP_ERROR CASESession::SendSigma2Resume() ReturnErrorOnFailure(tlvWriter.Finalize(&msg_R2_resume)); // Call delegate to send the msg to peer - ReturnErrorOnFailure(mExchangeCtxt->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma2Resume, std::move(msg_R2_resume), - SendFlags(SendMessageFlags::kExpectResponse))); + ReturnErrorOnFailure(mExchangeCtxt.Value()->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma2Resume, + std::move(msg_R2_resume), + SendFlags(SendMessageFlags::kExpectResponse))); mState = State::kSentSigma2Resume; @@ -1096,8 +1098,8 @@ CHIP_ERROR CASESession::SendSigma2() ReturnErrorOnFailure(mCommissioningHash.AddData(ByteSpan{ msg_R2->Start(), msg_R2->DataLength() })); // Call delegate to send the msg to peer - ReturnErrorOnFailure(mExchangeCtxt->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma2, std::move(msg_R2), - SendFlags(SendMessageFlags::kExpectResponse))); + ReturnErrorOnFailure(mExchangeCtxt.Value()->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma2, std::move(msg_R2), + SendFlags(SendMessageFlags::kExpectResponse))); mState = State::kSentSigma2; @@ -1148,7 +1150,8 @@ CHIP_ERROR CASESession::HandleSigma2Resume(System::PacketBufferHandle && msg) if (tlvReader.Next() != CHIP_END_OF_TLV) { SuccessOrExit(err = DecodeMRPParametersIfPresent(TLV::ContextTag(4), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); } ChipLogDetail(SecureChannel, "Peer assigned session session ID %d", responderSessionId); @@ -1341,7 +1344,8 @@ CHIP_ERROR CASESession::HandleSigma2(System::PacketBufferHandle && msg) if (tlvReader.Next() != CHIP_END_OF_TLV) { SuccessOrExit(err = DecodeMRPParametersIfPresent(TLV::ContextTag(kTag_Sigma2_ResponderMRPParams), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); } exit: @@ -1410,7 +1414,7 @@ CHIP_ERROR CASESession::SendSigma3a() { SuccessOrExit(err = helper->ScheduleWork()); mSendSigma3Helper = helper; - mExchangeCtxt->WillSendMessage(); + mExchangeCtxt.Value()->WillSendMessage(); mState = State::kSendSigma3Pending; } else @@ -1537,8 +1541,8 @@ CHIP_ERROR CASESession::SendSigma3c(SendSigma3Data & data, CHIP_ERROR status) SuccessOrExit(err); // Call delegate to send the Msg3 to peer - err = mExchangeCtxt->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma3, std::move(msg_R3), - SendFlags(SendMessageFlags::kExpectResponse)); + err = mExchangeCtxt.Value()->SendMessage(Protocols::SecureChannel::MsgType::CASE_Sigma3, std::move(msg_R3), + SendFlags(SendMessageFlags::kExpectResponse)); SuccessOrExit(err); ChipLogProgress(SecureChannel, "Sent Sigma3 msg"); @@ -1704,7 +1708,7 @@ CHIP_ERROR CASESession::HandleSigma3a(System::PacketBufferHandle && msg) SuccessOrExit(err = helper->ScheduleWork()); mHandleSigma3Helper = helper; - mExchangeCtxt->WillSendMessage(); + mExchangeCtxt.Value()->WillSendMessage(); mState = State::kHandleSigma3Pending; } @@ -2036,7 +2040,8 @@ CHIP_ERROR CASESession::ParseSigma1(TLV::ContiguousBufferTLVReader & tlvReader, if (err == CHIP_NO_ERROR && tlvReader.GetTag() == ContextTag(kInitiatorMRPParamsTag)) { ReturnErrorOnFailure(DecodeMRPParametersIfPresent(TLV::ContextTag(kInitiatorMRPParamsTag), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); err = tlvReader.Next(); } @@ -2092,18 +2097,19 @@ CHIP_ERROR CASESession::ValidateReceivedMessage(ExchangeContext * ec, const Payl // mExchangeCtxt can be nullptr if this is the first message (CASE_Sigma1) received by CASESession // via UnsolicitedMessageHandler. The exchange context is allocated by exchange manager and provided // to the handler (CASESession object). - if (mExchangeCtxt != nullptr) + if (mExchangeCtxt.HasValue()) { - if (mExchangeCtxt != ec) + if (&mExchangeCtxt.Value().Get() != ec) { ReturnErrorOnFailure(CHIP_ERROR_INVALID_ARGUMENT); } } else { - mExchangeCtxt = ec; + mExchangeCtxt.SetValue(ExchangeHandle(*ec)); + mExchangeCtxt.Emplace(*ec); } - mExchangeCtxt->UseSuggestedResponseTimeout(kExpectedHighProcessingTime); + mExchangeCtxt.Value()->UseSuggestedResponseTimeout(kExpectedHighProcessingTime); VerifyOrReturnError(!msg.IsNull(), CHIP_ERROR_INVALID_ARGUMENT); return CHIP_NO_ERROR; @@ -2128,7 +2134,7 @@ CHIP_ERROR CASESession::OnMessageReceived(ExchangeContext * ec, const PayloadHea // // Should you need to resume the CASESession, you could theoretically pass along the msg to a callback that gets // registered when setting mStopHandshakeAtState. - mExchangeCtxt->WillSendMessage(); + mExchangeCtxt.Value()->WillSendMessage(); return CHIP_NO_ERROR; } #endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST @@ -2138,7 +2144,7 @@ CHIP_ERROR CASESession::OnMessageReceived(ExchangeContext * ec, const PayloadHea msgType == Protocols::SecureChannel::MsgType::CASE_Sigma2Resume || msgType == Protocols::SecureChannel::MsgType::CASE_Sigma3) { - SuccessOrExit(err = mExchangeCtxt->FlushAcks()); + SuccessOrExit(err = mExchangeCtxt.Value()->FlushAcks()); } #endif // CHIP_CONFIG_SLOW_CRYPTO diff --git a/src/protocols/secure_channel/PASESession.cpp b/src/protocols/secure_channel/PASESession.cpp index 40dc67793604e2..ca2524d5e3f302 100644 --- a/src/protocols/secure_channel/PASESession.cpp +++ b/src/protocols/secure_channel/PASESession.cpp @@ -218,12 +218,12 @@ CHIP_ERROR PASESession::Pair(SessionManager & sessionManager, uint32_t peerSetUp mRole = CryptoContext::SessionRole::kInitiator; - mExchangeCtxt = exchangeCtxt; + mExchangeCtxt.Emplace(*exchangeCtxt); // When commissioning starts, the peer is assumed to be active. - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->MarkActiveRx(); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->MarkActiveRx(); - mExchangeCtxt->UseSuggestedResponseTimeout(kExpectedLowProcessingTime); + mExchangeCtxt.Value()->UseSuggestedResponseTimeout(kExpectedLowProcessingTime); mLocalMRPConfig = mrpLocalConfig.ValueOr(GetDefaultMRPConfig()); @@ -244,7 +244,7 @@ void PASESession::OnResponseTimeout(ExchangeContext * ec) { MATTER_TRACE_SCOPE("OnResponseTimeout", "PASESession"); VerifyOrReturn(ec != nullptr, ChipLogError(SecureChannel, "PASESession::OnResponseTimeout was called by null exchange")); - VerifyOrReturn(mExchangeCtxt == nullptr || mExchangeCtxt == ec, + VerifyOrReturn(!mExchangeCtxt.HasValue() || &mExchangeCtxt.Value().Get() == ec, ChipLogError(SecureChannel, "PASESession::OnResponseTimeout exchange doesn't match")); // If we were waiting for something, mNextExpectedMsg had better have a value. ChipLogError(SecureChannel, "PASESession timed out while waiting for a response from the peer. Expected message type was %u", @@ -308,8 +308,8 @@ CHIP_ERROR PASESession::SendPBKDFParamRequest() // Update commissioning hash with the pbkdf2 param request that's being sent. ReturnErrorOnFailure(mCommissioningHash.AddData(ByteSpan{ req->Start(), req->DataLength() })); - ReturnErrorOnFailure( - mExchangeCtxt->SendMessage(MsgType::PBKDFParamRequest, std::move(req), SendFlags(SendMessageFlags::kExpectResponse))); + ReturnErrorOnFailure(mExchangeCtxt.Value()->SendMessage(MsgType::PBKDFParamRequest, std::move(req), + SendFlags(SendMessageFlags::kExpectResponse))); mNextExpectedMsg.SetValue(MsgType::PBKDFParamResponse); @@ -364,7 +364,8 @@ CHIP_ERROR PASESession::HandlePBKDFParamRequest(System::PacketBufferHandle && ms if (tlvReader.Next() != CHIP_END_OF_TLV) { SuccessOrExit(err = DecodeMRPParametersIfPresent(TLV::ContextTag(5), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); } err = SendPBKDFParamResponse(ByteSpan(initiatorRandom), hasPBKDFParameters); @@ -428,8 +429,8 @@ CHIP_ERROR PASESession::SendPBKDFParamResponse(ByteSpan initiatorRandom, bool in ReturnErrorOnFailure(mCommissioningHash.AddData(ByteSpan{ resp->Start(), resp->DataLength() })); ReturnErrorOnFailure(SetupSpake2p()); - ReturnErrorOnFailure( - mExchangeCtxt->SendMessage(MsgType::PBKDFParamResponse, std::move(resp), SendFlags(SendMessageFlags::kExpectResponse))); + ReturnErrorOnFailure(mExchangeCtxt.Value()->SendMessage(MsgType::PBKDFParamResponse, std::move(resp), + SendFlags(SendMessageFlags::kExpectResponse))); ChipLogDetail(SecureChannel, "Sent PBKDF param response"); mNextExpectedMsg.SetValue(MsgType::PASE_Pake1); @@ -483,7 +484,8 @@ CHIP_ERROR PASESession::HandlePBKDFParamResponse(System::PacketBufferHandle && m if (tlvReader.Next() != CHIP_END_OF_TLV) { SuccessOrExit(err = DecodeMRPParametersIfPresent(TLV::ContextTag(5), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); } // TODO - Add a unit test that exercises mHavePBKDFParameters path @@ -508,7 +510,8 @@ CHIP_ERROR PASESession::HandlePBKDFParamResponse(System::PacketBufferHandle && m if (tlvReader.Next() != CHIP_END_OF_TLV) { SuccessOrExit(err = DecodeMRPParametersIfPresent(TLV::ContextTag(5), tlvReader)); - mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters(GetRemoteSessionParameters()); + mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->SetRemoteSessionParameters( + GetRemoteSessionParameters()); } } @@ -558,7 +561,7 @@ CHIP_ERROR PASESession::SendMsg1() ReturnErrorOnFailure(tlvWriter.Finalize(&msg)); ReturnErrorOnFailure( - mExchangeCtxt->SendMessage(MsgType::PASE_Pake1, std::move(msg), SendFlags(SendMessageFlags::kExpectResponse))); + mExchangeCtxt.Value()->SendMessage(MsgType::PASE_Pake1, std::move(msg), SendFlags(SendMessageFlags::kExpectResponse))); ChipLogDetail(SecureChannel, "Sent spake2p msg1"); mNextExpectedMsg.SetValue(MsgType::PASE_Pake2); @@ -620,7 +623,8 @@ CHIP_ERROR PASESession::HandleMsg1_and_SendMsg2(System::PacketBufferHandle && ms SuccessOrExit(err = tlvWriter.EndContainer(outerContainerType)); SuccessOrExit(err = tlvWriter.Finalize(&msg2)); - err = mExchangeCtxt->SendMessage(MsgType::PASE_Pake2, std::move(msg2), SendFlags(SendMessageFlags::kExpectResponse)); + err = + mExchangeCtxt.Value()->SendMessage(MsgType::PASE_Pake2, std::move(msg2), SendFlags(SendMessageFlags::kExpectResponse)); SuccessOrExit(err); mNextExpectedMsg.SetValue(MsgType::PASE_Pake3); @@ -696,7 +700,8 @@ CHIP_ERROR PASESession::HandleMsg2_and_SendMsg3(System::PacketBufferHandle && ms SuccessOrExit(err = tlvWriter.EndContainer(outerContainerType)); SuccessOrExit(err = tlvWriter.Finalize(&msg3)); - err = mExchangeCtxt->SendMessage(MsgType::PASE_Pake3, std::move(msg3), SendFlags(SendMessageFlags::kExpectResponse)); + err = + mExchangeCtxt.Value()->SendMessage(MsgType::PASE_Pake3, std::move(msg3), SendFlags(SendMessageFlags::kExpectResponse)); SuccessOrExit(err); mNextExpectedMsg.SetValue(MsgType::StatusReport); @@ -786,25 +791,25 @@ CHIP_ERROR PASESession::ValidateReceivedMessage(ExchangeContext * exchange, cons // mExchangeCtxt can be nullptr if this is the first message (PBKDFParamRequest) received by PASESession // via UnsolicitedMessageHandler. The exchange context is allocated by exchange manager and provided // to the handler (PASESession object). - if (mExchangeCtxt != nullptr) + if (mExchangeCtxt.HasValue()) { - if (mExchangeCtxt != exchange) + if (&mExchangeCtxt.Value().Get() != exchange) { ReturnErrorOnFailure(CHIP_ERROR_INVALID_ARGUMENT); } } else { - mExchangeCtxt = exchange; + mExchangeCtxt.Emplace(*exchange); } - if (!mExchangeCtxt->GetSessionHandle()->IsUnauthenticatedSession()) + if (!mExchangeCtxt.Value()->GetSessionHandle()->IsUnauthenticatedSession()) { ChipLogError(SecureChannel, "PASESession received PBKDFParamRequest over encrypted session. Ignoring."); return CHIP_ERROR_INCORRECT_STATE; } - mExchangeCtxt->UseSuggestedResponseTimeout(kExpectedHighProcessingTime); + mExchangeCtxt.Value()->UseSuggestedResponseTimeout(kExpectedHighProcessingTime); VerifyOrReturnError(!msg.IsNull(), CHIP_ERROR_INVALID_ARGUMENT); VerifyOrReturnError((mNextExpectedMsg.HasValue() && payloadHeader.HasMessageType(mNextExpectedMsg.Value())) || @@ -833,7 +838,7 @@ CHIP_ERROR PASESession::OnMessageReceived(ExchangeContext * exchange, const Payl if (msgType == MsgType::PBKDFParamRequest || msgType == MsgType::PBKDFParamResponse || msgType == MsgType::PASE_Pake1 || msgType == MsgType::PASE_Pake2 || msgType == MsgType::PASE_Pake3) { - SuccessOrExit(err = mExchangeCtxt->FlushAcks()); + SuccessOrExit(err = mExchangeCtxt.Value()->FlushAcks()); } #endif // CHIP_CONFIG_SLOW_CRYPTO diff --git a/src/protocols/secure_channel/PairingSession.cpp b/src/protocols/secure_channel/PairingSession.cpp index 6a6d03f43ebc65..1f7874bdf115dc 100644 --- a/src/protocols/secure_channel/PairingSession.cpp +++ b/src/protocols/secure_channel/PairingSession.cpp @@ -56,7 +56,7 @@ CHIP_ERROR PairingSession::ActivateSecureSession(const Transport::PeerAddress & void PairingSession::Finish() { - Transport::PeerAddress address = mExchangeCtxt->GetSessionHandle()->AsUnauthenticatedSession()->GetPeerAddress(); + Transport::PeerAddress address = mExchangeCtxt.Value()->GetSessionHandle()->AsUnauthenticatedSession()->GetPeerAddress(); // Discard the exchange so that Clear() doesn't try closing it. The exchange will handle that. DiscardExchange(); @@ -79,14 +79,15 @@ void PairingSession::Finish() void PairingSession::DiscardExchange() { - if (mExchangeCtxt != nullptr) + if (mExchangeCtxt.HasValue()) { // Make sure the exchange doesn't try to notify us when it closes, // since we might be dead by then. - mExchangeCtxt->SetDelegate(nullptr); + mExchangeCtxt.Value()->SetDelegate(nullptr); + // Null out mExchangeCtxt so that Clear() doesn't try closing it. The // exchange will handle that. - mExchangeCtxt = nullptr; + mExchangeCtxt.ClearValue(); } } @@ -227,19 +228,18 @@ bool PairingSession::IsSessionEstablishmentInProgress() void PairingSession::Clear() { - // Clear acts like the destructor if PairingSession, if it is call during - // middle of a pairing, means we should terminate the exchange. For normal - // path, the exchange should already be discarded before calling Clear. - if (mExchangeCtxt != nullptr) + // Clear acts like the destructor of PairingSession. If it is called during + // the middle of pairing, that means we should terminate the exchange. For the + // normal path, the exchange should already be discarded before calling Clear. + if (mExchangeCtxt.HasValue()) { - // The only time we reach this is if we are getting destroyed in the - // middle of our handshake. In that case, there is no point trying to - // do MRP resends of the last message we sent, so abort the exchange + // The only time we reach this is when we are getting destroyed in the + // middle of our handshake. In that case, there is no point in trying to + // do MRP resends of the last message we sent. So, abort the exchange // instead of just closing it. - mExchangeCtxt->Abort(); - mExchangeCtxt = nullptr; + mExchangeCtxt.Value()->Abort(); + mExchangeCtxt.ClearValue(); } - mSecureSessionHolder.Release(); mPeerSessionId.ClearValue(); mSessionManager = nullptr; diff --git a/src/protocols/secure_channel/PairingSession.h b/src/protocols/secure_channel/PairingSession.h index f49dbf7997f4e2..ea69f65bfaccb7 100644 --- a/src/protocols/secure_channel/PairingSession.h +++ b/src/protocols/secure_channel/PairingSession.h @@ -139,14 +139,14 @@ class DLL_EXPORT PairingSession : public SessionDelegate return CHIP_ERROR_INTERNAL; } - void SendStatusReport(Messaging::ExchangeContext * exchangeCtxt, uint16_t protocolCode) + void SendStatusReport(Optional & exchangeCtxt, uint16_t protocolCode) { Protocols::SecureChannel::GeneralStatusCode generalCode = (protocolCode == Protocols::SecureChannel::kProtocolCodeSuccess) ? Protocols::SecureChannel::GeneralStatusCode::kSuccess : Protocols::SecureChannel::GeneralStatusCode::kFailure; ChipLogDetail(SecureChannel, "Sending status report. Protocol code %d, exchange %d", protocolCode, - exchangeCtxt->GetExchangeId()); + exchangeCtxt.Value()->GetExchangeId()); Protocols::SecureChannel::StatusReport statusReport(generalCode, Protocols::SecureChannel::Id, protocolCode); @@ -159,7 +159,7 @@ class DLL_EXPORT PairingSession : public SessionDelegate System::PacketBufferHandle msg = bbuf.Finalize(); VerifyOrReturn(!msg.IsNull(), ChipLogError(SecureChannel, "Failed to allocate status report message")); - CHIP_ERROR err = exchangeCtxt->SendMessage(Protocols::SecureChannel::MsgType::StatusReport, std::move(msg)); + CHIP_ERROR err = exchangeCtxt.Value()->SendMessage(Protocols::SecureChannel::MsgType::StatusReport, std::move(msg)); if (err != CHIP_NO_ERROR) { ChipLogError(SecureChannel, "Failed to send status report message: %" CHIP_ERROR_FORMAT, err.Format()); @@ -238,9 +238,9 @@ class DLL_EXPORT PairingSession : public SessionDelegate SessionHolderWithDelegate mSecureSessionHolder; // mSessionManager is set if we actually allocate a secure session, so we // can clean it up later as needed. - SessionManager * mSessionManager = nullptr; - Messaging::ExchangeContext * mExchangeCtxt = nullptr; - SessionEstablishmentDelegate * mDelegate = nullptr; + SessionManager * mSessionManager = nullptr; + Optional mExchangeCtxt = NullOptional; + SessionEstablishmentDelegate * mDelegate = nullptr; // mLocalMRPConfig is our config which is sent to the other end and used by the peer session. // mRemoteSessionParams is received from other end and set to our session. From adcc07aecb0f7d0445825790ef0dda03075fca24 Mon Sep 17 00:00:00 2001 From: Pradip De Date: Mon, 11 Mar 2024 12:13:16 -0700 Subject: [PATCH 22/76] Remove redundant SetValue() for Optional ExchangeHandle in CASESession.cpp. (#32535) --- src/protocols/secure_channel/CASESession.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/protocols/secure_channel/CASESession.cpp b/src/protocols/secure_channel/CASESession.cpp index 5fda1968ef2d74..eec7c61763763a 100644 --- a/src/protocols/secure_channel/CASESession.cpp +++ b/src/protocols/secure_channel/CASESession.cpp @@ -2106,7 +2106,6 @@ CHIP_ERROR CASESession::ValidateReceivedMessage(ExchangeContext * ec, const Payl } else { - mExchangeCtxt.SetValue(ExchangeHandle(*ec)); mExchangeCtxt.Emplace(*ec); } mExchangeCtxt.Value()->UseSuggestedResponseTimeout(kExpectedHighProcessingTime); From 5aa5555df210f09a224007e26e2d8a368d1c9e90 Mon Sep 17 00:00:00 2001 From: Vijay Selvaraj Date: Mon, 11 Mar 2024 15:51:26 -0400 Subject: [PATCH 23/76] Added script to generate a basic RevocationSet from TestNet or MainNet (#30837) * Added script to generate a basic RevocationSet from TestNet or MainNet * Updated the script to generate a basic RevocationSet from TestNet or MainNet * Updated the script to generate a basic RevocationSet from TestNet or MainNet * Addressed generate-revocation-set.py review comments * Added comments to capture the follow-up work in the generate-revocation-set.py file. --- credentials/generate-revocation-set.py | 248 +++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 credentials/generate-revocation-set.py diff --git a/credentials/generate-revocation-set.py b/credentials/generate-revocation-set.py new file mode 100644 index 00000000000000..534edc15b0f203 --- /dev/null +++ b/credentials/generate-revocation-set.py @@ -0,0 +1,248 @@ +#!/usr/bin/python + +# +# Copyright (c) 2023 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. +# + +# Generates a basic RevocationSet from TestNet +# Usage: +# python ./credentials/generate-revocation-set.py --help + +import base64 +import json +import subprocess +import sys +from enum import Enum + +import click +import requests +from click_option_group import RequiredMutuallyExclusiveOptionGroup, optgroup +from cryptography import x509 + + +class RevocationType(Enum): + CRL = 1 + + +OID_VENDOR_ID = x509.ObjectIdentifier("1.3.6.1.4.1.37244.2.1") +OID_PRODUCT_ID = x509.ObjectIdentifier("1.3.6.1.4.1.37244.2.2") + +PRODUCTION_NODE_URL = "https://on.dcl.csa-iot.org:26657" +PRODUCTION_NODE_URL_REST = "https://on.dcl.csa-iot.org" +TEST_NODE_URL_REST = "https://on.test-net.dcl.csa-iot.org" + + +def use_dcld(dcld, production, cmdlist): + return [dcld] + cmdlist + (['--node', PRODUCTION_NODE_URL] if production else []) + + +def extract_single_integer_attribute(subject, oid): + attribute_list = subject.get_attributes_for_oid(oid) + + if len(attribute_list) == 1: + if attribute_list[0].value.isdigit(): + return int(attribute_list[0].value) + + return None + + +@click.command() +@click.help_option('-h', '--help') +@optgroup.group('Input data sources', cls=RequiredMutuallyExclusiveOptionGroup) +@optgroup.option('--use-main-net-dcld', type=str, default='', metavar='PATH', help="Location of `dcld` binary, to use `dcld` for mirroring MainNet.") +@optgroup.option('--use-test-net-dcld', type=str, default='', metavar='PATH', help="Location of `dcld` binary, to use `dcld` for mirroring TestNet.") +@optgroup.option('--use-main-net-http', is_flag=True, type=str, help="Use RESTful API with HTTPS against public MainNet observer.") +@optgroup.option('--use-test-net-http', is_flag=True, type=str, help="Use RESTful API with HTTPS against public TestNet observer.") +@optgroup.group('Optional arguments') +@optgroup.option('--output', default='sample_revocation_set_list.json', type=str, metavar='FILEPATH', help="Output filename (default: sample_revocation_set_list.json)") +def main(use_main_net_dcld, use_test_net_dcld, use_main_net_http, use_test_net_http, output): + """DCL PAA mirroring tools""" + + production = False + dcld = use_test_net_dcld + + if len(use_main_net_dcld) > 0: + dcld = use_main_net_dcld + production = True + + use_rest = use_main_net_http or use_test_net_http + if use_main_net_http: + production = True + + rest_node_url = PRODUCTION_NODE_URL_REST if production else TEST_NODE_URL_REST + + # TODO: Extract this to a helper function + if use_rest: + revocation_point_list = requests.get(f"{rest_node_url}/dcl/pki/revocation-points").json()["PkiRevocationDistributionPoint"] + else: + cmdlist = ['config', 'output', 'json'] + subprocess.Popen([dcld] + cmdlist) + + cmdlist = ['query', 'pki', 'all-revocation-points'] + + cmdpipe = subprocess.Popen(use_dcld(dcld, production, cmdlist), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + revocation_point_list = json.loads(cmdpipe.stdout.read())["PkiRevocationDistributionPoint"] + + revocation_set = [] + + for revocation_point in revocation_point_list: + # 1. Validate Revocation Type + if revocation_point["revocationType"] != RevocationType.CRL: + continue + + # 2. Parse the certificate + crl_signer_certificate = x509.load_pem_x509_certificate(revocation_point["crlSignerCertificate"]) + + vid = revocation_point["vid"] + pid = revocation_point["pid"] + is_paa = revocation_point["isPAA"] + + # 3. && 4. Validate VID/PID + # TODO: Need to support alternate representation of VID/PID (see spec "6.2.2.2. Encoding of Vendor ID and Product ID in subject and issuer fields") + crl_vid = extract_single_integer_attribute(crl_signer_certificate.subject, OID_VENDOR_ID) + crl_pid = extract_single_integer_attribute(crl_signer_certificate.subject, OID_PRODUCT_ID) + + if is_paa: + if crl_vid is not None: + if vid != crl_vid: + # TODO: Need to log all situations where a continue is called + continue + else: + if crl_vid is None or vid != crl_vid: + continue + if crl_pid is not None: + if pid != crl_pid: + continue + + # 5. Validate the certification path containing CRLSignerCertificate. + crl_signer_issuer_name = base64.b64encode(crl_signer_certificate.issuer.public_bytes()).decode('utf-8') + + crl_signer_authority_key_id = crl_signer_certificate.extensions.get_extension_for_oid( + x509.OID_AUTHORITY_KEY_IDENTIFIER).value.key_identifier + + paa_certificate = None + + # TODO: Extract this to a helper function + if use_rest: + response = requests.get( + f"{rest_node_url}/dcl/pki/certificates/{crl_signer_issuer_name}/{crl_signer_authority_key_id}").json()["approvedCertificates"]["certs"][0] + paa_certificate = response["pemCert"] + else: + cmdlist = ['query', 'pki', 'x509-cert', '-u', crl_signer_issuer_name, '-k', crl_signer_authority_key_id] + cmdpipe = subprocess.Popen(use_dcld(dcld, production, cmdlist), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + paa_certificate = json.loads(cmdpipe.stdout.read())["approvedCertificates"]["certs"][0]["pemCert"] + + if paa_certificate is None: + continue + + paa_certificate_object = x509.load_pem_x509_certificate(paa_certificate) + + try: + crl_signer_certificate.verify_directly_issued_by(paa_certificate_object) + except Exception: + continue + + # 6. Obtain the CRL + r = requests.get(revocation_point["dataURL"]) + crl_file = x509.load_der_x509_crl(r.content) + + # 7. Perform CRL File Validation + crl_authority_key_id = crl_file.extensions.get_extension_for_oid(x509.OID_AUTHORITY_KEY_IDENTIFIER).value.key_identifier + crl_signer_subject_key_id = crl_signer_certificate.extensions.get_extension_for_oid( + x509.OID_SUBJECT_KEY_IDENTIFIER).value.key_identifier + if crl_authority_key_id != crl_signer_subject_key_id: + continue + + issuer_subject_key_id = ''.join('{:02X}'.format(x) for x in crl_authority_key_id) + + same_issuer_points = None + + # TODO: Extract this to a helper function + if use_rest: + response = requests.get( + f"{rest_node_url}/dcl/pki/revocation-points/{issuer_subject_key_id}").json()["pkiRevocationDistributionPointsByIssuerSubjectKeyID"] + same_issuer_points = response["points"] + else: + cmdlist = ['query', 'pki', 'revocation-points', '--issuer-subject-key-id', issuer_subject_key_id] + cmdpipe = subprocess.Popen(use_dcld(dcld, production, cmdlist), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + same_issuer_points = json.loads(cmdpipe.stdout.read())[ + "pkiRevocationDistributionPointsByIssuerSubjectKeyID"]["points"] + + matching_entries = False + for same_issuer_point in same_issuer_points: + if same_issuer_point["vid"] == vid: + matching_entries = True + break + + if matching_entries: + try: + issuing_distribution_point = crl_file.extensions.get_extension_for_oid( + x509.OID_ISSUING_DISTRIBUTION_POINT).value + except Exception: + continue + + uri_list = issuing_distribution_point.full_name + if len(uri_list) == 1 and isinstance(uri_list[0], x509.UniformResourceIdentifier): + if uri_list[0].value != revocation_point["dataURL"]: + continue + else: + continue + + # 9. Assign CRL File Issuer + certificate_authority_name = base64.b64encode(crl_file.issuer.public_bytes()).decode('utf-8') + + serialnumber_list = [] + # 10. Iterate through the Revoked Certificates List + for revoked_cert in crl_file: + try: + revoked_cert_issuer = revoked_cert.extensions.get_extension_for_oid( + x509.CRLEntryExtensionOID.CERTIFICATE_ISSUER).value.get_values_for_type(x509.DirectoryName).value + + if revoked_cert_issuer is not None: + if revoked_cert_issuer != certificate_authority_name: + continue + except Exception: + pass + + # b. + try: + revoked_cert_authority_key_id = revoked_cert.extensions.get_extension_for_oid( + x509.OID_AUTHORITY_KEY_IDENTIFIER).value.key_identifier + + if revoked_cert_authority_key_id is None or revoked_cert_authority_key_id != crl_signer_subject_key_id: + continue + except Exception: + continue + + # c. and d. + serialnumber_list.append(bytes(str('{:02X}'.format(revoked_cert.serial_number)), 'utf-8').decode('utf-8')) + + issuer_name = base64.b64encode(crl_file.issuer.public_bytes()).decode('utf-8') + + revocation_set.append({"type": "revocation_set", + "issuer_subject_key_id": issuer_subject_key_id, + "issuer_name": issuer_name, + "revoked_serial_numbers": serialnumber_list}) + + with open(output, 'w+') as outfile: + json.dump(revocation_set, outfile, indent=4) + + +if __name__ == "__main__": + if len(sys.argv) == 1: + main.main(['--help']) + else: + main() From 622ca76afe23798c54c7529956a9136d7d440619 Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:20:17 +1300 Subject: [PATCH 24/76] Get the commissioner reusable more quickly after StopPairing() (#32473) * Get the commissioner reusable more quickly after StopPairing() Don't need to keep state and the CommissioneeDeviceProxy around; instead hold on to just the Session and evict it when the disarm request succeeds or fails. * Tweaks from review --- src/controller/CHIPDeviceController.cpp | 85 ++++++++++++++++------ src/controller/CHIPDeviceController.h | 4 +- src/controller/CommissioneeDeviceProxy.cpp | 9 +++ src/controller/CommissioneeDeviceProxy.h | 5 ++ 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 35c616ac0426db..0b384a3ee0c672 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -1658,13 +1658,30 @@ void DeviceCommissioner::OnBasicFailure(void * context, CHIP_ERROR error) commissioner->CommissioningStageComplete(error); } +static GeneralCommissioning::Commands::ArmFailSafe::Type DisarmFailsafeRequest() +{ + GeneralCommissioning::Commands::ArmFailSafe::Type request; + request.expiryLengthSeconds = 0; // Expire immediately. + request.breadcrumb = 0; + return request; +} + +static void MarkForEviction(const Optional & session) +{ + if (session.HasValue()) + { + session.Value()->AsSecureSession()->MarkForEviction(); + } +} + void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId, const CompletionStatus & completionStatus) { + // At this point, proxy == mDeviceBeingCommissioned, nodeId == mDeviceBeingCommissioned->GetDeviceId() + mCommissioningCompletionStatus = completionStatus; if (completionStatus.err == CHIP_NO_ERROR) { - CommissioneeDeviceProxy * commissionee = FindCommissioneeDevice(nodeId); if (commissionee != nullptr) { @@ -1674,13 +1691,40 @@ void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId CommissioningStageComplete(CHIP_NO_ERROR); SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); } - else if (completionStatus.failedStage.HasValue() && completionStatus.failedStage.Value() >= kWiFiNetworkSetup && - completionStatus.err != CHIP_ERROR_CANCELLED) + else if (completionStatus.err == CHIP_ERROR_CANCELLED) + { + // If we're cleaning up because cancellation has been requested via StopPairing(), expire the failsafe + // in the background and reset our state synchronously, so a new commissioning attempt can be started. + CommissioneeDeviceProxy * commissionee = FindCommissioneeDevice(nodeId); + SessionHolder session((commissionee == proxy) ? commissionee->DetachSecureSession().Value() + : proxy->GetSecureSession().Value()); + + auto request = DisarmFailsafeRequest(); + auto onSuccessCb = [session](const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatus, + const decltype(request)::ResponseType & responseData) { + ChipLogProgress(Controller, "Failsafe disarmed"); + MarkForEviction(session.Get()); + }; + auto onFailureCb = [session](CHIP_ERROR aError) { + ChipLogProgress(Controller, "Ignoring failure to disarm failsafe: %" CHIP_ERROR_FORMAT, aError.Format()); + MarkForEviction(session.Get()); + }; + + ChipLogProgress(Controller, "Disarming failsafe on device %p in background", proxy); + CHIP_ERROR err = InvokeCommandRequest(proxy->GetExchangeManager(), session.Get().Value(), kRootEndpointId, request, + onSuccessCb, onFailureCb); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Failed to send command to disarm fail-safe: %" CHIP_ERROR_FORMAT, err.Format()); + } + + CleanupDoneAfterError(); + } + else if (completionStatus.failedStage.HasValue() && completionStatus.failedStage.Value() >= kWiFiNetworkSetup) { // If we were already doing network setup, we need to retain the pase session and start again from network setup stage. // We do not need to reset the failsafe here because we want to keep everything on the device up to this point, so just - // send the completion callbacks (see "Commissioning Flows Error Handling" in the spec). This does not apply if - // we're cleaning up because cancellation has been requested via StopPairing(). + // send the completion callbacks (see "Commissioning Flows Error Handling" in the spec). CommissioningStageComplete(CHIP_NO_ERROR); SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); } @@ -1689,21 +1733,14 @@ void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId // If we've failed somewhere in the early stages (or we don't have a failedStage specified), we need to start from the // beginning. However, because some of the commands can only be sent once per arm-failsafe, we also need to force a reset on // the failsafe so we can start fresh on the next attempt. - GeneralCommissioning::Commands::ArmFailSafe::Type request; - request.expiryLengthSeconds = 0; // Expire immediately. - request.breadcrumb = 0; - ChipLogProgress(Controller, "Expiring failsafe on proxy %p", proxy); - mDeviceBeingCommissioned = proxy; - // We actually want to do the same thing on success or failure because we're already in a failure state - CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnDisarmFailsafe, OnDisarmFailsafeFailure, kRootEndpointId, - /* timeout = */ NullOptional); + ChipLogProgress(Controller, "Disarming failsafe on device %p", proxy); + auto request = DisarmFailsafeRequest(); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, OnDisarmFailsafe, OnDisarmFailsafeFailure, kRootEndpointId); if (err != CHIP_NO_ERROR) { - // We won't get any async callbacks here, so just pretend like the - // command errored out async. + // We won't get any async callbacks here, so just pretend like the command errored out async. ChipLogError(Controller, "Failed to send command to disarm fail-safe: %" CHIP_ERROR_FORMAT, err.Format()); - DisarmDone(); - return; + CleanupDoneAfterError(); } } } @@ -1713,17 +1750,17 @@ void DeviceCommissioner::OnDisarmFailsafe(void * context, { ChipLogProgress(Controller, "Failsafe disarmed"); DeviceCommissioner * commissioner = static_cast(context); - commissioner->DisarmDone(); + commissioner->CleanupDoneAfterError(); } void DeviceCommissioner::OnDisarmFailsafeFailure(void * context, CHIP_ERROR error) { ChipLogProgress(Controller, "Ignoring failure to disarm failsafe: %" CHIP_ERROR_FORMAT, error.Format()); DeviceCommissioner * commissioner = static_cast(context); - commissioner->DisarmDone(); + commissioner->CleanupDoneAfterError(); } -void DeviceCommissioner::DisarmDone() +void DeviceCommissioner::CleanupDoneAfterError() { // If someone nulled out our mDeviceBeingCommissioned, there's nothing else // to do here. @@ -1735,13 +1772,15 @@ void DeviceCommissioner::DisarmDone() // Signal completion - this will reset mDeviceBeingCommissioned. CommissioningStageComplete(CHIP_NO_ERROR); - SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); // If we've disarmed the failsafe, it's because we're starting again, so kill the pase connection. if (commissionee != nullptr) { ReleaseCommissioneeDevice(commissionee); } + + // Invoke callbacks last, after we have cleared up all state. + SendCommissioningCompleteCallbacks(nodeId, mCommissioningCompletionStatus); } void DeviceCommissioner::SendCommissioningCompleteCallbacks(NodeId nodeId, const CompletionStatus & completionStatus) @@ -2572,13 +2611,11 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio params.GetCompletionStatus().err.AsString()); } - // For now, we ignore errors coming in from the device since not all commissioning clusters are implemented on the device - // side. mCommissioningStage = step; mCommissioningDelegate = delegate; mDeviceBeingCommissioned = proxy; - // TODO: Extend timeouts to the DAC and Opcert requests. + // TODO: Extend timeouts to the DAC and Opcert requests. // TODO(cecille): We probably want something better than this for breadcrumbs. uint64_t breadcrumb = static_cast(step); diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h index ee4ac09ac84ebe..87d31dbae4abfc 100644 --- a/src/controller/CHIPDeviceController.h +++ b/src/controller/CHIPDeviceController.h @@ -908,7 +908,7 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, static void OnDisarmFailsafe(void * context, const app::Clusters::GeneralCommissioning::Commands::ArmFailSafeResponse::DecodableType & data); static void OnDisarmFailsafeFailure(void * context, CHIP_ERROR error); - void DisarmDone(); + void CleanupDoneAfterError(); static void OnArmFailSafeExtendedForDeviceAttestation( void * context, const chip::app::Clusters::GeneralCommissioning::Commands::ArmFailSafeResponse::DecodableType & data); static void OnFailedToExtendedArmFailSafeDeviceAttestation(void * context, CHIP_ERROR error); @@ -961,7 +961,7 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, CHIP_ERROR SendCommissioningCommand(DeviceProxy * device, const RequestObjectT & request, CommandResponseSuccessCallback successCb, CommandResponseFailureCallback failureCb, EndpointId endpoint, - Optional timeout); + Optional timeout = NullOptional); void SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, app::AttributePathParams * readPaths, size_t readPathsSize); void CancelCommissioningInteractions(); diff --git a/src/controller/CommissioneeDeviceProxy.cpp b/src/controller/CommissioneeDeviceProxy.cpp index 48ba4e4fd28552..376685abf5e679 100644 --- a/src/controller/CommissioneeDeviceProxy.cpp +++ b/src/controller/CommissioneeDeviceProxy.cpp @@ -66,6 +66,15 @@ void CommissioneeDeviceProxy::CloseSession() mPairing.Clear(); } +chip::Optional CommissioneeDeviceProxy::DetachSecureSession() +{ + auto session = mSecureSession.Get(); + mSecureSession.Release(); + mState = ConnectionState::NotConnected; + mPairing.Clear(); + return session; +} + CHIP_ERROR CommissioneeDeviceProxy::UpdateDeviceData(const Transport::PeerAddress & addr, const ReliableMessageProtocolConfig & config) { diff --git a/src/controller/CommissioneeDeviceProxy.h b/src/controller/CommissioneeDeviceProxy.h index fa9aa17e7df3ee..715601b9a88808 100644 --- a/src/controller/CommissioneeDeviceProxy.h +++ b/src/controller/CommissioneeDeviceProxy.h @@ -105,6 +105,11 @@ class CommissioneeDeviceProxy : public DeviceProxy, public SessionDelegate */ void CloseSession(); + /** + * Detaches the underlying session (if any) from this proxy and returns it. + */ + chip::Optional DetachSecureSession(); + void Disconnect() override { CloseSession(); } /** From 6a254ac316295bc6f0e1febd1ed4cb19e214ceb0 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:37:41 +0900 Subject: [PATCH 25/76] [Android] Support ICD Check in message (#32476) * Add Android Checkin * Restyled by clang-format * Restyled by clang-format * Add enableServerInteractions comment * Restyled by whitespace * Restyled by google-java-format * Update src/controller/java/src/chip/devicecontroller/ControllerParams.java Co-authored-by: mkardous-silabs <84793247+mkardous-silabs@users.noreply.github.com> * Update ControllerParams.java address comment --------- Co-authored-by: Restyled.io Co-authored-by: yunhanw-google Co-authored-by: mkardous-silabs <84793247+mkardous-silabs@users.noreply.github.com> --- .../com/google/chip/chiptool/ChipClient.kt | 7 ++++- .../java/AndroidDeviceControllerWrapper.cpp | 11 +++++++- .../java/AndroidDeviceControllerWrapper.h | 26 ++++++++++++------- .../java/CHIPDeviceController-JNI.cpp | 8 +++++- .../devicecontroller/ControllerParams.java | 18 +++++++++++++ .../src/matter/controller/ControllerParams.kt | 1 + .../matter/controller/MatterControllerImpl.kt | 1 + 7 files changed, 59 insertions(+), 13 deletions(-) diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt index 77c59d75d00c36..6372b14c5fa71f 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt @@ -48,7 +48,12 @@ object ChipClient { if (!this::chipDeviceController.isInitialized) { chipDeviceController = - ChipDeviceController(ControllerParams.newBuilder().setControllerVendorId(VENDOR_ID).build()) + ChipDeviceController( + ControllerParams.newBuilder() + .setControllerVendorId(VENDOR_ID) + .setEnableServerInteractions(true) + .build() + ) // Set delegate for attestation trust store for device attestation verifier. // It will replace the default attestation trust store. diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp index c965bb0f17584b..0143d36e8d3c8e 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.cpp +++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp @@ -104,7 +104,7 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, bool skipCommissioningComplete, - bool skipAttestationCertificateValidation, jstring countryCode, CHIP_ERROR * errInfoOnFailure) + bool skipAttestationCertificateValidation, jstring countryCode, bool enableServerInteractions, CHIP_ERROR * errInfoOnFailure) { if (errInfoOnFailure == nullptr) { @@ -351,6 +351,9 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( setupParams.controllerNOC = nocSpan; } + initParams.enableServerInteractions = enableServerInteractions; + setupParams.enableServerInteractions = enableServerInteractions; + *errInfoOnFailure = DeviceControllerFactory::GetInstance().Init(initParams); if (*errInfoOnFailure != CHIP_NO_ERROR) { @@ -393,6 +396,11 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( wrapper->getICDClientStorage()->UpdateFabricList(wrapper->Controller()->GetFabricIndex()); + auto engine = chip::app::InteractionModelEngine::GetInstance(); + *errInfoOnFailure = wrapper->mCheckInDelegate.Init(&wrapper->mICDClientStorage, engine); + *errInfoOnFailure = wrapper->mCheckInHandler.Init(DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(), + &wrapper->mICDClientStorage, &wrapper->mCheckInDelegate, engine); + memset(ipkBuffer.data(), 0, ipkBuffer.size()); if (*errInfoOnFailure != CHIP_NO_ERROR) @@ -769,6 +777,7 @@ void AndroidDeviceControllerWrapper::OnReadCommissioningInfo(const chip::Control // For ICD mUserActiveModeTriggerHint = info.icd.userActiveModeTriggerHint; + memset(mUserActiveModeTriggerInstructionBuffer, 0x00, kUserActiveModeTriggerInstructionBufferLen); CopyCharSpanToMutableCharSpan(info.icd.userActiveModeTriggerInstruction, mUserActiveModeTriggerInstruction); env->CallVoidMethod(mJavaObjectRef.ObjectRef(), onReadCommissioningInfoMethod, static_cast(info.basic.vendorId), diff --git a/src/controller/java/AndroidDeviceControllerWrapper.h b/src/controller/java/AndroidDeviceControllerWrapper.h index 5ccb2edb3b2050..fe56c02646942c 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.h +++ b/src/controller/java/AndroidDeviceControllerWrapper.h @@ -24,6 +24,8 @@ #include +#include +#include #include #include #include @@ -170,19 +172,21 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel * @param[in] skipCommissioningComplete whether to skip the CASE commissioningComplete command during commissioning * @param[out] errInfoOnFailure a pointer to a CHIP_ERROR that will be populated if this method returns nullptr */ - static AndroidDeviceControllerWrapper * AllocateNew( - JavaVM * vm, jobject deviceControllerObj, chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats, - chip::System::Layer * systemLayer, chip::Inet::EndPointManager * tcpEndPointManager, - chip::Inet::EndPointManager * udpEndPointManager, + static AndroidDeviceControllerWrapper * + AllocateNew(JavaVM * vm, jobject deviceControllerObj, chip::NodeId nodeId, chip::FabricId fabricId, + const chip::CATValues & cats, chip::System::Layer * systemLayer, + chip::Inet::EndPointManager * tcpEndPointManager, + chip::Inet::EndPointManager * udpEndPointManager, #ifdef JAVA_MATTER_CONTROLLER_TEST - ExampleOperationalCredentialsIssuerPtr opCredsIssuer, + ExampleOperationalCredentialsIssuerPtr opCredsIssuer, #else - AndroidOperationalCredentialsIssuerPtr opCredsIssuer, + AndroidOperationalCredentialsIssuerPtr opCredsIssuer, #endif - jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, - jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, - uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, bool skipCommissioningComplete, - bool skipAttestationCertificateValidation, jstring countryCode, CHIP_ERROR * errInfoOnFailure); + jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, + jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, + uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, + bool skipCommissioningComplete, bool skipAttestationCertificateValidation, jstring countryCode, + bool enableServerInteractions, CHIP_ERROR * errInfoOnFailure); void Shutdown(); @@ -221,6 +225,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::Crypto::RawKeySessionKeystore mSessionKeystore; chip::app::DefaultICDClientStorage mICDClientStorage; + chip::app::DefaultCheckInDelegate mCheckInDelegate; + chip::app::CheckInHandler mCheckInHandler; JavaVM * mJavaVM = nullptr; chip::JniGlobalReference mJavaObjectRef; diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index f1c7487e4e3f87..30c4c573541e96 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -350,6 +350,11 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getAdminSubject", "()J", &getAdminSubject); SuccessOrExit(err); + jmethodID getEnableServerInteractions; + err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getEnableServerInteractions", "()Z", + &getEnableServerInteractions); + SuccessOrExit(err); + { uint64_t fabricId = static_cast(env->CallLongMethod(controllerParams, getFabricId)); uint16_t listenPort = static_cast(env->CallIntMethod(controllerParams, getUdpListenPort)); @@ -370,6 +375,7 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr uint64_t adminSubject = static_cast(env->CallLongMethod(controllerParams, getAdminSubject)); jobject countryCodeOptional = env->CallObjectMethod(controllerParams, getCountryCode); jobject regulatoryLocationOptional = env->CallObjectMethod(controllerParams, getRegulatoryLocation); + bool enableServerInteractions = env->CallBooleanMethod(controllerParams, getEnableServerInteractions) == JNI_TRUE; jobject countryCode; err = chip::JniReferences::GetInstance().GetOptionalValue(countryCodeOptional, countryCode); @@ -387,7 +393,7 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr DeviceLayer::TCPEndPointManager(), DeviceLayer::UDPEndPointManager(), std::move(opCredsIssuer), keypairDelegate, rootCertificate, intermediateCertificate, operationalCertificate, ipk, listenPort, controllerVendorId, failsafeTimerSeconds, attemptNetworkScanWiFi, attemptNetworkScanThread, skipCommissioningComplete, - skipAttestationCertificateValidation, static_cast(countryCode), &err); + skipAttestationCertificateValidation, static_cast(countryCode), enableServerInteractions, &err); SuccessOrExit(err); if (caseFailsafeTimerSeconds > 0) diff --git a/src/controller/java/src/chip/devicecontroller/ControllerParams.java b/src/controller/java/src/chip/devicecontroller/ControllerParams.java index ca41cf900902af..88459305d9068e 100644 --- a/src/controller/java/src/chip/devicecontroller/ControllerParams.java +++ b/src/controller/java/src/chip/devicecontroller/ControllerParams.java @@ -23,6 +23,7 @@ public final class ControllerParams { @Nullable private final byte[] operationalCertificate; @Nullable private final byte[] ipk; private final long adminSubject; + private final boolean enableServerInteractions; /** @param udpListenPort the UDP listening port, or 0 to pick any available port. */ private ControllerParams(Builder builder) { @@ -43,6 +44,7 @@ private ControllerParams(Builder builder) { this.operationalCertificate = builder.operationalCertificate; this.ipk = builder.ipk; this.adminSubject = builder.adminSubject; + this.enableServerInteractions = builder.enableServerInteractions; } public long getFabricId() { @@ -114,6 +116,10 @@ public long getAdminSubject() { return adminSubject; } + public boolean getEnableServerInteractions() { + return enableServerInteractions; + } + /** Returns parameters with ephemerally generated operational credentials */ public static Builder newBuilder() { return new Builder(); @@ -152,6 +158,7 @@ public static class Builder { @Nullable private byte[] operationalCertificate = null; @Nullable private byte[] ipk = null; private long adminSubject = 0; + private boolean enableServerInteractions = false; private Builder() {} @@ -357,6 +364,17 @@ public Builder setAdminSubject(long adminSubject) { return this; } + /** + * Controls enabling server interactions on a controller. For ICD check-in message, this feature + * has to be enabled. + * + * @param enableServerInteractions indicates whether to enable server interactions. + */ + public Builder setEnableServerInteractions(boolean enableServerInteractions) { + this.enableServerInteractions = enableServerInteractions; + return this; + } + public ControllerParams build() { return new ControllerParams(this); } diff --git a/src/controller/java/src/matter/controller/ControllerParams.kt b/src/controller/java/src/matter/controller/ControllerParams.kt index d0fb23222ea68e..35ac25549910a7 100644 --- a/src/controller/java/src/matter/controller/ControllerParams.kt +++ b/src/controller/java/src/matter/controller/ControllerParams.kt @@ -32,6 +32,7 @@ constructor( val udpListenPort: Int = UDP_PORT_AUTO, val vendorId: Int = VENDOR_ID_TEST, val countryCode: String? = null, + val enableServerInteractions: Boolean = false, ) { companion object { /** Matter assigned vendor ID for Google. */ diff --git a/src/controller/java/src/matter/controller/MatterControllerImpl.kt b/src/controller/java/src/matter/controller/MatterControllerImpl.kt index 6ae71f33a0faac..dde29eeb7c5354 100644 --- a/src/controller/java/src/matter/controller/MatterControllerImpl.kt +++ b/src/controller/java/src/matter/controller/MatterControllerImpl.kt @@ -543,6 +543,7 @@ class MatterControllerImpl(params: ControllerParams) : MatterController { .setUdpListenPort(params.udpListenPort) .setControllerVendorId(params.vendorId) .setCountryCode(params.countryCode) + .setEnableServerInteractions(params.enableServerInteractions) if (config != null) { val intermediateCertificate = config.certificateData.intermediateCertificate From a97bad9386921ea7a0c1e8cfe0e71e0b298f6a85 Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:14:41 +1300 Subject: [PATCH 26/76] DeviceCommissioner: Track ExtendArmFailSafe conditionally and manage CASE callbacks accurately (#32522) * Handle --faults argument for example apps * Add CASEServerBusy fault injection point * DeviceCommissioner: Manage CASE callbacks accurately - Ensure all callbacks are unregistered once CASE setup is done. - Cancel CASE setup callbacks on StopPairing. * DeviceCommissioner: Track ExtendArmFailSafe conditionally Only pass a context into ExtendArmFailSafe callbacks and track them as the current cancelable invocation when it is actually a part of the sequential commissioning flow. Treat as "fire and forget" when the method is called by an external client, or when we're handling CASE retries. Fixes #32521 * Address review comments * Make fireAndForget non-optional for ExtendArmFailSafeInternal --- examples/platform/linux/Options.cpp | 32 +++++ src/controller/CHIPDeviceController.cpp | 136 +++++++++--------- src/controller/CHIPDeviceController.h | 14 +- .../Framework/CHIPTests/MTRPairingTests.m | 8 ++ src/lib/support/CHIPFaultInjection.cpp | 1 + src/lib/support/CHIPFaultInjection.h | 3 +- src/protocols/secure_channel/CASEServer.cpp | 6 +- 7 files changed, 124 insertions(+), 76 deletions(-) diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp index 3017e77901adef..8bf5c0b7f4e4b4 100644 --- a/examples/platform/linux/Options.cpp +++ b/examples/platform/linux/Options.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include @@ -36,6 +38,12 @@ #include // nogncheck #endif +#if CHIP_WITH_NLFAULTINJECTION +#include +#include +#include +#endif + using namespace chip; using namespace chip::ArgParser; @@ -94,6 +102,9 @@ enum #if CONFIG_BUILD_FOR_HOST_UNIT_TEST kDeviceOption_SubscriptionResumptionRetryIntervalSec = 0x1026, #endif +#if CHIP_WITH_NLFAULTINJECTION + kDeviceOption_FaultInjection = 0x1027, +#endif }; constexpr unsigned kAppUsageLength = 64; @@ -155,6 +166,9 @@ OptionDef sDeviceOptionDefs[] = { #if CONFIG_BUILD_FOR_HOST_UNIT_TEST { "subscription-capacity", kArgumentRequired, kDeviceOption_SubscriptionCapacity }, { "subscription-resumption-retry-interval", kArgumentRequired, kDeviceOption_SubscriptionResumptionRetryIntervalSec }, +#endif +#if CHIP_WITH_NLFAULTINJECTION + { "faults", kArgumentRequired, kDeviceOption_FaultInjection }, #endif {} }; @@ -286,6 +300,10 @@ const char * sDeviceOptionHelp = " Max number of subscriptions the device will allow\n" " --subscription-resumption-retry-interval\n" " subscription timeout resumption retry interval in seconds\n" +#endif +#if CHIP_WITH_NLFAULTINJECTION + " --faults \n" + " Inject specified fault(s) at runtime.\n" #endif "\n"; @@ -556,6 +574,20 @@ bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, case kDeviceOption_SubscriptionResumptionRetryIntervalSec: LinuxDeviceOptions::GetInstance().subscriptionResumptionRetryIntervalSec = static_cast(atoi(aValue)); break; +#endif +#if CHIP_WITH_NLFAULTINJECTION + case kDeviceOption_FaultInjection: { + constexpr nl::FaultInjection::GetManagerFn faultManagerFns[] = { FaultInjection::GetManager, + Inet::FaultInjection::GetManager, + System::FaultInjection::GetManager }; + Platform::ScopedMemoryString mutableArg(aValue, strlen(aValue)); // ParseFaultInjectionStr may mutate + if (!nl::FaultInjection::ParseFaultInjectionStr(mutableArg.Get(), faultManagerFns, ArraySize(faultManagerFns))) + { + PrintArgError("%s: Invalid fault injection specification\n", aProgram); + retval = false; + } + break; + } #endif default: PrintArgError("%s: INTERNAL ERROR: Unhandled option: %s\n", aProgram, aName); diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 0b384a3ee0c672..942a488d38c2cd 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -953,6 +953,20 @@ void DeviceCommissioner::CancelCommissioningInteractions() mInvokeCancelFn(); mInvokeCancelFn = nullptr; } + if (mOnDeviceConnectedCallback.IsRegistered()) + { + ChipLogDetail(Controller, "Cancelling CASE setup for step '%s'", StageToString(mCommissioningStage)); + CancelCASECallbacks(); + } +} + +void DeviceCommissioner::CancelCASECallbacks() +{ + mOnDeviceConnectedCallback.Cancel(); + mOnDeviceConnectionFailureCallback.Cancel(); +#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES + mOnDeviceConnectionRetryCallback.Cancel(); +#endif } CHIP_ERROR DeviceCommissioner::UnpairDevice(NodeId remoteDeviceId) @@ -1216,9 +1230,10 @@ void DeviceCommissioner::OnICDManagementRegisterClientResponse( commissioner->CommissioningStageComplete(CHIP_NO_ERROR, report); } -bool DeviceCommissioner::ExtendArmFailSafe(DeviceProxy * proxy, CommissioningStage step, uint16_t armFailSafeTimeout, - Optional commandTimeout, OnExtendFailsafeSuccess onSuccess, - OnExtendFailsafeFailure onFailure) +bool DeviceCommissioner::ExtendArmFailSafeInternal(DeviceProxy * proxy, CommissioningStage step, uint16_t armFailSafeTimeout, + Optional commandTimeout, + OnExtendFailsafeSuccess onSuccess, OnExtendFailsafeFailure onFailure, + bool fireAndForget) { using namespace System; using namespace System::Clock; @@ -1237,17 +1252,15 @@ bool DeviceCommissioner::ExtendArmFailSafe(DeviceProxy * proxy, CommissioningSta request.expiryLengthSeconds = armFailSafeTimeout; request.breadcrumb = breadcrumb; ChipLogProgress(Controller, "Arming failsafe (%u seconds)", request.expiryLengthSeconds); - CHIP_ERROR err = SendCommissioningCommand(proxy, request, onSuccess, onFailure, kRootEndpointId, commandTimeout); + CHIP_ERROR err = SendCommissioningCommand(proxy, request, onSuccess, onFailure, kRootEndpointId, commandTimeout, fireAndForget); if (err != CHIP_NO_ERROR) { - onFailure(this, err); - } - else - { - // TODO: Handle the situation when our command ends up erroring out - // asynchronously? - proxy->SetFailSafeExpirationTimestamp(newFailSafeTimeout); + onFailure((!fireAndForget) ? this : nullptr, err); + return true; // we have called onFailure already } + + // Note: The stored timestamp may become invalid if we fail asynchronously + proxy->SetFailSafeExpirationTimestamp(newFailSafeTimeout); return true; } @@ -1269,9 +1282,9 @@ void DeviceCommissioner::ExtendArmFailSafeForDeviceAttestation(const Credentials // Per spec, anything we do with the fail-safe armed must not time out // in less than kMinimumCommissioningStepTimeout. waitForFailsafeExtension = - ExtendArmFailSafe(mDeviceBeingCommissioned, mCommissioningStage, expiryLengthSeconds.Value(), - MakeOptional(kMinimumCommissioningStepTimeout), OnArmFailSafeExtendedForDeviceAttestation, - OnFailedToExtendedArmFailSafeDeviceAttestation); + ExtendArmFailSafeInternal(mDeviceBeingCommissioned, mCommissioningStage, expiryLengthSeconds.Value(), + MakeOptional(kMinimumCommissioningStepTimeout), OnArmFailSafeExtendedForDeviceAttestation, + OnFailedToExtendedArmFailSafeDeviceAttestation, /* fireAndForget = */ false); } else { @@ -1848,58 +1861,34 @@ void DeviceCommissioner::OnDeviceConnectedFn(void * context, Messaging::Exchange { // CASE session established. DeviceCommissioner * commissioner = static_cast(context); - VerifyOrReturn(commissioner != nullptr, ChipLogProgress(Controller, "Device connected callback with null context. Ignoring")); + VerifyOrDie(commissioner->mCommissioningStage == CommissioningStage::kFindOperational); + VerifyOrDie(commissioner->mDeviceBeingCommissioned->GetDeviceId() == sessionHandle->GetPeer().GetNodeId()); + commissioner->CancelCASECallbacks(); // ensure all CASE callbacks are unregistered - if (commissioner->mCommissioningStage != CommissioningStage::kFindOperational) - { - // This call is definitely not us finding our commissionee device. - // This is presumably us trying to re-establish CASE on MRP failure. - return; - } - - if (commissioner->mDeviceBeingCommissioned == nullptr || - commissioner->mDeviceBeingCommissioned->GetDeviceId() != sessionHandle->GetPeer().GetNodeId()) - { - // Not the device we are trying to commission. - return; - } - - if (commissioner->mCommissioningDelegate != nullptr) - { - CommissioningDelegate::CommissioningReport report; - report.Set(OperationalNodeFoundData(OperationalDeviceProxy(&exchangeMgr, sessionHandle))); - commissioner->CommissioningStageComplete(CHIP_NO_ERROR, report); - } + CommissioningDelegate::CommissioningReport report; + report.Set(OperationalNodeFoundData(OperationalDeviceProxy(&exchangeMgr, sessionHandle))); + commissioner->CommissioningStageComplete(CHIP_NO_ERROR, report); } void DeviceCommissioner::OnDeviceConnectionFailureFn(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) { // CASE session establishment failed. DeviceCommissioner * commissioner = static_cast(context); + VerifyOrDie(commissioner->mCommissioningStage == CommissioningStage::kFindOperational); + VerifyOrDie(commissioner->mDeviceBeingCommissioned->GetDeviceId() == peerId.GetNodeId()); + commissioner->CancelCASECallbacks(); // ensure all CASE callbacks are unregistered - ChipLogProgress(Controller, "Device connection failed. Error %s", ErrorStr(error)); - VerifyOrReturn(commissioner != nullptr, - ChipLogProgress(Controller, "Device connection failure callback with null context. Ignoring")); - - // Ensure that commissioning stage advancement is done based on seeing an error. - if (error == CHIP_NO_ERROR) + if (error != CHIP_NO_ERROR) { - ChipLogError(Controller, "Device connection failed without a valid error code. Making one up."); - error = CHIP_ERROR_INTERNAL; + ChipLogProgress(Controller, "Device connection failed. Error %" CHIP_ERROR_FORMAT, error.Format()); } - - if (commissioner->mDeviceBeingCommissioned == nullptr || - commissioner->mDeviceBeingCommissioned->GetDeviceId() != peerId.GetNodeId()) - { - // Not the device we are trying to commission. - return; - } - - if (commissioner->mCommissioningStage == CommissioningStage::kFindOperational && - commissioner->mCommissioningDelegate != nullptr) + else { - commissioner->CommissioningStageComplete(error); + // Ensure that commissioning stage advancement is done based on seeing an error. + ChipLogError(Controller, "Device connection failed without a valid error code."); + error = CHIP_ERROR_INTERNAL; } + commissioner->CommissioningStageComplete(error); } #if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES @@ -1926,6 +1915,8 @@ void DeviceCommissioner::OnDeviceConnectionRetryFn(void * context, const ScopedN ChipLogValueScopedNodeId(peerId), error.Format(), retryTimeout.count()); auto self = static_cast(context); + VerifyOrDie(self->mCommissioningStage == CommissioningStage::kFindOperational); + VerifyOrDie(self->mDeviceBeingCommissioned->GetDeviceId() == peerId.GetNodeId()); // We need to do the fail-safe arming over the PASE session. auto * commissioneeDevice = self->FindCommissioneeDevice(peerId.GetNodeId()); @@ -1950,11 +1941,11 @@ void DeviceCommissioner::OnDeviceConnectionRetryFn(void * context, const ScopedN { failsafeTimeout = static_cast(retryTimeout.count() + kDefaultFailsafeTimeout); } - // A false return from ExtendArmFailSafe is fine; we don't want to make the - // fail-safe shorter here. - self->ExtendArmFailSafe(commissioneeDevice, CommissioningStage::kFindOperational, failsafeTimeout, - MakeOptional(kMinimumCommissioningStepTimeout), OnExtendFailsafeForCASERetrySuccess, - OnExtendFailsafeForCASERetryFailure); + + // A false return is fine; we don't want to make the fail-safe shorter here. + self->ExtendArmFailSafeInternal(commissioneeDevice, CommissioningStage::kFindOperational, failsafeTimeout, + MakeOptional(kMinimumCommissioningStepTimeout), OnExtendFailsafeForCASERetrySuccess, + OnExtendFailsafeForCASERetryFailure, /* fireAndForget = */ true); } #endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES @@ -2552,19 +2543,22 @@ CHIP_ERROR DeviceCommissioner::SendCommissioningCommand(DeviceProxy * device, const RequestObjectT & request, CommandResponseSuccessCallback successCb, CommandResponseFailureCallback failureCb, EndpointId endpoint, - Optional timeout) + Optional timeout, bool fireAndForget) { - VerifyOrDie(!mInvokeCancelFn); // we don't make parallel calls + // Default behavior is to make sequential, cancellable calls tracked via mInvokeCancelFn. + // Fire-and-forget calls are not cancellable and don't receive `this` as context in callbacks. + VerifyOrDie(fireAndForget || !mInvokeCancelFn); // we don't make parallel (cancellable) calls - auto onSuccessCb = [context = this, successCb](const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatus, - const typename RequestObjectT::ResponseType & responseData) { + void * context = (!fireAndForget) ? this : nullptr; + auto onSuccessCb = [context, successCb](const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatus, + const typename RequestObjectT::ResponseType & responseData) { successCb(context, responseData); }; - auto onFailureCb = [context = this, failureCb](CHIP_ERROR aError) { failureCb(context, aError); }; + auto onFailureCb = [context, failureCb](CHIP_ERROR aError) { failureCb(context, aError); }; return InvokeCommandRequest(device->GetExchangeManager(), device->GetSecureSession().Value(), endpoint, request, onSuccessCb, - onFailureCb, NullOptional, timeout, &mInvokeCancelFn); + onFailureCb, NullOptional, timeout, (!fireAndForget) ? &mInvokeCancelFn : nullptr); } void DeviceCommissioner::SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, @@ -2626,8 +2620,8 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio // Make sure the fail-safe value we set here actually ends up being used // no matter what. proxy->SetFailSafeExpirationTimestamp(System::Clock::kZero); - VerifyOrDie(ExtendArmFailSafe(proxy, step, params.GetFailsafeTimerSeconds().ValueOr(kDefaultFailsafeTimeout), timeout, - OnArmFailSafe, OnBasicFailure)); + VerifyOrDie(ExtendArmFailSafeInternal(proxy, step, params.GetFailsafeTimerSeconds().ValueOr(kDefaultFailsafeTimeout), + timeout, OnArmFailSafe, OnBasicFailure, /* fireAndForget = */ false)); } break; case CommissioningStage::kReadCommissioningInfo: { @@ -3270,12 +3264,10 @@ void DeviceCommissioner::ExtendFailsafeBeforeNetworkEnable(DeviceProxy * device, failSafeTimeoutSecs = static_cast(failSafeTimeoutSecs + sigma1TimeoutSecs); } - // A false return from ExtendArmFailSafe is fine; we don't want to make the - // fail-safe shorter here. - if (!ExtendArmFailSafe(commissioneeDevice, step, failSafeTimeoutSecs, MakeOptional(kMinimumCommissioningStepTimeout), - OnArmFailSafe, OnBasicFailure)) + if (!ExtendArmFailSafeInternal(commissioneeDevice, step, failSafeTimeoutSecs, MakeOptional(kMinimumCommissioningStepTimeout), + OnArmFailSafe, OnBasicFailure, /* fireAndForget = */ false)) { - // Just move on to the next step. + // A false return is fine; we don't want to make the fail-safe shorter here. CommissioningStageComplete(CHIP_NO_ERROR, CommissioningDelegate::CommissioningReport()); } } diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h index 87d31dbae4abfc..a635edb958a632 100644 --- a/src/controller/CHIPDeviceController.h +++ b/src/controller/CHIPDeviceController.h @@ -765,7 +765,12 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, // onSuccess nor onFailure will be called. bool ExtendArmFailSafe(DeviceProxy * proxy, CommissioningStage step, uint16_t armFailSafeTimeout, Optional commandTimeout, OnExtendFailsafeSuccess onSuccess, - OnExtendFailsafeFailure onFailure); + OnExtendFailsafeFailure onFailure) + { + // If this method is called directly by a client, assume it's fire-and-forget (not a commissioning stage) + return ExtendArmFailSafeInternal(proxy, step, armFailSafeTimeout, commandTimeout, onSuccess, onFailure, + /* fireAndForget = */ true); + } private: DevicePairingDelegate * mPairingDelegate = nullptr; @@ -957,14 +962,19 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, CommissioneeDeviceProxy * FindCommissioneeDevice(const Transport::PeerAddress & peerAddress); void ReleaseCommissioneeDevice(CommissioneeDeviceProxy * device); + bool ExtendArmFailSafeInternal(DeviceProxy * proxy, CommissioningStage step, uint16_t armFailSafeTimeout, + Optional commandTimeout, OnExtendFailsafeSuccess onSuccess, + OnExtendFailsafeFailure onFailure, bool fireAndForget); + template CHIP_ERROR SendCommissioningCommand(DeviceProxy * device, const RequestObjectT & request, CommandResponseSuccessCallback successCb, CommandResponseFailureCallback failureCb, EndpointId endpoint, - Optional timeout = NullOptional); + Optional timeout = NullOptional, bool fireAndForget = false); void SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, app::AttributePathParams * readPaths, size_t readPathsSize); void CancelCommissioningInteractions(); + void CancelCASECallbacks(); #if CHIP_CONFIG_ENABLE_READ_CLIENT void ParseCommissioningInfo(); diff --git a/src/darwin/Framework/CHIPTests/MTRPairingTests.m b/src/darwin/Framework/CHIPTests/MTRPairingTests.m index 54b8a4991325b7..8bd34a2ff2c11e 100644 --- a/src/darwin/Framework/CHIPTests/MTRPairingTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPairingTests.m @@ -29,6 +29,7 @@ // Fixture: chip-all-clusters-app --KVS "$(mktemp -t chip-test-kvs)" --interface-id -1 \ --dac_provider credentials/development/commissioner_dut/struct_cd_origin_pid_vid_correct/test_case_vector.json \ --product-id 32768 --discriminator 3839 +// For manual testing, CASE retry code paths can be tested by adding --faults chip_CASEServerBusy_f1 (or similar) static const uint16_t kPairingTimeoutInSeconds = 10; static const uint16_t kTimeoutInSeconds = 3; @@ -243,6 +244,7 @@ - (void)test004_PairWithAttestationDelegateFailsafeExtensionLong - (void)doPairingAndWaitForProgress:(NSString *)trigger { XCTestExpectation * expectation = [self expectationWithDescription:@"Trigger message seen"]; + expectation.assertForOverFulfill = NO; MTRSetLogCallback(MTRLogTypeDetail, ^(MTRLogType type, NSString * moduleName, NSString * message) { if ([message containsString:trigger]) { [expectation fulfill]; @@ -298,4 +300,10 @@ - (void)test006_pairingAfterCancellation_ConfigRegulatoryCommand [self doPairingTestAfterCancellationAtProgress:@"Performing next commissioning step 'ConfigRegulatory'"]; } +- (void)test007_pairingAfterCancellation_FindOperational +{ + // Ensure CASE establishment has started by waiting for 'FindOrEstablishSession' + [self doPairingTestAfterCancellationAtProgress:@"FindOrEstablishSession:"]; +} + @end diff --git a/src/lib/support/CHIPFaultInjection.cpp b/src/lib/support/CHIPFaultInjection.cpp index 15cb5a40ec5b4d..b7ac95eea10343 100644 --- a/src/lib/support/CHIPFaultInjection.cpp +++ b/src/lib/support/CHIPFaultInjection.cpp @@ -53,6 +53,7 @@ static const nl::FaultInjection::Name sFaultNames[] = { #if CONFIG_NETWORK_LAYER_BLE "CHIPOBLESend", #endif // CONFIG_NETWORK_LAYER_BLE + "CASEServerBusy", }; /** diff --git a/src/lib/support/CHIPFaultInjection.h b/src/lib/support/CHIPFaultInjection.h index 44e582ba01d64d..ccc5488a7e5b75 100644 --- a/src/lib/support/CHIPFaultInjection.h +++ b/src/lib/support/CHIPFaultInjection.h @@ -71,7 +71,8 @@ typedef enum with 1 InvokeResponseMessage, dropping the response to the second request */ #if CONFIG_NETWORK_LAYER_BLE kFault_CHIPOBLESend, /**< Inject a GATT error when sending the first fragment of a chip message over BLE */ -#endif // CONFIG_NETWORK_LAYER_BLE +#endif + kFault_CASEServerBusy, /**< Respond to CASE_Sigma1 with a BUSY status */ kFault_NumItems, } Id; diff --git a/src/protocols/secure_channel/CASEServer.cpp b/src/protocols/secure_channel/CASEServer.cpp index df0984d4d94eee..fc6b4970ce0829 100644 --- a/src/protocols/secure_channel/CASEServer.cpp +++ b/src/protocols/secure_channel/CASEServer.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -79,7 +80,10 @@ CHIP_ERROR CASEServer::OnMessageReceived(Messaging::ExchangeContext * ec, const System::PacketBufferHandle && payload) { MATTER_TRACE_SCOPE("OnMessageReceived", "CASEServer"); - if (GetSession().GetState() != CASESession::State::kInitialized) + + bool busy = GetSession().GetState() != CASESession::State::kInitialized; + CHIP_FAULT_INJECT(FaultInjection::kFault_CASEServerBusy, busy = true); + if (busy) { // We are in the middle of CASE handshake From aa67091009a7b467796eeaf50b5d0c491e8ba5d9 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 12 Mar 2024 02:28:37 -0400 Subject: [PATCH 27/76] Log signed vs unsigned for TLV payloads. (#32540) Right now we just log the number when the type is integer. But people keep being confused why things don't work when they messed up unsigned vs signed bits. We should just log that information. --- src/app/MessageDef/MessageDefHelper.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/MessageDef/MessageDefHelper.cpp b/src/app/MessageDef/MessageDefHelper.cpp index 82fcf286d1751b..717519080ddde5 100644 --- a/src/app/MessageDef/MessageDefHelper.cpp +++ b/src/app/MessageDef/MessageDefHelper.cpp @@ -140,7 +140,7 @@ CHIP_ERROR CheckIMPayload(TLV::TLVReader & aReader, int aDepth, const char * aLa // TODO: Figure out how to not use PRId64 here, since it's not supported // on all libcs. - PRETTY_PRINT_SAMELINE("%" PRId64 ", ", value_s64); + PRETTY_PRINT_SAMELINE("%" PRId64 " (signed), ", value_s64); break; } @@ -151,7 +151,7 @@ CHIP_ERROR CheckIMPayload(TLV::TLVReader & aReader, int aDepth, const char * aLa // TODO: Figure out how to not use PRIu64 here, since it's not supported // on all libcs. - PRETTY_PRINT_SAMELINE("%" PRIu64 ", ", value_u64); + PRETTY_PRINT_SAMELINE("%" PRIu64 " (unsigned), ", value_u64); break; } From 64bf8cb18a203eb5a52c39eb039a4cf262d14a70 Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:20:57 +1300 Subject: [PATCH 28/76] Don't define __STDC_{FORMAT,LIMIT}_MACROS (#32542) * Don't define __STDC_{FORMAT,LIMIT}_MACROS The need for this workaround was removed in glibc years ago. See https://sourceware.org/bugzilla/show_bug.cgi?id=15366 * restyle --- src/ble/BLEEndPoint.cpp | 3 --- src/ble/BleLayer.h | 3 --- src/ble/BtpEngine.h | 4 ---- src/ble/tests/TestBleErrorStr.cpp | 8 -------- src/controller/CHIPDeviceController.cpp | 7 ------- src/credentials/CHIPCert.cpp | 4 ---- src/credentials/CHIPCertFromX509.cpp | 4 ---- src/credentials/CHIPCertToX509.cpp | 4 ---- src/credentials/GenerateChipX509Cert.cpp | 4 ---- src/inet/IPAddress-StringFuncts.cpp | 3 --- src/inet/IPAddress.cpp | 4 ---- src/inet/InetInterface.cpp | 4 ---- src/inet/tests/TestInetCommon.h | 4 ---- src/inet/tests/TestInetCommonOptions.cpp | 7 ------- src/inet/tests/TestInetCommonPosix.cpp | 7 ------- src/inet/tests/TestInetEndPoint.cpp | 4 ---- src/inet/tests/TestInetErrorStr.cpp | 8 -------- src/inet/tests/TestInetLayer.cpp | 4 ---- src/lib/asn1/ASN1Writer.cpp | 3 --- src/lib/core/ErrorStr.cpp | 4 ---- src/lib/core/TLVCircularBuffer.cpp | 4 ---- src/lib/core/TLVDebug.cpp | 4 ---- src/lib/core/TLVWriter.cpp | 4 ---- src/lib/core/tests/TestCHIPErrorStr.cpp | 8 -------- src/lib/support/Base64.cpp | 3 --- src/lib/support/CHIPArgParser.cpp | 7 ------- src/lib/support/TimeUtils.cpp | 3 --- src/lib/support/tests/TestPersistedCounter.cpp | 4 ---- src/lwip/standalone/arch/cc.h | 4 ---- src/messaging/ExchangeContext.cpp | 7 ------- src/messaging/ExchangeMessageDispatch.cpp | 8 -------- src/messaging/ExchangeMgr.cpp | 8 -------- src/system/SystemPacketBuffer.cpp | 4 ---- src/system/SystemStats.h | 5 ----- src/system/tests/TestSystemClock.cpp | 4 ---- src/system/tests/TestSystemErrorStr.cpp | 8 -------- src/system/tests/TestSystemPacketBuffer.cpp | 4 ---- src/system/tests/TestSystemTimer.cpp | 4 ---- src/system/tests/TestSystemWakeEvent.cpp | 4 ---- src/system/tests/TestTimeSource.cpp | 4 ---- src/tools/chip-cert/CertUtils.cpp | 2 -- src/tools/chip-cert/Cmd_GenAttCert.cpp | 4 ---- src/tools/chip-cert/Cmd_GenCD.cpp | 4 ---- src/tools/chip-cert/Cmd_GenCert.cpp | 4 ---- src/tools/chip-cert/Cmd_ResignCert.cpp | 4 ---- src/tools/spake2p/Cmd_GenVerifier.cpp | 4 ---- src/transport/raw/tests/TestPeerAddress.cpp | 8 -------- 47 files changed, 224 deletions(-) diff --git a/src/ble/BLEEndPoint.cpp b/src/ble/BLEEndPoint.cpp index 63708af678efbd..5f60c009753cbe 100644 --- a/src/ble/BLEEndPoint.cpp +++ b/src/ble/BLEEndPoint.cpp @@ -25,9 +25,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include diff --git a/src/ble/BleLayer.h b/src/ble/BleLayer.h index f175334173ba7f..af2267b770fcf8 100644 --- a/src/ble/BleLayer.h +++ b/src/ble/BleLayer.h @@ -47,9 +47,6 @@ #pragma once -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include diff --git a/src/ble/BtpEngine.h b/src/ble/BtpEngine.h index 7ec59e35a60c62..2055c2a9c1cfbb 100644 --- a/src/ble/BtpEngine.h +++ b/src/ble/BtpEngine.h @@ -27,10 +27,6 @@ #pragma once -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/ble/tests/TestBleErrorStr.cpp b/src/ble/tests/TestBleErrorStr.cpp index 4e940e7ca58e62..e2b13f4f4c63a3 100644 --- a/src/ble/tests/TestBleErrorStr.cpp +++ b/src/ble/tests/TestBleErrorStr.cpp @@ -24,14 +24,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 942a488d38c2cd..637e5debffae72 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -25,13 +25,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - // module header, comes first #include diff --git a/src/credentials/CHIPCert.cpp b/src/credentials/CHIPCert.cpp index 2af62ab2efe0f0..5bad0ab75076a9 100644 --- a/src/credentials/CHIPCert.cpp +++ b/src/credentials/CHIPCert.cpp @@ -25,10 +25,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/credentials/CHIPCertFromX509.cpp b/src/credentials/CHIPCertFromX509.cpp index d07b2a515b8644..d5c08b27de8bb0 100644 --- a/src/credentials/CHIPCertFromX509.cpp +++ b/src/credentials/CHIPCertFromX509.cpp @@ -24,10 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/credentials/CHIPCertToX509.cpp b/src/credentials/CHIPCertToX509.cpp index 3793062231ce7f..9e0549d13f4921 100644 --- a/src/credentials/CHIPCertToX509.cpp +++ b/src/credentials/CHIPCertToX509.cpp @@ -24,10 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/credentials/GenerateChipX509Cert.cpp b/src/credentials/GenerateChipX509Cert.cpp index 0dccaaa3a49fa6..2ce37b93d834aa 100644 --- a/src/credentials/GenerateChipX509Cert.cpp +++ b/src/credentials/GenerateChipX509Cert.cpp @@ -22,10 +22,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/inet/IPAddress-StringFuncts.cpp b/src/inet/IPAddress-StringFuncts.cpp index 12beec23ec2295..13bbb4e0f81b90 100644 --- a/src/inet/IPAddress-StringFuncts.cpp +++ b/src/inet/IPAddress-StringFuncts.cpp @@ -23,9 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include #include diff --git a/src/inet/IPAddress.cpp b/src/inet/IPAddress.cpp index 0fc8cd9d2284a9..c6059fb1f56bdb 100644 --- a/src/inet/IPAddress.cpp +++ b/src/inet/IPAddress.cpp @@ -27,10 +27,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/inet/InetInterface.cpp b/src/inet/InetInterface.cpp index a893f80f4d605b..db10bdba3ed769 100644 --- a/src/inet/InetInterface.cpp +++ b/src/inet/InetInterface.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/inet/tests/TestInetCommon.h b/src/inet/tests/TestInetCommon.h index 3665f3938967d8..dcc506856228d6 100644 --- a/src/inet/tests/TestInetCommon.h +++ b/src/inet/tests/TestInetCommon.h @@ -30,10 +30,6 @@ #pragma once -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/inet/tests/TestInetCommonOptions.cpp b/src/inet/tests/TestInetCommonOptions.cpp index 727b6a107b28f1..fc9c8df320a5c6 100644 --- a/src/inet/tests/TestInetCommonOptions.cpp +++ b/src/inet/tests/TestInetCommonOptions.cpp @@ -23,13 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include "TestInetCommonOptions.h" #include diff --git a/src/inet/tests/TestInetCommonPosix.cpp b/src/inet/tests/TestInetCommonPosix.cpp index 78126cbf5ff748..71437913819c24 100644 --- a/src/inet/tests/TestInetCommonPosix.cpp +++ b/src/inet/tests/TestInetCommonPosix.cpp @@ -27,13 +27,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include "TestInetCommon.h" #include "TestInetCommonOptions.h" diff --git a/src/inet/tests/TestInetEndPoint.cpp b/src/inet/tests/TestInetEndPoint.cpp index 4f5eba1a84bd36..f1684380446ef4 100644 --- a/src/inet/tests/TestInetEndPoint.cpp +++ b/src/inet/tests/TestInetEndPoint.cpp @@ -24,10 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/inet/tests/TestInetErrorStr.cpp b/src/inet/tests/TestInetErrorStr.cpp index 42120331ab9936..5689267b9c86b0 100644 --- a/src/inet/tests/TestInetErrorStr.cpp +++ b/src/inet/tests/TestInetErrorStr.cpp @@ -24,14 +24,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/inet/tests/TestInetLayer.cpp b/src/inet/tests/TestInetLayer.cpp index 47e83316228b5f..a43b3c7a08affc 100644 --- a/src/inet/tests/TestInetLayer.cpp +++ b/src/inet/tests/TestInetLayer.cpp @@ -25,10 +25,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/lib/asn1/ASN1Writer.cpp b/src/lib/asn1/ASN1Writer.cpp index 7d5a81f4cf1205..f916d138ae3c4d 100644 --- a/src/lib/asn1/ASN1Writer.cpp +++ b/src/lib/asn1/ASN1Writer.cpp @@ -24,9 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include #include diff --git a/src/lib/core/ErrorStr.cpp b/src/lib/core/ErrorStr.cpp index 5551ff60d413cd..a32ff6bc433344 100644 --- a/src/lib/core/ErrorStr.cpp +++ b/src/lib/core/ErrorStr.cpp @@ -17,10 +17,6 @@ */ #include -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include #include #include diff --git a/src/lib/core/TLVCircularBuffer.cpp b/src/lib/core/TLVCircularBuffer.cpp index 76a392f90f1644..264d41a10ff175 100644 --- a/src/lib/core/TLVCircularBuffer.cpp +++ b/src/lib/core/TLVCircularBuffer.cpp @@ -28,10 +28,6 @@ */ #include -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/lib/core/TLVDebug.cpp b/src/lib/core/TLVDebug.cpp index 4d922f6285ebd1..12c3a7933825c9 100644 --- a/src/lib/core/TLVDebug.cpp +++ b/src/lib/core/TLVDebug.cpp @@ -17,10 +17,6 @@ */ #include -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include #include diff --git a/src/lib/core/TLVWriter.cpp b/src/lib/core/TLVWriter.cpp index 2e946d0403267a..db019fb5488622 100644 --- a/src/lib/core/TLVWriter.cpp +++ b/src/lib/core/TLVWriter.cpp @@ -17,10 +17,6 @@ */ #include -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/lib/core/tests/TestCHIPErrorStr.cpp b/src/lib/core/tests/TestCHIPErrorStr.cpp index 06ba1c8040f8d1..8f21a2e9ec3397 100644 --- a/src/lib/core/tests/TestCHIPErrorStr.cpp +++ b/src/lib/core/tests/TestCHIPErrorStr.cpp @@ -24,14 +24,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/lib/support/Base64.cpp b/src/lib/support/Base64.cpp index 8c6bfe2e7c237d..fc3427b8361780 100644 --- a/src/lib/support/Base64.cpp +++ b/src/lib/support/Base64.cpp @@ -22,9 +22,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include "Base64.h" #include diff --git a/src/lib/support/CHIPArgParser.cpp b/src/lib/support/CHIPArgParser.cpp index ed123452442748..b791f16a560196 100644 --- a/src/lib/support/CHIPArgParser.cpp +++ b/src/lib/support/CHIPArgParser.cpp @@ -23,13 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include "CHIPArgParser.hpp" #if CHIP_CONFIG_ENABLE_ARG_PARSER diff --git a/src/lib/support/TimeUtils.cpp b/src/lib/support/TimeUtils.cpp index ff0a6f8221d98a..af70656c42d52f 100644 --- a/src/lib/support/TimeUtils.cpp +++ b/src/lib/support/TimeUtils.cpp @@ -23,9 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include #include diff --git a/src/lib/support/tests/TestPersistedCounter.cpp b/src/lib/support/tests/TestPersistedCounter.cpp index 558a425a48df74..e4cf11de9fbc9b 100644 --- a/src/lib/support/tests/TestPersistedCounter.cpp +++ b/src/lib/support/tests/TestPersistedCounter.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - #include #include diff --git a/src/lwip/standalone/arch/cc.h b/src/lwip/standalone/arch/cc.h index adeadea3bbacc4..74110797475de8 100644 --- a/src/lwip/standalone/arch/cc.h +++ b/src/lwip/standalone/arch/cc.h @@ -52,10 +52,6 @@ #ifndef __ARCH_CC_H__ #define __ARCH_CC_H__ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - /* Include some files for defining library routines */ #include #include diff --git a/src/messaging/ExchangeContext.cpp b/src/messaging/ExchangeContext.cpp index 15522ebcb76d77..74a861c1170b7f 100644 --- a/src/messaging/ExchangeContext.cpp +++ b/src/messaging/ExchangeContext.cpp @@ -20,13 +20,6 @@ * This file implements the ExchangeContext class. * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif #include #include diff --git a/src/messaging/ExchangeMessageDispatch.cpp b/src/messaging/ExchangeMessageDispatch.cpp index 950be48ae5802a..a94f1310549eb4 100644 --- a/src/messaging/ExchangeMessageDispatch.cpp +++ b/src/messaging/ExchangeMessageDispatch.cpp @@ -20,14 +20,6 @@ * This file provides implementation of ExchangeMessageDispatch class. */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/messaging/ExchangeMgr.cpp b/src/messaging/ExchangeMgr.cpp index 2ca22031094bb7..3971864bb7e06e 100644 --- a/src/messaging/ExchangeMgr.cpp +++ b/src/messaging/ExchangeMgr.cpp @@ -21,14 +21,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/system/SystemPacketBuffer.cpp b/src/system/SystemPacketBuffer.cpp index 32f4b8e3830077..0226ab783f2415 100644 --- a/src/system/SystemPacketBuffer.cpp +++ b/src/system/SystemPacketBuffer.cpp @@ -23,10 +23,6 @@ * mechanisms for manipulating packets of octet-serialized * data. */ -// Include standard C library limit macros -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif // Include module header #include diff --git a/src/system/SystemStats.h b/src/system/SystemStats.h index 1bda5f2c5b1623..24c1dad9ae36c5 100644 --- a/src/system/SystemStats.h +++ b/src/system/SystemStats.h @@ -24,11 +24,6 @@ #pragma once -// Include standard C library limit macros -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - // Include configuration headers #include #include diff --git a/src/system/tests/TestSystemClock.cpp b/src/system/tests/TestSystemClock.cpp index f0a5315fe9975a..63fcfaa3566e5d 100644 --- a/src/system/tests/TestSystemClock.cpp +++ b/src/system/tests/TestSystemClock.cpp @@ -15,10 +15,6 @@ * limitations under the License. */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/system/tests/TestSystemErrorStr.cpp b/src/system/tests/TestSystemErrorStr.cpp index cb67d1b30d5c1e..0296eb957c5dea 100644 --- a/src/system/tests/TestSystemErrorStr.cpp +++ b/src/system/tests/TestSystemErrorStr.cpp @@ -24,14 +24,6 @@ * */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/system/tests/TestSystemPacketBuffer.cpp b/src/system/tests/TestSystemPacketBuffer.cpp index 9bf2597d7a92b5..527d3a19a32e4b 100644 --- a/src/system/tests/TestSystemPacketBuffer.cpp +++ b/src/system/tests/TestSystemPacketBuffer.cpp @@ -23,10 +23,6 @@ * structure for network packet buffer management. */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include diff --git a/src/system/tests/TestSystemTimer.cpp b/src/system/tests/TestSystemTimer.cpp index cbfd22ac87476e..5cd499fdcb816f 100644 --- a/src/system/tests/TestSystemTimer.cpp +++ b/src/system/tests/TestSystemTimer.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/system/tests/TestSystemWakeEvent.cpp b/src/system/tests/TestSystemWakeEvent.cpp index 20bd65821aa4ae..46ed99dae31656 100644 --- a/src/system/tests/TestSystemWakeEvent.cpp +++ b/src/system/tests/TestSystemWakeEvent.cpp @@ -21,10 +21,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/system/tests/TestTimeSource.cpp b/src/system/tests/TestTimeSource.cpp index 04b3c55f43303a..daee4cfb974f8b 100644 --- a/src/system/tests/TestTimeSource.cpp +++ b/src/system/tests/TestTimeSource.cpp @@ -21,10 +21,6 @@ * the ability to compile and use the test implementation of the time source. */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include diff --git a/src/tools/chip-cert/CertUtils.cpp b/src/tools/chip-cert/CertUtils.cpp index cb6e0a1703b434..ce8365bc9ef6bf 100644 --- a/src/tools/chip-cert/CertUtils.cpp +++ b/src/tools/chip-cert/CertUtils.cpp @@ -24,8 +24,6 @@ * */ -#define __STDC_FORMAT_MACROS - #include "chip-cert.h" #include #include diff --git a/src/tools/chip-cert/Cmd_GenAttCert.cpp b/src/tools/chip-cert/Cmd_GenAttCert.cpp index 3c30991dc9e8dc..fce4ff141674e1 100644 --- a/src/tools/chip-cert/Cmd_GenAttCert.cpp +++ b/src/tools/chip-cert/Cmd_GenAttCert.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include "chip-cert.h" #include diff --git a/src/tools/chip-cert/Cmd_GenCD.cpp b/src/tools/chip-cert/Cmd_GenCD.cpp index acea3c5a1c4239..6d4e61159b7dea 100644 --- a/src/tools/chip-cert/Cmd_GenCD.cpp +++ b/src/tools/chip-cert/Cmd_GenCD.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include "chip-cert.h" #include diff --git a/src/tools/chip-cert/Cmd_GenCert.cpp b/src/tools/chip-cert/Cmd_GenCert.cpp index 9bc74c9226e6f0..ae07f27cb599fa 100644 --- a/src/tools/chip-cert/Cmd_GenCert.cpp +++ b/src/tools/chip-cert/Cmd_GenCert.cpp @@ -24,10 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include "chip-cert.h" namespace { diff --git a/src/tools/chip-cert/Cmd_ResignCert.cpp b/src/tools/chip-cert/Cmd_ResignCert.cpp index 425d295c918170..3e6d183a341734 100644 --- a/src/tools/chip-cert/Cmd_ResignCert.cpp +++ b/src/tools/chip-cert/Cmd_ResignCert.cpp @@ -24,10 +24,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include "chip-cert.h" namespace { diff --git a/src/tools/spake2p/Cmd_GenVerifier.cpp b/src/tools/spake2p/Cmd_GenVerifier.cpp index 04bfe995a91ead..7b4d7ce003ce95 100644 --- a/src/tools/spake2p/Cmd_GenVerifier.cpp +++ b/src/tools/spake2p/Cmd_GenVerifier.cpp @@ -23,10 +23,6 @@ * */ -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include "spake2p.h" #include diff --git a/src/transport/raw/tests/TestPeerAddress.cpp b/src/transport/raw/tests/TestPeerAddress.cpp index 8ad5380efa670d..6b7f9f886df085 100644 --- a/src/transport/raw/tests/TestPeerAddress.cpp +++ b/src/transport/raw/tests/TestPeerAddress.cpp @@ -17,14 +17,6 @@ * limitations under the License. */ -#ifndef __STDC_FORMAT_MACROS -#define __STDC_FORMAT_MACROS -#endif - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS -#endif - #include #include #include From 020b0e046c765b2e33e4d728a0c3de8b953e72a8 Mon Sep 17 00:00:00 2001 From: Axel Le Bourhis <45206070+axelnxp@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:00:37 +0100 Subject: [PATCH 29/76] [NXP][Zephyr] Fix deprecated rand32 inclusion (#32531) * [NXP][Zephyr] Fix deprecated rand32 inclusion Signed-off-by: Axel Le Bourhis * Restyled by clang-format * nrfconnect still relies on rand32 Signed-off-by: Axel Le Bourhis --------- Signed-off-by: Axel Le Bourhis Co-authored-by: Restyled.io --- src/platform/Zephyr/BLEManagerImpl.cpp | 4 ++++ src/platform/nxp/rt/rw61x/BUILD.gn | 4 ++-- .../rt_sdk/sdk_hook/zephyr/random/{rand32.cpp => random.cpp} | 2 +- .../nxp/rt_sdk/sdk_hook/zephyr/random/{rand32.h => random.h} | 0 4 files changed, 7 insertions(+), 3 deletions(-) rename third_party/nxp/rt_sdk/sdk_hook/zephyr/random/{rand32.cpp => random.cpp} (92%) rename third_party/nxp/rt_sdk/sdk_hook/zephyr/random/{rand32.h => random.h} (100%) diff --git a/src/platform/Zephyr/BLEManagerImpl.cpp b/src/platform/Zephyr/BLEManagerImpl.cpp index 07f30dc0632a6c..0b2336ea7a6cc9 100644 --- a/src/platform/Zephyr/BLEManagerImpl.cpp +++ b/src/platform/Zephyr/BLEManagerImpl.cpp @@ -40,7 +40,11 @@ #include #include #include +#if CHIP_DEVICE_LAYER_TARGET_NRFCONNECT #include +#else +#include +#endif #include #include diff --git a/src/platform/nxp/rt/rw61x/BUILD.gn b/src/platform/nxp/rt/rw61x/BUILD.gn index 5de785556ef7f7..0631c8ccd19c4e 100644 --- a/src/platform/nxp/rt/rw61x/BUILD.gn +++ b/src/platform/nxp/rt/rw61x/BUILD.gn @@ -76,8 +76,8 @@ static_library("nxp_platform") { if (chip_enable_ble) { sources += [ - # Adding rand32 file which defines the function sys_csrand_get which is called by BLEManagerImpl from Zephyr - "${nxp_sdk_build_root}/${nxp_sdk_name}/sdk_hook/zephyr/random/rand32.cpp", + # Adding random file which defines the function sys_csrand_get which is called by BLEManagerImpl from Zephyr + "${nxp_sdk_build_root}/${nxp_sdk_name}/sdk_hook/zephyr/random/random.cpp", "../../../Zephyr/BLEAdvertisingArbiter.cpp", "../../../Zephyr/BLEManagerImpl.cpp", "../../common/ble_zephyr/BLEManagerImpl.h", diff --git a/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/rand32.cpp b/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/random.cpp similarity index 92% rename from third_party/nxp/rt_sdk/sdk_hook/zephyr/random/rand32.cpp rename to third_party/nxp/rt_sdk/sdk_hook/zephyr/random/random.cpp index a54301c01bedb1..d6ea3cd3b156ce 100644 --- a/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/rand32.cpp +++ b/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/random.cpp @@ -6,8 +6,8 @@ * SPDX-License-Identifier: BSD-3-Clause */ -#include "rand32.h" #include +#include /** * Fill the buffer given as an arg with random values diff --git a/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/rand32.h b/third_party/nxp/rt_sdk/sdk_hook/zephyr/random/random.h similarity index 100% rename from third_party/nxp/rt_sdk/sdk_hook/zephyr/random/rand32.h rename to third_party/nxp/rt_sdk/sdk_hook/zephyr/random/random.h From 1c20301fbb3219549c0d81d96efa859d7128bba3 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 12 Mar 2024 10:28:02 -0400 Subject: [PATCH 30/76] Make it possible to dynamically set the "sender boost" for MRP. (#32537) CHIP_CONFIG_MRP_RETRY_INTERVAL_SENDER_BOOST is a compile-time constant meant to capture the fact that we might be on a high-latency radio connection. But the same binary might be used on different types of hardware that have different MRP requirements. This change makes it possible to control the behavior CHIP_CONFIG_MRP_RETRY_INTERVAL_SENDER_BOOST is used for at run time, based on the actual hardware involved, not just at compile time. The new functionality is gated by the default-off CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG compiler flag, so only systems that really need it pay the cost for it. --- src/messaging/ReliableMessageMgr.cpp | 8 ++++++++ src/messaging/ReliableMessageMgr.h | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/messaging/ReliableMessageMgr.cpp b/src/messaging/ReliableMessageMgr.cpp index 2445bf4611a06e..53e292c9b32b00 100644 --- a/src/messaging/ReliableMessageMgr.cpp +++ b/src/messaging/ReliableMessageMgr.cpp @@ -47,6 +47,10 @@ using namespace chip::System::Clock::Literals; namespace chip { namespace Messaging { +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG +Optional ReliableMessageMgr::sAdditionalMRPBackoffTime; +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + ReliableMessageMgr::RetransTableEntry::RetransTableEntry(ReliableMessageContext * rc) : ec(*rc->GetExchangeContext()), nextRetransTime(0), sendCount(0) { @@ -263,7 +267,11 @@ System::Clock::Timestamp ReliableMessageMgr::GetBackoff(System::Clock::Timestamp mrpBackoffTime += ICDConfigurationData::GetInstance().GetFastPollingInterval(); #endif +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + mrpBackoffTime += sAdditionalMRPBackoffTime.ValueOr(CHIP_CONFIG_MRP_RETRY_INTERVAL_SENDER_BOOST); +#else mrpBackoffTime += CHIP_CONFIG_MRP_RETRY_INTERVAL_SENDER_BOOST; +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG return mrpBackoffTime; } diff --git a/src/messaging/ReliableMessageMgr.h b/src/messaging/ReliableMessageMgr.h index 934094c83d4259..3401a3eda0c405 100644 --- a/src/messaging/ReliableMessageMgr.h +++ b/src/messaging/ReliableMessageMgr.h @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -205,6 +206,26 @@ class ReliableMessageMgr } #endif // CHIP_CONFIG_TEST +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + /** + * Set the value to add to the MRP backoff time we compute. This is meant to + * account for high network latency on the sending side (us) that can't be + * known to the message recipient and hence is not captured in the MRP + * parameters the message recipient communicates to us. + * + * If set to NullOptional falls back to the compile-time + * CHIP_CONFIG_MRP_RETRY_INTERVAL_SENDER_BOOST. + * + * This is a static, not a regular member, because API consumers may need to + * set this before actually bringing up the stack and having access to a + * ReliableMessageMgr. + */ + static void SetAdditionaMRPBackoffTime(const Optional & additionalTime) + { + sAdditionalMRPBackoffTime = additionalTime; + } +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + private: /** * Calculates the next retransmission time for the entry @@ -233,6 +254,10 @@ class ReliableMessageMgr ObjectPool mRetransTable; SessionUpdateDelegate * mSessionUpdateDelegate = nullptr; + +#if CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG + static Optional sAdditionalMRPBackoffTime; +#endif // CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG }; } // namespace Messaging From 9cac2770028c4991590d8f8cc36a12b3bf5867f2 Mon Sep 17 00:00:00 2001 From: Tennessee Carmel-Veilleux Date: Tue, 12 Mar 2024 11:10:03 -0400 Subject: [PATCH 31/76] Fix LevelControl Move when no motion requested (#32539) * Fix LevelControl Move when no motion requested - rate == 0 means "do not move", so handle it more efficiently without moving. Testing done: - Added an integration test for the behavior. Co-authored-by: volodymyr-zvarun-globallogic * Restyled by prettier-yaml --------- Co-authored-by: volodymyr-zvarun-globallogic Co-authored-by: Restyled.io --- .../clusters/level-control/level-control.cpp | 19 +++++-- .../suites/certification/Test_TC_LVL_4_1.yaml | 57 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/app/clusters/level-control/level-control.cpp b/src/app/clusters/level-control/level-control.cpp index b04d32fe69c448..0077c59509edc9 100644 --- a/src/app/clusters/level-control/level-control.cpp +++ b/src/app/clusters/level-control/level-control.cpp @@ -18,6 +18,8 @@ // clusters specific header #include "level-control.h" +#include + // this file contains all the common includes for clusters in the util #include #include @@ -904,7 +906,7 @@ static Status moveToLevelHandler(EndpointId endpoint, CommandId commandId, uint8 // The duration between events will be the transition time divided by the // distance we must move. - state->eventDurationMs = state->transitionTimeMs / actualStepSize; + state->eventDurationMs = state->transitionTimeMs / std::max(static_cast(1u), actualStepSize); state->elapsedTimeMs = 0; state->storedLevel = storedLevel; @@ -958,6 +960,14 @@ static void moveHandler(app::CommandHandler * commandObj, const app::ConcreteCom goto send_default_response; } + if (!rate.IsNull() && (rate.Value() == 0)) + { + // Move at a rate of zero is no move at all. Immediately succeed without touching anything. + ChipLogProgress(Zcl, "Immediate success due to move rate of 0 (would move at no rate)."); + status = Status::Success; + goto send_default_response; + } + // Cancel any currently active command before fiddling with the state. cancelEndpointTimerCallback(endpoint); @@ -1034,12 +1044,13 @@ static void moveHandler(app::CommandHandler * commandObj, const app::ConcreteCom status = Status::Success; goto send_default_response; } + // Already checked that defaultMoveRate.Value() != 0. state->eventDurationMs = MILLISECOND_TICKS_PER_SECOND / defaultMoveRate.Value(); } } else { - state->eventDurationMs = MILLISECOND_TICKS_PER_SECOND / rate.Value(); + state->eventDurationMs = MILLISECOND_TICKS_PER_SECOND / std::max(static_cast(1u), rate.Value()); } #else // Transition/rate is not supported so always use fastest transition time and ignore @@ -1175,7 +1186,7 @@ static void stepHandler(app::CommandHandler * commandObj, const app::ConcreteCom // milliseconds to reduce rounding errors in integer division. if (stepSize != actualStepSize) { - state->transitionTimeMs = (state->transitionTimeMs * actualStepSize / stepSize); + state->transitionTimeMs = (state->transitionTimeMs * actualStepSize / std::max(static_cast(1u), stepSize)); } } #else @@ -1187,7 +1198,7 @@ static void stepHandler(app::CommandHandler * commandObj, const app::ConcreteCom // The duration between events will be the transition time divided by the // distance we must move. - state->eventDurationMs = state->transitionTimeMs / actualStepSize; + state->eventDurationMs = state->transitionTimeMs / std::max(static_cast(1u), actualStepSize); state->elapsedTimeMs = 0; // storedLevel is not used for Step commands diff --git a/src/app/tests/suites/certification/Test_TC_LVL_4_1.yaml b/src/app/tests/suites/certification/Test_TC_LVL_4_1.yaml index 737f858b64c468..14ac5ea5a69ffd 100644 --- a/src/app/tests/suites/certification/Test_TC_LVL_4_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_LVL_4_1.yaml @@ -243,6 +243,63 @@ tests: response: value: 254 + - label: "Step 4f: TH reads the MinLevel attribute from the DUT" + PICS: LVL.S.A0002 && LVL.S.F01 + command: "readAttribute" + attribute: "MinLevel" + response: + value: 1 + saveAs: MinLevelValue + constraints: + type: int8u + + - label: "Step 4g: TH sends a MoveToLevel to set the level to MinLevel" + PICS: LVL.S.C01.Rsp && LVL.S.F01 + command: "MoveToLevel" + arguments: + values: + - name: "Level" + value: MinLevelValue + - name: "TransitionTime" + value: 0 + - name: "OptionsMask" + value: 0 + - name: "OptionsOverride" + value: 0 + + - label: + "Step 4h: TH sends a Move command to the DUT with MoveMode =0x00 (up) + and Rate =0 (units/s), expect success" + PICS: LVL.S.C01.Rsp + command: "Move" + arguments: + values: + - name: "MoveMode" + value: 0 + - name: "Rate" + value: 0 + - name: "OptionsMask" + value: 1 + - name: "OptionsOverride" + value: 1 + + - label: "Wait 5s" + cluster: "DelayCommands" + command: "WaitForMs" + arguments: + values: + - name: "ms" + value: 5000 + + - label: + "Step 4i: After another 5 seconds, TH reads CurrentLevel attribute + from DUT, expects mininum level." + PICS: LVL.S.C01.Rsp && LVL.S.A0000 + command: "readAttribute" + attribute: "CurrentLevel" + response: + value: MinLevelValue + - label: "Precondition send Off Command" cluster: "On/Off" PICS: OO.S.C00.Rsp From 9d2f3505f803e29a85ca80c851ee42c3b6a9f6f6 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 12 Mar 2024 11:34:18 -0400 Subject: [PATCH 32/76] Make use of ZAP's new support for specifying the parent of an endpoint. (#32487) Specific changes: * Remove unused FIXED_NETWORKS bit from endpoint_config.h. * Add FIXED_PARENT_ENDPOINTS which lists the parents of each fixed endpoint. * Condition the fixed endpoint initialization code on FIXED_ENDPOINT_COUNT > 0 and remove some Darwin hackery that was needed because it was not thus conditioned. * Add initialization of parentEndpointId for fixed endpoints from FIXED_PARENT_ENDPOINTS. At the moment FIXED_PARENT_ENDPOINTS uses 0 to mean "no parent specified", but I am hoping the ZAP folks will fix things so that we can just have kInvalidEndpointId in there and the code in emberAfEndpointConfigure can become simpler/smaller. --- .../app-templates/endpoint_config.h | 4 +-- .../app-templates/endpoint_config.h | 4 +-- src/app/util/attribute-storage.cpp | 26 +++++++++++++++---- .../templates/app/endpoint_config.zapt | 4 +-- .../CHIP/zap-generated/endpoint_config.h | 13 ---------- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h index 8c1c6c7564cc02..09b17447be9e52 100644 --- a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h +++ b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h @@ -3069,8 +3069,8 @@ static_assert(ATTRIBUTE_LARGEST <= CHIP_CONFIG_MAX_ATTRIBUTE_STORE_ELEMENT_SIZE, 0, 1, 2, 3 \ } -// Array of networks supported on each endpoint -#define FIXED_NETWORKS \ +// Array of parent endpoints for each endpoint +#define FIXED_PARENT_ENDPOINTS \ { \ 0, 0, 0, 0 \ } diff --git a/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h b/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h index 4e21190fdd22bd..0e6ba62431b71e 100644 --- a/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h +++ b/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h @@ -1207,8 +1207,8 @@ static_assert(ATTRIBUTE_LARGEST <= CHIP_CONFIG_MAX_ATTRIBUTE_STORE_ELEMENT_SIZE, 0, 1 \ } -// Array of networks supported on each endpoint -#define FIXED_NETWORKS \ +// Array of parent endpoints for each endpoint +#define FIXED_PARENT_ENDPOINTS \ { \ 0, 0 \ } diff --git a/src/app/util/attribute-storage.cpp b/src/app/util/attribute-storage.cpp index 431f3091a22977..3bce63e6da254c 100644 --- a/src/app/util/attribute-storage.cpp +++ b/src/app/util/attribute-storage.cpp @@ -130,11 +130,13 @@ constexpr const EmberAfCluster generatedClusters[] = GENERATED_CLUSTERS; #define ZAP_CLUSTER_INDEX(index) (&generatedClusters[index]) #endif +#if FIXED_ENDPOINT_COUNT > 0 constexpr const EmberAfEndpointType generatedEmberAfEndpointTypes[] = GENERATED_ENDPOINT_TYPES; constexpr const EmberAfDeviceType fixedDeviceTypeList[] = FIXED_DEVICE_TYPES; // Not const, because these need to mutate. DataVersion fixedEndpointDataVersions[ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT]; +#endif // FIXED_ENDPOINT_COUNT > 0 AttributeAccessInterface * gAttributeAccessOverrides = nullptr; AttributeAccessInterfaceCache gAttributeAccessInterfaceCache; @@ -183,10 +185,15 @@ void emberAfEndpointConfigure() static_assert(FIXED_ENDPOINT_COUNT <= std::numeric_limits::max(), "FIXED_ENDPOINT_COUNT must not exceed the size of the endpoint data type"); - uint16_t fixedEndpoints[] = FIXED_ENDPOINT_ARRAY; - uint16_t fixedDeviceTypeListLengths[] = FIXED_DEVICE_TYPE_LENGTHS; - uint16_t fixedDeviceTypeListOffsets[] = FIXED_DEVICE_TYPE_OFFSETS; - uint8_t fixedEmberAfEndpointTypes[] = FIXED_ENDPOINT_TYPES; + emberEndpointCount = FIXED_ENDPOINT_COUNT; + +#if FIXED_ENDPOINT_COUNT > 0 + + constexpr uint16_t fixedEndpoints[] = FIXED_ENDPOINT_ARRAY; + constexpr uint16_t fixedDeviceTypeListLengths[] = FIXED_DEVICE_TYPE_LENGTHS; + constexpr uint16_t fixedDeviceTypeListOffsets[] = FIXED_DEVICE_TYPE_OFFSETS; + constexpr uint8_t fixedEmberAfEndpointTypes[] = FIXED_ENDPOINT_TYPES; + constexpr EndpointId fixedParentEndpoints[] = FIXED_PARENT_ENDPOINTS; #if ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT > 0 // Initialize our data version storage. If @@ -201,7 +208,6 @@ void emberAfEndpointConfigure() } #endif // ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT > 0 - emberEndpointCount = FIXED_ENDPOINT_COUNT; DataVersion * currentDataVersions = fixedEndpointDataVersions; for (ep = 0; ep < FIXED_ENDPOINT_COUNT; ep++) { @@ -210,6 +216,14 @@ void emberAfEndpointConfigure() Span(&fixedDeviceTypeList[fixedDeviceTypeListOffsets[ep]], fixedDeviceTypeListLengths[ep]); emAfEndpoints[ep].endpointType = &generatedEmberAfEndpointTypes[fixedEmberAfEndpointTypes[ep]]; emAfEndpoints[ep].dataVersions = currentDataVersions; + if (fixedParentEndpoints[ep] == 0) + { + emAfEndpoints[ep].parentEndpointId = kInvalidEndpointId; + } + else + { + emAfEndpoints[ep].parentEndpointId = fixedParentEndpoints[ep]; + } emAfEndpoints[ep].bitmask.Set(EmberAfEndpointOptions::isEnabled); emAfEndpoints[ep].bitmask.Set(EmberAfEndpointOptions::isFlatComposition); @@ -219,6 +233,8 @@ void emberAfEndpointConfigure() currentDataVersions += emberAfClusterCountByIndex(ep, /* server = */ true); } +#endif // FIXED_ENDPOINT_COUNT > 0 + #if CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT if (MAX_ENDPOINT_COUNT > FIXED_ENDPOINT_COUNT) { diff --git a/src/app/zap-templates/templates/app/endpoint_config.zapt b/src/app/zap-templates/templates/app/endpoint_config.zapt index f67c779089a1f2..43ef32436d3aa3 100644 --- a/src/app/zap-templates/templates/app/endpoint_config.zapt +++ b/src/app/zap-templates/templates/app/endpoint_config.zapt @@ -95,7 +95,7 @@ static_assert(ATTRIBUTE_LARGEST <= CHIP_CONFIG_MAX_ATTRIBUTE_STORE_ELEMENT_SIZE, // Array of endpoint types supported on each endpoint #define FIXED_ENDPOINT_TYPES {{endpoint_fixed_endpoint_type_array}} -// Array of networks supported on each endpoint -#define FIXED_NETWORKS {{endpoint_fixed_network_array}} +// Array of parent endpoints for each endpoint +#define FIXED_PARENT_ENDPOINTS {{endpoint_fixed_parent_id_array}} {{/endpoint_config}} diff --git a/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h b/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h index 27cfbdb778d5f5..5f7965897a6570 100644 --- a/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h +++ b/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h @@ -36,16 +36,3 @@ #define GENERATED_ATTRIBUTES {} -#define GENERATED_ENDPOINT_TYPES {} - -#define FIXED_DEVICE_TYPES {} - -#define ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT 0 - -#define FIXED_ENDPOINT_ARRAY {} - -#define FIXED_DEVICE_TYPE_LENGTHS {} - -#define FIXED_DEVICE_TYPE_OFFSETS {} - -#define FIXED_ENDPOINT_TYPES {} From f31c1c57f4ad611fd2f2b3267116b691c66436f7 Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:48:31 -0400 Subject: [PATCH 33/76] [Silabs] Bugfix/redundant 917 ifdef (#32470) * SIWX_917 * SI917 * Renamed SiWx917 to SLI_917_SOC for clarity, removed unused defines from SiWx917.gni file * Using SLI_SI91X_MCU_INTERFACE instead of SIWX_917 --- examples/lighting-app/silabs/src/AppTask.cpp | 2 +- examples/platform/silabs/BaseApplication.cpp | 14 +- examples/platform/silabs/FreeRTOSConfig.h | 14 +- examples/platform/silabs/MatterConfig.cpp | 14 +- examples/platform/silabs/OTAConfig.cpp | 4 +- .../silabs/SiWx917/SiWx917/sl_wifi_if.c | 18 +- .../silabs/SiWx917/SiWx917/sl_wlan_config.h | 16 +- .../silabs/SilabsDeviceAttestationCreds.cpp | 2 +- examples/platform/silabs/display/demo-ui.c | 8 +- examples/platform/silabs/display/lcd.cpp | 4 +- .../efr32/rs911x/hal/rsi_hal_mcu_interrupt.c | 4 +- .../silabs/efr32/rs911x/sl_wlan_config.h | 218 +++--- .../thermostat/silabs/src/ThermostatUI.cpp | 2 +- .../silabs/KeyValueStoreManagerImpl.cpp | 4 +- src/platform/silabs/SilabsConfig.cpp | 8 +- src/platform/silabs/efr32/wifi/ethernetif.cpp | 18 +- .../silabs/efr32/wifi/wfx_host_events.h | 6 +- src/platform/silabs/rs911x/BLEManagerImpl.cpp | 12 +- src/platform/silabs/rs911x/rsi_ble_config.h | 632 +++++++++--------- src/platform/silabs/rs911x/wfx_sl_ble_init.h | 268 ++++---- third_party/silabs/SiWx917_sdk.gni | 7 +- 21 files changed, 631 insertions(+), 644 deletions(-) diff --git a/examples/lighting-app/silabs/src/AppTask.cpp b/examples/lighting-app/silabs/src/AppTask.cpp index bce7b06cb5cb07..7430bd2828d233 100644 --- a/examples/lighting-app/silabs/src/AppTask.cpp +++ b/examples/lighting-app/silabs/src/AppTask.cpp @@ -40,7 +40,7 @@ #include -#if (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917)) +#ifdef SL_CATALOG_SIMPLE_LED_LED1_PRESENT #define LIGHT_LED 1 #else #define LIGHT_LED 0 diff --git a/examples/platform/silabs/BaseApplication.cpp b/examples/platform/silabs/BaseApplication.cpp index c98882f30aba96..abd2c207da0b06 100644 --- a/examples/platform/silabs/BaseApplication.cpp +++ b/examples/platform/silabs/BaseApplication.cpp @@ -82,7 +82,7 @@ #define APP_EVENT_QUEUE_SIZE 10 #define EXAMPLE_VENDOR_ID 0xcafe -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) #define SYSTEM_STATE_LED 0 #endif // ENABLE_WSTK_LEDS #define APP_FUNCTION_BUTTON 0 @@ -104,7 +104,7 @@ TimerHandle_t sLightTimer; TaskHandle_t sAppTaskHandle; QueueHandle_t sAppEventQueue; -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) LEDWidget sStatusLED; #endif // ENABLE_WSTK_LEDS @@ -270,7 +270,7 @@ CHIP_ERROR BaseApplication::Init() ConfigurationMgr().LogDeviceConfig(); OutputQrCode(true /*refreshLCD at init*/); -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) LEDWidget::InitGpio(); sStatusLED.Init(SYSTEM_STATE_LED); #endif // ENABLE_WSTK_LEDS @@ -321,7 +321,7 @@ void BaseApplication::FunctionEventHandler(AppEvent * aEvent) bool BaseApplication::ActivateStatusLedPatterns() { bool isPatternSet = false; -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) #ifdef MATTER_DM_PLUGIN_IDENTIFY_SERVER if (gIdentify.mActive) { @@ -427,7 +427,7 @@ void BaseApplication::LightEventHandler() #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #if defined(ENABLE_WSTK_LEDS) -#if (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917)) +#ifdef SL_CATALOG_SIMPLE_LED_LED1_PRESENT // Update the status LED if factory reset has not been initiated. // // If system has "full connectivity", keep the LED On constantly. @@ -563,7 +563,7 @@ void BaseApplication::StartFactoryResetSequence() StartStatusLEDTimer(); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) // Turn off all LEDs before starting blink to make sure blink is // co-ordinated. sStatusLED.Set(false); @@ -596,7 +596,7 @@ void BaseApplication::StartStatusLEDTimer() void BaseApplication::StopStatusLEDTimer() { -#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT) || defined(SIWX_917))) +#if (defined(ENABLE_WSTK_LEDS) && (defined(SL_CATALOG_SIMPLE_LED_LED1_PRESENT))) sStatusLED.Set(false); #endif // ENABLE_WSTK_LEDS diff --git a/examples/platform/silabs/FreeRTOSConfig.h b/examples/platform/silabs/FreeRTOSConfig.h index 478dfa10be547f..fd6658c7af35e2 100644 --- a/examples/platform/silabs/FreeRTOSConfig.h +++ b/examples/platform/silabs/FreeRTOSConfig.h @@ -107,7 +107,7 @@ extern "C" { #include -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE #include "si91x_device.h" extern uint32_t SystemCoreClock; #else // For EFR32 @@ -169,23 +169,23 @@ extern uint32_t SystemCoreClock; #define configTIMER_QUEUE_LENGTH (10) #define configTIMER_TASK_STACK_DEPTH (1024) -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE #ifdef __NVIC_PRIO_BITS #undef __NVIC_PRIO_BITS #endif #define configPRIO_BITS 6 /* 6 priority levels. */ -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE /* Interrupt priorities used by the kernel port layer itself. These are generic to all Cortex-M ports, and do not rely on any particular library functions. */ #define configKERNEL_INTERRUPT_PRIORITY (255) /* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!! See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */ -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE #define configMAX_SYSCALL_INTERRUPT_PRIORITY 20 #else #define configMAX_SYSCALL_INTERRUPT_PRIORITY 48 -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE #define configENABLE_FPU 0 #define configENABLE_MPU 0 @@ -232,11 +232,11 @@ See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */ #ifndef configTOTAL_HEAP_SIZE #ifdef SL_WIFI #ifdef DIC_ENABLE -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE #define configTOTAL_HEAP_SIZE ((size_t) ((75 + EXTRA_HEAP_k) * 1024)) #else #define configTOTAL_HEAP_SIZE ((size_t) ((68 + EXTRA_HEAP_k) * 1024)) -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE #else #define configTOTAL_HEAP_SIZE ((size_t) ((42 + EXTRA_HEAP_k) * 1024)) #endif // DIC diff --git a/examples/platform/silabs/MatterConfig.cpp b/examples/platform/silabs/MatterConfig.cpp index 32bf04cf0e0f7b..1c148a5a1f0d1c 100644 --- a/examples/platform/silabs/MatterConfig.cpp +++ b/examples/platform/silabs/MatterConfig.cpp @@ -42,9 +42,9 @@ #include "MemMonitoring.h" #endif -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE #include "wfx_rsi.h" -#endif /* SIWX_917 */ +#endif /* SLI_SI91X_MCU_INTERFACE */ using namespace ::chip; using namespace ::chip::Inet; @@ -53,7 +53,7 @@ using namespace ::chip::DeviceLayer; #include // If building with the EFR32-provided crypto backend, we can use the // opaque keystore -#if CHIP_CRYPTO_PLATFORM && !(defined(SIWX_917)) +#if CHIP_CRYPTO_PLATFORM && !(defined(SLI_SI91X_MCU_INTERFACE)) #include static chip::DeviceLayer::Internal::Efr32PsaOperationalKeystore gOperationalKeystore; #endif @@ -229,7 +229,7 @@ CHIP_ERROR SilabsMatterConfig::InitMatter(const char * appName) initParams.testEventTriggerDelegate = &sTestEventTriggerDelegate; #endif // SILABS_TEST_EVENT_TRIGGER_ENABLED -#if CHIP_CRYPTO_PLATFORM && !(defined(SIWX_917)) +#if CHIP_CRYPTO_PLATFORM && !(defined(SLI_SI91X_MCU_INTERFACE)) // When building with EFR32 crypto, use the opaque key store // instead of the default (insecure) one. gOperationalKeystore.Init(); @@ -289,13 +289,13 @@ CHIP_ERROR SilabsMatterConfig::InitWiFi(void) #endif // SL_WFX_USE_SECURE_LINK #endif /* WF200_WIFI */ -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE sl_status_t status; if ((status = wfx_wifi_rsi_init()) != SL_STATUS_OK) { ReturnErrorOnFailure((CHIP_ERROR) status); } -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE return CHIP_NO_ERROR; } @@ -306,7 +306,7 @@ CHIP_ERROR SilabsMatterConfig::InitWiFi(void) // ================================================================================ extern "C" void vApplicationIdleHook(void) { -#if SIWX_917 && CHIP_CONFIG_ENABLE_ICD_SERVER +#if SLI_SI91X_MCU_INTERFACE && CHIP_CONFIG_ENABLE_ICD_SERVER sl_wfx_host_si91x_sleep_wakeup(); #endif } diff --git a/examples/platform/silabs/OTAConfig.cpp b/examples/platform/silabs/OTAConfig.cpp index 6305af08e628b5..f35c9770a6aab7 100644 --- a/examples/platform/silabs/OTAConfig.cpp +++ b/examples/platform/silabs/OTAConfig.cpp @@ -20,7 +20,7 @@ #include "silabs_utils.h" #include -#ifndef SIWX_917 +#ifndef SLI_SI91X_MCU_INTERFACE #include "application_properties.h" @@ -75,7 +75,7 @@ __attribute__((used)) ApplicationProperties_t sl_app_properties = { .longTokenSectionAddress = NULL, }; #endif // SL_CATALOG_GECKO_BOOTLOADER_INTERFACE_PRESENT -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE // Global OTA objects chip::DefaultOTARequestor gRequestorCore; diff --git a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c index c5044fcf255a8e..60489cd9b2ba30 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c +++ b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c @@ -41,7 +41,7 @@ #include "ble_config.h" -#if SL_ICD_ENABLED && SIWX_917 +#if SL_ICD_ENABLED && SLI_SI91X_MCU_INTERFACE #include "rsi_rom_power_save.h" #include "sl_si91x_button_pin_config.h" #include "sl_si91x_driver.h" @@ -50,7 +50,7 @@ // TODO: should be removed once we are getting the press interrupt for button 0 with sleep #define BUTTON_PRESSED 1 bool btn0_pressed = false; -#endif // SL_ICD_ENABLED && SIWX_917 +#endif // SL_ICD_ENABLED && SLI_SI91X_MCU_INTERFACE #include "dhcp_client.h" #include "sl_wifi.h" @@ -63,10 +63,10 @@ bool btn0_pressed = false; #define ADV_MULTIPROBE 1 #define ADV_SCAN_PERIODICITY 10 -#ifdef SIWX_917 +#if SLI_SI91X_MCU_INTERFACE #include "sl_si91x_trng.h" #define TRNGKEY_SIZE 4 -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE struct wfx_rsi wfx_rsi; @@ -215,7 +215,7 @@ sl_status_t join_callback_handler(sl_wifi_event_t event, char * result, uint32_t #if SL_ICD_ENABLED -#if SIWX_917 +#if SLI_SI91X_MCU_INTERFACE /****************************************************************** * @fn sl_wfx_host_si91x_sleep_wakeup() * @brief @@ -250,7 +250,7 @@ void sl_wfx_host_si91x_sleep_wakeup() } } } -#endif /* SIWX_917 */ +#endif // SLI_SI91X_MCU_INTERFACE /****************************************************************** * @fn wfx_rsi_power_save() @@ -373,7 +373,7 @@ static sl_status_t wfx_rsi_init(void) SILABS_LOG("sl_wifi_get_mac_address failed: %x", status); return status; } -#ifdef SIWX_917 + const uint32_t trngKey[TRNGKEY_SIZE] = { 0x16157E2B, 0xA6D2AE28, 0x8815F7AB, 0x3C4FCF09 }; // To check the Entropy of TRNG and verify TRNG functioning. @@ -391,7 +391,7 @@ static sl_status_t wfx_rsi_init(void) SILABS_LOG("TRNG Key Programming Failed"); return status; } -#endif // SIWX_917 + wfx_rsi.events = xEventGroupCreateStatic(&rsiDriverEventGroup); wfx_rsi.dev_state |= WFX_RSI_ST_DEV_READY; osSemaphoreRelease(sl_rs_ble_init_sem); @@ -697,7 +697,7 @@ void wfx_rsi_task(void * arg) #ifdef SL_WFX_CONFIG_SCAN | WFX_EVT_SCAN #endif /* SL_WFX_CONFIG_SCAN */ - | 0, + , pdTRUE, /* Clear the bits */ pdFALSE, /* Wait for any bit */ pdMS_TO_TICKS(250)); /* 250 mSec */ diff --git a/examples/platform/silabs/SiWx917/SiWx917/sl_wlan_config.h b/examples/platform/silabs/SiWx917/SiWx917/sl_wlan_config.h index d73989617fe6e3..813ab399af5ab8 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/sl_wlan_config.h +++ b/examples/platform/silabs/SiWx917/SiWx917/sl_wlan_config.h @@ -48,21 +48,11 @@ static const sl_wifi_device_configuration_t config = { #endif | SL_SI91X_TCP_IP_FEAT_ICMP | SL_SI91X_TCP_IP_FEAT_EXTENSION_VALID), .custom_feature_bit_map = (SL_SI91X_CUSTOM_FEAT_EXTENTION_VALID | RSI_CUSTOM_FEATURE_BIT_MAP), - .ext_custom_feature_bit_map = ( -#ifdef SLI_SI917 - (RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#else // defaults -#ifdef SLI_SI91X_MCU_INTERFACE - (SL_SI91X_EXT_FEAT_256K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#else - (SL_SI91X_EXT_FEAT_384K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#endif -#endif // SLI_SI917 - | (SL_SI91X_EXT_FEAT_BT_CUSTOM_FEAT_ENABLE) + .ext_custom_feature_bit_map = (RSI_EXT_CUSTOM_FEATURE_BIT_MAP | (SL_SI91X_EXT_FEAT_BT_CUSTOM_FEAT_ENABLE) #if (defined A2DP_POWER_SAVE_ENABLE) - | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2) + | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2) #endif - ), + ), .bt_feature_bit_map = (RSI_BT_FEATURE_BITMAP #if (RSI_BT_GATT_ON_CLASSIC) | SL_SI91X_BT_ATT_OVER_CLASSIC_ACL /* to support att over classic acl link */ diff --git a/examples/platform/silabs/SilabsDeviceAttestationCreds.cpp b/examples/platform/silabs/SilabsDeviceAttestationCreds.cpp index d442f520debcbc..bc69573c13286b 100644 --- a/examples/platform/silabs/SilabsDeviceAttestationCreds.cpp +++ b/examples/platform/silabs/SilabsDeviceAttestationCreds.cpp @@ -101,7 +101,7 @@ class DeviceAttestationCredsSilabs : public DeviceAttestationCredentialsProvider if (SilabsConfig::ConfigValueExists(SilabsConfig::kConfigKey_Creds_KeyId)) { // Provisioned DAC key -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE return CHIP_ERROR_NOT_IMPLEMENTED; #else uint32_t key_id = SILABS_CREDENTIALS_DAC_KEY_ID; diff --git a/examples/platform/silabs/display/demo-ui.c b/examples/platform/silabs/display/demo-ui.c index bccde3e3eaa449..479af92fd5bb90 100644 --- a/examples/platform/silabs/display/demo-ui.c +++ b/examples/platform/silabs/display/demo-ui.c @@ -25,9 +25,9 @@ #include "glib.h" #include "sl_component_catalog.h" #include "sl_memlcd.h" -#if SL_WIFI && !SIWX_917 +#if SL_WIFI && !SLI_SI91X_MCU_INTERFACE #include "spi_multiplex.h" -#endif // SL_WIFI && !SIWX_917 +#endif // SL_WIFI && !SLI_SI91X_MCU_INTERFACE #include #include @@ -106,9 +106,9 @@ void demoUIInit(GLIB_Context_t * context) sl_status_t updateDisplay(void) { sl_status_t status = SL_STATUS_OK; -#if SIWX_917 && SL_ICD_ENABLED && DISPLAY_ENABLED +#if SLI_SI91X_MCU_INTERFACE && SL_ICD_ENABLED && DISPLAY_ENABLED sl_memlcd_post_wakeup_init(); -#endif // SIWX_917 && SL_ICD_ENABLED && DISPLAY_ENABLED +#endif // SLI_SI91X_MCU_INTERFACE && SL_ICD_ENABLED && DISPLAY_ENABLED #if SL_LCDCTRL_MUX status = sl_wfx_host_pre_lcd_spi_transfer(); if (status != SL_STATUS_OK) diff --git a/examples/platform/silabs/display/lcd.cpp b/examples/platform/silabs/display/lcd.cpp index 43a134f923e24c..bf40cd0f6d543b 100644 --- a/examples/platform/silabs/display/lcd.cpp +++ b/examples/platform/silabs/display/lcd.cpp @@ -25,7 +25,7 @@ #include "dmd.h" #include "glib.h" -#if (SIWX_917) +#if (SLI_SI91X_MCU_INTERFACE) #include "rsi_chip.h" #endif @@ -66,7 +66,7 @@ CHIP_ERROR SilabsLCD::Init(uint8_t * name, bool initialState) } /* Enable the memory lcd */ -#if (SIWX_917) +#if (SLI_SI91X_MCU_INTERFACE) RSI_NPSSGPIO_InputBufferEn(SL_BOARD_ENABLE_DISPLAY_PIN, 1U); RSI_NPSSGPIO_SetPinMux(SL_BOARD_ENABLE_DISPLAY_PIN, 0); RSI_NPSSGPIO_SetDir(SL_BOARD_ENABLE_DISPLAY_PIN, 0); diff --git a/examples/platform/silabs/efr32/rs911x/hal/rsi_hal_mcu_interrupt.c b/examples/platform/silabs/efr32/rs911x/hal/rsi_hal_mcu_interrupt.c index f7a40551fdc5b5..079a5e0c9c32f4 100644 --- a/examples/platform/silabs/efr32/rs911x/hal/rsi_hal_mcu_interrupt.c +++ b/examples/platform/silabs/efr32/rs911x/hal/rsi_hal_mcu_interrupt.c @@ -38,7 +38,7 @@ #include "wfx_host_events.h" #include "wfx_rsi.h" -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) #include "sl_board_configuration.h" #include "sl_rsi_utility.h" @@ -61,7 +61,7 @@ void rsi_gpio_irq_cb(uint8_t irqnum) { if (irqnum != SL_WFX_HOST_PINOUT_SPI_IRQ) return; -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) sl_si91x_host_set_bus_event(NCP_HOST_BUS_RX_EVENT); #else GPIO_IntClear(1 << SL_WFX_HOST_PINOUT_SPI_IRQ); diff --git a/examples/platform/silabs/efr32/rs911x/sl_wlan_config.h b/examples/platform/silabs/efr32/rs911x/sl_wlan_config.h index c8eef5c60e830e..2ce2060cd7deeb 100644 --- a/examples/platform/silabs/efr32/rs911x/sl_wlan_config.h +++ b/examples/platform/silabs/efr32/rs911x/sl_wlan_config.h @@ -1,109 +1,109 @@ -/* - * - * Copyright (c) 2022 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. - */ - -#ifndef RSI_CONFIG_H -#define RSI_CONFIG_H - -#include "ble_config.h" -#include "sl_wifi_device.h" - -//! Enable feature -#define RSI_ENABLE 1 -//! Disable feature -#define RSI_DISABLE 0 - -static const sl_wifi_device_configuration_t config = { - .boot_option = LOAD_NWP_FW, - .mac_address = NULL, - .band = SL_SI91X_WIFI_BAND_2_4GHZ, - .region_code = US, - .boot_config = { .oper_mode = SL_SI91X_CLIENT_MODE, - .coex_mode = SL_SI91X_WLAN_BLE_MODE, - .feature_bit_map = -#ifdef SLI_SI91X_MCU_INTERFACE - (SL_SI91X_FEAT_SECURITY_OPEN | SL_SI91X_FEAT_WPS_DISABLE), -#else - (SL_SI91X_FEAT_SECURITY_OPEN | SL_SI91X_FEAT_AGGREGATION), -#endif - .tcp_ip_feature_bit_map = (SL_SI91X_TCP_IP_FEAT_DHCPV4_CLIENT | SL_SI91X_TCP_IP_FEAT_DNS_CLIENT | - SL_SI91X_TCP_IP_FEAT_SSL | SL_SI91X_TCP_IP_FEAT_BYPASS -#ifdef ipv6_FEATURE_REQUIRED - | SL_SI91X_TCP_IP_FEAT_DHCPV6_CLIENT | SL_SI91X_TCP_IP_FEAT_IPV6 -#endif - | SL_SI91X_TCP_IP_FEAT_ICMP | SL_SI91X_TCP_IP_FEAT_EXTENSION_VALID), - .custom_feature_bit_map = (SL_SI91X_CUSTOM_FEAT_EXTENTION_VALID | RSI_CUSTOM_FEATURE_BIT_MAP), - .ext_custom_feature_bit_map = ( -#ifdef SLI_SI917 - (RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#else // defaults -#ifdef SLI_SI91X_MCU_INTERFACE - (SL_SI91X_EXT_FEAT_256K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#else - (SL_SI91X_EXT_FEAT_384K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) -#endif -#endif - | (SL_SI91X_EXT_FEAT_BT_CUSTOM_FEAT_ENABLE) -#if (defined A2DP_POWER_SAVE_ENABLE) - | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2) -#endif - ), - .bt_feature_bit_map = (RSI_BT_FEATURE_BITMAP -#if (RSI_BT_GATT_ON_CLASSIC) - | SL_SI91X_BT_ATT_OVER_CLASSIC_ACL /* to support att over classic acl link */ -#endif - ), -#ifdef RSI_PROCESS_MAX_RX_DATA - .ext_tcp_ip_feature_bit_map = - (RSI_EXT_TCPIP_FEATURE_BITMAP | SL_SI91X_CONFIG_FEAT_EXTENTION_VALID | SL_SI91X_EXT_TCP_MAX_RECV_LENGTH), -#else - .ext_tcp_ip_feature_bit_map = (RSI_EXT_TCPIP_FEATURE_BITMAP | SL_SI91X_CONFIG_FEAT_EXTENTION_VALID), -#endif - //! ENABLE_BLE_PROTOCOL in bt_feature_bit_map - .ble_feature_bit_map = - ((SL_SI91X_BLE_MAX_NBR_PERIPHERALS(RSI_BLE_MAX_NBR_PERIPHERALS) | - SL_SI91X_BLE_MAX_NBR_CENTRALS(RSI_BLE_MAX_NBR_CENTRALS) | - SL_SI91X_BLE_MAX_NBR_ATT_SERV(RSI_BLE_MAX_NBR_ATT_SERV) | - SL_SI91X_BLE_MAX_NBR_ATT_REC(RSI_BLE_MAX_NBR_ATT_REC)) | - SL_SI91X_FEAT_BLE_CUSTOM_FEAT_EXTENTION_VALID | SL_SI91X_BLE_PWR_INX(RSI_BLE_PWR_INX) | - SL_SI91X_BLE_PWR_SAVE_OPTIONS(RSI_BLE_PWR_SAVE_OPTIONS) | SL_SI91X_916_BLE_COMPATIBLE_FEAT_ENABLE -#if RSI_BLE_GATT_ASYNC_ENABLE - | SL_SI91X_BLE_GATT_ASYNC_ENABLE -#endif - ), - - .ble_ext_feature_bit_map = ((SL_SI91X_BLE_NUM_CONN_EVENTS(RSI_BLE_NUM_CONN_EVENTS) | - SL_SI91X_BLE_NUM_REC_BYTES(RSI_BLE_NUM_REC_BYTES)) -#if RSI_BLE_INDICATE_CONFIRMATION_FROM_HOST - | SL_SI91X_BLE_INDICATE_CONFIRMATION_FROM_HOST // indication response from app -#endif -#if RSI_BLE_MTU_EXCHANGE_FROM_HOST - | SL_SI91X_BLE_MTU_EXCHANGE_FROM_HOST // MTU Exchange request initiation from app -#endif -#if RSI_BLE_SET_SCAN_RESP_DATA_FROM_HOST - | (SL_SI91X_BLE_SET_SCAN_RESP_DATA_FROM_HOST) // Set SCAN Resp Data from app -#endif -#if RSI_BLE_DISABLE_CODED_PHY_FROM_HOST - | (SL_SI91X_BLE_DISABLE_CODED_PHY_FROM_HOST) // Disable Coded PHY from app -#endif -#if BLE_SIMPLE_GATT - | SL_SI91X_BLE_GATT_INIT -#endif - ), - .config_feature_bit_map = (SL_SI91X_FEAT_SLEEP_GPIO_SEL_BITMAP | RSI_CONFIG_FEATURE_BITMAP) } -}; - -#endif \ No newline at end of file +/* + * + * Copyright (c) 2022 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. + */ + +#ifndef RSI_CONFIG_H +#define RSI_CONFIG_H + +#include "ble_config.h" +#include "sl_wifi_device.h" + +//! Enable feature +#define RSI_ENABLE 1 +//! Disable feature +#define RSI_DISABLE 0 + +static const sl_wifi_device_configuration_t config = { + .boot_option = LOAD_NWP_FW, + .mac_address = NULL, + .band = SL_SI91X_WIFI_BAND_2_4GHZ, + .region_code = US, + .boot_config = { .oper_mode = SL_SI91X_CLIENT_MODE, + .coex_mode = SL_SI91X_WLAN_BLE_MODE, + .feature_bit_map = +#ifdef SLI_SI91X_MCU_INTERFACE + (SL_SI91X_FEAT_SECURITY_OPEN | SL_SI91X_FEAT_WPS_DISABLE), +#else + (SL_SI91X_FEAT_SECURITY_OPEN | SL_SI91X_FEAT_AGGREGATION), +#endif + .tcp_ip_feature_bit_map = (SL_SI91X_TCP_IP_FEAT_DHCPV4_CLIENT | SL_SI91X_TCP_IP_FEAT_DNS_CLIENT | + SL_SI91X_TCP_IP_FEAT_SSL | SL_SI91X_TCP_IP_FEAT_BYPASS +#ifdef ipv6_FEATURE_REQUIRED + | SL_SI91X_TCP_IP_FEAT_DHCPV6_CLIENT | SL_SI91X_TCP_IP_FEAT_IPV6 +#endif + | SL_SI91X_TCP_IP_FEAT_ICMP | SL_SI91X_TCP_IP_FEAT_EXTENSION_VALID), + .custom_feature_bit_map = (SL_SI91X_CUSTOM_FEAT_EXTENTION_VALID | RSI_CUSTOM_FEATURE_BIT_MAP), + .ext_custom_feature_bit_map = ( +#ifdef SLI_SI917 + (RSI_EXT_CUSTOM_FEATURE_BIT_MAP) +#else // defaults +#ifdef SLI_SI91X_MCU_INTERFACE + (SL_SI91X_EXT_FEAT_256K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) +#else + (SL_SI91X_EXT_FEAT_384K_MODE | RSI_EXT_CUSTOM_FEATURE_BIT_MAP) +#endif +#endif + | (SL_SI91X_EXT_FEAT_BT_CUSTOM_FEAT_ENABLE) +#if (defined A2DP_POWER_SAVE_ENABLE) + | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2) +#endif + ), + .bt_feature_bit_map = (RSI_BT_FEATURE_BITMAP +#if (RSI_BT_GATT_ON_CLASSIC) + | SL_SI91X_BT_ATT_OVER_CLASSIC_ACL /* to support att over classic acl link */ +#endif + ), +#ifdef RSI_PROCESS_MAX_RX_DATA + .ext_tcp_ip_feature_bit_map = + (RSI_EXT_TCPIP_FEATURE_BITMAP | SL_SI91X_CONFIG_FEAT_EXTENTION_VALID | SL_SI91X_EXT_TCP_MAX_RECV_LENGTH), +#else + .ext_tcp_ip_feature_bit_map = (RSI_EXT_TCPIP_FEATURE_BITMAP | SL_SI91X_CONFIG_FEAT_EXTENTION_VALID), +#endif + //! ENABLE_BLE_PROTOCOL in bt_feature_bit_map + .ble_feature_bit_map = + ((SL_SI91X_BLE_MAX_NBR_PERIPHERALS(RSI_BLE_MAX_NBR_PERIPHERALS) | + SL_SI91X_BLE_MAX_NBR_CENTRALS(RSI_BLE_MAX_NBR_CENTRALS) | + SL_SI91X_BLE_MAX_NBR_ATT_SERV(RSI_BLE_MAX_NBR_ATT_SERV) | + SL_SI91X_BLE_MAX_NBR_ATT_REC(RSI_BLE_MAX_NBR_ATT_REC)) | + SL_SI91X_FEAT_BLE_CUSTOM_FEAT_EXTENTION_VALID | SL_SI91X_BLE_PWR_INX(RSI_BLE_PWR_INX) | + SL_SI91X_BLE_PWR_SAVE_OPTIONS(RSI_BLE_PWR_SAVE_OPTIONS) | SL_SI91X_916_BLE_COMPATIBLE_FEAT_ENABLE +#if RSI_BLE_GATT_ASYNC_ENABLE + | SL_SI91X_BLE_GATT_ASYNC_ENABLE +#endif + ), + + .ble_ext_feature_bit_map = ((SL_SI91X_BLE_NUM_CONN_EVENTS(RSI_BLE_NUM_CONN_EVENTS) | + SL_SI91X_BLE_NUM_REC_BYTES(RSI_BLE_NUM_REC_BYTES)) +#if RSI_BLE_INDICATE_CONFIRMATION_FROM_HOST + | SL_SI91X_BLE_INDICATE_CONFIRMATION_FROM_HOST // indication response from app +#endif +#if RSI_BLE_MTU_EXCHANGE_FROM_HOST + | SL_SI91X_BLE_MTU_EXCHANGE_FROM_HOST // MTU Exchange request initiation from app +#endif +#if RSI_BLE_SET_SCAN_RESP_DATA_FROM_HOST + | (SL_SI91X_BLE_SET_SCAN_RESP_DATA_FROM_HOST) // Set SCAN Resp Data from app +#endif +#if RSI_BLE_DISABLE_CODED_PHY_FROM_HOST + | (SL_SI91X_BLE_DISABLE_CODED_PHY_FROM_HOST) // Disable Coded PHY from app +#endif +#if BLE_SIMPLE_GATT + | SL_SI91X_BLE_GATT_INIT +#endif + ), + .config_feature_bit_map = (SL_SI91X_FEAT_SLEEP_GPIO_SEL_BITMAP | RSI_CONFIG_FEATURE_BITMAP) } +}; + +#endif diff --git a/examples/thermostat/silabs/src/ThermostatUI.cpp b/examples/thermostat/silabs/src/ThermostatUI.cpp index 77184151e1d1d3..f82ee36387e99a 100644 --- a/examples/thermostat/silabs/src/ThermostatUI.cpp +++ b/examples/thermostat/silabs/src/ThermostatUI.cpp @@ -25,7 +25,7 @@ #include "glib.h" #include "lcd.h" -#if SL_WIFI && !defined(SIWX_917) +#if SL_WIFI && !defined(SLI_SI91X_MCU_INTERFACE) // Only needed for wifi NCP devices #include "spi_multiplex.h" #endif // SL_WIFI diff --git a/src/platform/silabs/KeyValueStoreManagerImpl.cpp b/src/platform/silabs/KeyValueStoreManagerImpl.cpp index 5dbeba3e35edcd..8f886dba84ba67 100644 --- a/src/platform/silabs/KeyValueStoreManagerImpl.cpp +++ b/src/platform/silabs/KeyValueStoreManagerImpl.cpp @@ -164,7 +164,7 @@ void KeyValueStoreManagerImpl::ScheduleKeyMapSave(void) During commissioning, the key map will be modified multiples times subsequently. Commit the key map in nvm once it as stabilized. */ -#if SIWX_917 && CHIP_CONFIG_ENABLE_ICD_SERVER +#if SLI_SI91X_MCU_INTERFACE && CHIP_CONFIG_ENABLE_ICD_SERVER // TODO: Remove this when RTC timer is added MATTER-2705 SilabsConfig::WriteConfigValueBin(SilabsConfig::kConfigKey_KvsStringKeyMap, reinterpret_cast(mKvsKeyMap), sizeof(mKvsKeyMap)); @@ -172,7 +172,7 @@ void KeyValueStoreManagerImpl::ScheduleKeyMapSave(void) SystemLayer().StartTimer( std::chrono::duration_cast(System::Clock::Seconds32(SILABS_KVS_SAVE_DELAY_SECONDS)), KeyValueStoreManagerImpl::OnScheduledKeyMapSave, NULL); -#endif // defined(SIWX_917) && CHIP_CONFIG_ENABLE_ICD_SERVER +#endif // defined(SLI_SI91X_MCU_INTERFACE) && CHIP_CONFIG_ENABLE_ICD_SERVER } CHIP_ERROR KeyValueStoreManagerImpl::_Get(const char * key, void * value, size_t value_size, size_t * read_bytes_size, diff --git a/src/platform/silabs/SilabsConfig.cpp b/src/platform/silabs/SilabsConfig.cpp index f98ced75a3e1d8..6c362d41c2c330 100644 --- a/src/platform/silabs/SilabsConfig.cpp +++ b/src/platform/silabs/SilabsConfig.cpp @@ -33,7 +33,7 @@ #include #include -#ifndef SIWX_917 // 917soc/wifi-sdk implements the same nvm3 lock/unlock mechanism and it currently can't be overide. +#ifndef SLI_SI91X_MCU_INTERFACE // 917soc/wifi-sdk implements the same nvm3 lock/unlock mechanism and it currently can't be overide. #include #include // Substitute the GSDK weak nvm3_lockBegin and nvm3_lockEnd @@ -58,7 +58,7 @@ void nvm3_lockEnd(void) VerifyOrDie(nvm3_Sem != NULL); xSemaphoreGive(nvm3_Sem); } -#endif // !SIWX_917 +#endif // !SLI_SI91X_MCU_INTERFACE namespace chip { namespace DeviceLayer { @@ -78,9 +78,9 @@ CHIP_ERROR SilabsConfig::Init() void SilabsConfig::DeInit() { -#ifndef SIWX_917 +#ifndef SLI_SI91X_MCU_INTERFACE vSemaphoreDelete(nvm3_Sem); -#endif // !SIWX_917 +#endif // !SLI_SI91X_MCU_INTERFACE nvm3_close(nvm3_defaultHandle); } diff --git a/src/platform/silabs/efr32/wifi/ethernetif.cpp b/src/platform/silabs/efr32/wifi/ethernetif.cpp index 3dbd1b85b3ef9a..32d7ee1d079d0d 100644 --- a/src/platform/silabs/efr32/wifi/ethernetif.cpp +++ b/src/platform/silabs/efr32/wifi/ethernetif.cpp @@ -31,7 +31,7 @@ #include "FreeRTOS.h" #include "event_groups.h" #include "task.h" -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) #ifdef __cplusplus extern "C" { #endif @@ -46,7 +46,7 @@ extern "C" { #ifdef __cplusplus } #endif -#endif // (SIWX_917 | EXP_BOARD) +#endif // (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) #endif // WF200_WIFI #include "wfx_host_events.h" @@ -349,7 +349,7 @@ static SemaphoreHandle_t ethout_sem; ******************************************************************************/ static err_t low_level_output(struct netif * netif, struct pbuf * p) { -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) sl_wifi_buffer_t * buffer; sl_si91x_packet_t * packet; sl_status_t status = SL_STATUS_OK; @@ -385,7 +385,7 @@ static err_t low_level_output(struct netif * netif, struct pbuf * p) return ERR_IF; } /* Confirm if packet is allocated */ -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) status = sl_si91x_allocate_command_buffer(&buffer, (void **) &packet, sizeof(sl_si91x_packet_t) + framelength, SL_WIFI_ALLOCATE_COMMAND_BUFFER_WAIT_TIME_MS); VERIFY_STATUS_AND_RETURN(status); @@ -393,18 +393,18 @@ static err_t low_level_output(struct netif * netif, struct pbuf * p) #else // RS9116 packet = wfx_rsi_alloc_pkt(); if (!packet) -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE { SILABS_LOG("EN-RSI:No buf"); xSemaphoreGive(ethout_sem); -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) return SL_STATUS_ALLOCATION_FAILED; } memset(packet->desc, 0, sizeof(packet->desc)); #else // RS9116 return ERR_IF; } -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE #ifdef WIFI_DEBUG_ENABLED uint8_t * b = (uint8_t *) p->payload; SILABS_LOG("EN-RSI: Out [%02x:%02x:%02x:%02x:%02x:%02x][%02x:%02x:%02x:%02x:%02x:%02x]type=%02x%02x", b[0], b[1], b[2], b[3], @@ -428,7 +428,7 @@ static err_t low_level_output(struct netif * netif, struct pbuf * p) /* forward the generated packet to RSI to * send the data over wifi network */ -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) packet->length = framelength & 0xFFF; packet->command = RSI_SEND_RAW_DATA; if (sl_si91x_driver_send_data_packet(SI91X_WLAN_CMD_QUEUE, buffer, 1000)) @@ -452,7 +452,7 @@ static err_t low_level_output(struct netif * netif, struct pbuf * p) return ERR_OK; } -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) /***************************************************************************** * @fn void sl_si91x_host_process_data_frame(uint8_t *buf, int len) * @brief diff --git a/src/platform/silabs/efr32/wifi/wfx_host_events.h b/src/platform/silabs/efr32/wifi/wfx_host_events.h index c9f451f8ac063a..b7a7c936b8d6de 100644 --- a/src/platform/silabs/efr32/wifi/wfx_host_events.h +++ b/src/platform/silabs/efr32/wifi/wfx_host_events.h @@ -98,7 +98,7 @@ typedef struct __attribute__((__packed__)) sl_wfx_mib_req_s #include "wfx_msgs.h" -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) #include "sl_si91x_types.h" #include "sl_status.h" #include "sl_wifi_constants.h" @@ -361,7 +361,7 @@ void wfx_ip_changed_notify(int got_ip); #endif /* CHIP_DEVICE_CONFIG_ENABLE_IPV4 */ void wfx_ipv6_notify(int got_ip); -#if !(SIWX_917 | EXP_BOARD) +#if !(SLI_SI91X_MCU_INTERFACE | EXP_BOARD) void * wfx_rsi_alloc_pkt(void); #endif @@ -386,7 +386,7 @@ void sl_wfx_host_gpio_init(void); sl_status_t sl_wfx_host_process_event(sl_wfx_generic_message_t * event_payload); #endif -#if (SIWX_917 | EXP_BOARD) +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) void wfx_retry_interval_handler(bool is_wifi_disconnection_event, uint16_t retryJoin); sl_status_t sl_si91x_driver_send_data_packet(sl_si91x_queue_type_t queue_type, sl_wifi_buffer_t * buffer, uint32_t wait_time); sl_status_t sl_si91x_allocate_command_buffer(sl_wifi_buffer_t ** host_buffer, void ** buffer, uint32_t requested_buffer_size, diff --git a/src/platform/silabs/rs911x/BLEManagerImpl.cpp b/src/platform/silabs/rs911x/BLEManagerImpl.cpp index d7e79f59dc8318..7c6f5a58ffe717 100644 --- a/src/platform/silabs/rs911x/BLEManagerImpl.cpp +++ b/src/platform/silabs/rs911x/BLEManagerImpl.cpp @@ -28,7 +28,7 @@ #include "cmsis_os2.h" #include -#ifndef SIWX_917 +#ifndef SLI_SI91X_MCU_INTERFACE #include "rail.h" #endif #include @@ -42,7 +42,7 @@ extern "C" { #include "wfx_host_events.h" #include "wfx_rsi.h" #include "wfx_sl_ble_init.h" -#if !(SIWX_917 | EXP_BOARD) +#if !(SLI_SI91X_MCU_INTERFACE | EXP_BOARD) #include #endif #include @@ -58,11 +58,11 @@ extern "C" { #include #include -#ifdef SIWX_917 +#ifdef SLI_SI91X_MCU_INTERFACE extern "C" { #include "sl_si91x_trng.h" } -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE #if CHIP_ENABLE_ADDITIONAL_DATA_ADVERTISING #include @@ -85,7 +85,7 @@ using namespace ::chip::DeviceLayer::Internal; void sl_ble_init() { uint8_t randomAddrBLE[RSI_BLE_ADDR_LENGTH] = { 0 }; -#if SIWX_917 +#if SLI_SI91X_MCU_INTERFACE sl_status_t sl_status; //! Get Random number of desired length sl_status = sl_si91x_trng_get_random_num((uint32_t *) randomAddrBLE, RSI_BLE_ADDR_LENGTH); @@ -100,7 +100,7 @@ void sl_ble_init() #else uint64_t randomAddr = chip::Crypto::GetRandU64(); memcpy(randomAddrBLE, &randomAddr, RSI_BLE_ADDR_LENGTH); -#endif // SIWX_917 +#endif // SLI_SI91X_MCU_INTERFACE // registering the GAP callback functions rsi_ble_gap_register_callbacks(NULL, NULL, rsi_ble_on_disconnect_event, NULL, NULL, NULL, rsi_ble_on_enhance_conn_status_event, diff --git a/src/platform/silabs/rs911x/rsi_ble_config.h b/src/platform/silabs/rs911x/rsi_ble_config.h index 72f830b423ef9d..c5a07e25ed12d2 100644 --- a/src/platform/silabs/rs911x/rsi_ble_config.h +++ b/src/platform/silabs/rs911x/rsi_ble_config.h @@ -1,316 +1,316 @@ -/******************************************************************************* - * @file rsi_ble_config.h - * @brief - ******************************************************************************* - * # License - * Copyright 2020 Silicon Laboratories Inc. www.silabs.com - ******************************************************************************* - * - * The licensor of this software is Silicon Laboratories Inc. Your use of this - * software is governed by the terms of Silicon Labs Master Software License - * Agreement (MSLA) available at - * www.silabs.com/about-us/legal/master-software-license-agreement. This - * software is distributed to you in Source Code format and is governed by the - * sections of the MSLA applicable to Source Code. - * - ******************************************************************************/ -#pragma once - -#include "rsi_ble_apis.h" -#if (SIWX_917 | EXP_BOARD) -#include "rsi_bt_common_apis.h" -#include "rsi_user.h" -#else -#include -#endif - -/****************************************************** - * * Macros - * ******************************************************/ -//! application event list -// TODO: remove this define after integration of the new wifi sdk -#ifndef RSI_FAILURE -// failure return value -#define RSI_FAILURE -1 -#endif - -#define RSI_BLE_CONN_EVENT (0x01) -#define RSI_BLE_DISCONN_EVENT (0x02) -#define RSI_BLE_GATT_WRITE_EVENT (0x03) -#define RSI_BLE_MTU_EVENT (0x04) -#define RSI_BLE_GATT_INDICATION_CONFIRMATION (0x05) -#define RSI_BLE_RESP_ATT_VALUE (0x06) -#define RSI_BLE_EVENT_GATT_RD (0x08) -#define RSI_BLE_ADDR_LENGTH 6 - -#define RSI_SSID (0x0D) -#define RSI_SECTYPE (0x0E) -#define RSI_BLE_WLAN_DISCONN_NOTIFY (0x0F) -#define RSI_WLAN_ALREADY (0x10) -#define RSI_WLAN_NOT_ALREADY (0x11) -#define RSI_BLE_WLAN_TIMEOUT_NOTIFY (0x12) -#define RSI_BLE_WLAN_JOIN_STATUS (0x15) -#define RSI_APP_FW_VERSION (0x13) -#define RSI_BLE_WLAN_DISCONNECT_STATUS (0x14) - -#define RSI_REM_DEV_ADDR_LEN (18) -#define RSI_REM_DEV_NAME_LEN (31) - -#define RSI_BLE_DEV_NAME "CCP_DEVICE" -#define RSI_BLE_SET_RAND_ADDR "00:23:A7:12:34:56" -#define RSI_BLE_EVENT_GATT_RD (0x08) -#define RSI_BLE_ADDR_LENGTH 6 - -#define CLEAR_WHITELIST (0x00) -#define ADD_DEVICE_TO_WHITELIST (0x01) -#define DELETE_DEVICE_FROM_WHITELIST (0x02) - -#define CLEAR_ACCEPTLIST (0x00) -#define ADD_DEVICE_TO_ACCEPTLIST (0x01) -#define DELETE_DEVICE_FROM_ACCEPTLIST (0x02) - -#define RSI_BLE_TX_OCTETS 251 -#define RSI_BLE_TX_TIME 2120 // microseconds -#define RSI_BLE_MATTER_CUSTOM_SERVICE_DATA_LENGTH 240 - -#define GATT_READ_ZERO_OFFSET 0x00 -#define GATT_READ_RESP 0x00 -#define ALL_PHYS (0x00) - -#define RSI_BLE_DEV_ADDR_RESOLUTION_ENABLE (0) - -#define RSI_OPERMODE_WLAN_BLE (13) - -/***********************************************************************************************************************************************/ -//! Characteristic Presenatation Format Fields -/***********************************************************************************************************************************************/ -#define RSI_BLE_UINT8_FORMAT (0x04) -#define RSI_BLE_EXPONENT (0x00) -#define RSI_BLE_PERCENTAGE_UNITS_UUID (0x27AD) -#define RSI_BLE_NAME_SPACE (0x01) -#define RSI_BLE_DESCRIPTION (0x010B) - -//! BLE characteristic custom service uuid -#define RSI_BLE_CUSTOM_SERVICE_UUID (0xFFF6) -#define RSI_BLE_CUSTOM_LEVEL_UUID (0x1FF1) - -#if (defined(RSI_M4_INTERFACE) || defined(SLI_SI91X_MCU_INTERFACE)) -#define RSI_BLE_MAX_NBR_ATT_REC (20) - -#if (SIWX_917 | EXP_BOARD) -#define RSI_BLE_MAX_NBR_PERIPHERALS (1) -#else -#define RSI_BLE_MAX_NBR_SLAVES (1) -#endif // (SIWX_917 | EXP_BOARD) - -#define RSI_BLE_NUM_CONN_EVENTS (2) -#else -#define RSI_BLE_MAX_NBR_ATT_REC (80) - -#if (SIWX_917 | EXP_BOARD) -#define RSI_BLE_MAX_NBR_PERIPHERALS (3) -#else -#define RSI_BLE_MAX_NBR_SLAVES (3) -#endif // (SIWX_917 | EXP_BOARD) - -#define RSI_BLE_NUM_CONN_EVENTS (20) -#endif // (defined(RSI_M4_INTERFACE) || defined(SLI_SI91X_MCU_INTERFACE)) - -#define RSI_BLE_MAX_NBR_ATT_SERV (10) - -#define RSI_BLE_GATT_ASYNC_ENABLE (1) -#define RSI_BLE_GATT_INIT (0) - -#define RSI_BLE_START_SCAN (0x01) -#define RSI_BLE_STOP_SCAN (0x00) - -#define RSI_BLE_SCAN_TYPE SCAN_TYPE_ACTIVE -#define RSI_BLE_SCAN_FILTER_TYPE SCAN_FILTER_TYPE_ALL -/* Number of BLE GATT RECORD SIZE IN (n*16 BYTES), eg:(0x40*16)=1024 bytes */ -#define RSI_BLE_NUM_REC_BYTES (0x40) - -#define RSI_BLE_INDICATE_CONFIRMATION_FROM_HOST (0) - -/*=======================================================================*/ -//! Advertising command parameters -/*=======================================================================*/ - -#define RSI_BLE_ADV_TYPE UNDIR_CONN -#define RSI_BLE_ADV_FILTER_TYPE ALLOW_SCAN_REQ_ANY_CONN_REQ_ANY -#define RSI_BLE_ADV_DIR_ADDR_TYPE LE_RANDOM_ADDRESS -#define RSI_BLE_ADV_DIR_ADDR "00:15:83:6A:64:17" - -//! Reduced the BLE adv interval time to match with EFR BLE -#define RSI_BLE_ADV_INT_MIN (0x20) -#define RSI_BLE_ADV_INT_MAX (0x20) - -#define RSI_BLE_ADV_CHANNEL_MAP (0x07) - -//! Advertise status -//! Start the advertising process -#define RSI_BLE_START_ADV (0x01) -//! Stop the advertising process -#define RSI_BLE_STOP_ADV (0x00) - -//! BLE Tx Power Index On Air -#define RSI_BLE_PWR_INX (30) - -//! BLE Active H/w Pwr Features -#define BLE_DISABLE_DUTY_CYCLING (0) -#define BLE_DUTY_CYCLING (1) -#define BLR_DUTY_CYCLING (2) -#define BLE_4X_PWR_SAVE_MODE (4) -#define RSI_BLE_PWR_SAVE_OPTIONS BLE_DISABLE_DUTY_CYCLING - -//! Advertise types - -/* Advertising will be visible(discoverable) to all the devices. - * Scanning/Connection is also accepted from all devices - * */ -#define UNDIR_CONN (0x80) - -/* Advertising will be visible(discoverable) to the particular device - * mentioned in RSI_BLE_ADV_DIR_ADDR only. - * Scanning and Connection will be accepted from that device only. - * */ -#define DIR_CONN (0x81) - -/* Advertising will be visible(discoverable) to all the devices. - * Scanning will be accepted from all the devices. - * Connection will be not be accepted from any device. - * */ -#define UNDIR_SCAN (0x82) - -/* Advertising will be visible(discoverable) to all the devices. - * Scanning and Connection will not be accepted from any device - * */ -#define UNDIR_NON_CONN (0x83) - -/* Advertising will be visible(discoverable) to the particular device - * mentioned in RSI_BLE_ADV_DIR_ADDR only. - * Scanning and Connection will be accepted from that device only. - * */ -#define DIR_CONN_LOW_DUTY_CYCLE (0x84) - -//! Advertising flags -#define LE_LIMITED_DISCOVERABLE (0x01) -#define LE_GENERAL_DISCOVERABLE (0x02) -#define LE_BR_EDR_NOT_SUPPORTED (0x04) - -//! Advertise filters -#define ALLOW_SCAN_REQ_ANY_CONN_REQ_ANY (0x00) -#define ALLOW_SCAN_REQ_WHITE_LIST_CONN_REQ_ANY (0x01) -#define ALLOW_SCAN_REQ_ANY_CONN_REQ_WHITE_LIST (0x02) -#define ALLOW_SCAN_REQ_WHITE_LIST_CONN_REQ_WHITE_LIST (0x03) - -//! Address types -#define LE_PUBLIC_ADDRESS (0x00) -#define LE_RANDOM_ADDRESS (0x01) -#define LE_RESOLVABLE_PUBLIC_ADDRESS (0x02) -#define LE_RESOLVABLE_RANDOM_ADDRESS (0x03) - -/*=======================================================================*/ - -/*=======================================================================*/ -//! Connection parameters -/*=======================================================================*/ -#define LE_SCAN_INTERVAL (0x0100) -#define LE_SCAN_WINDOW (0x0050) - -#define CONNECTION_INTERVAL_MIN (0x00A0) -#define CONNECTION_INTERVAL_MAX (0x00A0) - -#define CONNECTION_LATENCY (0x0000) -#define SUPERVISION_TIMEOUT (0x07D0) // 2000 - -/*=======================================================================*/ - -/*=======================================================================*/ -//! Scan command parameters -/*=======================================================================*/ - -#define SL_WFX_BLE_SCAN_TYPE SCAN_TYPE_ACTIVE -#define SL_WFX_BLE_SCAN_FILTER_TYPE SCAN_FILTER_TYPE_ALL - -//! Scan status -#define SL_WFX_BLE_START_SCAN (0x01) -#define SL_WFX_BLE_STOP_SCAN (0x00) - -//! Scan types -#define SCAN_TYPE_ACTIVE (0x01) -#define SCAN_TYPE_PASSIVE (0x00) - -//! Scan filters -#define SCAN_FILTER_TYPE_ALL (0x00) -#define SCAN_FILTER_TYPE_ONLY_WHITE_LIST (0x01) - -#define SL_WFX_SEL_INTERNAL_ANTENNA (0x00) -#define SL_WFX_SEL_EXTERNAL_ANTENNA (0x01) - -#define SL_WFX_BT_CTRL_REMOTE_USER_TERMINATED (0x4E13) -#define SL_WFX_BT_CTRL_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES (0x4E14) -#define SL_WFX_BT_CTRL_REMOTE_POWERING_OFF (0x4E15) -#define SL_WFX_BT_CTRL_TERMINATED_MIC_FAILURE (0x4E3D) -#define SL_WFX_BT_FAILED_TO_ESTABLISH_CONN (0x4E3E) -#define SL_WFX_BT_INVALID_RANGE (0x4E60) - -/***********************************************************************************************************************************************/ -//! RS9116 Firmware Configurations -/***********************************************************************************************************************************************/ -/*=======================================================================*/ -//! Power save command parameters -/*=======================================================================*/ - -#define BLE_ATT_REC_SIZE (500) -#define NO_OF_VAL_ATT (5) //! Attribute value count - -#if (SIWX_917 | EXP_BOARD) -#define RSI_BLE_MAX_NBR_CENTRALS (1) -#define FRONT_END_SWITCH_SEL2 BIT(30) -#define RSI_FEATURE_BIT_MAP \ - (SL_SI91X_FEAT_ULP_GPIO_BASED_HANDSHAKE | SL_SI91X_FEAT_DEV_TO_HOST_ULP_GPIO_1) //! To set wlan feature select bit map -#define RSI_TCP_IP_FEATURE_BIT_MAP \ - (SL_SI91X_TCP_IP_FEAT_DHCPV4_CLIENT) //! TCP/IP feature select bitmap for selecting TCP/IP features -#define RSI_CUSTOM_FEATURE_BIT_MAP SL_SI91X_CUSTOM_FEAT_EXTENTION_VALID //! To set custom feature select bit map - -#ifdef CHIP_9117 -#if WIFI_ENABLE_SECURITY_WPA3_TRANSITION // Adding Support for WPA3 transition -#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP \ - (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(1) | SL_SI91X_RAM_LEVEL_NWP_BASIC_MCU_ADV | \ - SL_SI91X_EXT_FEAT_FRONT_END_SWITCH_PINS_ULP_GPIO_4_5_0 | SL_SI91X_EXT_FEAT_IEEE_80211W) -#else -#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP \ - (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(1) | SL_SI91X_RAM_LEVEL_NWP_BASIC_MCU_ADV | \ - SL_SI91X_EXT_FEAT_FRONT_END_SWITCH_PINS_ULP_GPIO_4_5_0) -#endif /* WIFI_ENABLE_SECURITY_WPA3_TRANSITION */ -#else // EXP_BOARD -#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2)) -#endif /* CHIP_9117 */ - -#define RSI_EXT_TCPIP_FEATURE_BITMAP 0 -#define RSI_BT_FEATURE_BITMAP (SL_SI91X_BT_RF_TYPE | SL_SI91X_ENABLE_BLE_PROTOCOL) -#define RSI_CONFIG_FEATURE_BITMAP 0 -#define RSI_TCP_IP_BYPASS RSI_ENABLE //! TCP IP BYPASS feature checks -#else -#define RSI_BLE_MAX_NBR_MASTERS (1) -#define RSI_HAND_SHAKE_TYPE GPIO_BASED -#endif -/***********************************************************************************************************************************************/ -//! user defined structure -/***********************************************************************************************************************************************/ -typedef struct rsi_ble_att_list_s -{ - uuid_t char_uuid; - uint16_t handle; - uint16_t value_len; - uint16_t max_value_len; - uint8_t char_val_prop; - void * value; -} rsi_ble_att_list_t; -typedef struct rsi_ble_s -{ - uint8_t DATA[BLE_ATT_REC_SIZE]; - uint16_t DATA_ix; - uint16_t att_rec_list_count; - rsi_ble_att_list_t att_rec_list[NO_OF_VAL_ATT]; -} rsi_ble_t; \ No newline at end of file +/******************************************************************************* + * @file rsi_ble_config.h + * @brief + ******************************************************************************* + * # License + * Copyright 2020 Silicon Laboratories Inc. www.silabs.com + ******************************************************************************* + * + * The licensor of this software is Silicon Laboratories Inc. Your use of this + * software is governed by the terms of Silicon Labs Master Software License + * Agreement (MSLA) available at + * www.silabs.com/about-us/legal/master-software-license-agreement. This + * software is distributed to you in Source Code format and is governed by the + * sections of the MSLA applicable to Source Code. + * + ******************************************************************************/ +#pragma once + +#include "rsi_ble_apis.h" +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) +#include "rsi_bt_common_apis.h" +#include "rsi_user.h" +#else +#include +#endif + +/****************************************************** + * * Macros + * ******************************************************/ +//! application event list +// TODO: remove this define after integration of the new wifi sdk +#ifndef RSI_FAILURE +// failure return value +#define RSI_FAILURE -1 +#endif + +#define RSI_BLE_CONN_EVENT (0x01) +#define RSI_BLE_DISCONN_EVENT (0x02) +#define RSI_BLE_GATT_WRITE_EVENT (0x03) +#define RSI_BLE_MTU_EVENT (0x04) +#define RSI_BLE_GATT_INDICATION_CONFIRMATION (0x05) +#define RSI_BLE_RESP_ATT_VALUE (0x06) +#define RSI_BLE_EVENT_GATT_RD (0x08) +#define RSI_BLE_ADDR_LENGTH 6 + +#define RSI_SSID (0x0D) +#define RSI_SECTYPE (0x0E) +#define RSI_BLE_WLAN_DISCONN_NOTIFY (0x0F) +#define RSI_WLAN_ALREADY (0x10) +#define RSI_WLAN_NOT_ALREADY (0x11) +#define RSI_BLE_WLAN_TIMEOUT_NOTIFY (0x12) +#define RSI_BLE_WLAN_JOIN_STATUS (0x15) +#define RSI_APP_FW_VERSION (0x13) +#define RSI_BLE_WLAN_DISCONNECT_STATUS (0x14) + +#define RSI_REM_DEV_ADDR_LEN (18) +#define RSI_REM_DEV_NAME_LEN (31) + +#define RSI_BLE_DEV_NAME "CCP_DEVICE" +#define RSI_BLE_SET_RAND_ADDR "00:23:A7:12:34:56" +#define RSI_BLE_EVENT_GATT_RD (0x08) +#define RSI_BLE_ADDR_LENGTH 6 + +#define CLEAR_WHITELIST (0x00) +#define ADD_DEVICE_TO_WHITELIST (0x01) +#define DELETE_DEVICE_FROM_WHITELIST (0x02) + +#define CLEAR_ACCEPTLIST (0x00) +#define ADD_DEVICE_TO_ACCEPTLIST (0x01) +#define DELETE_DEVICE_FROM_ACCEPTLIST (0x02) + +#define RSI_BLE_TX_OCTETS 251 +#define RSI_BLE_TX_TIME 2120 // microseconds +#define RSI_BLE_MATTER_CUSTOM_SERVICE_DATA_LENGTH 240 + +#define GATT_READ_ZERO_OFFSET 0x00 +#define GATT_READ_RESP 0x00 +#define ALL_PHYS (0x00) + +#define RSI_BLE_DEV_ADDR_RESOLUTION_ENABLE (0) + +#define RSI_OPERMODE_WLAN_BLE (13) + +/***********************************************************************************************************************************************/ +//! Characteristic Presenatation Format Fields +/***********************************************************************************************************************************************/ +#define RSI_BLE_UINT8_FORMAT (0x04) +#define RSI_BLE_EXPONENT (0x00) +#define RSI_BLE_PERCENTAGE_UNITS_UUID (0x27AD) +#define RSI_BLE_NAME_SPACE (0x01) +#define RSI_BLE_DESCRIPTION (0x010B) + +//! BLE characteristic custom service uuid +#define RSI_BLE_CUSTOM_SERVICE_UUID (0xFFF6) +#define RSI_BLE_CUSTOM_LEVEL_UUID (0x1FF1) + +#if (defined(RSI_M4_INTERFACE) || defined(SLI_SI91X_MCU_INTERFACE)) +#define RSI_BLE_MAX_NBR_ATT_REC (20) + +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) +#define RSI_BLE_MAX_NBR_PERIPHERALS (1) +#else +#define RSI_BLE_MAX_NBR_SLAVES (1) +#endif // (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) + +#define RSI_BLE_NUM_CONN_EVENTS (2) +#else +#define RSI_BLE_MAX_NBR_ATT_REC (80) + +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) +#define RSI_BLE_MAX_NBR_PERIPHERALS (3) +#else +#define RSI_BLE_MAX_NBR_SLAVES (3) +#endif // (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) + +#define RSI_BLE_NUM_CONN_EVENTS (20) +#endif // (defined(RSI_M4_INTERFACE) || defined(SLI_SI91X_MCU_INTERFACE)) + +#define RSI_BLE_MAX_NBR_ATT_SERV (10) + +#define RSI_BLE_GATT_ASYNC_ENABLE (1) +#define RSI_BLE_GATT_INIT (0) + +#define RSI_BLE_START_SCAN (0x01) +#define RSI_BLE_STOP_SCAN (0x00) + +#define RSI_BLE_SCAN_TYPE SCAN_TYPE_ACTIVE +#define RSI_BLE_SCAN_FILTER_TYPE SCAN_FILTER_TYPE_ALL +/* Number of BLE GATT RECORD SIZE IN (n*16 BYTES), eg:(0x40*16)=1024 bytes */ +#define RSI_BLE_NUM_REC_BYTES (0x40) + +#define RSI_BLE_INDICATE_CONFIRMATION_FROM_HOST (0) + +/*=======================================================================*/ +//! Advertising command parameters +/*=======================================================================*/ + +#define RSI_BLE_ADV_TYPE UNDIR_CONN +#define RSI_BLE_ADV_FILTER_TYPE ALLOW_SCAN_REQ_ANY_CONN_REQ_ANY +#define RSI_BLE_ADV_DIR_ADDR_TYPE LE_RANDOM_ADDRESS +#define RSI_BLE_ADV_DIR_ADDR "00:15:83:6A:64:17" + +//! Reduced the BLE adv interval time to match with EFR BLE +#define RSI_BLE_ADV_INT_MIN (0x20) +#define RSI_BLE_ADV_INT_MAX (0x20) + +#define RSI_BLE_ADV_CHANNEL_MAP (0x07) + +//! Advertise status +//! Start the advertising process +#define RSI_BLE_START_ADV (0x01) +//! Stop the advertising process +#define RSI_BLE_STOP_ADV (0x00) + +//! BLE Tx Power Index On Air +#define RSI_BLE_PWR_INX (30) + +//! BLE Active H/w Pwr Features +#define BLE_DISABLE_DUTY_CYCLING (0) +#define BLE_DUTY_CYCLING (1) +#define BLR_DUTY_CYCLING (2) +#define BLE_4X_PWR_SAVE_MODE (4) +#define RSI_BLE_PWR_SAVE_OPTIONS BLE_DISABLE_DUTY_CYCLING + +//! Advertise types + +/* Advertising will be visible(discoverable) to all the devices. + * Scanning/Connection is also accepted from all devices + * */ +#define UNDIR_CONN (0x80) + +/* Advertising will be visible(discoverable) to the particular device + * mentioned in RSI_BLE_ADV_DIR_ADDR only. + * Scanning and Connection will be accepted from that device only. + * */ +#define DIR_CONN (0x81) + +/* Advertising will be visible(discoverable) to all the devices. + * Scanning will be accepted from all the devices. + * Connection will be not be accepted from any device. + * */ +#define UNDIR_SCAN (0x82) + +/* Advertising will be visible(discoverable) to all the devices. + * Scanning and Connection will not be accepted from any device + * */ +#define UNDIR_NON_CONN (0x83) + +/* Advertising will be visible(discoverable) to the particular device + * mentioned in RSI_BLE_ADV_DIR_ADDR only. + * Scanning and Connection will be accepted from that device only. + * */ +#define DIR_CONN_LOW_DUTY_CYCLE (0x84) + +//! Advertising flags +#define LE_LIMITED_DISCOVERABLE (0x01) +#define LE_GENERAL_DISCOVERABLE (0x02) +#define LE_BR_EDR_NOT_SUPPORTED (0x04) + +//! Advertise filters +#define ALLOW_SCAN_REQ_ANY_CONN_REQ_ANY (0x00) +#define ALLOW_SCAN_REQ_WHITE_LIST_CONN_REQ_ANY (0x01) +#define ALLOW_SCAN_REQ_ANY_CONN_REQ_WHITE_LIST (0x02) +#define ALLOW_SCAN_REQ_WHITE_LIST_CONN_REQ_WHITE_LIST (0x03) + +//! Address types +#define LE_PUBLIC_ADDRESS (0x00) +#define LE_RANDOM_ADDRESS (0x01) +#define LE_RESOLVABLE_PUBLIC_ADDRESS (0x02) +#define LE_RESOLVABLE_RANDOM_ADDRESS (0x03) + +/*=======================================================================*/ + +/*=======================================================================*/ +//! Connection parameters +/*=======================================================================*/ +#define LE_SCAN_INTERVAL (0x0100) +#define LE_SCAN_WINDOW (0x0050) + +#define CONNECTION_INTERVAL_MIN (0x00A0) +#define CONNECTION_INTERVAL_MAX (0x00A0) + +#define CONNECTION_LATENCY (0x0000) +#define SUPERVISION_TIMEOUT (0x07D0) // 2000 + +/*=======================================================================*/ + +/*=======================================================================*/ +//! Scan command parameters +/*=======================================================================*/ + +#define SL_WFX_BLE_SCAN_TYPE SCAN_TYPE_ACTIVE +#define SL_WFX_BLE_SCAN_FILTER_TYPE SCAN_FILTER_TYPE_ALL + +//! Scan status +#define SL_WFX_BLE_START_SCAN (0x01) +#define SL_WFX_BLE_STOP_SCAN (0x00) + +//! Scan types +#define SCAN_TYPE_ACTIVE (0x01) +#define SCAN_TYPE_PASSIVE (0x00) + +//! Scan filters +#define SCAN_FILTER_TYPE_ALL (0x00) +#define SCAN_FILTER_TYPE_ONLY_WHITE_LIST (0x01) + +#define SL_WFX_SEL_INTERNAL_ANTENNA (0x00) +#define SL_WFX_SEL_EXTERNAL_ANTENNA (0x01) + +#define SL_WFX_BT_CTRL_REMOTE_USER_TERMINATED (0x4E13) +#define SL_WFX_BT_CTRL_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES (0x4E14) +#define SL_WFX_BT_CTRL_REMOTE_POWERING_OFF (0x4E15) +#define SL_WFX_BT_CTRL_TERMINATED_MIC_FAILURE (0x4E3D) +#define SL_WFX_BT_FAILED_TO_ESTABLISH_CONN (0x4E3E) +#define SL_WFX_BT_INVALID_RANGE (0x4E60) + +/***********************************************************************************************************************************************/ +//! RS9116 Firmware Configurations +/***********************************************************************************************************************************************/ +/*=======================================================================*/ +//! Power save command parameters +/*=======================================================================*/ + +#define BLE_ATT_REC_SIZE (500) +#define NO_OF_VAL_ATT (5) //! Attribute value count + +#if (SLI_SI91X_MCU_INTERFACE | EXP_BOARD) +#define RSI_BLE_MAX_NBR_CENTRALS (1) +#define FRONT_END_SWITCH_SEL2 BIT(30) +#define RSI_FEATURE_BIT_MAP \ + (SL_SI91X_FEAT_ULP_GPIO_BASED_HANDSHAKE | SL_SI91X_FEAT_DEV_TO_HOST_ULP_GPIO_1) //! To set wlan feature select bit map +#define RSI_TCP_IP_FEATURE_BIT_MAP \ + (SL_SI91X_TCP_IP_FEAT_DHCPV4_CLIENT) //! TCP/IP feature select bitmap for selecting TCP/IP features +#define RSI_CUSTOM_FEATURE_BIT_MAP SL_SI91X_CUSTOM_FEAT_EXTENTION_VALID //! To set custom feature select bit map + +#ifdef CHIP_9117 +#if WIFI_ENABLE_SECURITY_WPA3_TRANSITION // Adding Support for WPA3 transition +#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP \ + (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(1) | SL_SI91X_RAM_LEVEL_NWP_BASIC_MCU_ADV | \ + SL_SI91X_EXT_FEAT_FRONT_END_SWITCH_PINS_ULP_GPIO_4_5_0 | SL_SI91X_EXT_FEAT_IEEE_80211W) +#else +#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP \ + (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(1) | SL_SI91X_RAM_LEVEL_NWP_BASIC_MCU_ADV | \ + SL_SI91X_EXT_FEAT_FRONT_END_SWITCH_PINS_ULP_GPIO_4_5_0) +#endif /* WIFI_ENABLE_SECURITY_WPA3_TRANSITION */ +#else // EXP_BOARD +#define RSI_EXT_CUSTOM_FEATURE_BIT_MAP (SL_SI91X_EXT_FEAT_LOW_POWER_MODE | SL_SI91X_EXT_FEAT_XTAL_CLK_ENABLE(2)) +#endif /* CHIP_9117 */ + +#define RSI_EXT_TCPIP_FEATURE_BITMAP 0 +#define RSI_BT_FEATURE_BITMAP (SL_SI91X_BT_RF_TYPE | SL_SI91X_ENABLE_BLE_PROTOCOL) +#define RSI_CONFIG_FEATURE_BITMAP 0 +#define RSI_TCP_IP_BYPASS RSI_ENABLE //! TCP IP BYPASS feature checks +#else +#define RSI_BLE_MAX_NBR_MASTERS (1) +#define RSI_HAND_SHAKE_TYPE GPIO_BASED +#endif +/***********************************************************************************************************************************************/ +//! user defined structure +/***********************************************************************************************************************************************/ +typedef struct rsi_ble_att_list_s +{ + uuid_t char_uuid; + uint16_t handle; + uint16_t value_len; + uint16_t max_value_len; + uint8_t char_val_prop; + void * value; +} rsi_ble_att_list_t; +typedef struct rsi_ble_s +{ + uint8_t DATA[BLE_ATT_REC_SIZE]; + uint16_t DATA_ix; + uint16_t att_rec_list_count; + rsi_ble_att_list_t att_rec_list[NO_OF_VAL_ATT]; +} rsi_ble_t; diff --git a/src/platform/silabs/rs911x/wfx_sl_ble_init.h b/src/platform/silabs/rs911x/wfx_sl_ble_init.h index 26ee258300c5de..19a28888f1864d 100644 --- a/src/platform/silabs/rs911x/wfx_sl_ble_init.h +++ b/src/platform/silabs/rs911x/wfx_sl_ble_init.h @@ -1,134 +1,134 @@ -/******************************************************************************* - * @file wfx_sl_ble_init.h - * @brief - ******************************************************************************* - * # License - * Copyright 2021 Silicon Laboratories Inc. www.silabs.com - ******************************************************************************* - * - * Copyright (c) 2022 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 files - * */ - -#ifndef WFX_SL_BLE_INIT -#define WFX_SL_BLE_INIT -#ifndef RSI_BLE_ENABLE -#define RSI_BLE_ENABLE (1) -#endif // RSI_BLE_ENABLE - -// BLE include file to refer BLE APIs -#include "FreeRTOS.h" -#include "ble_config.h" -#include "event_groups.h" -#include "task.h" -#include "timers.h" -#include "wfx_host_events.h" -#include "wfx_rsi.h" -#include -#include -#include -#if !(SIWX_917 | EXP_BOARD) -#include -#include -#include -#include -#include -#endif -#include -#include -#include -#include -#include - -typedef struct sl_wfx_msg_s -{ - uint8_t connectionHandle; - uint8_t bondingHandle; - uint32_t event_num; - uint16_t reason; - uint16_t event_id; - uint16_t resp_status; - rsi_ble_event_mtu_t rsi_ble_mtu; - rsi_ble_event_write_t rsi_ble_write; - rsi_ble_event_enhance_conn_status_t resp_enh_conn; - rsi_ble_event_disconnect_t * resp_disconnect; - rsi_ble_read_req_t * rsi_ble_read_req; - rsi_ble_set_att_resp_t rsi_ble_event_set_att_rsp; - uint32_t ble_app_event_map; - uint32_t ble_app_event_mask; - uint16_t rsi_ble_measurement_hndl; - uint16_t rsi_ble_gatt_server_client_config_hndl; - uint16_t subscribed; - -} sl_wfx_msg_t; - -#define ATT_REC_IN_HOST (0) - -#define RSI_BT_CTRL_REMOTE_USER_TERMINATED (0x4E13) -#define RSI_BT_CTRL_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES (0x4E14) -#define RSI_BT_CTRL_REMOTE_POWERING_OFF (0x4E15) -#define RSI_BT_CTRL_TERMINATED_MIC_FAILURE (0x4E3D) -#define RSI_BT_FAILED_TO_ESTABLISH_CONN (0x4E3E) -#define RSI_BT_INVALID_RANGE (0x4E60) - -#define RSI_BLE_MATTER_CUSTOM_SERVICE_UUID (0) -#define RSI_BLE_MATTER_CUSTOM_SERVICE_SIZE (2) -#define RSI_BLE_MATTER_CUSTOM_SERVICE_VALUE_16 (0xFFF6) -#define RSI_BLE_MATTER_CUSTOM_SERVICE_DATA (0x00) - -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_SIZE (16) -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_RESERVED 0x00, 0x00, 0x00 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_1 0x18EE2EF5 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_2 0x263D -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_3 0x4559 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_4 0x9F, 0x95, 0x9C, 0x4F, 0x11, 0x9D, 0x9F, 0x42 -#define RSI_BLE_CHARACTERISTIC_RX_ATTRIBUTE_HANDLE_LOCATION (1) -#define RSI_BLE_CHARACTERISTIC_RX_VALUE_HANDLE_LOCATION (2) - -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_SIZE (16) -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_RESERVED 0x00, 0x00, 0x00 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_1 0x18EE2EF5 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_2 0x263D -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_3 0x4559 -#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_4 0x9F, 0x95, 0x9C, 0x4F, 0x12, 0x9D, 0x9F, 0x42 -#define RSI_BLE_CHARACTERISTIC_TX_ATTRIBUTE_HANDLE_LOCATION (3) -#define RSI_BLE_CHARACTERISTIC_TX_MEASUREMENT_HANDLE_LOCATION (4) -#define RSI_BLE_CHARACTERISTIC_TX_GATT_SERVER_CLIENT_HANDLE_LOCATION (5) - -// ALL Ble functions -void rsi_ble_on_connect_event(rsi_ble_event_conn_status_t * resp_conn); -void rsi_ble_on_disconnect_event(rsi_ble_event_disconnect_t * resp_disconnect, uint16_t reason); -void rsi_ble_on_enhance_conn_status_event(rsi_ble_event_enhance_conn_status_t * resp_enh_conn); -void rsi_ble_on_gatt_write_event(uint16_t event_id, rsi_ble_event_write_t * rsi_ble_write); -void rsi_ble_on_mtu_event(rsi_ble_event_mtu_t * rsi_ble_mtu); -void rsi_ble_on_event_indication_confirmation(uint16_t resp_status, rsi_ble_set_att_resp_t * rsi_ble_event_set_att_rsp); -void rsi_ble_on_read_req_event(uint16_t event_id, rsi_ble_read_req_t * rsi_ble_read_req); -void rsi_gatt_add_attribute_to_list(rsi_ble_t * p_val, uint16_t handle, uint16_t data_len, uint8_t * data, uuid_t uuid, - uint8_t char_prop); -void rsi_ble_add_char_serv_att(void * serv_handler, uint16_t handle, uint8_t val_prop, uint16_t att_val_handle, - uuid_t att_val_uuid); -void rsi_ble_add_char_val_att(void * serv_handler, uint16_t handle, uuid_t att_type_uuid, uint8_t val_prop, uint8_t * data, - uint8_t data_len, uint8_t auth_read); -uint32_t rsi_ble_add_matter_service(void); -void rsi_ble_app_set_event(uint32_t event_num); -int32_t rsi_ble_app_get_event(void); -void rsi_ble_app_clear_event(uint32_t event_num); -void rsi_ble_app_init_events(); -void rsi_ble_event_handling_task(void); - -#endif +/******************************************************************************* + * @file wfx_sl_ble_init.h + * @brief + ******************************************************************************* + * # License + * Copyright 2021 Silicon Laboratories Inc. www.silabs.com + ******************************************************************************* + * + * Copyright (c) 2022 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 files + * */ + +#ifndef WFX_SL_BLE_INIT +#define WFX_SL_BLE_INIT +#ifndef RSI_BLE_ENABLE +#define RSI_BLE_ENABLE (1) +#endif // RSI_BLE_ENABLE + +// BLE include file to refer BLE APIs +#include "FreeRTOS.h" +#include "ble_config.h" +#include "event_groups.h" +#include "task.h" +#include "timers.h" +#include "wfx_host_events.h" +#include "wfx_rsi.h" +#include +#include +#include +#if !(SLI_SI91X_MCU_INTERFACE | EXP_BOARD) +#include +#include +#include +#include +#include +#endif +#include +#include +#include +#include +#include + +typedef struct sl_wfx_msg_s +{ + uint8_t connectionHandle; + uint8_t bondingHandle; + uint32_t event_num; + uint16_t reason; + uint16_t event_id; + uint16_t resp_status; + rsi_ble_event_mtu_t rsi_ble_mtu; + rsi_ble_event_write_t rsi_ble_write; + rsi_ble_event_enhance_conn_status_t resp_enh_conn; + rsi_ble_event_disconnect_t * resp_disconnect; + rsi_ble_read_req_t * rsi_ble_read_req; + rsi_ble_set_att_resp_t rsi_ble_event_set_att_rsp; + uint32_t ble_app_event_map; + uint32_t ble_app_event_mask; + uint16_t rsi_ble_measurement_hndl; + uint16_t rsi_ble_gatt_server_client_config_hndl; + uint16_t subscribed; + +} sl_wfx_msg_t; + +#define ATT_REC_IN_HOST (0) + +#define RSI_BT_CTRL_REMOTE_USER_TERMINATED (0x4E13) +#define RSI_BT_CTRL_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES (0x4E14) +#define RSI_BT_CTRL_REMOTE_POWERING_OFF (0x4E15) +#define RSI_BT_CTRL_TERMINATED_MIC_FAILURE (0x4E3D) +#define RSI_BT_FAILED_TO_ESTABLISH_CONN (0x4E3E) +#define RSI_BT_INVALID_RANGE (0x4E60) + +#define RSI_BLE_MATTER_CUSTOM_SERVICE_UUID (0) +#define RSI_BLE_MATTER_CUSTOM_SERVICE_SIZE (2) +#define RSI_BLE_MATTER_CUSTOM_SERVICE_VALUE_16 (0xFFF6) +#define RSI_BLE_MATTER_CUSTOM_SERVICE_DATA (0x00) + +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_SIZE (16) +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_RESERVED 0x00, 0x00, 0x00 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_1 0x18EE2EF5 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_2 0x263D +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_3 0x4559 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_RX_VALUE_128_DATA_4 0x9F, 0x95, 0x9C, 0x4F, 0x11, 0x9D, 0x9F, 0x42 +#define RSI_BLE_CHARACTERISTIC_RX_ATTRIBUTE_HANDLE_LOCATION (1) +#define RSI_BLE_CHARACTERISTIC_RX_VALUE_HANDLE_LOCATION (2) + +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_SIZE (16) +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_RESERVED 0x00, 0x00, 0x00 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_1 0x18EE2EF5 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_2 0x263D +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_3 0x4559 +#define RSI_BLE_CUSTOM_CHARACTERISTIC_TX_VALUE_128_DATA_4 0x9F, 0x95, 0x9C, 0x4F, 0x12, 0x9D, 0x9F, 0x42 +#define RSI_BLE_CHARACTERISTIC_TX_ATTRIBUTE_HANDLE_LOCATION (3) +#define RSI_BLE_CHARACTERISTIC_TX_MEASUREMENT_HANDLE_LOCATION (4) +#define RSI_BLE_CHARACTERISTIC_TX_GATT_SERVER_CLIENT_HANDLE_LOCATION (5) + +// ALL Ble functions +void rsi_ble_on_connect_event(rsi_ble_event_conn_status_t * resp_conn); +void rsi_ble_on_disconnect_event(rsi_ble_event_disconnect_t * resp_disconnect, uint16_t reason); +void rsi_ble_on_enhance_conn_status_event(rsi_ble_event_enhance_conn_status_t * resp_enh_conn); +void rsi_ble_on_gatt_write_event(uint16_t event_id, rsi_ble_event_write_t * rsi_ble_write); +void rsi_ble_on_mtu_event(rsi_ble_event_mtu_t * rsi_ble_mtu); +void rsi_ble_on_event_indication_confirmation(uint16_t resp_status, rsi_ble_set_att_resp_t * rsi_ble_event_set_att_rsp); +void rsi_ble_on_read_req_event(uint16_t event_id, rsi_ble_read_req_t * rsi_ble_read_req); +void rsi_gatt_add_attribute_to_list(rsi_ble_t * p_val, uint16_t handle, uint16_t data_len, uint8_t * data, uuid_t uuid, + uint8_t char_prop); +void rsi_ble_add_char_serv_att(void * serv_handler, uint16_t handle, uint8_t val_prop, uint16_t att_val_handle, + uuid_t att_val_uuid); +void rsi_ble_add_char_val_att(void * serv_handler, uint16_t handle, uuid_t att_type_uuid, uint8_t val_prop, uint8_t * data, + uint8_t data_len, uint8_t auth_read); +uint32_t rsi_ble_add_matter_service(void); +void rsi_ble_app_set_event(uint32_t event_num); +int32_t rsi_ble_app_get_event(void); +void rsi_ble_app_clear_event(uint32_t event_num); +void rsi_ble_app_init_events(); +void rsi_ble_event_handling_task(void); + +#endif diff --git a/third_party/silabs/SiWx917_sdk.gni b/third_party/silabs/SiWx917_sdk.gni index 0853c0304a8b61..f2d501ef6aa462 100644 --- a/third_party/silabs/SiWx917_sdk.gni +++ b/third_party/silabs/SiWx917_sdk.gni @@ -160,7 +160,8 @@ template("siwx917_sdk") { "RS911X_WIFI=1", "RSI_WLAN_ENABLE", "SLI_SI91X_ENABLE_OS=1", - "SLI_SI91X_MCU_INTERFACE=1", #Enable CCP bus Interface + "SLI_SI91X_MCU_INTERFACE=1", #Enable CCP bus Interface, Differentiation: + # 1->SOC and 0->NCP "RSI_WLAN_API_ENABLE", "NVM3_DEFAULT_NVM_SIZE=40960", "NVM3_DEFAULT_MAX_OBJECT_SIZE=4092", @@ -171,8 +172,6 @@ template("siwx917_sdk") { "__HEAP_SIZE=0", "PLATFORM_HEADER=\"platform-header.h\"", "USE_NVM3=1", - "SIWX_917=1", - "CHIP_9117=1", "SLI_SI91X_ENABLE_BLE=1", "SL_SI91X_ENABLE_LITTLE_ENDIAN=1", "TINYCRYPT_PRIMITIVES", @@ -183,7 +182,6 @@ template("siwx917_sdk") { "SLI_SI91X_MCU_ENABLE_FLASH_BASED_EXECUTION=1", "SL_WIFI_COMPONENT_INCLUDED=1", "SLI_SI917=1", - "SI917=1", "ROMDRIVER_PRESENT=1", "SL_CATALOG_FREERTOS_KERNEL_PRESENT=1", "SLI_SI91X_MCU_CONFIG_RADIO_BOARD_BASE_VER=1", @@ -203,7 +201,6 @@ template("siwx917_sdk") { "ENABLE_DEBUG_MODULE=1", "SI91X_SYSRTC_PRESENT=1", "TA_DEEP_SLEEP_COMMON_FLASH=1", - "SI917_SOC=1", "SI91X_PLATFORM=1", "SL_NET_COMPONENT_INCLUDED=1", "SRAM_BASE=0x0cUL", From 8fb91a39206edbde8ba55432748ae60dc5fffc7a Mon Sep 17 00:00:00 2001 From: Jeff Tung <100387939+jtung-apple@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:58:43 -0700 Subject: [PATCH 34/76] [Darwin] MTRDevice isCachePrimed parts list check fix (#32509) --- src/darwin/Framework/CHIP/MTRDevice.mm | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index fa6b524fb6fab6..f854242dfbeb1f 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -2208,14 +2208,19 @@ - (BOOL)_isCachePrimedWithInitialConfigurationData } // Check if we have cached descriptor clusters for each listed endpoint - for (NSDictionary * endpointDataValue in partsList) { + for (NSDictionary * endpointDictionary in partsList) { + NSDictionary * endpointDataValue = endpointDictionary[MTRDataKey]; + if (![endpointDataValue isKindOfClass:[NSDictionary class]]) { + MTR_LOG_ERROR("%@ unexpected parts list dictionary %@ data value class %@", self, endpointDictionary, [endpointDataValue class]); + continue; + } if (![MTRUnsignedIntegerValueType isEqual:endpointDataValue[MTRTypeKey]]) { - MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + MTR_LOG_ERROR("%@ unexpected parts list data value %@ item type %@", self, endpointDataValue, endpointDataValue[MTRTypeKey]); continue; } NSNumber * endpoint = endpointDataValue[MTRValueKey]; if (![endpoint isKindOfClass:[NSNumber class]]) { - MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + MTR_LOG_ERROR("%@ unexpected parts list item value class %@", self, [endpoint class]); continue; } NSDictionary * descriptorDeviceTypeListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID)]]; From a31de1d70aac8ca24bef12c1933d0171534a090c Mon Sep 17 00:00:00 2001 From: chirag-silabs <100861685+chirag-silabs@users.noreply.github.com> Date: Tue, 12 Mar 2024 22:49:47 +0530 Subject: [PATCH 35/76] [Silabs] [WiFi] Modifying the temp_reset struct from pointer to variable (#32504) * modifying the temp_reset struct from pointer to variable * Restyled by clang-format --------- Co-authored-by: Restyled.io --- .../silabs/SiWx917/SiWx917/sl_wifi_if.c | 33 +++++++++-------- .../platform/silabs/efr32/rs911x/rsi_if.c | 35 +++++++++---------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c index 60489cd9b2ba30..079f12f6288cd5 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c +++ b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c @@ -94,7 +94,7 @@ extern osSemaphoreId_t sl_rs_ble_init_sem; * This file implements the interface to the wifi sdk */ -wfx_wifi_scan_ext_t * temp_reset; +static wfx_wifi_scan_ext_t temp_reset; volatile sl_status_t callback_status = SL_STATUS_OK; @@ -132,13 +132,13 @@ int32_t wfx_rsi_get_ap_ext(wfx_wifi_scan_ext_t * extra_info) sl_wifi_statistics_t test = { 0 }; status = sl_wifi_get_statistics(SL_WIFI_CLIENT_INTERFACE, &test); VERIFY_STATUS_AND_RETURN(status); - extra_info->beacon_lost_count = test.beacon_lost_count - temp_reset->beacon_lost_count; - extra_info->beacon_rx_count = test.beacon_rx_count - temp_reset->beacon_rx_count; - extra_info->mcast_rx_count = test.mcast_rx_count - temp_reset->mcast_rx_count; - extra_info->mcast_tx_count = test.mcast_tx_count - temp_reset->mcast_tx_count; - extra_info->ucast_rx_count = test.ucast_rx_count - temp_reset->ucast_rx_count; - extra_info->ucast_tx_count = test.ucast_tx_count - temp_reset->ucast_tx_count; - extra_info->overrun_count = test.overrun_count - temp_reset->overrun_count; + extra_info->beacon_lost_count = test.beacon_lost_count - temp_reset.beacon_lost_count; + extra_info->beacon_rx_count = test.beacon_rx_count - temp_reset.beacon_rx_count; + extra_info->mcast_rx_count = test.mcast_rx_count - temp_reset.mcast_rx_count; + extra_info->mcast_tx_count = test.mcast_tx_count - temp_reset.mcast_tx_count; + extra_info->ucast_rx_count = test.ucast_rx_count - temp_reset.ucast_rx_count; + extra_info->ucast_tx_count = test.ucast_tx_count - temp_reset.ucast_tx_count; + extra_info->overrun_count = test.overrun_count - temp_reset.overrun_count; return status; } @@ -156,13 +156,13 @@ int32_t wfx_rsi_reset_count() sl_status_t status = SL_STATUS_OK; status = sl_wifi_get_statistics(SL_WIFI_CLIENT_INTERFACE, &test); VERIFY_STATUS_AND_RETURN(status); - temp_reset->beacon_lost_count = test.beacon_lost_count; - temp_reset->beacon_rx_count = test.beacon_rx_count; - temp_reset->mcast_rx_count = test.mcast_rx_count; - temp_reset->mcast_tx_count = test.mcast_tx_count; - temp_reset->ucast_rx_count = test.ucast_rx_count; - temp_reset->ucast_tx_count = test.ucast_tx_count; - temp_reset->overrun_count = test.overrun_count; + temp_reset.beacon_lost_count = test.beacon_lost_count; + temp_reset.beacon_rx_count = test.beacon_rx_count; + temp_reset.mcast_rx_count = test.mcast_rx_count; + temp_reset.mcast_tx_count = test.mcast_tx_count; + temp_reset.ucast_rx_count = test.ucast_rx_count; + temp_reset.ucast_tx_count = test.ucast_tx_count; + temp_reset.overrun_count = test.overrun_count; return status; } @@ -182,8 +182,6 @@ int32_t wfx_rsi_disconnect() sl_status_t join_callback_handler(sl_wifi_event_t event, char * result, uint32_t result_length, void * arg) { wfx_rsi.dev_state &= ~(WFX_RSI_ST_STA_CONNECTING); - temp_reset = (wfx_wifi_scan_ext_t *) malloc(sizeof(wfx_wifi_scan_ext_t)); - memset(temp_reset, 0, sizeof(wfx_wifi_scan_ext_t)); if (SL_WIFI_CHECK_IF_EVENT_FAILED(event)) { SILABS_LOG("F: Join Event received with %u bytes payload\n", result_length); @@ -200,6 +198,7 @@ sl_status_t join_callback_handler(sl_wifi_event_t event, char * result, uint32_t /* * Join was complete - Do the DHCP */ + memset(&temp_reset, 0, sizeof(wfx_wifi_scan_ext_t)); SILABS_LOG("join_callback_handler: join completed."); SILABS_LOG("%c: Join Event received with %u bytes payload\n", *result, result_length); xEventGroupSetBits(wfx_rsi.events, WFX_EVT_STA_CONN); diff --git a/examples/platform/silabs/efr32/rs911x/rsi_if.c b/examples/platform/silabs/efr32/rs911x/rsi_if.c index 1ba06d0e715c16..d229251a7a5cf9 100644 --- a/examples/platform/silabs/efr32/rs911x/rsi_if.c +++ b/examples/platform/silabs/efr32/rs911x/rsi_if.c @@ -88,7 +88,7 @@ extern rsi_semaphore_handle_t sl_rs_ble_init_sem; * This file implements the interface to the RSI SAPIs */ static uint8_t wfx_rsi_drv_buf[WFX_RSI_BUF_SZ]; -static wfx_wifi_scan_ext_t * temp_reset; +static wfx_wifi_scan_ext_t temp_reset; /****************************************************************** * @fn int32_t wfx_rsi_get_ap_info(wfx_wifi_scan_result_t *ap) @@ -133,13 +133,13 @@ int32_t wfx_rsi_get_ap_ext(wfx_wifi_scan_ext_t * extra_info) else { rsi_wlan_ext_stats_t * test = (rsi_wlan_ext_stats_t *) buff; - extra_info->beacon_lost_count = test->beacon_lost_count - temp_reset->beacon_lost_count; - extra_info->beacon_rx_count = test->beacon_rx_count - temp_reset->beacon_rx_count; - extra_info->mcast_rx_count = test->mcast_rx_count - temp_reset->mcast_rx_count; - extra_info->mcast_tx_count = test->mcast_tx_count - temp_reset->mcast_tx_count; - extra_info->ucast_rx_count = test->ucast_rx_count - temp_reset->ucast_rx_count; - extra_info->ucast_tx_count = test->ucast_tx_count - temp_reset->ucast_tx_count; - extra_info->overrun_count = test->overrun_count - temp_reset->overrun_count; + extra_info->beacon_lost_count = test->beacon_lost_count - temp_reset.beacon_lost_count; + extra_info->beacon_rx_count = test->beacon_rx_count - temp_reset.beacon_rx_count; + extra_info->mcast_rx_count = test->mcast_rx_count - temp_reset.mcast_rx_count; + extra_info->mcast_tx_count = test->mcast_tx_count - temp_reset.mcast_tx_count; + extra_info->ucast_rx_count = test->ucast_rx_count - temp_reset.ucast_rx_count; + extra_info->ucast_tx_count = test->ucast_tx_count - temp_reset.ucast_tx_count; + extra_info->overrun_count = test->overrun_count - temp_reset.overrun_count; } return status; } @@ -163,14 +163,14 @@ int32_t wfx_rsi_reset_count() } else { - rsi_wlan_ext_stats_t * test = (rsi_wlan_ext_stats_t *) buff; - temp_reset->beacon_lost_count = test->beacon_lost_count; - temp_reset->beacon_rx_count = test->beacon_rx_count; - temp_reset->mcast_rx_count = test->mcast_rx_count; - temp_reset->mcast_tx_count = test->mcast_tx_count; - temp_reset->ucast_rx_count = test->ucast_rx_count; - temp_reset->ucast_tx_count = test->ucast_tx_count; - temp_reset->overrun_count = test->overrun_count; + rsi_wlan_ext_stats_t * test = (rsi_wlan_ext_stats_t *) buff; + temp_reset.beacon_lost_count = test->beacon_lost_count; + temp_reset.beacon_rx_count = test->beacon_rx_count; + temp_reset.mcast_rx_count = test->mcast_rx_count; + temp_reset.mcast_tx_count = test->mcast_tx_count; + temp_reset.ucast_rx_count = test->ucast_rx_count; + temp_reset.ucast_tx_count = test->ucast_tx_count; + temp_reset.overrun_count = test->overrun_count; } return status; } @@ -236,8 +236,6 @@ static void wfx_rsi_join_cb(uint16_t status, const uint8_t * buf, const uint16_t { SILABS_LOG("%s: status: %02x", __func__, status); wfx_rsi.dev_state &= ~WFX_RSI_ST_STA_CONNECTING; - temp_reset = (wfx_wifi_scan_ext_t *) malloc(sizeof(wfx_wifi_scan_ext_t)); - memset(temp_reset, 0, sizeof(wfx_wifi_scan_ext_t)); if (status != RSI_SUCCESS) { /* @@ -253,6 +251,7 @@ static void wfx_rsi_join_cb(uint16_t status, const uint8_t * buf, const uint16_t /* * Join was complete - Do the DHCP */ + memset(&temp_reset, 0, sizeof(wfx_wifi_scan_ext_t)); SILABS_LOG("%s: join completed.", __func__); xEventGroupSetBits(wfx_rsi.events, WFX_EVT_STA_CONN); wfx_rsi.join_retries = 0; From 9a947d8648c7a51404e6de66aea08b018a25dfe4 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Wed, 13 Mar 2024 02:22:56 +0900 Subject: [PATCH 36/76] [Android] Fix discover Commissionable Device API issue when discovered multiple devices (#32459) * Fix Android Commissionable Device API when searched multiple devices * Fix Android Commissionable Device API when searched multiple devices * Fix from comment, fix edge case issue * Restyled by google-java-format --------- Co-authored-by: Restyled.io --- .../com/google/chip/chiptool/ChipClient.kt | 5 ++- .../AbstractDnssdDiscoveryController.cpp | 3 +- .../platform/NsdManagerServiceBrowser.java | 12 +++++- .../platform/NsdManagerServiceResolver.java | 40 +++++++++++++------ .../platform/NsdServiceFinderAndResolver.java | 26 ++++-------- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt index 6372b14c5fa71f..f8686ce3220b70 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt @@ -74,7 +74,10 @@ object ChipClient { AndroidBleManager(context), PreferencesKeyValueStoreManager(context), PreferencesConfigurationManager(context), - NsdManagerServiceResolver(context), + NsdManagerServiceResolver( + context, + NsdManagerServiceResolver.NsdManagerResolverAvailState() + ), NsdManagerServiceBrowser(context), ChipMdnsCallbackImpl(), DiagnosticDataProviderImpl(context) diff --git a/src/controller/AbstractDnssdDiscoveryController.cpp b/src/controller/AbstractDnssdDiscoveryController.cpp index d0f6d5dbd4066e..2443c5fe0b1ac9 100644 --- a/src/controller/AbstractDnssdDiscoveryController.cpp +++ b/src/controller/AbstractDnssdDiscoveryController.cpp @@ -35,7 +35,8 @@ void AbstractDnssdDiscoveryController::OnNodeDiscovered(const chip::Dnssd::Disco continue; } if (strcmp(discoveredNode.resolutionData.hostName, nodeData.resolutionData.hostName) == 0 && - discoveredNode.resolutionData.port == nodeData.resolutionData.port) + discoveredNode.resolutionData.port == nodeData.resolutionData.port && + discoveredNode.resolutionData.ipAddress == nodeData.resolutionData.ipAddress) { discoveredNode = nodeData; if (mDeviceDiscoveryDelegate != nullptr) diff --git a/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java b/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java index 3532182f65d483..f98acdfb1c7278 100644 --- a/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java +++ b/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java @@ -34,10 +34,19 @@ public class NsdManagerServiceBrowser implements ServiceBrowser { private final NsdManager nsdManager; private MulticastLock multicastLock; private Handler mainThreadHandler; + private final long timeout; private HashMap callbackMap; public NsdManagerServiceBrowser(Context context) { + this(context, BROWSE_SERVICE_TIMEOUT); + } + + /** + * @param context application context + * @param timeout Timeout value in case there is no response after calling browse + */ + public NsdManagerServiceBrowser(Context context, long timeout) { this.nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); this.mainThreadHandler = new Handler(Looper.getMainLooper()); @@ -46,6 +55,7 @@ public NsdManagerServiceBrowser(Context context) { .createMulticastLock("chipBrowseMulticastLock"); this.multicastLock.setReferenceCounted(true); callbackMap = new HashMap<>(); + this.timeout = timeout; } @Override @@ -62,7 +72,7 @@ public void run() { } }; startDiscover(serviceType, callbackHandle, contextHandle, chipMdnsCallback); - mainThreadHandler.postDelayed(timeoutRunnable, BROWSE_SERVICE_TIMEOUT); + mainThreadHandler.postDelayed(timeoutRunnable, timeout); } public void startDiscover( diff --git a/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java b/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java index c01855172fcfe6..1f1a9efa4307f0 100644 --- a/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java +++ b/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java @@ -22,13 +22,14 @@ import android.net.nsd.NsdServiceInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.MulticastLock; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -38,30 +39,38 @@ public class NsdManagerServiceResolver implements ServiceResolver { private static final long RESOLVE_SERVICE_TIMEOUT = 30000; private final NsdManager nsdManager; private MulticastLock multicastLock; - private Handler mainThreadHandler; private List registrationListeners = new ArrayList<>(); private final CopyOnWriteArrayList mMFServiceName = new CopyOnWriteArrayList<>(); @Nullable private final NsdManagerResolverAvailState nsdManagerResolverAvailState; + private final long timeout; /** * @param context application context * @param nsdManagerResolverAvailState Passing NsdManagerResolverAvailState allows * NsdManagerServiceResolver to synchronize on the usage of NsdManager's resolveService() API + * @param timeout Timeout value in case there is no response after calling resolve */ public NsdManagerServiceResolver( - Context context, @Nullable NsdManagerResolverAvailState nsdManagerResolverAvailState) { + Context context, + @Nullable NsdManagerResolverAvailState nsdManagerResolverAvailState, + long timeout) { this.nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); - this.mainThreadHandler = new Handler(Looper.getMainLooper()); this.multicastLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) .createMulticastLock("chipMulticastLock"); this.multicastLock.setReferenceCounted(true); this.nsdManagerResolverAvailState = nsdManagerResolverAvailState; + this.timeout = timeout; } public NsdManagerServiceResolver(Context context) { - this(context, null); + this(context, null, RESOLVE_SERVICE_TIMEOUT); + } + + public NsdManagerServiceResolver( + Context context, @Nullable NsdManagerResolverAvailState nsdManagerResolverAvailState) { + this(context, nsdManagerResolverAvailState, RESOLVE_SERVICE_TIMEOUT); } @Override @@ -82,6 +91,10 @@ public void resolve( + serviceType + "'"); + if (nsdManagerResolverAvailState != null) { + nsdManagerResolverAvailState.acquireResolver(); + } + Runnable timeoutRunnable = new Runnable() { @Override @@ -92,14 +105,18 @@ public void run() { Log.d(TAG, "resolve: Timing out"); if (multicastLock.isHeld()) { multicastLock.release(); + } - if (nsdManagerResolverAvailState != null) { - nsdManagerResolverAvailState.signalFree(); - } + if (nsdManagerResolverAvailState != null) { + nsdManagerResolverAvailState.signalFree(); } } }; + ScheduledFuture resolveTimeoutExecutor = + Executors.newSingleThreadScheduledExecutor() + .schedule(timeoutRunnable, timeout, TimeUnit.MILLISECONDS); + NsdServiceFinderAndResolver serviceFinderResolver = new NsdServiceFinderAndResolver( this.nsdManager, @@ -107,13 +124,10 @@ public void run() { callbackHandle, contextHandle, chipMdnsCallback, - timeoutRunnable, multicastLock, - mainThreadHandler, + resolveTimeoutExecutor, nsdManagerResolverAvailState); serviceFinderResolver.start(); - - mainThreadHandler.postDelayed(timeoutRunnable, RESOLVE_SERVICE_TIMEOUT); } @Override diff --git a/src/platform/android/java/chip/platform/NsdServiceFinderAndResolver.java b/src/platform/android/java/chip/platform/NsdServiceFinderAndResolver.java index fc3425f7bd8889..70ffdbc0102a62 100644 --- a/src/platform/android/java/chip/platform/NsdServiceFinderAndResolver.java +++ b/src/platform/android/java/chip/platform/NsdServiceFinderAndResolver.java @@ -21,7 +21,6 @@ import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.net.wifi.WifiManager.MulticastLock; -import android.os.Handler; import android.util.Log; import androidx.annotation.Nullable; import java.util.concurrent.Executors; @@ -38,9 +37,8 @@ class NsdServiceFinderAndResolver implements NsdManager.DiscoveryListener { private final long callbackHandle; private final long contextHandle; private final ChipMdnsCallback chipMdnsCallback; - private final Runnable timeoutRunnable; private final MulticastLock multicastLock; - private final Handler mainThreadHandler; + private final ScheduledFuture resolveTimeoutExecutor; @Nullable private final NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState; @@ -53,18 +51,16 @@ public NsdServiceFinderAndResolver( final long callbackHandle, final long contextHandle, final ChipMdnsCallback chipMdnsCallback, - final Runnable timeoutRunnable, final MulticastLock multicastLock, - final Handler mainThreadHandler, + final ScheduledFuture resolveTimeoutExecutor, final NsdManagerServiceResolver.NsdManagerResolverAvailState nsdManagerResolverAvailState) { this.nsdManager = nsdManager; this.targetServiceInfo = targetServiceInfo; this.callbackHandle = callbackHandle; this.contextHandle = contextHandle; this.chipMdnsCallback = chipMdnsCallback; - this.timeoutRunnable = timeoutRunnable; this.multicastLock = multicastLock; - this.mainThreadHandler = mainThreadHandler; + this.resolveTimeoutExecutor = resolveTimeoutExecutor; this.nsdManagerResolverAvailState = nsdManagerResolverAvailState; } @@ -101,16 +97,9 @@ public void onServiceFound(NsdServiceInfo service) { if (stopDiscoveryRunnable.cancel(false)) { nsdManager.stopServiceDiscovery(this); - if (multicastLock.isHeld()) { - multicastLock.release(); - } } - if (nsdManagerResolverAvailState != null) { - nsdManagerResolverAvailState.acquireResolver(); - } - - resolveService(service, callbackHandle, contextHandle, chipMdnsCallback, timeoutRunnable); + resolveService(service, callbackHandle, contextHandle, chipMdnsCallback); } else { Log.d(TAG, "onServiceFound: found service not a target for resolution, ignoring " + service); } @@ -120,8 +109,7 @@ private void resolveService( NsdServiceInfo serviceInfo, final long callbackHandle, final long contextHandle, - final ChipMdnsCallback chipMdnsCallback, - Runnable timeoutRunnable) { + final ChipMdnsCallback chipMdnsCallback) { this.nsdManager.resolveService( serviceInfo, new NsdManager.ResolveListener() { @@ -152,7 +140,7 @@ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { nsdManagerResolverAvailState.signalFree(); } } - mainThreadHandler.removeCallbacks(timeoutRunnable); + resolveTimeoutExecutor.cancel(false); } @Override @@ -188,7 +176,7 @@ public void onServiceResolved(NsdServiceInfo serviceInfo) { nsdManagerResolverAvailState.signalFree(); } } - mainThreadHandler.removeCallbacks(timeoutRunnable); + resolveTimeoutExecutor.cancel(false); } }); } From ad14dbc9b5b1c8230bd29be4873f9367143bfc31 Mon Sep 17 00:00:00 2001 From: Jean-Francois Penven <67962328+jepenven-silabs@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:50:53 -0400 Subject: [PATCH 37/76] add used attribute to two functions (#32545) --- examples/platform/silabs/SoftwareFaultReports.cpp | 2 +- examples/platform/silabs/heap_4_silabs.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/platform/silabs/SoftwareFaultReports.cpp b/examples/platform/silabs/SoftwareFaultReports.cpp index 0b730576807628..fd9be7f7b9f02b 100644 --- a/examples/platform/silabs/SoftwareFaultReports.cpp +++ b/examples/platform/silabs/SoftwareFaultReports.cpp @@ -82,7 +82,7 @@ void OnSoftwareFaultEventHandler(const char * faultRecordString) /** * Log register contents to UART when a hard fault occurs. */ -extern "C" void debugHardfault(uint32_t * sp) +extern "C" __attribute__((used)) void debugHardfault(uint32_t * sp) { #if SILABS_LOG_ENABLED uint32_t cfsr = SCB->CFSR; diff --git a/examples/platform/silabs/heap_4_silabs.c b/examples/platform/silabs/heap_4_silabs.c index a19bff034362fb..8abb295caefd2d 100644 --- a/examples/platform/silabs/heap_4_silabs.c +++ b/examples/platform/silabs/heap_4_silabs.c @@ -647,7 +647,7 @@ void * __wrap_realloc(void * ptr, size_t new_size) return pvPortRealloc(ptr, new_size); } -void * __wrap_calloc(size_t num, size_t size) +__attribute__((used)) void * __wrap_calloc(size_t num, size_t size) { return pvPortCalloc(num, size); } From 1404f807cc476fa2afa267de0f88d5a85881da61 Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Tue, 12 Mar 2024 13:28:14 -0700 Subject: [PATCH 38/76] [ICD] Fix wrong MaxICDClientInfoSize (#32500) * fix wrong MaxICDClientInfoSize * Restyled by clang-format --------- Co-authored-by: Restyled.io --- src/app/icd/client/DefaultICDClientStorage.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/icd/client/DefaultICDClientStorage.h b/src/app/icd/client/DefaultICDClientStorage.h index 7306dbafdf65d2..8a2d44115abccf 100644 --- a/src/app/icd/client/DefaultICDClientStorage.h +++ b/src/app/icd/client/DefaultICDClientStorage.h @@ -155,8 +155,10 @@ class DefaultICDClientStorage : public ICDClientStorage static constexpr size_t MaxICDClientInfoSize() { // All the fields added together - return TLV::EstimateStructOverhead(sizeof(NodeId), sizeof(FabricIndex), sizeof(uint32_t), sizeof(uint32_t), - sizeof(uint64_t), sizeof(Crypto::Symmetric128BitsKeyByteArray)); + return TLV::EstimateStructOverhead(sizeof(NodeId), sizeof(FabricIndex), sizeof(uint32_t) /*start_icd_counter*/, + sizeof(uint32_t) /*offset*/, sizeof(uint64_t) /*monitored_subject*/, + sizeof(Crypto::Symmetric128BitsKeyByteArray) /*aes_key_handle*/, + sizeof(Crypto::Symmetric128BitsKeyByteArray) /*hmac_key_handle*/); } static constexpr size_t MaxICDCounterSize() From deca2a0214d77e6c3835796c42aca453aa5e2b8e Mon Sep 17 00:00:00 2001 From: Petru Lauric <81822411+plauric@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:35:19 -0400 Subject: [PATCH 39/76] fix rvc resume after err pics code (#32485) --- examples/rvc-app/rvc-common/pics/rvc-app-pics-values | 1 + src/python_testing/TC_RVCOPSTATE_2_3.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/rvc-app/rvc-common/pics/rvc-app-pics-values b/examples/rvc-app/rvc-common/pics/rvc-app-pics-values index 2a6adeb31e38f6..669abd26c33f94 100644 --- a/examples/rvc-app/rvc-common/pics/rvc-app-pics-values +++ b/examples/rvc-app/rvc-common/pics/rvc-app-pics-values @@ -40,6 +40,7 @@ RVCOPSTATE.S.M.ERR_WATER_TANK_EMPTY=1 RVCOPSTATE.S.M.ERR_WATER_TANK_MISSING=1 RVCOPSTATE.S.M.ERR_WATER_TANK_LID_OPEN=1 RVCOPSTATE.S.M.ERR_MOP_CLEANING_PAD_MISSING=1 +RVCOPSTATE.S.M.RESUME_AFTER_ERR=0 RVCRUNM.S=1 RVCRUNM.S.A0000=1 diff --git a/src/python_testing/TC_RVCOPSTATE_2_3.py b/src/python_testing/TC_RVCOPSTATE_2_3.py index aa755315fb7d46..f41e6bcb0de8b1 100644 --- a/src/python_testing/TC_RVCOPSTATE_2_3.py +++ b/src/python_testing/TC_RVCOPSTATE_2_3.py @@ -234,7 +234,7 @@ async def test_TC_RVCOPSTATE_2_3(self): await self.send_resume_cmd_with_check(13, op_errors.kNoError) - if self.check_pics("OPSTATE.S.M.RESUME_AFTER_ERR"): + if self.check_pics("RVCOPSTATE.S.M.RESUME_AFTER_ERR"): self.print_instruction(16, "Manually put the device in the Running state") await self.read_operational_state_with_check(17, op_states.kRunning) From bf7241d87a207db7c02f92e5f66ff16ee4c3b74f Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 12 Mar 2024 18:37:14 -0400 Subject: [PATCH 40/76] Add a Matter.framework API for configuring MRP parameters. (#32548) * Add a Matter.framework API for configuring MRP parameters. Also fixes a typo in the ReliableMessageMgr API Matter.framework uses. * Address review comment. --- examples/darwin-framework-tool/BUILD.gn | 5 ++ .../commands/common/CHIPCommandBridge.h | 4 +- .../commands/configuration/Commands.h | 36 ++++++++++ .../configuration/ResetMRPParametersCommand.h | 35 ++++++++++ .../ResetMRPParametersCommand.mm | 24 +++++++ .../configuration/SetMRPParametersCommand.h | 52 ++++++++++++++ .../configuration/SetMRPParametersCommand.mm | 48 +++++++++++++ examples/darwin-framework-tool/main.mm | 2 + .../CHIP/MTRDeviceControllerFactory.h | 33 +++++++++ .../CHIP/MTRDeviceControllerFactory.mm | 67 +++++++++++++++++++ .../Matter.xcodeproj/project.pbxproj | 28 ++++++++ .../Framework/chip_xcode_build_connector.sh | 1 + src/messaging/ReliableMessageMgr.h | 2 +- 13 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 examples/darwin-framework-tool/commands/configuration/Commands.h create mode 100644 examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.h create mode 100644 examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.mm create mode 100644 examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.h create mode 100644 examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.mm diff --git a/examples/darwin-framework-tool/BUILD.gn b/examples/darwin-framework-tool/BUILD.gn index 6228643a958f81..665dccf4848df4 100644 --- a/examples/darwin-framework-tool/BUILD.gn +++ b/examples/darwin-framework-tool/BUILD.gn @@ -193,6 +193,11 @@ executable("darwin-framework-tool") { "commands/common/MTRLogging.h", "commands/common/RemoteDataModelLogger.h", "commands/common/RemoteDataModelLogger.mm", + "commands/configuration/Commands.h", + "commands/configuration/ResetMRPParametersCommand.h", + "commands/configuration/ResetMRPParametersCommand.mm", + "commands/configuration/SetMRPParametersCommand.h", + "commands/configuration/SetMRPParametersCommand.mm", "commands/delay/Commands.h", "commands/delay/SleepCommand.h", "commands/delay/SleepCommand.mm", diff --git a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h index 842ded8f839ab7..7dc5b848e22324 100644 --- a/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h +++ b/examples/darwin-framework-tool/commands/common/CHIPCommandBridge.h @@ -34,8 +34,8 @@ inline constexpr char kIdentityGamma[] = "gamma"; class CHIPCommandBridge : public Command { public: - CHIPCommandBridge(const char * commandName) - : Command(commandName) + CHIPCommandBridge(const char * commandName, const char * helpText = nullptr) + : Command(commandName, helpText) { AddArgument("commissioner-name", &mCommissionerName); AddArgument("commissioner-nodeId", 0, UINT64_MAX, &mCommissionerNodeId, diff --git a/examples/darwin-framework-tool/commands/configuration/Commands.h b/examples/darwin-framework-tool/commands/configuration/Commands.h new file mode 100644 index 00000000000000..8ee148e63b8f8b --- /dev/null +++ b/examples/darwin-framework-tool/commands/configuration/Commands.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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. + * + */ + +#pragma once + +#import + +#include "ResetMRPParametersCommand.h" +#include "SetMRPParametersCommand.h" + +void registerCommandsConfiguration(Commands & commands) +{ + const char * clusterName = "Configuration"; + + commands_list clusterCommands = { + make_unique(), + make_unique(), + }; + + commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for configuring various state of the Matter framework."); +} diff --git a/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.h b/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.h new file mode 100644 index 00000000000000..10cfe2f177c66d --- /dev/null +++ b/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 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. + */ + +#pragma once + +#import + +#include "../common/CHIPCommandBridge.h" + +class ResetMRPParametersCommand : public CHIPCommandBridge +{ +public: + ResetMRPParametersCommand() : CHIPCommandBridge("reset-mrp-parameters", "Reset MRP parameters to default values.") {} + +protected: + /////////// CHIPCommandBridge Interface ///////// + CHIP_ERROR RunCommand() override; + + // Our command is synchronous, so no need to wait. + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::kZero; } +}; diff --git a/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.mm b/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.mm new file mode 100644 index 00000000000000..26b82e540ef716 --- /dev/null +++ b/examples/darwin-framework-tool/commands/configuration/ResetMRPParametersCommand.mm @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 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 "ResetMRPParametersCommand.h" + +CHIP_ERROR ResetMRPParametersCommand::RunCommand() +{ + MTRSetMessageReliabilityParameters(nil, nil, nil, nil); + return CHIP_NO_ERROR; +} diff --git a/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.h b/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.h new file mode 100644 index 00000000000000..e9dc2faeb06c6d --- /dev/null +++ b/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 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. + */ + +#pragma once + +#import + +#include "../common/CHIPCommandBridge.h" + +class SetMRPParametersCommand : public CHIPCommandBridge +{ +public: + SetMRPParametersCommand() : + CHIPCommandBridge("set-mrp-parameters", "Set various MRP parameters. At least one value must be provided.") + { + AddArgument("idle-interval", 0, UINT32_MAX, &mIdleRetransmitMs, + "Our MRP idle interval (SII) in milliseconds. Defaults to current value if not set."); + AddArgument("active-interval", 0, UINT32_MAX, &mActiveRetransmitMs, + "Our MRP active interval (SAI) in milliseconds. Defaults to current value if not set."); + AddArgument("active-threshold", 0, UINT32_MAX, &mActiveThresholdMs, + "Our MRP active threshold: how long we stay in active mode before transitioning to idle mode. Defaults to " + "current value if not set."); + AddArgument("additional-retransmit-delay", 0, UINT32_MAX, &mAdditionalRetransmitDelayMs, + "Additional delay between retransmits that we do. Defaults to current value if not set."); + } + +protected: + /////////// CHIPCommandBridge Interface ///////// + CHIP_ERROR RunCommand() override; + + // Our command is synchronous, so no need to wait. + chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::kZero; } + + chip::Optional mIdleRetransmitMs; + chip::Optional mActiveRetransmitMs; + chip::Optional mActiveThresholdMs; + chip::Optional mAdditionalRetransmitDelayMs; +}; diff --git a/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.mm b/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.mm new file mode 100644 index 00000000000000..9ec49e7a87d942 --- /dev/null +++ b/examples/darwin-framework-tool/commands/configuration/SetMRPParametersCommand.mm @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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 "SetMRPParametersCommand.h" + +using namespace chip; + +namespace { + +template +NSNumber * _Nullable AsNumber(const Optional & value) +{ + if (!value.HasValue()) { + return nil; + } + + return @(value.Value()); +} + +} // anonymous namespace + +CHIP_ERROR SetMRPParametersCommand::RunCommand() +{ + if (!mIdleRetransmitMs.HasValue() && !mActiveRetransmitMs.HasValue() && !mActiveThresholdMs.HasValue() && !mAdditionalRetransmitDelayMs.HasValue()) { + ChipLogError(chipTool, "set-mrp-parameters needs to have at least one argument provided"); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + MTRSetMessageReliabilityParameters(AsNumber(mIdleRetransmitMs), + AsNumber(mActiveRetransmitMs), + AsNumber(mActiveThresholdMs), + AsNumber(mAdditionalRetransmitDelayMs)); + return CHIP_NO_ERROR; +} diff --git a/examples/darwin-framework-tool/main.mm b/examples/darwin-framework-tool/main.mm index 0ba2d1553e9c88..5f31cb6abf1cd1 100644 --- a/examples/darwin-framework-tool/main.mm +++ b/examples/darwin-framework-tool/main.mm @@ -22,6 +22,7 @@ #include "commands/bdx/Commands.h" #include "commands/common/Commands.h" +#include "commands/configuration/Commands.h" #include "commands/delay/Commands.h" #include "commands/discover/Commands.h" #include "commands/interactive/Commands.h" @@ -46,6 +47,7 @@ int main(int argc, const char * argv[]) registerCommandsPayload(commands); registerClusterOtaSoftwareUpdateProviderInteractive(commands); registerCommandsStorage(commands); + registerCommandsConfiguration(commands); registerClusters(commands); return commands.Run(argc, (char **) argv); } diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h index 7d5b19d68bacca..d1efa99cda14e6 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h @@ -179,6 +179,39 @@ MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @end +/** + * Set the Message Reliability Protocol parameters for all controllers. This + * allows control over retransmit delays to account for high-latency networks. + * + * Setting all arguments to nil will reset to the MRP parameters to their + * default values. + * + * Setting some arguments to non-nil will change just those values, keeping + * current values for any arguments that are nil (not resetting them to + * defaults). + * + * Non-nil arguments are specified as an integer number of milliseconds. + * + * @param idleRetransmitMs the minimal interval between retransmits for someone + * sending messages to us, when they think we are + * "idle" and might have our radio only turned on + * intermittently. + * @param activeRetransmitMs the minimal interval between retransmits for + * someone sending messages to us, when they think we + * are "active" and have the radio turned on + * consistently. + * @param activeThresholdMs the amount of time we will stay in "active" mode after + * network activity. + * @param additionalRetransmitDelayMs additional delay between retransmits for + * messages we send, on top of whatever delay + * the other side requests via its MRP + * parameters. + */ +MTR_EXTERN MTR_NEWLY_AVAILABLE void MTRSetMessageReliabilityParameters(NSNumber * _Nullable idleRetransmitMs, + NSNumber * _Nullable activeRetransmitMs, + NSNumber * _Nullable activeThresholdMs, + NSNumber * _Nullable additionalRetransmitDelayMs); + MTR_DEPRECATED( "Please use MTRDeviceControllerFactoryParams", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)) @interface MTRControllerFactoryParams : MTRDeviceControllerFactoryParams diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm index d89df5a4b09c4d..2ccf89634646e8 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm @@ -48,10 +48,12 @@ #import "MTRServerCluster_Internal.h" #import "MTRServerEndpoint_Internal.h" #import "MTRSessionResumptionStorageBridge.h" +#import "MTRUnfairLock.h" #import "NSDataSpanConversion.h" #import +#include #include #include #include @@ -62,6 +64,8 @@ #include #include #include +#include +#include #include #include @@ -968,6 +972,33 @@ - (MTRDeviceController * _Nullable)maybeInitializeOTAProvider:(MTRDeviceControll return controller; } +- (void)resetOperationalAdvertising +{ + if (![self checkIsRunning:nil]) { + // No need to reset anything; we are not running, so not + // advertising. + return; + } + + if (!self.advertiseOperational) { + // No need to reset anything; we are not advertising the things that + // would need to get reset. + return; + } + + std::lock_guard lock(_controllersLock); + if (_controllers.count != 0) { + // We have a running controller. That means we likely need to reset + // operational advertising for that controller. + dispatch_async(_chipWorkQueue, ^{ + // StartServer() is the only API we have for resetting DNS-SD + // advertising. It sure would be nice if there were a "restart" + // that was a no-op if the DNS-SD server was not already + // running. + app::DnssdServer::Instance().StartServer(); + }); + } +} @end @implementation MTRDeviceControllerFactory (InternalMethods) @@ -1449,3 +1480,39 @@ - (void)setCdCerts:(nullable NSArray *)cdCerts } @end + +void MTRSetMessageReliabilityParameters(NSNumber * _Nullable idleRetransmitMs, + NSNumber * _Nullable activeRetransmitMs, + NSNumber * _Nullable activeThresholdMs, + NSNumber * _Nullable additionalRetransmitDelayMs) +{ + bool resetAdvertising = false; + if (idleRetransmitMs == nil && activeRetransmitMs == nil && activeThresholdMs == nil && additionalRetransmitDelayMs == nil) { + Messaging::ReliableMessageMgr::SetAdditionalMRPBackoffTime(NullOptional); + resetAdvertising = ReliableMessageProtocolConfig::SetLocalMRPConfig(NullOptional); + } else { + if (additionalRetransmitDelayMs != nil) { + System::Clock::Milliseconds64 additionalBackoff(additionalRetransmitDelayMs.unsignedLongLongValue); + Messaging::ReliableMessageMgr::SetAdditionalMRPBackoffTime(MakeOptional(additionalBackoff)); + } + + // Get current MRP parameters, then override the things we were asked to + // override. + ReliableMessageProtocolConfig mrpConfig = GetLocalMRPConfig().ValueOr(GetDefaultMRPConfig()); + if (idleRetransmitMs != nil) { + mrpConfig.mIdleRetransTimeout = System::Clock::Milliseconds32(idleRetransmitMs.unsignedLongValue); + } + if (activeRetransmitMs != nil) { + mrpConfig.mActiveRetransTimeout = System::Clock::Milliseconds32(activeRetransmitMs.unsignedLongValue); + } + if (activeThresholdMs != nil) { + mrpConfig.mActiveThresholdTime = System::Clock::Milliseconds32(activeThresholdMs.unsignedLongValue); + } + + resetAdvertising = ReliableMessageProtocolConfig::SetLocalMRPConfig(MakeOptional(mrpConfig)); + } + + if (resetAdvertising) { + [[MTRDeviceControllerFactory sharedInstance] resetOperationalAdvertising]; + } +} diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index 2f69cdf60f6da3..606d0f144544a9 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -133,6 +133,11 @@ 5117DD3929A931AE00FFA1AA /* MTROperationalBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = 5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */; }; 511913FB28C100EF009235E9 /* MTRBaseSubscriptionCallback.mm in Sources */ = {isa = PBXBuildFile; fileRef = 511913F928C100EF009235E9 /* MTRBaseSubscriptionCallback.mm */; }; 511913FC28C100EF009235E9 /* MTRBaseSubscriptionCallback.h in Headers */ = {isa = PBXBuildFile; fileRef = 511913FA28C100EF009235E9 /* MTRBaseSubscriptionCallback.h */; }; + 512431252BA0C8B7000BC136 /* Commands.h in Headers */ = {isa = PBXBuildFile; fileRef = 512431182BA0C09A000BC136 /* Commands.h */; }; + 512431262BA0C8BA000BC136 /* ResetMRPParametersCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 512431192BA0C09A000BC136 /* ResetMRPParametersCommand.h */; }; + 512431272BA0C8BF000BC136 /* SetMRPParametersCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 5124311B2BA0C09A000BC136 /* SetMRPParametersCommand.h */; }; + 512431282BA0C8BF000BC136 /* SetMRPParametersCommand.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5124311C2BA0C09A000BC136 /* SetMRPParametersCommand.mm */; }; + 512431292BA0C8BF000BC136 /* ResetMRPParametersCommand.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5124311A2BA0C09A000BC136 /* ResetMRPParametersCommand.mm */; }; 5129BCFD26A9EE3300122DDF /* MTRError.h in Headers */ = {isa = PBXBuildFile; fileRef = 5129BCFC26A9EE3300122DDF /* MTRError.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51339B1F2A0DA64D00C798C1 /* MTRCertificateValidityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */; }; 5136661328067D550025EDAE /* MTRDeviceController_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5136660F28067D540025EDAE /* MTRDeviceController_Internal.h */; }; @@ -529,6 +534,11 @@ 5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTROperationalBrowser.h; sourceTree = ""; }; 511913F928C100EF009235E9 /* MTRBaseSubscriptionCallback.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRBaseSubscriptionCallback.mm; sourceTree = ""; }; 511913FA28C100EF009235E9 /* MTRBaseSubscriptionCallback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRBaseSubscriptionCallback.h; sourceTree = ""; }; + 512431182BA0C09A000BC136 /* Commands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Commands.h; sourceTree = ""; }; + 512431192BA0C09A000BC136 /* ResetMRPParametersCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResetMRPParametersCommand.h; sourceTree = ""; }; + 5124311A2BA0C09A000BC136 /* ResetMRPParametersCommand.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResetMRPParametersCommand.mm; sourceTree = ""; }; + 5124311B2BA0C09A000BC136 /* SetMRPParametersCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SetMRPParametersCommand.h; sourceTree = ""; }; + 5124311C2BA0C09A000BC136 /* SetMRPParametersCommand.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SetMRPParametersCommand.mm; sourceTree = ""; }; 5129BCFC26A9EE3300122DDF /* MTRError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRError.h; sourceTree = ""; }; 51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRCertificateValidityTests.m; sourceTree = ""; }; 5136660F28067D540025EDAE /* MTRDeviceController_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceController_Internal.h; sourceTree = ""; }; @@ -814,6 +824,7 @@ 037C3D7B2991BD4F00B7EEE2 /* commands */ = { isa = PBXGroup; children = ( + 5124311D2BA0C09A000BC136 /* configuration */, B4FCD56C2B603A6300832859 /* bdx */, B4E262182AA0CFFE00DBA5BC /* delay */, 03FB93DA2A46200A0048CB35 /* discover */, @@ -1112,6 +1123,18 @@ path = TestHelpers; sourceTree = ""; }; + 5124311D2BA0C09A000BC136 /* configuration */ = { + isa = PBXGroup; + children = ( + 512431182BA0C09A000BC136 /* Commands.h */, + 512431192BA0C09A000BC136 /* ResetMRPParametersCommand.h */, + 5124311A2BA0C09A000BC136 /* ResetMRPParametersCommand.mm */, + 5124311B2BA0C09A000BC136 /* SetMRPParametersCommand.h */, + 5124311C2BA0C09A000BC136 /* SetMRPParametersCommand.mm */, + ); + path = configuration; + sourceTree = ""; + }; 51D0B1312B618C4F006E3511 /* ServerEndpoint */ = { isa = PBXGroup; children = ( @@ -1447,6 +1470,7 @@ 037C3DB12991BD5000B7EEE2 /* OpenCommissioningWindowCommand.h in Headers */, 039145E92993179300257B3E /* GetCommissionerNodeIdCommand.h in Headers */, 037C3DCE2991BD5100B7EEE2 /* CHIPCommandBridge.h in Headers */, + 512431272BA0C8BF000BC136 /* SetMRPParametersCommand.h in Headers */, 037C3DD22991BD5200B7EEE2 /* InteractiveCommands.h in Headers */, 037C3DAF2991BD4F00B7EEE2 /* DeviceControllerDelegateBridge.h in Headers */, B4FCD5712B603A6300832859 /* DownloadLogCommand.h in Headers */, @@ -1473,7 +1497,9 @@ 037C3DCC2991BD5100B7EEE2 /* MTRError_Utils.h in Headers */, 037C3DAD2991BD4F00B7EEE2 /* PairingCommandBridge.h in Headers */, 037C3DBB2991BD5000B7EEE2 /* Commands.h in Headers */, + 512431262BA0C8BA000BC136 /* ResetMRPParametersCommand.h in Headers */, 03FB93DF2A46200A0048CB35 /* Commands.h in Headers */, + 512431252BA0C8B7000BC136 /* Commands.h in Headers */, 037C3DB22991BD5000B7EEE2 /* PreWarmCommissioningCommand.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1797,6 +1823,8 @@ 037C3DD42991BD5200B7EEE2 /* logging.mm in Sources */, B45374012A9FEC4F00807602 /* unix-sockets.c in Sources */, 03F430A82994112B00166449 /* editline.c in Sources */, + 512431282BA0C8BF000BC136 /* SetMRPParametersCommand.mm in Sources */, + 512431292BA0C8BF000BC136 /* ResetMRPParametersCommand.mm in Sources */, B45373E92A9FEBC100807602 /* server.c in Sources */, 037C3DB32991BD5000B7EEE2 /* OpenCommissioningWindowCommand.mm in Sources */, 037C3DAE2991BD4F00B7EEE2 /* PairingCommandBridge.mm in Sources */, diff --git a/src/darwin/Framework/chip_xcode_build_connector.sh b/src/darwin/Framework/chip_xcode_build_connector.sh index e802e99cd0cfcd..78023f97a469d8 100755 --- a/src/darwin/Framework/chip_xcode_build_connector.sh +++ b/src/darwin/Framework/chip_xcode_build_connector.sh @@ -98,6 +98,7 @@ declare -a args=( 'chip_build_tests=false' 'chip_enable_wifi=false' 'chip_enable_python_modules=false' + 'chip_device_config_enable_dynamic_mrp_config=true' 'chip_log_message_max_size=4096' # might as well allow nice long log messages 'chip_disable_platform_kvs=true' 'enable_fuzz_test_targets=false' diff --git a/src/messaging/ReliableMessageMgr.h b/src/messaging/ReliableMessageMgr.h index 3401a3eda0c405..ae953cbddd5ad5 100644 --- a/src/messaging/ReliableMessageMgr.h +++ b/src/messaging/ReliableMessageMgr.h @@ -220,7 +220,7 @@ class ReliableMessageMgr * set this before actually bringing up the stack and having access to a * ReliableMessageMgr. */ - static void SetAdditionaMRPBackoffTime(const Optional & additionalTime) + static void SetAdditionalMRPBackoffTime(const Optional & additionalTime) { sAdditionalMRPBackoffTime = additionalTime; } From 12d8487e4542f6f13f8f96785dac7ec70f93b1d4 Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Tue, 12 Mar 2024 20:42:31 -0700 Subject: [PATCH 41/76] Not call OnReadCommissioningInfo when error happens (#32551) --- src/controller/CHIPDeviceController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 637e5debffae72..2c67693b30dd4d 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -1983,7 +1983,7 @@ void DeviceCommissioner::ParseCommissioningInfo() // return. auto attributeCache = std::move(mAttributeCache); - if (mPairingDelegate != nullptr) + if (mPairingDelegate != nullptr && err == CHIP_NO_ERROR) { mPairingDelegate->OnReadCommissioningInfo(info); } From dd9e5c8151aa458721eb4ed97da623dc247f0149 Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:56:51 -0400 Subject: [PATCH 42/76] [Silabs] Wifi Scan Semaphore (#32538) * Added a timer and mutex to replace the osThreadYield loops * Applied suggesitons, changed comment style and added release to prevent blocking on next start Timer Co-authored-by: mkardous-silabs <84793247+mkardous-silabs@users.noreply.github.com> * Using semaphore only * Moved init log, changed semaphore create failure status --- examples/platform/silabs/MatterConfig.cpp | 1 + .../silabs/SiWx917/SiWx917/sl_wifi_if.c | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/examples/platform/silabs/MatterConfig.cpp b/examples/platform/silabs/MatterConfig.cpp index 1c148a5a1f0d1c..dd2368f566d252 100644 --- a/examples/platform/silabs/MatterConfig.cpp +++ b/examples/platform/silabs/MatterConfig.cpp @@ -293,6 +293,7 @@ CHIP_ERROR SilabsMatterConfig::InitWiFi(void) sl_status_t status; if ((status = wfx_wifi_rsi_init()) != SL_STATUS_OK) { + SILABS_LOG("wfx_wifi_rsi_init failed with status: %x", status); ReturnErrorOnFailure((CHIP_ERROR) status); } #endif // SLI_SI91X_MCU_INTERFACE diff --git a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c index 079f12f6288cd5..79ead87460de26 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c +++ b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c @@ -98,6 +98,9 @@ static wfx_wifi_scan_ext_t temp_reset; volatile sl_status_t callback_status = SL_STATUS_OK; +// Scan semaphore +static osSemaphoreId_t sScanSemaphore; + /****************************************************************** * @fn int32_t wfx_rsi_get_ap_info(wfx_wifi_scan_result_t *ap) * @brief @@ -305,8 +308,16 @@ int32_t wfx_wifi_rsi_init(void) status = sl_wifi_init(&config, NULL, sl_wifi_default_event_handler); if (status != SL_STATUS_OK) { - SILABS_LOG("wfx_wifi_rsi_init failed %x", status); + return status; + } + + // Create Sempaphore for scan + sScanSemaphore = osSemaphoreNew(1, 0, NULL); + if (sScanSemaphore == NULL) + { + return SL_STATUS_ALLOCATION_FAILED; } + return status; } @@ -421,6 +432,8 @@ sl_status_t scan_callback_handler(sl_wifi_event_t event, sl_wifi_scan_result_t * #else wfx_rsi.sec.security = WFX_SEC_WPA2; #endif /* WIFI_ENABLE_SECURITY_WPA3_TRANSITION */ + + osSemaphoreRelease(sScanSemaphore); return SL_STATUS_FAIL; } wfx_rsi.sec.security = WFX_SEC_UNSPECIFIED; @@ -457,6 +470,8 @@ sl_status_t scan_callback_handler(sl_wifi_event_t event, sl_wifi_scan_result_t * } wfx_rsi.dev_state &= ~WFX_RSI_ST_SCANSTARTED; scan_results_complete = true; + + osSemaphoreRelease(sScanSemaphore); return SL_STATUS_OK; } sl_status_t show_scan_results(sl_wifi_scan_result_t * scan_result) @@ -501,6 +516,7 @@ sl_status_t bg_scan_callback_handler(sl_wifi_event_t event, sl_wifi_scan_result_ { callback_status = show_scan_results(result); bg_scan_results_complete = true; + osSemaphoreRelease(sScanSemaphore); return SL_STATUS_OK; } /*************************************************************************************** @@ -529,12 +545,7 @@ static void wfx_rsi_save_ap_info() // translation #endif if (SL_STATUS_IN_PROGRESS == status) { - const uint32_t start = osKernelGetTickCount(); - while (!scan_results_complete && (osKernelGetTickCount() - start) <= WIFI_SCAN_TIMEOUT_TICK) - { - osThreadYield(); - } - status = scan_results_complete ? callback_status : SL_STATUS_TIMEOUT; + osSemaphoreAcquire(sScanSemaphore, WIFI_SCAN_TIMEOUT_TICK); } } @@ -817,12 +828,7 @@ void wfx_rsi_task(void * arg) status = sl_wifi_start_scan(SL_WIFI_CLIENT_2_4GHZ_INTERFACE, NULL, &wifi_scan_configuration); if (SL_STATUS_IN_PROGRESS == status) { - const uint32_t start = osKernelGetTickCount(); - while (!bg_scan_results_complete && (osKernelGetTickCount() - start) <= WIFI_SCAN_TIMEOUT_TICK) - { - osThreadYield(); - } - status = bg_scan_results_complete ? callback_status : SL_STATUS_TIMEOUT; + osSemaphoreAcquire(sScanSemaphore, WIFI_SCAN_TIMEOUT_TICK); } } } From c9dc9271278334bc4fafd24e0bd715564f2c512a Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:58:38 -0400 Subject: [PATCH 43/76] [Silabs] Typing Event, state and wfx_rsi (#32546) * Added event enum for events and states, added a typedef for the wfi_rsi struct and changed name so the variable is not named like the type * Converted new type to PascalCase --- .../silabs/SiWx917/SiWx917/sl_wifi_if.c | 2 +- .../platform/silabs/SiWx917/SiWx917/wfx_rsi.h | 53 +++++++++++-------- .../platform/silabs/efr32/rs911x/rsi_if.c | 2 +- .../platform/silabs/efr32/rs911x/wfx_rsi.h | 6 +-- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c index 79ead87460de26..c0cde4d20d3316 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c +++ b/examples/platform/silabs/SiWx917/SiWx917/sl_wifi_if.c @@ -68,7 +68,7 @@ bool btn0_pressed = false; #define TRNGKEY_SIZE 4 #endif // SLI_SI91X_MCU_INTERFACE -struct wfx_rsi wfx_rsi; +WfxRsi_t wfx_rsi; /* Declare a variable to hold the data associated with the created event group. */ StaticEventGroup_t rsiDriverEventGroup; diff --git a/examples/platform/silabs/SiWx917/SiWx917/wfx_rsi.h b/examples/platform/silabs/SiWx917/SiWx917/wfx_rsi.h index 998b3ac1ef5d7a..c7642485025815 100644 --- a/examples/platform/silabs/SiWx917/SiWx917/wfx_rsi.h +++ b/examples/platform/silabs/SiWx917/SiWx917/wfx_rsi.h @@ -33,30 +33,37 @@ * Various events fielded by the wfx_rsi task * Make sure that we only use 8 bits (otherwise freeRTOS - may need some changes) */ -#define WFX_EVT_STA_CONN (0x01) -#define WFX_EVT_STA_DISCONN (0x02) -#define WFX_EVT_AP_START (0x04) -#define WFX_EVT_AP_STOP (0x08) -#define WFX_EVT_SCAN (0x10) /* This is used as scan result and start */ -#define WFX_EVT_STA_START_JOIN (0x20) -#define WFX_EVT_STA_DO_DHCP (0x40) -#define WFX_EVT_STA_DHCP_DONE (0x80) +typedef enum +{ + WFX_EVT_STA_CONN = (1 << 0), + WFX_EVT_STA_DISCONN = (1 << 1), + WFX_EVT_AP_START = (1 << 2), + WFX_EVT_AP_STOP = (1 << 3), + WFX_EVT_SCAN = (1 << 4), /* This is used as scan result and start */ + WFX_EVT_STA_START_JOIN = (1 << 5), + WFX_EVT_STA_DO_DHCP = (1 << 6), + WFX_EVT_STA_DHCP_DONE = (1 << 7) +} WfxEventType_e; -#define WFX_RSI_ST_DEV_READY (0x01) -#define WFX_RSI_ST_AP_READY (0x02) -#define WFX_RSI_ST_STA_PROVISIONED (0x04) -#define WFX_RSI_ST_STA_CONNECTING (0x08) -#define WFX_RSI_ST_STA_CONNECTED (0x10) -#define WFX_RSI_ST_STA_DHCP_DONE (0x40) /* Requested to do DHCP after conn */ -#define WFX_RSI_ST_STA_MODE (0x80) /* Enable Station Mode */ -#define WFX_RSI_ST_AP_MODE (0x100) /* Enable AP Mode */ -#define WFX_RSI_ST_STA_READY (WFX_RSI_ST_STA_CONNECTED | WFX_RSI_ST_STA_DHCP_DONE) -#define WFX_RSI_ST_STARTED (0x200) /* RSI task started */ -#define WFX_RSI_ST_SCANSTARTED (0x400) /* Scan Started */ -#define WFX_RSI_ST_SLEEP_READY (0x800) /* Notify the M4 to go to sleep*/ +typedef enum +{ + WFX_RSI_ST_DEV_READY = (1 << 0), + WFX_RSI_ST_AP_READY = (1 << 1), + WFX_RSI_ST_STA_PROVISIONED = (1 << 2), + WFX_RSI_ST_STA_CONNECTING = (1 << 3), + WFX_RSI_ST_STA_CONNECTED = (1 << 4), + WFX_RSI_ST_STA_DHCP_DONE = (1 << 6), /* Requested to do DHCP after conn */ + WFX_RSI_ST_STA_MODE = (1 << 7), /* Enable Station Mode */ + WFX_RSI_ST_AP_MODE = (1 << 8), /* Enable AP Mode */ + WFX_RSI_ST_STA_READY = (WFX_RSI_ST_STA_CONNECTED | WFX_RSI_ST_STA_DHCP_DONE), + WFX_RSI_ST_STARTED = (1 << 9), /* RSI task started */ + WFX_RSI_ST_SCANSTARTED = (1 << 10), /* Scan Started */ + WFX_RSI_ST_SLEEP_READY = (1 << 11) /* Notify the M4 to go to sleep*/ +} WfxStateType_e; -struct wfx_rsi +typedef struct wfx_rsi_s { + // TODO: Change tp WfxEventType_e once the event queue is implemented EventGroupHandle_t events; TaskHandle_t drv_task; TaskHandle_t wlan_task; @@ -77,9 +84,9 @@ struct wfx_rsi sl_wfx_mac_address_t ap_bssid; /* To which our STA is connected */ uint16_t join_retries; uint8_t ip4_addr[4]; /* Not sure if this is enough */ -}; +} WfxRsi_t; -extern struct wfx_rsi wfx_rsi; +extern WfxRsi_t wfx_rsi; #ifdef __cplusplus extern "C" { #endif diff --git a/examples/platform/silabs/efr32/rs911x/rsi_if.c b/examples/platform/silabs/efr32/rs911x/rsi_if.c index d229251a7a5cf9..00efeee09a4efb 100644 --- a/examples/platform/silabs/efr32/rs911x/rsi_if.c +++ b/examples/platform/silabs/efr32/rs911x/rsi_if.c @@ -866,4 +866,4 @@ int32_t wfx_rsi_send_data(void * p, uint16_t len) return status; } -struct wfx_rsi wfx_rsi; +WfxRsi_t wfx_rsi; diff --git a/examples/platform/silabs/efr32/rs911x/wfx_rsi.h b/examples/platform/silabs/efr32/rs911x/wfx_rsi.h index 07217056cef11b..86591df72d36d3 100644 --- a/examples/platform/silabs/efr32/rs911x/wfx_rsi.h +++ b/examples/platform/silabs/efr32/rs911x/wfx_rsi.h @@ -53,7 +53,7 @@ #define WFX_RSI_ST_SCANSTARTED (0x400) /* Scan Started */ #define WFX_RSI_ST_SLEEP_READY (0x800) /* Notify the M4 to go to sleep*/ -struct wfx_rsi +typedef struct wfx_rsi_s { EventGroupHandle_t events; TaskHandle_t drv_task; @@ -76,9 +76,9 @@ struct wfx_rsi sl_wfx_mac_address_t ap_bssid; /* To which our STA is connected */ uint16_t join_retries; uint8_t ip4_addr[4]; /* Not sure if this is enough */ -}; +} WfxRsi_t; -extern struct wfx_rsi wfx_rsi; +extern WfxRsi_t wfx_rsi; #ifdef __cplusplus extern "C" { #endif From 1160f97b9061de3ec9e383de0f5111663d25fd3a Mon Sep 17 00:00:00 2001 From: Justin Wood Date: Wed, 13 Mar 2024 07:13:03 -0700 Subject: [PATCH 44/76] Add a way for a client to know if the device cache is primed in Darwin (#32556) * Initial commit * Restyled by whitespace * Restyled by clang-format * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Anush Nadathur * Update src/darwin/Framework/CHIP/MTRDevice.h Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Adding header * Restyled by clang-format --------- Co-authored-by: Restyled.io Co-authored-by: Anush Nadathur Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> --- src/darwin/Framework/CHIP/MTRDevice.h | 8 ++++++++ src/darwin/Framework/CHIP/MTRDevice.mm | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 50ca8d41b272fa..d2404c2899836e 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -64,6 +64,14 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) */ @property (nonatomic, readonly) MTRDeviceState state; +/** + * Is the state cache primed for this device? + * + * This verifies that both the MTRDeviceController has a storage delegate, and a subscription has been set up and the resulting state has been cached. If this is true this means most state is ready to cache and will not require a round trip to the accessory. + * + */ +@property (readonly) BOOL deviceCachePrimed; + /** * The estimated device system start time. * diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index f854242dfbeb1f..4c6b4a21e828d9 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -33,6 +33,7 @@ #import "MTRError_Internal.h" #import "MTREventTLVValueDecoder_Internal.h" #import "MTRLogging_Internal.h" +#import "MTRUnfairLock.h" #import "zap-generated/MTRCommandPayloads_Internal.h" #include "lib/core/CHIPError.h" @@ -2017,6 +2018,12 @@ - (void)setAttributeValues:(NSArray *)attributeValues reportChan os_unfair_lock_unlock(&self->_lock); } +- (BOOL)deviceCachePrimed +{ + std::lock_guard lock(_lock); + return [self _isCachePrimedWithInitialConfigurationData]; +} + // If value is non-nil, associate with expectedValueID // If value is nil, remove only if expectedValueID matches // previousValue is an out parameter From ddbbdcbed827a1c02387a9b5d168e76de893e096 Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Thu, 14 Mar 2024 02:52:57 +0900 Subject: [PATCH 45/76] [Android] Fix onNOCChainGeneration terminate issue (#32235) * Fix onNOCChainGeneration terminate issue * Restyled by clang-format * update to static_cast --------- Co-authored-by: Restyled.io --- .../java/CHIPDeviceController-JNI.cpp | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 30c4c573541e96..8890c881dc0c2c 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -171,44 +171,54 @@ JNI_METHOD(jint, onNOCChainGeneration) ChipLogProgress(Controller, "setNOCChain() called"); jmethodID getRootCertificate; + jmethodID getIntermediateCertificate; + jmethodID getOperationalCertificate; + jmethodID getIpk; + jmethodID getAdminSubject; + + jbyteArray rootCertificate = nullptr; + jbyteArray intermediateCertificate = nullptr; + jbyteArray operationalCertificate = nullptr; + jbyteArray ipk = nullptr; + + Optional adminSubjectOptional; + uint64_t adminSubject = chip::kUndefinedNodeId; + + CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + + Optional ipkOptional; + uint8_t ipkValue[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES]; + Crypto::IdentityProtectionKeySpan ipkTempSpan(ipkValue); + + VerifyOrExit(controllerParams != nullptr, ChipLogError(Controller, "controllerParams is null!")); + err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getRootCertificate", "()[B", &getRootCertificate); - VerifyOrReturnValue(err == CHIP_NO_ERROR, static_cast(err.AsInteger())); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Find getRootCertificate method fail!")); - jmethodID getIntermediateCertificate; err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getIntermediateCertificate", "()[B", &getIntermediateCertificate); - VerifyOrReturnValue(err == CHIP_NO_ERROR, static_cast(err.AsInteger())); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Find getIntermediateCertificate method fail!")); - jmethodID getOperationalCertificate; err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getOperationalCertificate", "()[B", &getOperationalCertificate); - VerifyOrReturnValue(err == CHIP_NO_ERROR, static_cast(err.AsInteger())); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Find getOperationalCertificate method fail!")); - jmethodID getIpk; err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getIpk", "()[B", &getIpk); - VerifyOrReturnValue(err == CHIP_NO_ERROR, static_cast(err.AsInteger())); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Find getIpk method fail!")); - jmethodID getAdminSubject; err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getAdminSubject", "()J", &getAdminSubject); - VerifyOrReturnValue(err == CHIP_NO_ERROR, static_cast(err.AsInteger())); - - jbyteArray rootCertificate = (jbyteArray) env->CallObjectMethod(controllerParams, getRootCertificate); - VerifyOrReturnValue(rootCertificate != nullptr, static_cast(CHIP_ERROR_BAD_REQUEST.AsInteger())); + VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Find getAdminSubject method fail!")); - jbyteArray intermediateCertificate = (jbyteArray) env->CallObjectMethod(controllerParams, getIntermediateCertificate); - VerifyOrReturnValue(intermediateCertificate != nullptr, static_cast(CHIP_ERROR_BAD_REQUEST.AsInteger())); + rootCertificate = static_cast(env->CallObjectMethod(controllerParams, getRootCertificate)); + VerifyOrExit(rootCertificate != nullptr, err = CHIP_ERROR_BAD_REQUEST); - jbyteArray operationalCertificate = (jbyteArray) env->CallObjectMethod(controllerParams, getOperationalCertificate); - VerifyOrReturnValue(operationalCertificate != nullptr, static_cast(CHIP_ERROR_BAD_REQUEST.AsInteger())); + intermediateCertificate = static_cast(env->CallObjectMethod(controllerParams, getIntermediateCertificate)); + VerifyOrExit(intermediateCertificate != nullptr, err = CHIP_ERROR_BAD_REQUEST); - // use ipk and adminSubject from CommissioningParameters if not set in ControllerParams - CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + operationalCertificate = static_cast(env->CallObjectMethod(controllerParams, getOperationalCertificate)); + VerifyOrExit(operationalCertificate != nullptr, err = CHIP_ERROR_BAD_REQUEST); - Optional ipkOptional; - uint8_t ipkValue[CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES]; - Crypto::IdentityProtectionKeySpan ipkTempSpan(ipkValue); - - jbyteArray ipk = (jbyteArray) env->CallObjectMethod(controllerParams, getIpk); + ipk = static_cast(env->CallObjectMethod(controllerParams, getIpk)); if (ipk != nullptr) { JniByteArray jByteArrayIpk(env, ipk); @@ -217,7 +227,7 @@ JNI_METHOD(jint, onNOCChainGeneration) { ChipLogError(Controller, "Invalid IPK size %u and expect %u", static_cast(jByteArrayIpk.byteSpan().size()), static_cast(sizeof(ipkValue))); - return CHIP_ERROR_INVALID_IPK.AsInteger(); + ExitNow(err = CHIP_ERROR_INVALID_IPK); } memcpy(&ipkValue[0], jByteArrayIpk.byteSpan().data(), jByteArrayIpk.byteSpan().size()); @@ -229,8 +239,7 @@ JNI_METHOD(jint, onNOCChainGeneration) ipkOptional.SetValue(commissioningParams.GetIpk().Value()); } - Optional adminSubjectOptional; - uint64_t adminSubject = static_cast(env->CallLongMethod(controllerParams, getAdminSubject)); + adminSubject = static_cast(env->CallLongMethod(controllerParams, getAdminSubject)); if (adminSubject == kUndefinedNodeId) { // if no value pass in ControllerParams, use value from CommissioningParameters @@ -243,20 +252,27 @@ JNI_METHOD(jint, onNOCChainGeneration) // NOTE: we are allowing adminSubject to not be set since the OnNOCChainGeneration callback makes this field // optional and includes logic to handle the case where it is not set. It would also make sense to return // an error here since that use case may not be realistic. - - JniByteArray jByteArrayRcac(env, rootCertificate); - JniByteArray jByteArrayIcac(env, intermediateCertificate); - JniByteArray jByteArrayNoc(env, operationalCertificate); + { + JniByteArray jByteArrayRcac(env, rootCertificate); + JniByteArray jByteArrayIcac(env, intermediateCertificate); + JniByteArray jByteArrayNoc(env, operationalCertificate); #ifndef JAVA_MATTER_CONTROLLER_TEST - err = wrapper->GetAndroidOperationalCredentialsIssuer()->NOCChainGenerated(CHIP_NO_ERROR, jByteArrayNoc.byteSpan(), - jByteArrayIcac.byteSpan(), jByteArrayRcac.byteSpan(), - ipkOptional, adminSubjectOptional); + err = wrapper->GetAndroidOperationalCredentialsIssuer()->NOCChainGenerated( + CHIP_NO_ERROR, jByteArrayNoc.byteSpan(), jByteArrayIcac.byteSpan(), jByteArrayRcac.byteSpan(), ipkOptional, + adminSubjectOptional); - if (err != CHIP_NO_ERROR) - { - ChipLogError(Controller, "Failed to SetNocChain for the device: %" CHIP_ERROR_FORMAT, err.Format()); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Failed to SetNocChain for the device: %" CHIP_ERROR_FORMAT, err.Format()); + } + return static_cast(err.AsInteger()); +#endif // JAVA_MATTER_CONTROLLER_TEST } +exit: +#ifndef JAVA_MATTER_CONTROLLER_TEST + err = wrapper->GetAndroidOperationalCredentialsIssuer()->NOCChainGenerated(err, ByteSpan(), ByteSpan(), ByteSpan(), ipkOptional, + adminSubjectOptional); #endif // JAVA_MATTER_CONTROLLER_TEST return static_cast(err.AsInteger()); } From a6f3b37e4d7b696253f0c7ebdf50af21ccc9b2da Mon Sep 17 00:00:00 2001 From: Justin Wood Date: Wed, 13 Mar 2024 12:41:59 -0700 Subject: [PATCH 46/76] Updating documentation for this property (#32561) * Initial commit * Restyled by whitespace * Restyled by clang-format * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Anush Nadathur * Update src/darwin/Framework/CHIP/MTRDevice.h Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Adding header * Restyled by clang-format * Updating docs --------- Co-authored-by: Restyled.io Co-authored-by: Anush Nadathur Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> --- src/darwin/Framework/CHIP/MTRDevice.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index d2404c2899836e..3bb39e982bcf98 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -65,12 +65,16 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) @property (nonatomic, readonly) MTRDeviceState state; /** - * Is the state cache primed for this device? + * Is the device cache primed for this device? * - * This verifies that both the MTRDeviceController has a storage delegate, and a subscription has been set up and the resulting state has been cached. If this is true this means most state is ready to cache and will not require a round trip to the accessory. + * This will be true after the deviceCachePrimed: delegate callback has been called, false if not. + * + * Please note if you have a storage delegate implemented, the cache is then stored persistently, so + * the would then only be called once, ever - and this property would basically always be true + * if a subscription has ever been established. * */ -@property (readonly) BOOL deviceCachePrimed; +@property (readonly) BOOL deviceCachePrimed MTR_NEWLY_AVAILABLE; /** * The estimated device system start time. From 11c0af319bab01390d8e8ad760e97313bb076f5b Mon Sep 17 00:00:00 2001 From: Jakub Latusek Date: Wed, 13 Mar 2024 20:59:07 +0100 Subject: [PATCH 47/76] Add pigweed support for openiotsdk (#32488) * Add pigweed support for openiotsdk * Review fixes --- config/openiotsdk/chip-gn/.gn | 11 ++++ config/openiotsdk/chip-gn/BUILD.gn | 5 +- scripts/examples/openiotsdk_example.sh | 2 + src/BUILD.gn | 3 +- .../openiotsdk/unit-tests/CMakeLists.txt | 35 +++++++++++- .../openiotsdk/unit-tests/main/main_ns.cpp | 9 +-- .../openiotsdk/unit-tests/main/main_ns_nl.cpp | 55 +++++++++++++++++++ .../openiotsdk/unit-tests/test_components.txt | 24 -------- .../unit-tests/test_components_nl.txt | 24 ++++++++ 9 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 src/test_driver/openiotsdk/unit-tests/main/main_ns_nl.cpp create mode 100644 src/test_driver/openiotsdk/unit-tests/test_components_nl.txt diff --git a/config/openiotsdk/chip-gn/.gn b/config/openiotsdk/chip-gn/.gn index 4b9894b0f943ac..f2404233d6f74d 100644 --- a/config/openiotsdk/chip-gn/.gn +++ b/config/openiotsdk/chip-gn/.gn @@ -14,6 +14,7 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") +import("//build_overrides/pigweed.gni") # The location of the build configuration file. buildconfig = "//build/config/BUILDCONFIG.gn" @@ -26,4 +27,14 @@ default_args = { target_os = "cmsis-rtos" import("${chip_root}/config/openiotsdk/chip-gn/args.gni") + + pw_sys_io_BACKEND = dir_pw_sys_io_stdio + + pw_assert_BACKEND = dir_pw_assert_log + pw_log_BACKEND = dir_pw_log_basic + + pw_build_LINK_DEPS = [ + "$dir_pw_assert:impl", + "$dir_pw_log:impl", + ] } diff --git a/config/openiotsdk/chip-gn/BUILD.gn b/config/openiotsdk/chip-gn/BUILD.gn index 6e6fb51d404bb1..94e2f28635d114 100644 --- a/config/openiotsdk/chip-gn/BUILD.gn +++ b/config/openiotsdk/chip-gn/BUILD.gn @@ -19,7 +19,10 @@ group("openiotsdk") { deps = [ "${chip_root}/src/lib" ] if (chip_build_tests) { - deps += [ "${chip_root}/src:tests" ] + deps += [ + "${chip_root}/src:tests", + "${chip_root}/src/lib/support:pw_tests_wrapper", + ] } } diff --git a/scripts/examples/openiotsdk_example.sh b/scripts/examples/openiotsdk_example.sh index 36c748cc8d1aa2..a4bfd939289f97 100755 --- a/scripts/examples/openiotsdk_example.sh +++ b/scripts/examples/openiotsdk_example.sh @@ -53,6 +53,8 @@ readarray -t SUPPORTED_APP_NAMES <"$CHIP_ROOT"/examples/platform/openiotsdk/supp SUPPORTED_APP_NAMES+=("unit-tests") readarray -t TEST_NAMES <"$CHIP_ROOT"/src/test_driver/openiotsdk/unit-tests/test_components.txt +readarray -t TEST_NAMES_NL <"$CHIP_ROOT"/src/test_driver/openiotsdk/unit-tests/test_components_nl.txt +TEST_NAMES+=("${TEST_NAMES_NL[@]}") function show_usage() { cat < #include "openiotsdk_platform.h" -#include -#include +#include #include -constexpr nl_test_output_logger_t NlTestLogger::nl_test_logger; - using namespace ::chip; int main() @@ -36,8 +33,6 @@ int main() return EXIT_FAILURE; } - nlTestSetLogger(&NlTestLogger::nl_test_logger); - ChipLogAutomation("Open IoT SDK unit-tests start"); if (openiotsdk_network_init(true)) @@ -47,7 +42,7 @@ int main() } ChipLogAutomation("Open IoT SDK unit-tests run..."); - int status = RunRegisteredUnitTests(); + int status = chip::test::RunAllTests(); ChipLogAutomation("Test status: %d", status); ChipLogAutomation("Open IoT SDK unit-tests completed"); diff --git a/src/test_driver/openiotsdk/unit-tests/main/main_ns_nl.cpp b/src/test_driver/openiotsdk/unit-tests/main/main_ns_nl.cpp new file mode 100644 index 00000000000000..f7e511b8d183cf --- /dev/null +++ b/src/test_driver/openiotsdk/unit-tests/main/main_ns_nl.cpp @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2024 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 "openiotsdk_platform.h" +#include +#include +#include + +constexpr nl_test_output_logger_t NlTestLogger::nl_test_logger; + +using namespace ::chip; + +int main() +{ + if (openiotsdk_platform_init()) + { + ChipLogAutomation("ERROR: Open IoT SDK platform initialization failed"); + return EXIT_FAILURE; + } + + nlTestSetLogger(&NlTestLogger::nl_test_logger); + + ChipLogAutomation("Open IoT SDK unit-tests start"); + + if (openiotsdk_network_init(true)) + { + ChipLogAutomation("ERROR: Network initialization failed"); + return EXIT_FAILURE; + } + + ChipLogAutomation("Open IoT SDK unit-tests run..."); + int status = RunRegisteredUnitTests(); + ChipLogAutomation("Test status: %d", status); + ChipLogAutomation("Open IoT SDK unit-tests completed"); + + return EXIT_SUCCESS; +} diff --git a/src/test_driver/openiotsdk/unit-tests/test_components.txt b/src/test_driver/openiotsdk/unit-tests/test_components.txt index 03fa4aee9b70f4..e69de29bb2d1d6 100644 --- a/src/test_driver/openiotsdk/unit-tests/test_components.txt +++ b/src/test_driver/openiotsdk/unit-tests/test_components.txt @@ -1,24 +0,0 @@ -accesstest -AppTests -ASN1Tests -BDXTests -ChipCryptoTests -CoreTests -CredentialsTest -DataModelTests -InetLayerTests -MdnsTests -MessagingLayerTests -MinimalMdnsCoreTests -MinimalMdnsRecordsTests -MinimalMdnsRespondersTests -PlatformTests -RawTransportTests -RetransmitTests -SecureChannelTests -SetupPayloadTests -SupportTests -SystemLayerTests -TestShell -TransportLayerTests -UserDirectedCommissioningTests diff --git a/src/test_driver/openiotsdk/unit-tests/test_components_nl.txt b/src/test_driver/openiotsdk/unit-tests/test_components_nl.txt new file mode 100644 index 00000000000000..03fa4aee9b70f4 --- /dev/null +++ b/src/test_driver/openiotsdk/unit-tests/test_components_nl.txt @@ -0,0 +1,24 @@ +accesstest +AppTests +ASN1Tests +BDXTests +ChipCryptoTests +CoreTests +CredentialsTest +DataModelTests +InetLayerTests +MdnsTests +MessagingLayerTests +MinimalMdnsCoreTests +MinimalMdnsRecordsTests +MinimalMdnsRespondersTests +PlatformTests +RawTransportTests +RetransmitTests +SecureChannelTests +SetupPayloadTests +SupportTests +SystemLayerTests +TestShell +TransportLayerTests +UserDirectedCommissioningTests From c83bc238879e2e07fca06d3d4bdb11208fc6f75a Mon Sep 17 00:00:00 2001 From: Raul Marquez <130402456+raul-marquez-csa@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:06:47 -0700 Subject: [PATCH 48/76] YAML-Updates-2-23-2024 (#32296) * Updates Test_TC_PRS_2_1.yaml * Updates Test_TC_TMP_2_1.yaml * Updates Test_TC_DLOG_2_1.yaml * Removes Test_TC_DLOG_2_2.yaml and Test_TC_DLOG_3_1.yaml * Fix restyle * Updates Test_TC_CNET_4_12.yaml * Updates Test_TC_PWRTL_1_1.yaml * Updates Test_TC_EEVSE_2_1.yaml * Updates Test_TC_GRPKEY_2_1.yaml * Updates Test_TC_GRPKEY_2_2.yaml * Restores Test_TC_GRPKEY_2_2.yaml * Updates Test_TC_CNET_4_12.yaml * Fix restyle * Restores Test_TC_PRS_2_1.yaml * Updates Test_TC_DLOG_2_1.yaml * Fix restyle --- .../certification/Test_TC_CNET_4_12.yaml | 163 +++++-- .../certification/Test_TC_DLOG_2_1.yaml | 410 ++++++++++++++---- .../certification/Test_TC_DLOG_2_2.yaml | 116 ----- .../certification/Test_TC_DLOG_3_1.yaml | 108 ----- .../certification/Test_TC_EEVSE_2_1.yaml | 4 - .../certification/Test_TC_GRPKEY_2_1.yaml | 96 +--- .../certification/Test_TC_PWRTL_1_1.yaml | 25 ++ .../suites/certification/Test_TC_TMP_2_1.yaml | 2 +- 8 files changed, 499 insertions(+), 425 deletions(-) delete mode 100644 src/app/tests/suites/certification/Test_TC_DLOG_2_2.yaml delete mode 100644 src/app/tests/suites/certification/Test_TC_DLOG_3_1.yaml diff --git a/src/app/tests/suites/certification/Test_TC_CNET_4_12.yaml b/src/app/tests/suites/certification/Test_TC_CNET_4_12.yaml index 8180610da3e8ec..3c2b11b4ba294d 100644 --- a/src/app/tests/suites/certification/Test_TC_CNET_4_12.yaml +++ b/src/app/tests/suites/certification/Test_TC_CNET_4_12.yaml @@ -72,7 +72,7 @@ tests: [1659623217.387917][9816:9821] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Attribute 0x0000_0001 DataVersion: 4196844346 [1659623217.388007][9816:9821] CHIP:TOO: Networks: 1 entries [1659623217.388058][9816:9821] CHIP:TOO: [1]: { - [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 2111111122222222 + [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 1111111122222221 [1659623217.388110][9816:9821] CHIP:TOO: Connected: TRUE [1659623217.388129][9816:9821] CHIP:TOO: } disabled: true @@ -89,11 +89,11 @@ tests: field set to th_xpan and Breadcrumb field set to 1" PICS: CNET.S.C04.Rsp && CNET.S.C05.Tx verification: | - ./chip-tool networkcommissioning remove-network hex: 1 0 + ./chip-tool networkcommissioning remove-network hex: 1 0 --Breadcrumb 1 Below is an example: - ./chip-tool networkcommissioning remove-network hex:2111111122222222 22 0 + ./chip-tool networkcommissioning remove-network hex:1111111122222221 22 0 --Breadcrumb 1 Via the TH (chip-tool), Verify the NetworkConfigResponse that contains Networking Status value as 0 (success). @@ -112,11 +112,11 @@ tests: PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET and Breadcrumb field set to 1" PICS: CNET.S.C03.Rsp && CNET.S.C05.Tx verification: | - ./chip-tool networkcommissioning add-or-update-thread-network-network hex: 1 0 + ./chip-tool networkcommissioning add-or-update-thread-network-network hex: 1 0 --Breadcrumb 1 Below is an example: - ./chip-tool networkcommissioning add-or-update-thread-network hex:0e080000000000010000000300000c35060004001fffe0020831111111222222220708fd6958bdf99a83e6051000112233445566778899aabbccddeeff030e4f70656e54687265616444656d6b0102123404101fdf27d94ddb7edc69dc3e72a0ca0ae10c0402a0f7f8 22 0 (second network dataset value) + ./chip-tool networkcommissioning add-or-update-thread-network hex:0e08000000000001000035060004001fffe00708fdd235604ef7ccb50c0402a0f7f8051000112233445566778899aabbccddeeff030f4f70656e54687265616444656d6f3104101dfb97da1e39dc596e886f52cb870a84000300000f0208111111112222222201021234 22 0 --Breadcrumb 1(second network dataset value) Via the TH (chip-tool), Verify the NetworkConfigResponse that contains Networking Status value as 0 (success). @@ -139,7 +139,7 @@ tests: [1659623451.276073][9891:9896] CHIP:TOO: Networks: 1 entries [1659623451.276194][9891:9896] CHIP:TOO: [1]: { - [1659623451.276268][9891:9896] CHIP:TOO: NetworkID: 3111111122222222 + [1659623451.276268][9891:9896] CHIP:TOO: NetworkID: 1111111122222222 [1659623451.276326][9891:9896] CHIP:TOO: Connected: FALSE [1659623451.276381][9891:9896] CHIP:TOO: } disabled: true @@ -150,11 +150,11 @@ tests: field set to 2" PICS: CNET.S.C06.Rsp verification: | - ./chip-tool networkcommissioning connect-network hex: 1 0 + ./chip-tool networkcommissioning connect-network hex: 1 0 --Breadcrumb 2 Below is an example: - ./chip-tool networkcommissioning connect-network hex:3111111122222222 22 0 --Breadcrumb 2 + ./chip-tool networkcommissioning connect-network hex:1111111122222222 22 0 --Breadcrumb 2 Via the TH (chip-tool), Verify the ConnectNetworkResponse that contains Networking Status value as 0 (success). @@ -171,7 +171,17 @@ tests: "Step 8: TH discovers and connects to DUT on the PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET operational network" verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning read networks 22 0 + + Via the TH (chip-tool), Verify: + -the Networks attribute has NetworkID that should be as th_xpan(second network id). + -that the connected status should be the type of bool value as TRUE. + + [1659623451.276073][9891:9896] CHIP:TOO: Networks: 1 entries + [1659623451.276194][9891:9896] CHIP:TOO: [1]: { + [1659623451.276268][9891:9896] CHIP:TOO: NetworkID: 1111111122222222 + [1659623451.276326][9891:9896] CHIP:TOO: Connected: TRUE + [1659623451.276381][9891:9896] CHIP:TOO: } disabled: true - label: @@ -179,9 +189,6 @@ tests: cluster of the DUT" PICS: CNET.S.C06.Rsp verification: | - Mark as not applicable and proceed to next step - - ./chip-tool generalcommissioning read breadcrumb 22 0 Verify "breadcrumb value is set to 2" on the TH(Chip-tool) Log: @@ -195,8 +202,6 @@ tests: "Step 10: TH sends ArmFailSafe command to the DUT with ExpiryLengthSeconds set to 0" verification: | - Mark as not applicable and proceed to next step - ./chip-tool generalcommissioning arm-fail-safe 0 1 22 0 Verify "ArmFailSafeResponse" on the TH(Chip-tool) Log: @@ -214,25 +219,54 @@ tests: "Step 11: TH ensures it can communicate on PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET" verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning read networks 22 0 + + Via the TH (chip-tool), Verify: + -the Networks attribute has NetworkID that should be as th_xpan(first network id). + -that the connected status should be the type of bool value as TRUE. + + [1659623217.387917][9816:9821] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Attribute 0x0000_0001 DataVersion: 4196844346 + [1659623217.388007][9816:9821] CHIP:TOO: Networks: 1 entries + [1659623217.388058][9816:9821] CHIP:TOO: [1]: { + [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 1111111122222221 + [1659623217.388110][9816:9821] CHIP:TOO: Connected: TRUE + [1659623217.388129][9816:9821] CHIP:TOO: } disabled: true - label: "Step 12: TH discovers and connects to DUT on the PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET operational network" verification: | - Mark as not applicable and proceed to next step - ./chip-tool networkcommissioning read networks 22 0 - Verify "Networks entiries and its status" on the TH(Chip-tool) Log: + Via the TH (chip-tool), Verify: + -the Networks attribute has NetworkID that should be as th_xpan(first network id). + -that the connected status should be the type of bool value as TRUE. + + [1659623217.387917][9816:9821] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Attribute 0x0000_0001 DataVersion: 4196844346 + [1659623217.388007][9816:9821] CHIP:TOO: Networks: 1 entries + [1659623217.388058][9816:9821] CHIP:TOO: [1]: { + [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 1111111122222221 + [1659623217.388110][9816:9821] CHIP:TOO: Connected: TRUE + [1659623217.388129][9816:9821] CHIP:TOO: } disabled: true - label: "Step 13: TH sends ArmFailSafe command to the DUT with ExpiryLengthSeconds set to 900" verification: | - Mark as not applicable and proceed to next step + ./chip-tool generalcommissioning arm-fail-safe 900 1 22 0 + + Via the TH (chip-tool), Verify the DUT sends ArmFailSafe with timeout as 900 secs to the TH. + + [1657808518.577084][5979:5984] CHIP:DMG: Received Command Response Data, Endpoint=0 Cluster=0x0000_0030 Command=0x0000_0001 + [1657808518.577181][5979:5984] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0030 Command 0x0000_0001 + [1657808518.577311][5979:5984] CHIP:TOO: ArmFailSafeResponse: { + [1657808518.577409][5979:5984] CHIP:TOO: errorCode: 0 + [1657808518.577466][5979:5984] CHIP:TOO: debugText: + [1657808518.577518][5979:5984] CHIP:TOO: } + [1657808518.577604][5979:5984] CHIP:DMG: ICR moving to [AwaitingDe] + [1657808518.577705][5979:5984] CHIP:EM: Sending Standalone Ack for MessageCounter:240383707 on exchange 56756i disabled: true - label: @@ -240,7 +274,21 @@ tests: field set to th_xpan and Breadcrumb field set to 1" PICS: CNET.S.C04.Rsp && CNET.S.C05.Tx verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning remove-network hex: 1 0 --Breadcrumb 1 + + Below is an example: + + ./chip-tool networkcommissioning remove-network hex:1111111122222221 22 0 --Breadcrumb 1 + + Via the TH (chip-tool), Verify the NetworkConfigResponse that contains Networking Status value as 0 (success). + + [1659623277.664477][9824:9831] CHIP:DMG: Received Command Response Data, Endpoint=0 Cluster=0x0000_0031 Command=0x0000_0005 + [1659623277.664556][9824:9831] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Command 0x0000_0005 + [1659623277.664667][9824:9831] CHIP:TOO: NetworkConfigResponse: { + [1659623277.664758][9824:9831] CHIP:TOO: networkingStatus: 0 + [1659623277.664810][9824:9831] CHIP:TOO: networkIndex: 0 + [1659623277.664843][9824:9831] CHIP:TOO: } + [1659623277.664907][9824:9831] CHIP:DMG: ICR moving to [AwaitingDe] disabled: true - label: @@ -249,7 +297,20 @@ tests: PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET and Breadcrumb field set to 1" PICS: CNET.S.C03.Rsp && CNET.S.C05.Tx verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning add-or-update-thread-network-network hex: 1 0 --Breadcrumb 1 + + Below is an example: + + ./chip-tool networkcommissioning add-or-update-thread-network hex:0e08000000000001000035060004001fffe00708fdd235604ef7ccb50c0402a0f7f8051000112233445566778899aabbccddeeff030f4f70656e54687265616444656d6f3104101dfb97da1e39dc596e886f52cb870a84000300000f0208111111112222222201021234 22 0 --Breadcrumb 1(second network dataset value) + + Via the TH (chip-tool), Verify the NetworkConfigResponse that contains Networking Status value as 0 (success). + + [1659623353.963125][9870:9875] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Command 0x0000_0005 + [1659623353.963312][9870:9875] CHIP:TOO: NetworkConfigResponse: { + [1659623353.963406][9870:9875] CHIP:TOO: networkingStatus: 0 + [1659623353.963473][9870:9875] CHIP:TOO: networkIndex: 0 + [1659623353.963530][9870:9875] CHIP:TOO: } + [1659623353.963623][9870:9875] CHIP:DMG: ICR moving to [AwaitingDe] disabled: true - label: @@ -258,14 +319,39 @@ tests: PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET and Breadcrumb field set to 3" PICS: CNET.S.C06.Rsp && CNET.S.C07.Tx verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning connect-network hex: 1 0 --Breadcrumb 3 + + Below is an example: + + ./chip-tool networkcommissioning connect-network hex:1111111122222222 22 0 --Breadcrumb 3 + + Via the TH (chip-tool), Verify the ConnectNetworkResponse that contains Networking Status value as 0 (success). + + 1 Command=0x0000_0007 + [1659623520.296630][9903:9908] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Command 0x0000_0007 + [1659623520.296853][9903:9908] CHIP:TOO: ConnectNetworkResponse: { + [1659623520.296935][9903:9908] CHIP:TOO: networkingStatus: 0 + [1659623520.296987][9903:9908] CHIP:TOO: errorValue: null + [1659623520.297037][9903:9908] CHIP:TOO: } + [1659623520.297124][9903:9908] CHIP:DMG: ICR moving to [AwaitingDe] disabled: true - label: "Step 17: TH discovers and connects to DUT on the PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET operational network" verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning read networks 22 0 + + Via the TH (chip-tool), Verify: + -the Networks attribute has NetworkID that should be as th_xpan(Second network id). + -that the connected status should be the type of bool value as TRUE. + + [1659623217.387917][9816:9821] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Attribute 0x0000_0001 DataVersion: 4196844346 + [1659623217.388007][9816:9821] CHIP:TOO: Networks: 1 entries + [1659623217.388058][9816:9821] CHIP:TOO: [1]: { + [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 1111111122222222 + [1659623217.388110][9816:9821] CHIP:TOO: Connected: TRUE + [1659623217.388129][9816:9821] CHIP:TOO: } disabled: true - label: @@ -273,16 +359,41 @@ tests: cluster of the DUT" PICS: CNET.S.C06.Rsp verification: | - Mark as not applicable and proceed to next step + ./chip-tool generalcommissioning read breadcrumb 22 0 + + Verify "breadcrumb value is set to 3" on the TH(Chip-tool) Log: + + [1659623558.934419][9911:9917] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0030 Attribute 0x0000_0000 DataVersion: 808037534 + [1659623558.934529][9911:9917] CHIP:TOO: Breadcrumb: 3 + [1659623558.934681][9911:9917] CHIP:EM: Sending Standalone Ack for MessageCounter:244248455 on exchange 8477i disabled: true - label: "Step 19: TH sends the CommissioningComplete command to the DUT" verification: | - Mark as not applicable and proceed to next step + ./chip-tool generalcommissioning commissioning-complete 22 0 + + Via the TH (chip-tool), Verify the DUT sends CommissioningComplete command and the errorCode field is 0(OK). + + [1657734803.411199][7802:7808] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0030 Command 0x0000_0005 + [1657734803.411256][7802:7808] CHIP:TOO: CommissioningCompleteResponse: { + [1657734803.411306][7802:7808] CHIP:TOO: errorCode: 0 + [1657734803.411333][7802:7808] CHIP:TOO: debugText: + [1657734803.411356][7802:7808] CHIP:TOO: } disabled: true - label: "Step 20: TH reads Networks attribute from the DUT" PICS: CNET.S.A0001 verification: | - Mark as not applicable and proceed to next step + ./chip-tool networkcommissioning read networks 22 0 + + Via the TH (chip-tool), Verify: + -the Networks attribute has NetworkID that should be as th_xpan(Second network id). + -that the connected status should be the type of bool value as TRUE. + + [1659623217.387917][9816:9821] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0031 Attribute 0x0000_0001 DataVersion: 4196844346 + [1659623217.388007][9816:9821] CHIP:TOO: Networks: 1 entries + [1659623217.388058][9816:9821] CHIP:TOO: [1]: { + [1659623217.388089][9816:9821] CHIP:TOO: NetworkID: 1111111122222222 + [1659623217.388110][9816:9821] CHIP:TOO: Connected: TRUE + [1659623217.388129][9816:9821] CHIP:TOO: } disabled: true diff --git a/src/app/tests/suites/certification/Test_TC_DLOG_2_1.yaml b/src/app/tests/suites/certification/Test_TC_DLOG_2_1.yaml index 8937b63fd15a22..45111b016974d2 100644 --- a/src/app/tests/suites/certification/Test_TC_DLOG_2_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_DLOG_2_1.yaml @@ -29,138 +29,394 @@ tests: - label: "Precondition" verification: | DUT supports BDX + Length of TransferFileDesignator is zero + Length of TransferFileDesignator is within 32 characters + Length of TransferFileDesignator is equal to 32 characters + Length of TransferFileDesignator is greater than 32 characters + To send a message that mismatches the current transfer mode disabled: true - - label: "Step 1: Reboot DUT" + - label: "Step 1: Commission DUT to TH" verification: | disabled: true - - label: "Step 2: Commission DUT to TH" + - label: + "Step 2: TH sends RetrieveLogsRequest Command to DUT with + RequestedProtocol argument as BDX : RetrieveLogsRequest (Intent = + EndUserSupport, RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_OK_FULL_LENGTH)" + PICS: MCORE.BDX.Initiator + verification: | + diagnosticlogs retrieve-logs-request 0 1 1 0 --TransferFileDesignator Length_123456789123456789123.txt + + On TH(chip-tool), Verify that the DUT sends SendInit message with TransferFileDesignator field set to Length_1234567891234567891 + + 1707966626.594544][10635:10638] CHIP:ATM: SendInit + [1707966626.594550][10635:10638] CHIP:ATM: Proposed Transfer Control: 0x10 + [1707966626.594558][10635:10638] CHIP:ATM: Range Control: 0x0 + [1707966626.594563][10635:10638] CHIP:ATM: Proposed Max Block Size: 1024 + [1707966626.594569][10635:10638] CHIP:ATM: Start Offset: 0x0000000000000000 + [1707966626.594577][10635:10638] CHIP:ATM: Proposed Max Length: 0x0000000000000000 + [1707966626.594584][10635:10638] CHIP:ATM: File Designator Length: 32 + [1707966626.594588][10635:10638] CHIP:ATM: File Designator: Length_123456789123456789123.txt + + Note: end_user_support_log > 1024 bytes so that BDX inititation happens from DUT + SendInitMsgfromDUT = true + disabled: true + + - label: + "Step 3: if (SendInitMsgfromDUT = true) TH Sends BDX SendAccept + message to DUT" + PICS: MCORE.BDX.Initiator + verification: | + On chip-tool TH will send the SendAccept Message to the DUT + + [1707894873.734698][34353:34356] CHIP:ATM: Sending BDX Message + [1707894873.734710][34353:34356] CHIP:ATM: SendAccept + [1707894873.734715][34353:34356] CHIP:ATM: Transfer Control: 0x10 + [1707894873.734720][34353:34356] CHIP:ATM: Max Block Size: 1024 + [1707894874.235405][34353:34356] CHIP:BDX: Got an event MsgToSend + + On TH(chip-tool), verify that the DUT responds by sending RetrieveLogsResponse Command with Success(0) status code to TH after receiving the SendAccept Message + 1707894874.239127][34353:34356] CHIP:TOO: Endpoint: 0 Cluster: 0x0000_0032 Command 0x0000_0001 + [1707894874.239189][34353:34356] CHIP:TOO: RetrieveLogsResponse: { + [1707894874.239208][34353:34356] CHIP:TOO: status: 0 + [1707894874.239219][34353:34356] CHIP:TOO: logContent: + [1707894874.239227][34353:34356] CHIP:TOO: } + + Check for the size of the file that was specified in the File Deginator field of the RetrieveLogsRequest Command sent to DUT and verify that the size is greater than 1024 bytes. + The file: Length_123456789123456789123.txt (as mentioned in the file designator field) is transferred to /tmp folder on the build system and the size is > 1024 bytes + + Note: The file can be stored in any location on the build system. The current SDK imlemnetstion stores the log file transferred using BDX protocol in /tmp folder. + disabled: true + + - label: + "Step 4: if (SendInitMsgfromDUT = false) TH does not send BDX + SendAccept message to DUT" + PICS: MCORE.BDX.Initiator + verification: | + As SendInitMsgfromDUT = true this step is not applicable + disabled: true + + - label: + "Step 5: Repeat Steps from 2 to 4 by setting Intent field to + NetworkDiag and CrashLogs" verification: | disabled: true - label: - "Step 3: TH sends RetrieveLogsRequest Command (Intent = - EndUserSupport,TransferFileDesignator = 'test.txt', RequestedProtocol= - BDX) to DUT" + "Step 2a: TH sends RetrieveLogsRequest Command to DUT with + RequestedProtocol argument as BDX : RetrieveLogsRequest (Intent = + EndUserSupport, RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_OK_FULL_LENGTH)" + PICS: MCORE.BDX.Initiator + verification: | + Repeating steps from 2 to 4 by setting Intent field to NetworkDiag + + diagnosticlogs retrieve-logs-request 1 1 1 0 --TransferFileDesignator Length_123456789123456789123.txt + + Note: nw_log does not exist and DUT does not initiate the BDX transfer + SendInitMsgfromDUT = false + disabled: true + + - label: + "Step 3a: if (SendInitMsgfromDUT = true) TH Sends BDX SendAccept + message to DUT" + PICS: MCORE.BDX.Initiator verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 1 "test.txt" 1 0 + SendInitMsgfromDUT = false this step is not applicable + disabled: true + - label: + "Step 4a: if (SendInitMsgfromDUT = false) TH does not send BDX + SendAccept message to DUT" + PICS: MCORE.BDX.Initiator + verification: | + On TH(chip-tool), verify that the DUT responds by sending RetrieveLogsResponse Command with NoLogs(2) status code to TH and LogContent field is empty - [1651207333.385887][2441:2446] CHIP:DMG: StatusIB = - [1651207333.385937][2441:2446] CHIP:DMG: { - [1651207333.385985][2441:2446] CHIP:DMG: status = 0x00 (SUCCESS), - [1651207333.386037][2441:2446] CHIP:DMG: }, + [1707967219.637228][10723:10726] CHIP:TOO: RetrieveLogsResponse: { + [1707967219.637242][10723:10726] CHIP:TOO: status: 2 + [1707967219.637248][10723:10726] CHIP:TOO: logContent: + [1707967219.637253][10723:10726] CHIP:TOO: } disabled: true - label: - "Step 4: Verify that the DUT initiates a BDX Transfer, sending a BDX - SendInit message with the File Designator field of the message set to - the value of the TransferFileDesignator field of the - RetrieveLogsRequest" + "Step 2b: TH sends RetrieveLogsRequest Command to DUT with + RequestedProtocol argument as BDX : RetrieveLogsRequest (Intent = + EndUserSupport, RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_OK_FULL_LENGTH)" + PICS: MCORE.BDX.Initiator verification: | - Not Verifiable + Repeating steps from 2 to 4 by setting Intent field to CrashLogs + + diagnosticlogs retrieve-logs-request 2 1 1 0 --TransferFileDesignator Length_123456789123456789123.txt + + Note: crash_log < 1024 Bytes and DUT does not inittiate the BDX transfer + SendInitMsgfromDUT = false disabled: true - - label: "Step 5: TH Sends BDX SendAccept message" - PICS: DLOG.S.C01.Tx + - label: + "Step 3b: if (SendInitMsgfromDUT = true) TH Sends BDX SendAccept + message to DUT" + PICS: MCORE.BDX.Initiator verification: | - Not Verifiable + SendInitMsgfromDUT = false this step is not applicable disabled: true - label: - "Step 6: Verify that DUT sends RetrieveLogsResponse Command,Verify - that the Status field is set to Success,If LogContent size < 1024 - octets,Verify that the BDX transfer is not initiated from DUT Verify - that DUT sends RetrieveLogsResponse command with a Status field set to - Exhausted Note: In this case steps 5 and 6 does not hold good. else - Verify that the BDX transfer is initiated from DUT Verify that the - LogContent field of RetrieveLogsResponse is empty Verify that DUT - sends RetrieveLogsResponse command with a Status field set to Success - Verify that UTCTimeStamp is included in the RetrieveLogsResponse - command Verify that TimeSinceBoot is included in the - RetrieveLogsResponse command Note: In this case steps 5 and 6 holds - good." - PICS: DLOG.S.C01.Tx + "Step 4b: if (SendInitMsgfromDUT = false) TH does not send BDX + SendAccept message to DUT" + PICS: MCORE.BDX.Initiator verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 1 "test.txt" 1 0 + On TH(chip-tool), verify that the DUT responds by sending RetrieveLogsResponse Command with Exhausted(1) status code to TH and LogContent field of RetrieveLogsResponse contains at most 1024 bytes - [1651207369.743117][2450:2455] CHIP:DMG: StatusIB = - [1651207369.743155][2450:2455] CHIP:DMG: { - [1651207369.743192][2450:2455] CHIP:DMG: status = 0x00 (SUCCESS), - [1651207369.743228][2450:2455] CHIP:DMG: }, + 1707894938.009997][34371:34374] CHIP:TOO: RetrieveLogsResponse: { + [1707894938.010025][34371:34374] CHIP:TOO: status: 1 + [1707894938.010057][34371:34374] CHIP:TOO: logContent: RetrieveLogsResponse: { + [1707967525.484222][10866:10869] CHIP:TOO: status: 1 + [1707967525.484229][10866:10869] CHIP:TOO: logContent: 353535350A + [1707967525.484233][10866:10869] CHIP:TOO: } disabled: true - label: - "Step 7: TH sends RetrieveLogsRequest Command (Intent = - EndUserSupport,TransferFileDesignator = 'test.txt', RequestedProtocol= - BDX) to DUT" + "Step 6: TH sends RetrieveLogsRequest Command to DUT with + RequestedProtocol argument as BDX : RetrieveLogsRequest(Intent = + EndUserSupport, RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_OK_NORMAL)" + PICS: MCORE.BDX.Initiator verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 1 "test.txt" 1 0 + "diagnosticlogs retrieve-logs-request 0 1 1 0 --TransferFileDesignator Length_1234567.txt + + On TH(chip-tool), Verify that the DUT sends SendInit message with TransferFileDesignator field set to Length_1234567891234567891 + [1707967645.770994][10882:10885] CHIP:ATM: SendInit + [1707967645.770997][10882:10885] CHIP:ATM: Proposed Transfer Control: 0x10 + [1707967645.771001][10882:10885] CHIP:ATM: Range Control: 0x0 + [1707967645.771004][10882:10885] CHIP:ATM: Proposed Max Block Size: 1024 + [1707967645.771009][10882:10885] CHIP:ATM: Start Offset: 0x0000000000000000 + [1707967645.771011][10882:10885] CHIP:ATM: Proposed Max Length: 0x0000000000000000 + [1707967645.771014][10882:10885] CHIP:ATM: File Designator Length: 18 + [1707967645.771018][10882:10885] CHIP:ATM: File Designator: Length_1234567.txt - [1651207386.883337][2457:2462] CHIP:DMG: - [1651207386.883383][2457:2462] CHIP:DMG: StatusIB = - [1651207386.883443][2457:2462] CHIP:DMG: { - [1651207386.883498][2457:2462] CHIP:DMG: status = 0x00 (SUCCESS), - [1651207386.883563][2457:2462] CHIP:DMG: }, + Note: end_user_support_log > 1024 bytes so that BDX inititation happens from DUT + SendInitMsgfromDUT = true" disabled: true - label: - "Step 8: Verify that the DUT initiates a BDX Transfer, sending a BDX - SendInit message with the File Designator field of the message set to - the value of the TransferFileDesignator field of the - RetrieveLogsRequest" - PICS: DLOG.S.C01.Tx + "Step 7: if (SendInitMsgfromDUT = true) TH Sends + StatusReport(GeneralCode: FAILURE, ProtocolId: BDX, ProtocolCode: + TH_LOG_ERROR_TRANSFER_METHOD_NOT_SUPPORTED) to DUT" + PICS: MCORE.BDX.Initiator verification: | - Not Verifiable + Not Verifiable. This step requires additional API for error injection.(Not available in the Chip-tool) + disabled: true + + - label: + "Step 8: TH sends RetrieveLogsRequest Command RequestedProtocol as + ResponsePayload : RetrieveLogsRequest(Intent = + EndUserSupport,RequestedProtocol = ResponsePayload) Repeat this step + by setting Intent field to . NetworkDiag . CrashLogs" + PICS: MCORE.BDX.Initiator + verification: | + "diagnosticlogs retrieve-logs-request 0 0 1 0 + + On TH(chip-tool), Verify that the DUT responds with Success(0) status code for the RetrieveLogsResponse command Verify that LogContent field contains at most 1024 bytes + + RetrieveLogsResponse: { + [1707901602.742523][36080:36083] CHIP:TOO: status: 0 + [1707901602.742542][36080:36083] CHIP:TOO: logContent: 31353238303033363031313533353030333730303234303030303234303133653234303230313138333530313331303034373032313533313031316230323330383230323137303630393261383634383836663730643031303730326130383230323038333038323032303430323031303333313064333030623036303936303836343830313635303330343032303133303832303137303036303932613836343838366637306430313037303161303832303136313034383230313564313532343030303132353031663166663336303230353030383030353031383030353032383030353033383030353034383030353035383030353036383030353037383030353038383030353039383030353061383030353062383030353063383030353064383030353065383030353066383030353130383030353131383030353132383030353133383030353134383030353135383030353136383030353137383030353138383030353139383030353161383030353162383030353163383030353164383030353165383030353166383030353230383030353231383030353232383030353233383030353234383030353235383030353236383030353237383030353238383030353239383030353261383030353262383030353263383030353264383030353265383030353266383030353330383030353331383030353332383030353333383030353334383030353335383030353336383030353337383030353338383030353339383030353361383030353362383030353363383030353364383030353365383030353366383030353430383030353431383030353432383030353433383030353434383030353435383030353436383030353437383030353438383030353439383030353461383030353462383030353463383030353464383030353465383030353466383030353530383030353531383030353532383030353533383030353534383030353535383030353536383030353537383030353538383030353539383030353561383030353562383030353563383030353564383030353565383030353566383030353630383030353631383030353632383030353633383031383234303331363263303431333433353334313330333033303330333035333537343333303330333033303330326433303330323430353030323430363030323430373031323430383030313833313763333037613032303130333830313466653334336639353939343737363362363165653435333931333133333834393466653637643865333030623036303936303836343830633733653461363039363038363438303630393630383634383036303936303836343830363039363038363438303630393630383634383036303936303836 + + Repeat this setp by setting Intent filed to NetworkDiag + diagnosticlogs retrieve-logs-request 1 0 1 0 + + On TH(chip-tool), verify that the DUT responds with NoLogs(2) status code to TH for the RetrieveLogsResponse command and LogContent field is empty + + [1707967219.637228][10723:10726] CHIP:TOO: RetrieveLogsResponse: { + [1707967219.637242][10723:10726] CHIP:TOO: status: 2 + [1707967219.637248][10723:10726] CHIP:TOO: logContent: + [1707967219.637253][10723:10726] CHIP:TOO: } + + Repeat this setp by setting Intent filed to Crash_log + diagnosticlogs retrieve-logs-request 2 0 1 0 + + On TH(chip-tool), verify that the DUT responds with success(0) status code to TH for the RetrieveLogsResponse command and LogContent field of RetrieveLogsResponse contains at most 1024 bytes + + [1707982645.639415][11765:11767] CHIP:TOO: RetrieveLogsResponse: { + [1707982645.639457][11765:11767] CHIP:TOO: status: 0 + [1707982645.639489][11765:11767] CHIP:TOO: logContent: logContent: 353535350A + }" disabled: true - label: - "Step 9: TH Sends StatusReport(GeneralCode: FAILURE, ProtocolId: BDX, - ProtocolCode: TRANSFER_METHOD_NOT_SUPPORTED)" + "Step 9: TH sends RetrieveLogsRequest Command to DUT without + TransferFileDesignator argument : RetrieveLogsRequest(Intent = + EndUserSupport, RequestedProtocol= BDX)" + PICS: MCORE.BDX.Initiator verification: | - Not Verifiable + "diagnosticlogs retrieve-logs-request 0 1 1 0 + + On TH(chip-tool), Verify that the DUT responds with INVALID_COMMAND for the RetrieveLogsRequest that was sent without TransferFileDesignator + + [1707924172.241489][42120:42123] CHIP:DMG: InvokeResponseIB = + [1707924172.241494][42120:42123] CHIP:DMG: { + [1707924172.241497][42120:42123] CHIP:DMG: CommandStatusIB = + [1707924172.241500][42120:42123] CHIP:DMG: { + [1707924172.241503][42120:42123] CHIP:DMG: CommandPathIB = + [1707924172.241507][42120:42123] CHIP:DMG: { + [1707924172.241511][42120:42123] CHIP:DMG: EndpointId = 0x0, + [1707924172.241514][42120:42123] CHIP:DMG: ClusterId = 0x32, + [1707924172.241519][42120:42123] CHIP:DMG: CommandId = 0x0, + [1707924172.241527][42120:42123] CHIP:DMG: }, + [1707924172.241536][42120:42123] CHIP:DMG: + [1707924172.241539][42120:42123] CHIP:DMG: StatusIB = + [1707924172.241543][42120:42123] CHIP:DMG: { + [1707924172.241546][42120:42123] CHIP:DMG: status = 0x85 (INVALID_COMMAND), + [1707924172.241549][42120:42123] CHIP:DMG: }, + [1707924172.241553][42120:42123] CHIP:DMG: + [1707924172.241555][42120:42123] CHIP:DMG: }, + [1707924172.241560][42120:42123] CHIP:DMG: + [1707924172.241562][42120:42123] CHIP:DMG: }, + [1707924172.241567][42120:42123] CHIP:DMG: + [1707924172.241573][42120:42123] CHIP:DMG: ], + [1707924172.241577][42120:42123] CHIP:DMG: + [1707924172.241579][42120:42123] CHIP:DMG: InteractionModelRevision = 11 + [1707924172.241582][42120:42123] CHIP:DMG: }," disabled: true - label: - "Step 10: Verify that DUT sends RetrieveLogsResponse command with a - Status field set to Denied" + "Step 10: TH sends RetrieveLogsRequest Command to DUT that does not + support BDX : RetrieveLogsRequest(Intent = EndUserSupport, + RequestedProtocol= BDX, TransferFileDesignator = TH_LOG_OK_NORMAL)" + PICS: "!MCORE.BDX.Initiator" verification: | - Not Verifiable + "diagnosticlogs retrieve-logs-request 0 1 1 0 --TransferFileDesignator Length_1234567.txt + + On TH(chip-tool), Verify that the DUT responds with Exhausted(1) status code for the RetrieveLogsResponse command with the LogContent field containing at most 1024 bytes + + [1707979121.749537][7593:7596] CHIP:TOO: RetrieveLogsResponse: { + [1707979121.749565][7593:7596] CHIP:TOO: status: 1 + [1707979121.749593][7593:7596] CHIP:TOO: logContent: 31353238303033363031313533353030333730303234303030303234303133653234303230313138333530313331303034373032313533313031316230323330383230323137303630393261383634383836663730643031303730326130383230323038333038323032303430323031303333313064333030623036303936303836343830313635303330343032303133303832303137303036303932613836343838366637306430313037303161303832303136313034383230313564313532343030303132353031663166663336303230353030383030353031383030353032383030353033383030353034383030353035383030353036383030353037383030353038383030353039383030353061383030353062383030353063383030353064383030353065383030353066383030353130383030353131383030353132383030353133383030353134383030353135383030353136383030353137383030353138383030353139383030353161383030353162383030353163383030353164383030353165383030353166383030353230383030353231383030353232383030353233383030353234383030353235383030353236383030353237383030353238383030353239383030353261383030353262383030353263383030353264383030353265383030353266383030353330383030353331383030353332383030353333383030353334383030353335383030353336383030353337383030353338383030353339383030353361383030353362383030353363383030353364383030353365383030353366383030353430383030353431383030353432383030353433383030353434383030353435383030353436383030353437383030353438383030353439383030353461383030353462383030353463383030353464383030353465383030353466383030353530383030353531383030353532383030353533383030353534383030353535383030353536383030353537383030353538383030353539383030353561383030353562383030353563383030353564383030353565383030353566383030353630383030353631383030353632383030353633383031383234303331363263303431333433353334313330333033303330333035333537343333303330333033303330326433303330323430353030323430363030323430373031323430383030313833313763333037613032303130333830313466653334336639353939343737363362363165653435333931333133333834393466653637643865333030623036303936303836343830633733653461363039363038363438303630393630383634383036303936303836343830363039363038363438303630393630383634383036303936303836}" disabled: true - label: - "Step 11: TH sends RetrieveLogsRequest Command (Intent = - EndUserSupport, RequestedProtocol = ResponsePayload) to DUT" + "Step 12: TH sends RetrieveLogsRequest Command to DUT with Invalid + RequestedProtocol : RetrieveLogsRequest(Intent = + EndUserSupport,RequestedProtocol= 2, TransferFileDesignator = + TH_LOG_OK_NORMAL)" + PICS: MCORE.BDX.Initiator verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 0 "test.txt" 1 0 + "diagnosticlogs retrieve-logs-request 0 2 1 0 --TransferFileDesignator Length_1234567.txt - [1651207416.783607][2465:2470] CHIP:DMG: StatusIB = - [1651207416.783676][2465:2470] CHIP:DMG: { - [1651207416.783722][2465:2470] CHIP:DMG: status = 0x00 (SUCCESS), - [1651207416.783766][2465:2470] CHIP:DMG: }, + On TH(chip-tool), Verify that the DUT responds with INVALID_COMMAND for the RetrieveLogsRequest that was sent Invalid RequestedProtocol(2) + + 707901794.468418][36124:36127] CHIP:DMG: InvokeResponseMessage = + [1707901794.468425][36124:36127] CHIP:DMG: { + [1707901794.468433][36124:36127] CHIP:DMG: suppressResponse = false, + [1707901794.468440][36124:36127] CHIP:DMG: InvokeResponseIBs = + [1707901794.468452][36124:36127] CHIP:DMG: [ + [1707901794.468459][36124:36127] CHIP:DMG: InvokeResponseIB = + [1707901794.468471][36124:36127] CHIP:DMG: { + [1707901794.468477][36124:36127] CHIP:DMG: CommandStatusIB = + [1707901794.468485][36124:36127] CHIP:DMG: { + [1707901794.468492][36124:36127] CHIP:DMG: CommandPathIB = + [1707901794.468501][36124:36127] CHIP:DMG: { + [1707901794.468510][36124:36127] CHIP:DMG: EndpointId = 0x0, + [1707901794.468518][36124:36127] CHIP:DMG: ClusterId = 0x32, + [1707901794.468526][36124:36127] CHIP:DMG: CommandId = 0x0, + [1707901794.468533][36124:36127] CHIP:DMG: }, + [1707901794.468545][36124:36127] CHIP:DMG: + [1707901794.468552][36124:36127] CHIP:DMG: StatusIB = + [1707901794.468560][36124:36127] CHIP:DMG: { + [1707901794.468569][36124:36127] CHIP:DMG: status = 0x85 (INVALID_COMMAND), + [1707901794.468576][36124:36127] CHIP:DMG: }, + [1707901794.468584][36124:36127] CHIP:DMG: + [1707901794.468591][36124:36127] CHIP:DMG: }, + [1707901794.468602][36124:36127] CHIP:DMG: + [1707901794.468608][36124:36127] CHIP:DMG: }, + [1707901794.468619][36124:36127] CHIP:DMG: + [1707901794.468624][36124:36127] CHIP:DMG: ], + [1707901794.468635][36124:36127] CHIP:DMG:" disabled: true - label: - "Step 12: Verify that the BDX transfer is not initiated from DUT, - Verify that the LogContent field of RetrieveLogsResponse has the size - < = 1024 octets" + "Step 13: TH sends RetrieveLogsRequest Command with Invalid + TransferFileDesignator length : RetrieveLogsRequest(Intent = + EndUserSupport,RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_ERROR_EMPTY)" verification: | - Not Verifiable + "diagnosticlogs retrieve-logs-request 0 1 1 0 --TransferFileDesignator '' + + [1707904517.151453][36678:36681] CHIP:DMG: ICR moving to [ResponseRe] + [1707904517.151489][36678:36681] CHIP:DMG: InvokeResponseMessage = + [1707904517.151501][36678:36681] CHIP:DMG: { + [1707904517.151511][36678:36681] CHIP:DMG: suppressResponse = false, + [1707904517.151522][36678:36681] CHIP:DMG: InvokeResponseIBs = + [1707904517.151541][36678:36681] CHIP:DMG: [ + [1707904517.151548][36678:36681] CHIP:DMG: InvokeResponseIB = + [1707904517.151565][36678:36681] CHIP:DMG: { + [1707904517.151573][36678:36681] CHIP:DMG: CommandStatusIB = + [1707904517.151582][36678:36681] CHIP:DMG: { + [1707904517.151590][36678:36681] CHIP:DMG: CommandPathIB = + [1707904517.151599][36678:36681] CHIP:DMG: { + [1707904517.151613][36678:36681] CHIP:DMG: EndpointId = 0x0, + [1707904517.151627][36678:36681] CHIP:DMG: ClusterId = 0x32, + [1707904517.151640][36678:36681] CHIP:DMG: CommandId = 0x0, + [1707904517.151652][36678:36681] CHIP:DMG: }, + [1707904517.151670][36678:36681] CHIP:DMG: + [1707904517.151681][36678:36681] CHIP:DMG: StatusIB = + [1707904517.151696][36678:36681] CHIP:DMG: { + [1707904517.151708][36678:36681] CHIP:DMG: status = 0x87 (CONSTRAINT_ERROR), + [1707904517.151720][36678:36681] CHIP:DMG: }, + [1707904517.151734][36678:36681] CHIP:DMG: + [1707904517.151745][36678:36681] CHIP:DMG: }, + [1707904517.151763][36678:36681] CHIP:DMG: + [1707904517.151772][36678:36681] CHIP:DMG: }, + [1707904517.151790][36678:36681] CHIP:DMG: + [1707904517.151798][36678:36681] CHIP:DMG: ], + [1707904517.151816][36678:36681] CHIP:DMG: + [1707904517.151824][36678:36681] CHIP:DMG: InteractionModelRevision = 11 + [1707904517.151830][36678:36681] CHIP:DMG: }," disabled: true - label: - "Step 13: Verify that DUT sends RetrieveLogsResponse command with a - Status field set to Success, Verify that UTCTimeStamp is included in - the RetrieveLogsResponse command,Verify that TimeSinceBoot is included - in the RetrieveLogsResponse command" + "Step 14: TH sends RetrieveLogsRequest Command to DUT with Invalid + TransferFileDesignator length : RetrieveLogsRequest(Intent = + EndUserSupport,RequestedProtocol= BDX, TransferFileDesignator = + TH_LOG_BAD_LENGTH)" verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 1 "test.txt" 1 0 + "diagnosticlogs retrieve-logs-request 0 1 1 0 --TransferFileDesignator Length_1234567891234567891234567891212345.txt + + On TH(chip-tool), Verify that the DUT responds with CONSTRAINT_ERRORfor the RetrieveLogsRequest that was sent Invalid Invalid TransferFileDesignator length(> 32) - [1651207438.423557][2475:2480] CHIP:DMG: StatusIB = - [1651207438.423594][2475:2480] CHIP:DMG: { - [1651207438.423648][2475:2480] CHIP:DMG: status = 0x00 (SUCCESS), - [1651207438.423708][2475:2480] CHIP:DMG: }, + [1707904517.151453][36678:36681] CHIP:DMG: ICR moving to [ResponseRe] + [1707904517.151489][36678:36681] CHIP:DMG: InvokeResponseMessage = + [1707904517.151501][36678:36681] CHIP:DMG: { + [1707904517.151511][36678:36681] CHIP:DMG: suppressResponse = false, + [1707904517.151522][36678:36681] CHIP:DMG: InvokeResponseIBs = + [1707904517.151541][36678:36681] CHIP:DMG: [ + [1707904517.151548][36678:36681] CHIP:DMG: InvokeResponseIB = + [1707904517.151565][36678:36681] CHIP:DMG: { + [1707904517.151573][36678:36681] CHIP:DMG: CommandStatusIB = + [1707904517.151582][36678:36681] CHIP:DMG: { + [1707904517.151590][36678:36681] CHIP:DMG: CommandPathIB = + [1707904517.151599][36678:36681] CHIP:DMG: { + [1707904517.151613][36678:36681] CHIP:DMG: EndpointId = 0x0, + [1707904517.151627][36678:36681] CHIP:DMG: ClusterId = 0x32, + [1707904517.151640][36678:36681] CHIP:DMG: CommandId = 0x0, + [1707904517.151652][36678:36681] CHIP:DMG: }, + [1707904517.151670][36678:36681] CHIP:DMG: + [1707904517.151681][36678:36681] CHIP:DMG: StatusIB = + [1707904517.151696][36678:36681] CHIP:DMG: { + [1707904517.151708][36678:36681] CHIP:DMG: status = 0x87 (CONSTRAINT_ERROR), + [1707904517.151720][36678:36681] CHIP:DMG: }, + [1707904517.151734][36678:36681] CHIP:DMG: + [1707904517.151745][36678:36681] CHIP:DMG: }, + [1707904517.151763][36678:36681] CHIP:DMG: + [1707904517.151772][36678:36681] CHIP:DMG: }, + [1707904517.151790][36678:36681] CHIP:DMG: + [1707904517.151798][36678:36681] CHIP:DMG: ], + [1707904517.151816][36678:36681] CHIP:DMG: + [1707904517.151824][36678:36681] CHIP:DMG: InteractionModelRevision = 11 + [1707904517.151830][36678:36681] CHIP:DMG: }," disabled: true diff --git a/src/app/tests/suites/certification/Test_TC_DLOG_2_2.yaml b/src/app/tests/suites/certification/Test_TC_DLOG_2_2.yaml deleted file mode 100644 index 800553226d6cf0..00000000000000 --- a/src/app/tests/suites/certification/Test_TC_DLOG_2_2.yaml +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) 2021 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. -# Auto-generated scripts for harness use only, please review before automation. The endpoints and cluster names are currently set to default - -name: - 55.2.2. [TC-DLOG-2.2] Diagnostic Logs Cluster Commands Checks without BDX - [DUT-Server] - -PICS: - - DLOG.S - -config: - nodeId: 0x12344321 - cluster: "Basic Information" - endpoint: 0 - -tests: - - label: "Precondition" - verification: | - DUT does not support BDX - disabled: true - - - label: "Step 1: Commission DUT to TH" - verification: | - - disabled: true - - - label: - "Step 2: TH sends RetrieveLogsRequest Command (Intent = - EndUserSupport,TransferFileDesignator = 'test.txt',RequestedProtocol= - BDX) to DUT" - verification: | - sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 0 "test.txt" 1 0 - - [1646209207.288823][3223:3228] CHIP:DMG: InvokeResponseMessage = - [1646209207.288859][3223:3228] CHIP:DMG: { - [1646209207.288893][3223:3228] CHIP:DMG: suppressResponse = false, - [1646209207.288928][3223:3228] CHIP:DMG: InvokeResponseIBs = - [1646209207.288972][3223:3228] CHIP:DMG: [ - [1646209207.289006][3223:3228] CHIP:DMG: InvokeResponseIB = - [1646209207.289056][3223:3228] CHIP:DMG: { - [1646209207.289096][3223:3228] CHIP:DMG: CommandStatusIB = - [1646209207.289146][3223:3228] CHIP:DMG: { - [1646209207.289189][3223:3228] CHIP:DMG: CommandPathIB = - [1646209207.289237][3223:3228] CHIP:DMG: { - [1646209207.289285][3223:3228] CHIP:DMG: EndpointId = 0x0, - [1646209207.289338][3223:3228] CHIP:DMG: ClusterId = 0x32, - [1646209207.289391][3223:3228] CHIP:DMG: CommandId = 0x0, - [1646209207.289434][3223:3228] CHIP:DMG: }, - [1646209207.289479][3223:3228] CHIP:DMG: - [1646209207.289521][3223:3228] CHIP:DMG: StatusIB = - [1646209207.289573][3223:3228] CHIP:DMG: { - [1646209207.289619][3223:3228] CHIP:DMG: status = 0x0, - [1646209207.289666][3223:3228] CHIP:DMG: }, - [1646209207.289715][3223:3228] CHIP:DMG: - [1646209207.289756][3223:3228] CHIP:DMG: }, - [1646209207.289804][3223:3228] CHIP:DMG: - [1646209207.289842][3223:3228] CHIP:DMG: }, - [1646209207.289889][3223:3228] CHIP:DMG: - [1646209207.289923][3223:3228] CHIP:DMG: ], - [1646209207.289966][3223:3228] CHIP:DMG: - [1646209207.289999][3223:3228] CHIP:DMG: InteractionModelRevision = 1 - [1646209207.290032][3223:3228] CHIP:DMG: }, - [1646209207.290116][3223:3228] CHIP:DMG: Received Command Response Status for Endpoint=0 Cluster=0x0000_0032 Command=0x0000_0000 Status=0x0 - disabled: true - - - label: - "Step 3: Verify that the BDX transfer is not initiated from DUT,Verify - that DUT sends RetrieveLogsResponse command,Verify that the LogContent - field of RetrieveLogsResponse command has the DUT log entries up to < - = 1024 octets,Verify that Status field is set to Exhausted" - verification: | - ubuntu@ubuntu:~/apps$ sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 0 "test.txt" 1 0 - - [1646209207.288823][3223:3228] CHIP:DMG: InvokeResponseMessage = - [1646209207.288859][3223:3228] CHIP:DMG: { - [1646209207.288893][3223:3228] CHIP:DMG: suppressResponse = false, - [1646209207.288928][3223:3228] CHIP:DMG: InvokeResponseIBs = - [1646209207.288972][3223:3228] CHIP:DMG: [ - [1646209207.289006][3223:3228] CHIP:DMG: InvokeResponseIB = - [1646209207.289056][3223:3228] CHIP:DMG: { - [1646209207.289096][3223:3228] CHIP:DMG: CommandStatusIB = - [1646209207.289146][3223:3228] CHIP:DMG: { - [1646209207.289189][3223:3228] CHIP:DMG: CommandPathIB = - [1646209207.289237][3223:3228] CHIP:DMG: { - [1646209207.289285][3223:3228] CHIP:DMG: EndpointId = 0x0, - [1646209207.289338][3223:3228] CHIP:DMG: ClusterId = 0x32, - [1646209207.289391][3223:3228] CHIP:DMG: CommandId = 0x0, - [1646209207.289434][3223:3228] CHIP:DMG: }, - [1646209207.289479][3223:3228] CHIP:DMG: - [1646209207.289521][3223:3228] CHIP:DMG: StatusIB = - [1646209207.289573][3223:3228] CHIP:DMG: { - [1646209207.289619][3223:3228] CHIP:DMG: status = 0x0, - [1646209207.289666][3223:3228] CHIP:DMG: }, - [1646209207.289715][3223:3228] CHIP:DMG: - [1646209207.289756][3223:3228] CHIP:DMG: }, - [1646209207.289804][3223:3228] CHIP:DMG: - [1646209207.289842][3223:3228] CHIP:DMG: }, - [1646209207.289889][3223:3228] CHIP:DMG: - [1646209207.289923][3223:3228] CHIP:DMG: ], - [1646209207.289966][3223:3228] CHIP:DMG: - [1646209207.289999][3223:3228] CHIP:DMG: InteractionModelRevision = 1 - [1646209207.290032][3223:3228] CHIP:DMG: }, - [1646209207.290116][3223:3228] CHIP:DMG: Received Command Response Status for Endpoint=0 Cluster=0x0000_0032 Command=0x0000_0000 Status=0x0 - disabled: true diff --git a/src/app/tests/suites/certification/Test_TC_DLOG_3_1.yaml b/src/app/tests/suites/certification/Test_TC_DLOG_3_1.yaml deleted file mode 100644 index 0cba5b0421fa35..00000000000000 --- a/src/app/tests/suites/certification/Test_TC_DLOG_3_1.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2021 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. -# Auto-generated scripts for harness use only, please review before automation. The endpoints and cluster names are currently set to default - -name: 55.3.1. [TC-DLOG-3.1] Diagnostic Logs Cluster Commands Checks[DUT-Client] - -PICS: - - DLOG.C - -config: - nodeId: 0x12344321 - cluster: "Basic Information" - endpoint: 0 - -tests: - - label: "Note" - verification: | - For DUT as client test cases, Chip-tool command used below are an example to verify the functionality. For certification test, we expect DUT should have a capability or way to run the equivalent command. - disabled: true - - - label: "Precondition" - verification: | - DUT supports BDX - TH supports BDX - disabled: true - - - label: "Step 1: Commission DUT to TH" - verification: | - - disabled: true - - - label: "Step 2: DUT sends RetrieveLogsRequest Command to TH" - PICS: DLOG.C.C00.Tx - verification: | - ./chip-tool diagnosticlogs retrieve-logs-request 0 0 "test.txt" 1 0 - - - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] StatusIB = - [1646215088531] [15387:2221674] CHIP: [DMG] { - [1646215088531] [15387:2221674] CHIP: [DMG] status = 0x0, - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] ], - [1646215088532] [15387:2221674] CHIP: [DMG] - [1646215088532] [15387:2221674] CHIP: [DMG] InteractionModelRevision = 1 - [1646215088532] [15387:2221674] CHIP: [DMG] }, - [1646215088532] [15387:2221674] CHIP: [DMG] Received Command Response Status for Endpoint=0 Cluster=0x0000_0032 Command=0x0000_0000 Status=0x0 - disabled: true - - - label: "Step 3: In case TH initiates a BDX Transfer" - verification: | - grl_matter@GRL-Matters-MacBook-Air debug % sudo ./chip-tool diagnosticlogs retrieve-logs-request 0 1 "test.txt" 1 0 - - - [1646208340.192138][3171:3176] CHIP:DMG: - [1646208340.192177][3171:3176] CHIP:DMG: StatusIB = - [1646208340.192224][3171:3176] CHIP:DMG: { - [1646208340.192271][3171:3176] CHIP:DMG: status = 0x0, - [1646208340.192319][3171:3176] CHIP:DMG: }, - [1646208340.192362][3171:3176] CHIP:DMG: - [1646208340.192401][3171:3176] CHIP:DMG: }, - [1646208340.192450][3171:3176] CHIP:DMG: - [1646208340.192486][3171:3176] CHIP:DMG: }, - [1646208340.192530][3171:3176] CHIP:DMG: - [1646208340.192562][3171:3176] CHIP:DMG: ], - [1646208340.192602][3171:3176] CHIP:DMG: - [1646208340.192634][3171:3176] CHIP:DMG: InteractionModelRevision = 1 - [1646208340.192665][3171:3176] CHIP:DMG: }, - [1646208340.192744][3171:3176] CHIP:DMG: Received Command Response Status for Endpoint=0 Cluster=0x0000_0032 Command=0x0000_0000 Status=0x0 - disabled: true - - - label: "Step 4: In case TH does not initiate BDX Transfer" - verification: | - ./chip-tool diagnosticlogs retrieve-logs-request 0 0 "test.txt" 1 0 - - - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] StatusIB = - [1646215088531] [15387:2221674] CHIP: [DMG] { - [1646215088531] [15387:2221674] CHIP: [DMG] status = 0x0, - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] }, - [1646215088531] [15387:2221674] CHIP: [DMG] - [1646215088531] [15387:2221674] CHIP: [DMG] ], - [1646215088532] [15387:2221674] CHIP: [DMG] - [1646215088532] [15387:2221674] CHIP: [DMG] InteractionModelRevision = 1 - [1646215088532] [15387:2221674] CHIP: [DMG] }, - [1646215088532] [15387:2221674] CHIP: [DMG] Received Command Response Status for Endpoint=0 Cluster=0x0000_0032 Command=0x0000_0000 Status=0x0 - disabled: true diff --git a/src/app/tests/suites/certification/Test_TC_EEVSE_2_1.yaml b/src/app/tests/suites/certification/Test_TC_EEVSE_2_1.yaml index a448a969c7e44e..9079aa4a2cfa37 100644 --- a/src/app/tests/suites/certification/Test_TC_EEVSE_2_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_EEVSE_2_1.yaml @@ -88,7 +88,6 @@ tests: constraints: type: amperage_ma minValue: 0 - maxValue: 80000 - label: "Step 8: TH reads from the DUT the MinimumChargeCurrent attribute" PICS: EEVSE.S.A0006 @@ -98,7 +97,6 @@ tests: constraints: type: amperage_ma minValue: 0 - maxValue: 80000 - label: "Step 9: TH reads from the DUT the MaximumChargeCurrent attribute" PICS: EEVSE.S.A0007 @@ -108,7 +106,6 @@ tests: constraints: type: amperage_ma minValue: 0 - maxValue: 80000 - label: "Step 10: TH reads from the DUT the MaximumDischargeCurrent attribute" @@ -119,7 +116,6 @@ tests: constraints: type: amperage_ma minValue: 0 - maxValue: 80000 - label: "Step 11: TH writes to the DUT the UserMaximumChargeCurrent attribute diff --git a/src/app/tests/suites/certification/Test_TC_GRPKEY_2_1.yaml b/src/app/tests/suites/certification/Test_TC_GRPKEY_2_1.yaml index 7bc77c9534bbb0..218e0274f7e75c 100644 --- a/src/app/tests/suites/certification/Test_TC_GRPKEY_2_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_GRPKEY_2_1.yaml @@ -68,34 +68,7 @@ tests: value: [{ FabricIndex: 1, GroupId: 0x0103, GroupKeySetID: 0x01a3 }] - label: - "Step 4: TH sends KeySetWrite command in the GroupKeyManagement - cluster to DUT. GroupKeySet fields are as follows:1)GroupKeySetID: - 0x01a3 2)GroupKeySecurityPolicy: TrustFirst (0) 3)EpochKey0: - d0d1d2d3d4d5d6d7d8d9dadbdcdddedf 4)EpochStartTime0:1 5)EpochKey1: - d1d1d2d3d4d5d6d7d8d9dadbdcdddedf 6)EpochStartTime1: - 18446744073709551613 7)EpochKey2: d2d1d2d3d4d5d6d7d8d9dadbdcdddedf - 8)EpochStartTime2: 18446744073709551614" - PICS: GRPKEY.S.C00.Rsp - command: "KeySetWrite" - arguments: - values: - - name: GroupKeySet - value: - { - GroupKeySetID: 0x01a3, - GroupKeySecurityPolicy: 0, - EpochKey0: "hex:d0d1d2d3d4d5d6d7d8d9dadbdcdddedf", - EpochStartTime0: 1, - EpochKey1: "hex:d1d1d2d3d4d5d6d7d8d9dadbdcdddedf", - EpochStartTime1: "18446744073709551613", - EpochKey2: "hex:d2d1d2d3d4d5d6d7d8d9dadbdcdddedf", - EpochStartTime2: "18446744073709551614", - } - - # Step 5 does not exist in the test plan. - - - label: - "Step 6: TH reads GroupTable attribute from GroupKeyManagement cluster + "Step 4: TH reads GroupTable attribute from GroupKeyManagement cluster on DUT." PICS: GRPKEY.S.A0001 command: "readAttribute" @@ -104,47 +77,7 @@ tests: value: [] - label: - "Step 7a: TH attempts to write to the GroupTable attribute from - GroupKeyManagement cluster on DUT with one entry as follows:1)GroupId: - 0x0104 2)Endpoints: [PIXIT.G.ENDPOINT] 3)GroupName: 'Test Group2'" - PICS: GRPKEY.S.A0001 && G.S.F00 - command: "writeAttribute" - attribute: "GroupTable" - arguments: - value: - [ - { - FabricIndex: 1, - GroupId: 0x0104, - Endpoints: [Groups.Endpoint], - GroupName: "Test Group2", - }, - ] - response: - error: UNSUPPORTED_WRITE - - - label: - "Step 7b: TH attempts to write to the GroupTable attribute from - GroupKeyManagement cluster on DUT with one entry as follows:1)GroupId: - 0x0104 2)Endpoints: [PIXIT.G.ENDPOINT] 3)GroupName: '' " - PICS: GRPKEY.S.A0001 && !G.S.F00 - command: "writeAttribute" - attribute: "GroupTable" - arguments: - value: - [ - { - FabricIndex: 1, - GroupId: 0x0104, - Endpoints: [Groups.Endpoint], - GroupName: "", - }, - ] - response: - error: UNSUPPORTED_WRITE - - - label: - "Step 8: TH reads MaxGroupsPerFabric attribute from GroupKeyManagement + "Step 5: TH reads MaxGroupsPerFabric attribute from GroupKeyManagement cluster on DUT using a fabric-filtered read." PICS: GRPKEY.S.A0002 command: "readAttribute" @@ -158,18 +91,7 @@ tests: maxValue: 65535 - label: - "Step 9: TH attempts to write MaxGroupsPerFabric attribute of - GroupKeyManagement cluster to the same value as read in step 8." - PICS: GRPKEY.S.A0002 - command: "writeAttribute" - attribute: "MaxGroupsPerFabric" - arguments: - value: MaxGroupsPerFabricValue - response: - error: UNSUPPORTED_WRITE - - - label: - "Step 10: TH reads MaxGroupKeysPerFabric attribute from + "Step 6: TH reads MaxGroupKeysPerFabric attribute from GroupKeyManagement cluster on DUT using a fabric-filtered read." PICS: GRPKEY.S.A0003 command: "readAttribute" @@ -181,15 +103,3 @@ tests: type: int16u minValue: 1 maxValue: 65535 - - - label: - "Step 11: TH attempts to write MaxGroupKeysPerFabric attribute of - GroupKeyManagement cluster on DUT to the same value as read in step - 10." - PICS: GRPKEY.S.A0003 - command: "writeAttribute" - attribute: "MaxGroupKeysPerFabric" - arguments: - value: MaxGroupKeysPerFabricValue - response: - error: UNSUPPORTED_WRITE diff --git a/src/app/tests/suites/certification/Test_TC_PWRTL_1_1.yaml b/src/app/tests/suites/certification/Test_TC_PWRTL_1_1.yaml index cfe6fd860be240..da8b19b8bf7e1b 100644 --- a/src/app/tests/suites/certification/Test_TC_PWRTL_1_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_PWRTL_1_1.yaml @@ -117,3 +117,28 @@ tests: constraints: type: list contains: [0, 1] + + - label: "Step 5*: TH reads EventList attribute from DUT" + PICS: PICS_EVENT_LIST_ENABLED + command: "readAttribute" + attribute: "EventList" + response: + value: [] + constraints: + type: list + + - label: "Step 6: TH reads the AcceptedCommandList attribute from the DUT" + command: "readAttribute" + attribute: "AcceptedCommandList" + response: + value: [] + constraints: + type: list + + - label: "Step 7: TH reads the GeneratedCommandList attribute from the DUT" + command: "readAttribute" + attribute: "GeneratedCommandList" + response: + value: [] + constraints: + type: list diff --git a/src/app/tests/suites/certification/Test_TC_TMP_2_1.yaml b/src/app/tests/suites/certification/Test_TC_TMP_2_1.yaml index 7f0af0d3bb9f03..a239c77b01dd05 100644 --- a/src/app/tests/suites/certification/Test_TC_TMP_2_1.yaml +++ b/src/app/tests/suites/certification/Test_TC_TMP_2_1.yaml @@ -50,7 +50,7 @@ tests: saveAs: CurrentMaxMeasured constraints: type: int16s - minValue: CurrentMinMeasured + minValue: CurrentMinMeasured+1 maxValue: 32767 - label: "Step 4: TH reads the MeasuredValue attribute from the DUT" From 7a39e509990a1f115ec408aa995dd66dd37b9e1a Mon Sep 17 00:00:00 2001 From: Jeff Tung <100387939+jtung-apple@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:07:30 -0700 Subject: [PATCH 49/76] [Darwin] MTRDevice attribute storage mutable array enumeration fix (#32559) * [Darwin] MTRDevice attribute storage mutable array enumeration fix * Added unit test and minor fixes * Unit test fix * Addressed review comments * Fixed / clarified logging for nodeID, endpoint, cluster, attribute * Fixed unit test race condition --- .../CHIP/MTRDeviceControllerDataStore.mm | 216 +++++++++++++----- .../CHIPTests/MTRPerControllerStorageTests.m | 32 +++ .../TestHelpers/MTRTestDeclarations.h | 5 + 3 files changed, 196 insertions(+), 57 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm index 0495211b76b449..26d68921591f75 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -329,12 +329,12 @@ - (BOOL)_storeAttributeCacheValue:(id)value forKey:(NSString *)key sharingType:MTRStorageSharingTypeNotShared]; } -- (void)_removeAttributeCacheValueForKey:(NSString *)key +- (BOOL)_removeAttributeCacheValueForKey:(NSString *)key { - [_storageDelegate controller:_controller - removeValueForKey:key - securityLevel:MTRStorageSecurityLevelSecure - sharingType:MTRStorageSharingTypeNotShared]; + return [_storageDelegate controller:_controller + removeValueForKey:key + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; } static NSString * sAttributeCacheNodeIndexKey = @"attrCacheNodeIndex"; @@ -349,9 +349,9 @@ - (BOOL)_storeNodeIndex:(NSArray *)nodeIndex return [self _storeAttributeCacheValue:nodeIndex forKey:sAttributeCacheNodeIndexKey]; } -- (void)_deleteNodeIndex +- (BOOL)_deleteNodeIndex { - [self _removeAttributeCacheValueForKey:sAttributeCacheNodeIndexKey]; + return [self _removeAttributeCacheValueForKey:sAttributeCacheNodeIndexKey]; } static NSString * sAttributeCacheEndpointIndexKeyPrefix = @"attrCacheEndpointIndex"; @@ -371,9 +371,9 @@ - (BOOL)_storeEndpointIndex:(NSArray *)endpointIndex forNodeID:(NSNu return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]]; } -- (void)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID +- (BOOL)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID { - [self _removeAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID]]; + return [self _removeAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID]]; } static NSString * sAttributeCacheClusterIndexKeyPrefix = @"attrCacheClusterIndex"; @@ -393,9 +393,9 @@ - (BOOL)_storeClusterIndex:(NSArray *)clusterIndex forNodeID:(NSNumb return [self _storeAttributeCacheValue:clusterIndex forKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; } -- (void)_deleteClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +- (BOOL)_deleteClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID { - [self _removeAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; + return [self _removeAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; } static NSString * sAttributeCacheAttributeIndexKeyPrefix = @"attrCacheAttributeIndex"; @@ -415,9 +415,9 @@ - (BOOL)_storeAttributeIndex:(NSArray *)attributeIndex forNodeID:(NS return [self _storeAttributeCacheValue:attributeIndex forKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; } -- (void)_deleteAttributeIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +- (BOOL)_deleteAttributeIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID { - [self _removeAttributeCacheValueForKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; + return [self _removeAttributeCacheValueForKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; } static NSString * sAttributeCacheAttributeValueKeyPrefix = @"attrCacheAttributeValue"; @@ -437,13 +437,17 @@ - (BOOL)_storeAttributeValue:(NSDictionary *)value forNodeID:(NSNumber *)nodeID return [self _storeAttributeCacheValue:value forKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]]; } -- (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID +- (BOOL)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID { - [self _removeAttributeCacheValueForKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]]; + return [self _removeAttributeCacheValueForKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]]; } #pragma - Attribute Cache management +#ifndef ATTRIBUTE_CACHE_VERBOSE_LOGGING +#define ATTRIBUTE_CACHE_VERBOSE_LOGGING 0 +#endif + - (nullable NSArray *)getStoredAttributesForNodeID:(NSNumber *)nodeID { __block NSMutableArray * attributesToReturn = nil; @@ -451,6 +455,10 @@ - (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber * // Fetch node index NSArray * nodeIndex = [self _fetchNodeIndex]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Fetch got %lu values for nodeIndex", static_cast(nodeIndex.count)); +#endif + if (![nodeIndex containsObject:nodeID]) { // Sanity check and delete if nodeID exists in index NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; @@ -458,6 +466,8 @@ - (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber * MTR_LOG_ERROR("Persistent attribute cache contains orphaned entry for nodeID %@ - deleting", nodeID); [self clearStoredAttributesForNodeID:nodeID]; } + + MTR_LOG_INFO("Fetch got no value for endpointIndex @ node 0x%016llX", nodeID.unsignedLongLongValue); attributesToReturn = nil; return; } @@ -465,17 +475,33 @@ - (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber * // Fetch endpoint index NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Fetch got %lu values for endpointIndex @ node 0x%016llX", static_cast(endpointIndex.count), nodeID.unsignedLongLongValue); +#endif + for (NSNumber * endpointID in endpointIndex) { // Fetch endpoint index NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Fetch got %lu values for clusterIndex @ node 0x%016llX %u", static_cast(clusterIndex.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); +#endif + for (NSNumber * clusterID in clusterIndex) { // Fetch endpoint index NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Fetch got %lu values for attributeIndex @ node 0x%016llX endpoint %u cluster 0x%08lX", static_cast(attributeIndex.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); +#endif + for (NSNumber * attributeID in attributeIndex) { NSDictionary * value = [self _fetchAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Fetch got %u values for attribute value @ node 0x%016llX endpoint %u cluster 0x%08lX attribute 0x%08lX", value ? 1 : 0, nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue, attributeID.unsignedLongValue); +#endif + if (value) { if (!attributesToReturn) { attributesToReturn = [NSMutableArray array]; @@ -495,83 +521,108 @@ - (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber * return attributesToReturn; } +#ifdef DEBUG +- (void)unitTestPruneEmptyStoredAttributesBranches +{ + dispatch_sync(_storageDelegateQueue, ^{ + [self _pruneEmptyStoredAttributesBranches]; + }); +} +#endif + - (void)_pruneEmptyStoredAttributesBranches { dispatch_assert_queue(_storageDelegateQueue); + NSUInteger storeFailures = 0; + // Fetch node index - NSMutableArray * nodeIndex = [self _fetchNodeIndex].mutableCopy; - NSUInteger nodeIndexCount = nodeIndex.count; + NSArray * nodeIndex = [self _fetchNodeIndex]; + NSMutableArray * nodeIndexCopy = [nodeIndex mutableCopy]; - NSUInteger storeFailures = 0; for (NSNumber * nodeID in nodeIndex) { // Fetch endpoint index - NSMutableArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID].mutableCopy; - NSUInteger endpointIndexCount = endpointIndex.count; + NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + NSMutableArray * endpointIndexCopy = [endpointIndex mutableCopy]; for (NSNumber * endpointID in endpointIndex) { // Fetch endpoint index - NSMutableArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID].mutableCopy; - NSUInteger clusterIndexCount = clusterIndex.count; + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + NSMutableArray * clusterIndexCopy = [clusterIndex mutableCopy]; for (NSNumber * clusterID in clusterIndex) { // Fetch endpoint index - NSMutableArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID].mutableCopy; - NSUInteger attributeIndexCount = attributeIndex.count; + NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + NSMutableArray * attributeIndexCopy = [attributeIndex mutableCopy]; for (NSNumber * attributeID in attributeIndex) { NSDictionary * value = [self _fetchAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; if (!value) { - [attributeIndex removeObject:attributeID]; + [attributeIndexCopy removeObject:attributeID]; } } - if (!attributeIndex.count) { - [clusterIndex removeObject:clusterID]; - } else if (attributeIndex.count != attributeIndexCount) { - BOOL success = [self _storeAttributeIndex:attributeIndex forNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + if (attributeIndex.count != attributeIndexCopy.count) { + BOOL success; + if (attributeIndexCopy.count) { + success = [self _storeAttributeIndex:attributeIndexCopy forNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + } else { + [clusterIndexCopy removeObject:clusterID]; + success = [self _deleteAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + } if (!success) { storeFailures++; - MTR_LOG_INFO("Store failed for attributeIndex"); + MTR_LOG_INFO("Store failed in _pruneEmptyStoredAttributesBranches for attributeIndex (%lu) @ node 0x%016llX endpoint %u cluster 0x%08lX", static_cast(attributeIndexCopy.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); } } } - if (!clusterIndex.count) { - [endpointIndex removeObject:endpointID]; - } else if (clusterIndex.count != clusterIndexCount) { - BOOL success = [self _storeClusterIndex:clusterIndex forNodeID:nodeID endpointID:endpointID]; + if (clusterIndex.count != clusterIndexCopy.count) { + BOOL success; + if (clusterIndexCopy.count) { + success = [self _storeClusterIndex:clusterIndexCopy forNodeID:nodeID endpointID:endpointID]; + } else { + [endpointIndexCopy removeObject:endpointID]; + success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + } if (!success) { storeFailures++; - MTR_LOG_INFO("Store failed for clusterIndex"); + MTR_LOG_INFO("Store failed in _pruneEmptyStoredAttributesBranches for clusterIndex (%lu) @ node 0x%016llX endpoint %u", static_cast(clusterIndexCopy.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); } } } - if (!endpointIndex.count) { - [nodeIndex removeObject:nodeID]; - } else if (endpointIndex.count != endpointIndexCount) { - BOOL success = [self _storeEndpointIndex:endpointIndex forNodeID:nodeID]; + if (endpointIndex.count != endpointIndexCopy.count) { + BOOL success; + if (endpointIndexCopy.count) { + success = [self _storeEndpointIndex:endpointIndexCopy forNodeID:nodeID]; + } else { + [nodeIndexCopy removeObject:nodeID]; + success = [self _deleteEndpointIndexForNodeID:nodeID]; + } if (!success) { storeFailures++; - MTR_LOG_INFO("Store failed for endpointIndex"); + MTR_LOG_INFO("Store failed in _pruneEmptyStoredAttributesBranches for endpointIndex (%lu) @ node 0x%016llX", static_cast(endpointIndexCopy.count), nodeID.unsignedLongLongValue); } } } - if (!nodeIndex.count) { - [self _deleteNodeIndex]; - } else if (nodeIndex.count != nodeIndexCount) { - BOOL success = [self _storeNodeIndex:nodeIndex]; + if (nodeIndex.count != nodeIndexCopy.count) { + BOOL success; + if (nodeIndexCopy.count) { + success = [self _storeNodeIndex:nodeIndexCopy]; + } else { + success = [self _deleteNodeIndex]; + } if (!success) { storeFailures++; - MTR_LOG_INFO("Store failed for nodeIndex"); + MTR_LOG_INFO("Store failed in _pruneEmptyStoredAttributesBranches for nodeIndex (%lu)", static_cast(nodeIndexCopy.count)); } } if (storeFailures) { - MTR_LOG_ERROR("Store failed in _pruneEmptyStoredAttributesBranches: %lu", (unsigned long) storeFailures); + MTR_LOG_ERROR("Store failed in _pruneEmptyStoredAttributesBranches: failure count %lu", static_cast(storeFailures)); } } @@ -584,6 +635,10 @@ - (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NS MTRAttributePath * path = dataValue[MTRAttributePathKey]; NSDictionary * value = dataValue[MTRDataKey]; +#if ATTRIBUTE_CACHE_VERBOSE_LOGGING + MTR_LOG_INFO("Attempt to store attribute value @ node 0x%016llX endpoint %u cluster 0x%08lX attribute 0x%08lX", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue, path.attribute.unsignedLongValue); +#endif + BOOL storeFailed = NO; // Ensure node index exists NSArray * nodeIndex = [self _fetchNodeIndex]; @@ -609,7 +664,7 @@ - (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NS } if (storeFailed) { storeFailures++; - MTR_LOG_INFO("Store failed for endpointIndex"); + MTR_LOG_INFO("Store failed for endpointIndex @ node 0x%016llX", nodeID.unsignedLongLongValue); continue; } @@ -623,7 +678,7 @@ - (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NS } if (storeFailed) { storeFailures++; - MTR_LOG_INFO("Store failed for clusterIndex"); + MTR_LOG_INFO("Store failed for clusterIndex @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue); continue; } @@ -640,56 +695,100 @@ - (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NS } if (storeFailed) { storeFailures++; - MTR_LOG_INFO("Store failed for attributeIndex"); + MTR_LOG_INFO("Store failed for attributeIndex @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue); continue; } // Store value - storeFailed = [self _storeAttributeValue:value forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute]; + storeFailed = ![self _storeAttributeValue:value forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute]; if (storeFailed) { storeFailures++; - MTR_LOG_INFO("Store failed for attribute value"); + MTR_LOG_INFO("Store failed for attribute value @ node 0x%016llX endpoint %u cluster 0x%08lX attribute 0x%08lX", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue, path.attribute.unsignedLongValue); } } // In the rare event that store fails, allow all attribute store attempts to go through and prune empty branches at the end altogether. if (storeFailures) { [self _pruneEmptyStoredAttributesBranches]; - MTR_LOG_ERROR("Store failed in -storeAttributeValues:forNodeID: %lu", (unsigned long) storeFailures); + MTR_LOG_ERROR("Store failed in -storeAttributeValues:forNodeID: failure count %lu", static_cast(storeFailures)); } }); } - (void)_clearStoredAttributesForNodeID:(NSNumber *)nodeID { + NSUInteger endpointsClearAttempts = 0; + NSUInteger clustersClearAttempts = 0; + NSUInteger attributesClearAttempts = 0; + NSUInteger endpointsCleared = 0; + NSUInteger clustersCleared = 0; + NSUInteger attributesCleared = 0; + // Fetch endpoint index NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + endpointsClearAttempts += endpointIndex.count; for (NSNumber * endpointID in endpointIndex) { // Fetch cluster index NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + clustersClearAttempts += clusterIndex.count; for (NSNumber * clusterID in clusterIndex) { // Fetch attribute index NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + attributesClearAttempts += attributeIndex.count; for (NSNumber * attributeID in attributeIndex) { - [self _deleteAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; + BOOL success = [self _deleteAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; + if (!success) { + MTR_LOG_INFO("Delete failed for attribute value @ node 0x%016llX endpoint %u cluster 0x%08lX attribute 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue, attributeID.unsignedLongValue); + } else { + attributesCleared++; + } } - [self _deleteAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + BOOL success = [self _deleteAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + if (!success) { + MTR_LOG_INFO("Delete failed for attributeIndex @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue); + } else { + clustersCleared++; + } } - [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + BOOL success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + if (!success) { + MTR_LOG_INFO("Delete failed for clusterIndex @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue); + } else { + endpointsCleared++; + } + } + + BOOL success = [self _deleteEndpointIndexForNodeID:nodeID]; + if (!success) { + MTR_LOG_INFO("Delete failed for endpointrIndex @ node 0x%016llX", nodeID.unsignedLongLongValue); } - [self _deleteEndpointIndexForNodeID:nodeID]; + MTR_LOG_INFO("clearStoredAttributesForNodeID: deleted endpoints %lu/%lu clusters %lu/%lu attributes %lu/%lu", static_cast(endpointsCleared), static_cast(endpointsClearAttempts), static_cast(clustersCleared), static_cast(clustersClearAttempts), static_cast(attributesCleared), static_cast(attributesClearAttempts)); } - (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID { dispatch_async(_storageDelegateQueue, ^{ [self _clearStoredAttributesForNodeID:nodeID]; + NSArray * nodeIndex = [self _fetchNodeIndex]; + NSMutableArray * nodeIndexCopy = [nodeIndex mutableCopy]; + [nodeIndexCopy removeObject:nodeID]; + if (nodeIndex.count != nodeIndexCopy.count) { + BOOL success; + if (nodeIndexCopy.count) { + success = [self _storeNodeIndex:nodeIndexCopy]; + } else { + success = [self _deleteNodeIndex]; + } + if (!success) { + MTR_LOG_INFO("Store failed in clearStoredAttributesForNodeID for nodeIndex (%lu)", static_cast(nodeIndexCopy.count)); + } + } }); } @@ -703,7 +802,10 @@ - (void)clearAllStoredAttributes [self _clearStoredAttributesForNodeID:nodeID]; } - [self _deleteNodeIndex]; + BOOL success = [self _deleteNodeIndex]; + if (!success) { + MTR_LOG_INFO("Delete failed for nodeIndex"); + } }); } diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m index 38795bcc8cef87..efa53d9dabcefb 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -1139,6 +1139,38 @@ - (void)test008_TestDataStoreDirect dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1003)]; XCTAssertEqual(dataStoreValues.count, 0); + // Test MTRDeviceControllerDataStore _pruneEmptyStoredAttributesBranches + // - Clear cache + // - Store an attribute + // - Manually delete it from the test storage delegate + // - Call _pruneEmptyStoredAttributesBranches + [controller.controllerDataStore clearAllStoredAttributes]; + + NSArray * testAttribute = @[ + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(1) attributeID:@(1)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(111) } }, + ]; + [controller.controllerDataStore storeAttributeValues:testAttribute forNodeID:@(2001)]; + + // store is async, so remove on the same queue to ensure order + dispatch_sync(_storageQueue, ^{ + NSString * testAttributeValueKey = [controller.controllerDataStore _attributeValueKeyForNodeID:@(2001) endpointID:@(1) clusterID:@(1) attributeID:@(1)]; + [storageDelegate controller:controller removeValueForKey:testAttributeValueKey securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared]; + }); + [controller.controllerDataStore unitTestPruneEmptyStoredAttributesBranches]; + + // Now check the indexes are pruned + NSString * testAttributeIndexKey = [controller.controllerDataStore _attributeIndexKeyForNodeID:@(2001) endpointID:@(1) clusterID:@(1)]; + id testAttributeIndex = [storageDelegate controller:controller valueForKey:testAttributeIndexKey securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared]; + XCTAssertNil(testAttributeIndex); + NSString * testClusterIndexKey = [controller.controllerDataStore _clusterIndexKeyForNodeID:@(2001) endpointID:@(1)]; + id testClusterIndex = [storageDelegate controller:controller valueForKey:testClusterIndexKey securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared]; + XCTAssertNil(testClusterIndex); + NSString * testEndpointIndexKey = [controller.controllerDataStore _endpointIndexKeyForNodeID:@(2001)]; + id testEndpointIndex = [storageDelegate controller:controller valueForKey:testEndpointIndexKey securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared]; + XCTAssertNil(testEndpointIndex); + id testNodeIndex = [storageDelegate controller:controller valueForKey:@"attrCacheNodeIndex" securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared]; + XCTAssertNil(testNodeIndex); + [controller shutdown]; XCTAssertFalse([controller isRunning]); } diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h index 5b77d0ec06aad7..02a711ee183470 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h @@ -28,6 +28,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NSNumber *)nodeID; - (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID; - (void)clearAllStoredAttributes; +- (void)unitTestPruneEmptyStoredAttributesBranches; +- (NSString *)_endpointIndexKeyForNodeID:(NSNumber *)nodeID; +- (NSString *)_clusterIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID; +- (NSString *)_attributeIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID; +- (NSString *)_attributeValueKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID; @end // Declare internal methods for testing From 9343d3a02cde8d3148f25d855d8d8447f61329b1 Mon Sep 17 00:00:00 2001 From: Anush Nadathur Date: Wed, 13 Mar 2024 14:19:35 -0700 Subject: [PATCH 50/76] Added macro variants with metric support matching the ones in CodeUtils (#32553) - Added Verify/Return/Log/Succ/Exit XYZ WithMetric macro variants for use in metrics - Added unit tests for these macro variants --- src/tracing/metric_macros.h | 376 ++++++++++++++++++++++++- src/tracing/tests/TestMetricEvents.cpp | 335 +++++++++++++++++++++- 2 files changed, 700 insertions(+), 11 deletions(-) diff --git a/src/tracing/metric_macros.h b/src/tracing/metric_macros.h index 4ff85f353701b3..26dde8cd3230a0 100644 --- a/src/tracing/metric_macros.h +++ b/src/tracing/metric_macros.h @@ -27,13 +27,258 @@ #if MATTER_TRACING_ENABLED /** - * @def SuccessOrExitWithMetric(kMetriKey, error) + * @def ReturnErrorOnFailureWithMetric(kMetricKey, expr) + * + * @brief + * This macros emits the specified metric with error code and returns the error code, + * if the expression returns an error. For a CHIP_ERROR expression, this means any value + * other than CHIP_NO_ERROR. For an integer expression, this means non-zero. + * + * Example usage: + * + * @code + * ReturnErrorOnFailureWithMetric(kMetricKey, channel->SendMsg(msg)); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * does not evaluate to CHIP_NO_ERROR. Value of the metric is to the + * result of the expression. + * @param[in] expr An expression to be tested. + */ +#define ReturnErrorOnFailureWithMetric(kMetricKey, expr) \ + do \ + { \ + auto __err = (expr); \ + if (!::chip::ChipError::IsSuccess(__err)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, __err); \ + return __err; \ + } \ + } while (false) + +/** + * @def ReturnLogErrorOnFailureWithMetric(kMetricKey, expr) + * + * @brief + * Returns the error code if the expression returns something different + * than CHIP_NO_ERROR. In addition, a metric is emitted with the specified metric key and + * error code as the value of the metric. + * + * Example usage: + * + * @code + * ReturnLogErrorOnFailureWithMetric(kMetricKey, channel->SendMsg(msg)); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * does not evaluate to CHIP_NO_ERROR. Value of the metric is to the + * result of the expression. + * @param[in] expr A scalar expression to be evaluated against CHIP_NO_ERROR. + */ +#define ReturnLogErrorOnFailureWithMetric(kMetricKey, expr) \ + do \ + { \ + CHIP_ERROR __err = (expr); \ + if (__err != CHIP_NO_ERROR) \ + { \ + MATTER_LOG_METRIC(kMetricKey, __err); \ + ChipLogError(NotSpecified, "%s at %s:%d", ErrorStr(__err), __FILE__, __LINE__); \ + return __err; \ + } \ + } while (false) + +/** + * @def ReturnOnFailureWithMetric(kMetricKey, expr) + * + * @brief + * Returns if the expression returns an error. For a CHIP_ERROR expression, this means any value other + * than CHIP_NO_ERROR. For an integer expression, this means non-zero. If the expression evaluates to + * anything but CHIP_NO_ERROR, a metric with the specified key is emitted along with error as the value. + * + * Example usage: + * + * @code + * ReturnOnFailureWithMetric(kMetricKey, channel->SendMsg(msg)); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * does not evaluate to CHIP_NO_ERROR. Value of the metric is to the + * result of the expression. + * @param[in] expr An expression to be tested. + */ +#define ReturnOnFailureWithMetric(kMetricKey, expr) \ + do \ + { \ + auto __err = (expr); \ + if (!::chip::ChipError::IsSuccess(__err)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, __err); \ + return; \ + } \ + } while (false) + +/** + * @def VerifyOrReturnWithMetric(kMetricKey, expr, ...) + * + * @brief + * Returns from the void function if expression evaluates to false. If the expression evaluates + * to false, a metric with the specified key is emitted. + * + * Example usage: + * + * @code + * VerifyOrReturnWithMetric(kMetricKey, param != nullptr, LogError("param is nullptr")); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * to false. Value of the metric is set to false. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] ... Statements to execute before returning. Optional. + */ +#define VerifyOrReturnWithMetric(kMetricKey, expr, ...) \ + do \ + { \ + if (!(expr)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, false); \ + __VA_ARGS__; \ + return; \ + } \ + } while (false) + +/** + * @def VerifyOrReturnErrorWithMetric(kMetricKey, expr, code, ...) + * + * @brief + * Returns a specified error code if expression evaluates to false. If the expression evaluates + * to false, a metric with the specified key is emitted with the value set to the code. + * + * Example usage: + * + * @code + * VerifyOrReturnErrorWithMetric(kMetricKey, param != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * to false. Value of the metric is to code. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] code A value to return if @a expr is false. + * @param[in] ... Statements to execute before returning. Optional. + */ +#define VerifyOrReturnErrorWithMetric(kMetricKey, expr, code, ...) \ + VerifyOrReturnValueWithMetric(kMetricKey, expr, code, ##__VA_ARGS__) + +/** + * @def VerifyOrReturnValueWithMetric(kMetricKey, expr, value, ...) + * + * @brief + * Returns a specified value if expression evaluates to false. If the expression evaluates + * to false, a metric with the specified key is emitted with the value set to value. + * + * Example usage: + * + * @code + * VerifyOrReturnValueWithMetric(kMetricKey, param != nullptr, Foo()); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * to false. Value of the metric is to value. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] value A value to return if @a expr is false. + * @param[in] ... Statements to execute before returning. Optional. + */ +#define VerifyOrReturnValueWithMetric(kMetricKey, expr, value, ...) \ + do \ + { \ + if (!(expr)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, value); \ + __VA_ARGS__; \ + return (value); \ + } \ + } while (false) + +/** + * @def VerifyOrReturnLogErrorWithMetric(kMetricKey, expr, code) + * + * @brief + * Returns and print a specified error code if expression evaluates to false. + * If the expression evaluates to false, a metric with the specified key is emitted + * with the value set to code. + * + * Example usage: + * + * @code + * VerifyOrReturnLogErrorWithMetric(kMetricKey, param != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * to false. Value of the metric is to code. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] code A value to return if @a expr is false. + */ +#if CHIP_CONFIG_ERROR_SOURCE +#define VerifyOrReturnLogErrorWithMetric(kMetricKey, expr, code) \ + do \ + { \ + if (!(expr)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, code); \ + ChipLogError(NotSpecified, "%s at %s:%d", ErrorStr(code), __FILE__, __LINE__); \ + return code; \ + } \ + } while (false) +#else // CHIP_CONFIG_ERROR_SOURCE +#define VerifyOrReturnLogErrorWithMetric(kMetricKey, expr, code) \ + do \ + { \ + if (!(expr)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, code); \ + ChipLogError(NotSpecified, "%s:%d false: %" CHIP_ERROR_FORMAT, #expr, __LINE__, code.Format()); \ + return code; \ + } \ + } while (false) +#endif // CHIP_CONFIG_ERROR_SOURCE + +/** + * @def ReturnErrorCodeWithMetricIf(kMetricKey, expr, code) + * + * @brief + * Returns a specified error code if expression evaluates to true + * If the expression evaluates to true, a metric with the specified key is emitted + * with the value set to code. + * + * Example usage: + * + * @code + * ReturnErrorCodeWithMetricIf(kMetricKey, state == kInitialized, CHIP_NO_ERROR); + * ReturnErrorCodeWithMetricIf(kMetricKey, state == kInitialized, CHIP_ERROR_INCORRECT_STATE); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * to true. Value of the metric is to code. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] code A value to return if @a expr is false. + */ +#define ReturnErrorCodeWithMetricIf(kMetricKey, expr, code) \ + do \ + { \ + if (expr) \ + { \ + MATTER_LOG_METRIC(kMetricKey, code); \ + return code; \ + } \ + } while (false) + +/** + * @def SuccessOrExitWithMetric(kMetricKey, error) * * @brief * This checks for the specified error, which is expected to * commonly be successful (CHIP_NO_ERROR), and branches to * the local label 'exit' if the error is not success. - * If error is not a success, a metric with key kMetriKey is emitted with + * If error is not a success, a metric with key kMetricKey is emitted with * the error code as the value of the metric. * * Example Usage: @@ -100,9 +345,110 @@ #define VerifyOrExitWithMetric(kMetricKey, aCondition, anAction) \ nlEXPECT_ACTION(aCondition, exit, MATTER_LOG_METRIC((kMetricKey), (anAction))) -/* - * Utility Macros to support optional arguments for MATTER_LOG_METRIC_XYZ macros +/** + * @def ExitNowWithMetric(kMetricKey, ...) + * + * @brief + * This unconditionally executes @a ... and branches to the local + * label 'exit'. In addition a metric is emitted with the specified key. + * + * @note The use of this interface implies neither success nor + * failure for the overall exit status of the enclosing function + * body. + * + * Example Usage: + * + * @code + * CHIP_ERROR ReadAll(Reader& reader) + * { + * CHIP_ERROR err; + * + * while (true) + * { + * err = reader.ReadNext(); + * if (err == CHIP_ERROR_AT_END) + * ExitNowWithMetric(kMetricKey, err = CHIP_NO_ERROR); + * SuccessOrExit(err); + * DoSomething(); + * } + * + * exit: + * return err; + * } + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted. + * @param[in] ... Statements to execute. Optional. */ +#define ExitNowWithMetric(kMetricKey, ...) \ + do \ + { \ + MATTER_LOG_METRIC(kMetricKey); \ + __VA_ARGS__; \ + goto exit; \ + } while (0) + +/** + * @def LogErrorOnFailureWithMetric(kMetricKey, expr) + * + * @brief + * Logs a message if the expression returns something different than CHIP_NO_ERROR. + * In addition, a metric is emitted with the specified key and value set to result + * of the expression in case it evaluates to anything other than CHIP_NO_ERROR. + * + * Example usage: + * + * @code + * LogErrorOnFailureWithMetric(kMetricKey, channel->SendMsg(msg)); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted if the expr evaluates + * does not evaluate to CHIP_NO_ERROR. Value of the metric is to the + * result of the expression. + * @param[in] expr A scalar expression to be evaluated against CHIP_NO_ERROR. + */ +#define LogErrorOnFailureWithMetric(kMetricKey, expr) \ + do \ + { \ + CHIP_ERROR __err = (expr); \ + if (__err != CHIP_NO_ERROR) \ + { \ + MATTER_LOG_METRIC(kMetricKey, __err); \ + ChipLogError(NotSpecified, "%s at %s:%d", ErrorStr(__err), __FILE__, __LINE__); \ + } \ + } while (false) + +/** + * @def VerifyOrDoWithMetric(kMetricKey, expr, ...) + * + * @brief + * Do something if expression evaluates to false. If the expression evaluates to false a metric + * with the specified key is emitted with value set to false. + * + * Example usage: + * + * @code + * VerifyOrDoWithMetric(param != nullptr, LogError("param is nullptr")); + * @endcode + * + * @param[in] kMetricKey Metric key for the metric event to be emitted. + * Value of the metric is set to false. + * @param[in] expr A Boolean expression to be evaluated. + * @param[in] ... Statements to execute. + */ +#define VerifyOrDoWithMetric(kMetricKey, expr, ...) \ + do \ + { \ + if (!(expr)) \ + { \ + MATTER_LOG_METRIC(kMetricKey, false); \ + __VA_ARGS__; \ + } \ + } while (false) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility Macros to support optional arguments for MATTER_LOG_METRIC_XYZ macros +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Utility to always return the 4th argument from macro parameters #define __GET_4TH_ARG(_a1, _a2, _a3, _a4, ...) _a4 @@ -222,10 +568,32 @@ // Remap Success, Return, and Verify macros to the ones without metrics //////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define ReturnErrorOnFailureWithMetric(kMetricKey, expr) ReturnErrorOnFailure(expr) + +#define ReturnLogErrorOnFailureWithMetric(kMetricKey, expr) ReturnLogErrorOnFailure(expr) + +#define ReturnOnFailureWithMetric(kMetricKey, expr) ReturnOnFailure(expr) + +#define VerifyOrReturnWithMetric(kMetricKey, expr, ...) VerifyOrReturn(expr, ##__VA_ARGS__) + +#define VerifyOrReturnErrorWithMetric(kMetricKey, expr, code, ...) VerifyOrReturnValue(expr, code, ##__VA_ARGS__) + +#define VerifyOrReturnValueWithMetric(kMetricKey, expr, value, ...) VerifyOrReturnValue(expr, value, ##__VA_ARGS__) + +#define VerifyOrReturnLogErrorWithMetric(kMetricKey, expr, code) VerifyOrReturnLogError(expr, code) + +#define ReturnErrorCodeWithMetricIf(kMetricKey, expr, code) ReturnErrorCodeIf(expr, code) + #define SuccessOrExitWithMetric(kMetricKey, aStatus) SuccessOrExit(aStatus) #define VerifyOrExitWithMetric(kMetricKey, aCondition, anAction) VerifyOrExit(aCondition, anAction) +#define ExitNowWithMetric(kMetricKey, ...) ExitNow(##__VA_ARGS__) + +#define LogErrorOnFailureWithMetric(kMetricKey, expr) LogErrorOnFailure(expr) + +#define VerifyOrDoWithMetric(kMetricKey, expr, ...) VerifyOrDo(expr, ##__VA_ARGS__) + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Map all MATTER_LOG_METRIC_XYZ macros to noops //////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/tracing/tests/TestMetricEvents.cpp b/src/tracing/tests/TestMetricEvents.cpp index c52a5fc91b9e93..72e159df7cf7fa 100644 --- a/src/tracing/tests/TestMetricEvents.cpp +++ b/src/tracing/tests/TestMetricEvents.cpp @@ -324,14 +324,335 @@ void TestSuccessOrExitWithMetric(nlTestSuite * inSuite, void * inContext) inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); } +static CHIP_ERROR InvokeReturnErrorOnFailureWithMetric(MetricKey key, const CHIP_ERROR & error) +{ + ReturnErrorOnFailureWithMetric(key, error); + return CHIP_NO_ERROR; +} + +void TestReturnErrorOnFailureWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + CHIP_ERROR err = InvokeReturnErrorOnFailureWithMetric("event0", CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeReturnErrorOnFailureWithMetric("event1", CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + + err = InvokeReturnErrorOnFailureWithMetric("event2", CHIP_ERROR_BAD_REQUEST); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_BAD_REQUEST); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_INCORRECT_STATE), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", CHIP_ERROR_BAD_REQUEST), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static CHIP_ERROR InvokeReturnLogErrorOnFailureWithMetric(MetricKey key, const CHIP_ERROR & error) +{ + ReturnLogErrorOnFailureWithMetric(key, error); + return CHIP_NO_ERROR; +} + +void TestReturnLogErrorOnFailureWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + CHIP_ERROR err = InvokeReturnLogErrorOnFailureWithMetric("event0", CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeReturnLogErrorOnFailureWithMetric("event1", CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + + err = InvokeReturnLogErrorOnFailureWithMetric("event2", CHIP_ERROR_BAD_REQUEST); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_BAD_REQUEST); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_INCORRECT_STATE), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", CHIP_ERROR_BAD_REQUEST), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static void InvokeReturnOnFailureWithMetric(MetricKey key, const CHIP_ERROR & error) +{ + ReturnOnFailureWithMetric(key, error); + return; +} + +void TestReturnOnFailureWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + InvokeReturnOnFailureWithMetric("event0", CHIP_NO_ERROR); + + InvokeReturnOnFailureWithMetric("event1", CHIP_ERROR_INCORRECT_STATE); + + InvokeReturnOnFailureWithMetric("event2", CHIP_ERROR_BAD_REQUEST); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_INCORRECT_STATE), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", CHIP_ERROR_BAD_REQUEST), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static void InvokeVerifyOrReturnWithMetric(MetricKey key, bool result) +{ + VerifyOrReturnWithMetric(key, result); + return; +} + +void TestInvokeVerifyOrReturnWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + InvokeVerifyOrReturnWithMetric("event0", DoubleOf(2) == 4); + InvokeVerifyOrReturnWithMetric("event1", DoubleOf(3) == 9); + InvokeVerifyOrReturnWithMetric("event2", DoubleOf(4) == 8); + InvokeVerifyOrReturnWithMetric("event3", DoubleOf(5) == 11); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", false), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", false), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static CHIP_ERROR InvokeVerifyOrReturnErrorWithMetric(MetricKey key, bool expr, const CHIP_ERROR & error) +{ + VerifyOrReturnErrorWithMetric(key, expr, error); + return CHIP_NO_ERROR; +} + +void TestVerifyOrReturnErrorWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + CHIP_ERROR err = InvokeVerifyOrReturnErrorWithMetric("event0", DoubleOf(2) == 4, CHIP_ERROR_BAD_REQUEST); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeVerifyOrReturnErrorWithMetric("event1", DoubleOf(3) == 9, CHIP_ERROR_ACCESS_DENIED); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_ACCESS_DENIED); + + err = InvokeVerifyOrReturnErrorWithMetric("event2", DoubleOf(4) == 8, CHIP_ERROR_BUSY); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeVerifyOrReturnErrorWithMetric("event3", DoubleOf(5) == 11, CHIP_ERROR_CANCELLED); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_CANCELLED); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_ACCESS_DENIED), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", CHIP_ERROR_CANCELLED), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +template +static return_code_type InvokeVerifyOrReturnValueWithMetric(MetricKey key, bool expr, return_code_type retval) +{ + VerifyOrReturnValueWithMetric(key, expr, retval); + return return_code_type(); +} + +void TestVerifyOrReturnValueWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + auto retval = InvokeVerifyOrReturnValueWithMetric("event0", DoubleOf(2) == 4, 0); + NL_TEST_ASSERT(inSuite, retval == 0); + + auto err = InvokeVerifyOrReturnValueWithMetric("event1", DoubleOf(3) == 9, CHIP_ERROR_ACCESS_DENIED); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_ACCESS_DENIED); + + err = InvokeVerifyOrReturnValueWithMetric("event2", DoubleOf(4) == 8, CHIP_ERROR_BAD_REQUEST); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + retval = InvokeVerifyOrReturnValueWithMetric("event3", DoubleOf(5) == 11, 16); + NL_TEST_ASSERT(inSuite, retval == 16); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_ACCESS_DENIED), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", 16), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +static CHIP_ERROR InvokeVerifyOrReturnLogErrorWithMetric(MetricKey key, bool expr, const CHIP_ERROR & error) +{ + VerifyOrReturnLogErrorWithMetric(key, expr, error); + return CHIP_NO_ERROR; +} + +void TestVerifyOrReturnLogErrorWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + auto err = InvokeVerifyOrReturnLogErrorWithMetric("event0", DoubleOf(2) == 4, CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeVerifyOrReturnLogErrorWithMetric("event1", DoubleOf(3) == 9, CHIP_ERROR_CANCELLED); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_CANCELLED); + + err = InvokeVerifyOrReturnLogErrorWithMetric("event2", DoubleOf(4) == 8, CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = InvokeVerifyOrReturnLogErrorWithMetric("event3", DoubleOf(5) == 11, CHIP_ERROR_BAD_REQUEST); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_BAD_REQUEST); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event1", CHIP_ERROR_CANCELLED), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", CHIP_ERROR_BAD_REQUEST), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +template +static return_code_type InvokeReturnErrorCodeWithMetricIf(MetricKey key, bool expr, const return_code_type & code) +{ + ReturnErrorCodeWithMetricIf(key, expr, code); + return return_code_type(); +} + +void TestReturnErrorCodeWithMetricIf(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + auto err = InvokeReturnErrorCodeWithMetricIf("event0", DoubleOf(2) == 4, CHIP_ERROR_DUPLICATE_KEY_ID); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_DUPLICATE_KEY_ID); + + auto retval = InvokeReturnErrorCodeWithMetricIf("event1", DoubleOf(3) == 9, 11); + NL_TEST_ASSERT(inSuite, retval == 0); + + retval = InvokeReturnErrorCodeWithMetricIf("event2", DoubleOf(4) == 8, 22); + NL_TEST_ASSERT(inSuite, retval == 22); + + err = InvokeReturnErrorCodeWithMetricIf("event3", DoubleOf(5) == 11, CHIP_ERROR_ACCESS_DENIED); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event0", CHIP_ERROR_DUPLICATE_KEY_ID), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", 22), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +void TestExitNowWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + chip::ChipError err = CHIP_NO_ERROR; + + ExitNowWithMetric("event0", err = CHIP_ERROR_BUSY); + +exit: + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event0"), + }; + + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_BUSY); + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +void TestLogErrorOnFailureWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + LogErrorOnFailureWithMetric("event0", CHIP_ERROR_BAD_REQUEST); + LogErrorOnFailureWithMetric("event1", CHIP_NO_ERROR); + LogErrorOnFailureWithMetric("event2", CHIP_ERROR_DATA_NOT_ALIGNED); + LogErrorOnFailureWithMetric("event3", CHIP_ERROR_BUSY); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event0", CHIP_ERROR_BAD_REQUEST), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", CHIP_ERROR_DATA_NOT_ALIGNED), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", CHIP_ERROR_BUSY), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + +void TestVerifyOrDoWithMetric(nlTestSuite * inSuite, void * inContext) +{ + MetricEventBackend backend; + ScopedRegistration scope(backend); + + VerifyOrDoWithMetric("event0", DoubleOf(2) == 5); + VerifyOrDoWithMetric("event1", DoubleOf(3) == 6); + VerifyOrDoWithMetric("event2", DoubleOf(4) == 7, MATTER_LOG_METRIC("event4", 10)); + VerifyOrDoWithMetric("event3", DoubleOf(5) == 8); + VerifyOrDoWithMetric("event5", DoubleOf(6) == 12); + + std::vector expected = { + MetricEvent(MetricEvent::Type::kInstantEvent, "event0", false), + MetricEvent(MetricEvent::Type::kInstantEvent, "event2", false), + MetricEvent(MetricEvent::Type::kInstantEvent, "event4", 10), + MetricEvent(MetricEvent::Type::kInstantEvent, "event3", false), + }; + + NL_TEST_ASSERT(inSuite, backend.GetMetricEvents().size() == expected.size()); + NL_TEST_ASSERT( + inSuite, std::equal(backend.GetMetricEvents().begin(), backend.GetMetricEvents().end(), expected.begin(), expected.end())); +} + static const nlTest sMetricTests[] = { - NL_TEST_DEF("BasicMetricEvent", TestBasicMetricEvent), // - NL_TEST_DEF("InstantMetricEvent", TestInstantMetricEvent), // - NL_TEST_DEF("BeginEndMetricEvent", TestBeginEndMetricEvent), // - NL_TEST_DEF("ScopedMetricEvent", TestScopedMetricEvent), // - NL_TEST_DEF("VerifyOrExitWithMetric", TestVerifyOrExitWithMetric), // - NL_TEST_DEF("SuccessOrExitWithMetric", TestSuccessOrExitWithMetric), // - NL_TEST_SENTINEL() // + NL_TEST_DEF("BasicMetricEvent", TestBasicMetricEvent), // + NL_TEST_DEF("InstantMetricEvent", TestInstantMetricEvent), // + NL_TEST_DEF("BeginEndMetricEvent", TestBeginEndMetricEvent), // + NL_TEST_DEF("ScopedMetricEvent", TestScopedMetricEvent), // + NL_TEST_DEF("VerifyOrExitWithMetric", TestVerifyOrExitWithMetric), // + NL_TEST_DEF("SuccessOrExitWithMetric", TestSuccessOrExitWithMetric), // + NL_TEST_DEF("ReturnErrorOnFailureWithMetric", TestReturnErrorOnFailureWithMetric), // + NL_TEST_DEF("ReturnLogErrorOnFailureWithMetric", TestReturnLogErrorOnFailureWithMetric), // + NL_TEST_DEF("ReturnOnFailureWithMetric", TestReturnOnFailureWithMetric), // + NL_TEST_DEF("VerifyOrReturnWithMetric", TestInvokeVerifyOrReturnWithMetric), // + NL_TEST_DEF("VerifyOrReturnErrorWithMetric", TestVerifyOrReturnErrorWithMetric), // + NL_TEST_DEF("VerifyOrReturnValueWithMetric", TestVerifyOrReturnValueWithMetric), // + NL_TEST_DEF("VerifyOrReturnLogErrorWithMetric", TestVerifyOrReturnLogErrorWithMetric), // + NL_TEST_DEF("ReturnErrorCodeWithMetricIf", TestReturnErrorCodeWithMetricIf), // + NL_TEST_DEF("ExitNowWithMetric", TestExitNowWithMetric), // + NL_TEST_DEF("LogErrorOnFailureWithMetric", TestLogErrorOnFailureWithMetric), // + NL_TEST_DEF("VerifyOrDoWithMetric", TestVerifyOrDoWithMetric), // + NL_TEST_SENTINEL() // }; } // namespace From eecc2a6dbba7bcb8b00cdab6e5547d097190bdcc Mon Sep 17 00:00:00 2001 From: Justin Wood Date: Wed, 13 Mar 2024 15:06:38 -0700 Subject: [PATCH 51/76] More mistakes in documentation (#32563) * Initial commit * Restyled by whitespace * Restyled by clang-format * Update src/darwin/Framework/CHIP/MTRDevice.mm Co-authored-by: Anush Nadathur * Update src/darwin/Framework/CHIP/MTRDevice.h Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Adding header * Restyled by clang-format * Updating docs * Fixing missing language --------- Co-authored-by: Restyled.io Co-authored-by: Anush Nadathur Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> --- src/darwin/Framework/CHIP/MTRDevice.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 3bb39e982bcf98..989ed44f7667be 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -70,8 +70,8 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) * This will be true after the deviceCachePrimed: delegate callback has been called, false if not. * * Please note if you have a storage delegate implemented, the cache is then stored persistently, so - * the would then only be called once, ever - and this property would basically always be true - * if a subscription has ever been established. + * the delegate would then only be called once, ever - and this property would basically always be true + * if a subscription has ever been established at any point in the past. * */ @property (readonly) BOOL deviceCachePrimed MTR_NEWLY_AVAILABLE; From c90481f6a5356cbddaae2d461db1fb3982432dd4 Mon Sep 17 00:00:00 2001 From: Raul Marquez <130402456+raul-marquez-csa@users.noreply.github.com> Date: Wed, 13 Mar 2024 21:31:34 -0700 Subject: [PATCH 52/76] Matter SDK Doctor (#32554) * Adds Matter SDK Doctor * Fix restyle * Fix restyle * Fix restyle * Fix restyle --- scripts/sdk-doctor/_network.sh | 80 ++++++++++++++++ scripts/sdk-doctor/_os.sh | 69 ++++++++++++++ scripts/sdk-doctor/_repo.sh | 152 ++++++++++++++++++++++++++++++ scripts/sdk-doctor/_version.sh | 27 ++++++ scripts/sdk-doctor/sdk-doctor.sh | 153 +++++++++++++++++++++++++++++++ 5 files changed, 481 insertions(+) create mode 100755 scripts/sdk-doctor/_network.sh create mode 100755 scripts/sdk-doctor/_os.sh create mode 100755 scripts/sdk-doctor/_repo.sh create mode 100755 scripts/sdk-doctor/_version.sh create mode 100755 scripts/sdk-doctor/sdk-doctor.sh diff --git a/scripts/sdk-doctor/_network.sh b/scripts/sdk-doctor/_network.sh new file mode 100755 index 00000000000000..940161ca5c7cea --- /dev/null +++ b/scripts/sdk-doctor/_network.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# +# Copyright (c) 2024 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. + +# Function to display information for each network interface +display_interface_info() { + interface=$1 + echo "Interface: $interface" + + # Check if interface is up + if ip link show "$interface" | grep -q 'state UP'; then + echo " Status: UP" + else + echo " Status: DOWN" + fi + + # Get and display the IPv4 address + ipv4_address=$(ip -4 addr show "$interface" | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + [ -z "$ipv4_address" ] && ipv4_address="N/A" + echo " IPv4 Address: $ipv4_address" + + # Get and display the IPv6 address + ipv6_address=$(ip -6 addr show "$interface" | grep -oP '(?<=inet6\s)[a-f0-9:]+') + [ -z "$ipv6_address" ] && ipv6_address="N/A" + echo " IPv6 Address: $ipv6_address" + + # Get and display the subnet mask + subnet_mask=$(ifconfig "$interface" | grep -oP '(?<=Mask:)[0-9.]+') + [ -z "$subnet_mask" ] && subnet_mask="N/A" + echo " Subnet Mask: $subnet_mask" + + # Get and display the MAC address + mac_address=$(ip link show "$interface" | grep -oP '(?<=ether\s)[a-f0-9:]+') + [ -z "$mac_address" ] && mac_address="N/A" + echo " MAC Address: $mac_address" +} + +# Get a list of all network interfaces +interfaces=$(ip link show | grep -oP '(?<=^\d: )[e-w]+[0-9a-zA-Z-]+') + +# Iterate over each interface and display relevant information +for intf in "$interfaces"; do + display_interface_info "$intf" + echo "" +done + +# Get and display the default gateway +default_gateway=$(ip route | grep default | awk '{print $3}') +[ -z "$default_gateway" ] && default_gateway="N/A" +echo "Default Gateway: $default_gateway" + +# Get and display the DNS server information +mapfile -t dns_servers < <(grep nameserver /etc/resolv.conf | awk '{print $2}') +if [ ${#dns_servers[@]} -eq 0 ]; then + echo "DNS Servers: N/A" +else + echo "DNS Servers: ${dns_servers[*]}" +fi +echo + +# Check if Internet is available +echo "Checking Internet availability..." +if ping -c 1 8.8.8.8 &>/dev/null; then + echo "Internet is available" +else + echo "Internet is not available" +fi diff --git a/scripts/sdk-doctor/_os.sh b/scripts/sdk-doctor/_os.sh new file mode 100755 index 00000000000000..e22da32e743cfa --- /dev/null +++ b/scripts/sdk-doctor/_os.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# +# Copyright (c) 2024 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. + +ROOT_DIR=$(realpath "$(dirname "$0")"/../..) +cd "$ROOT_DIR" + +# Function to display OS information +get_os_info() { + if [ -f /etc/os-release ]; then + # If available, use /etc/os-release file + . /etc/os-release + echo "Name: $NAME" + echo "Version: $VERSION" + echo "ID: $ID" + echo "ID Like: $ID_LIKE" + elif [ -f /etc/*-release ]; then + # If /etc/os-release is not available, use other available /etc/*-release file + echo "OS Information from /etc/*-release:" + cat /etc/*-release + else + # Print a message if unable to determine OS information + echo "Cannot determine OS information." + fi +} + +# Function to display kernel information +get_kernel_info() { + echo "Kernel Information:" + uname -a | fold -w 88 -s +} + +# Function to display CPU information +get_cpu_info() { + echo "CPU Information:" + lscpu | grep -E "^Architecture:|^CPU op-mode\(s\):|^CPU\(s\):|^Vendor ID:|^Model name:|^CPU max MHz:|^CPU min MHz:" | + sed 's/^[ \t]*//;s/[ \t]*$//' +} + +# Function to display memory information +get_memory_info() { + echo "Memory Information:" + free -h +} + +# Call the functions to display the information +get_os_info +echo + +get_kernel_info +echo + +get_cpu_info +echo + +get_memory_info diff --git a/scripts/sdk-doctor/_repo.sh b/scripts/sdk-doctor/_repo.sh new file mode 100755 index 00000000000000..de619387274117 --- /dev/null +++ b/scripts/sdk-doctor/_repo.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +# +# Copyright (c) 2024 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. + +ROOT_DIR=$(realpath "$(dirname "$0")"/../..) +cd "$ROOT_DIR" + +get_repo_and_branch_info() { + # Input validation + if [ -z "$1" ]; then + echo "Please provide a path." + return 1 + fi + + path="$1" + repo_friendly_name="Matter SDK" + + if [ "$path" != "." ]; then + title_case_path=$(echo "$path" | awk '{ for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2)); }1') + repo_friendly_name=$title_case_path + fi + + # Check if the directory exists + if [ ! -d "$path" ]; then + echo "Directory '$path' does not exist." + return 1 + fi + + cd "$path" + + # Get the URL of the remote origin + remote_url=$(git config --get remote.origin.url) + + if [ -n "$remote_url" ]; then + # Extract the repository name from the URL + repo_name=$(basename -s .git "$remote_url") + + # Calculate the necessary padding to align the end pipe + total_length=95 # Adjust this based on your frame width + text_length=${#repo_friendly_name}+${#repo_name}+4 # 4 for the ": " and two spaces around the repo name + padding_length=$((total_length - text_length)) + + echo '+-----------------------------------------------------------------------------------------------+' + printf "| %s: %s%*s|\n" "$repo_friendly_name" "$repo_name" "$padding_length" "" + echo '+-----------------------------------------------------------------------------------------------+' + else + # Print error message if there is no remote URL + echo "Not a Git repository or no remote set" + return 1 + fi + + # Get the current branch and its tracking branch + git_status=$(git status) + tracking_branch_info=$(echo "$git_status" | grep "Your branch is up to date with") + + # Extract the fork owner and branch from the tracking branch info + if [[ $tracking_branch_info =~ Your\ branch\ is\ up\ to\ date\ with\ \'([^\']+)\' ]]; then + fork_owner_and_branch="${BASH_REMATCH[1]}" + else + fork_owner_and_branch="Not set or not a tracking branch" + fi + + # Get the commit SHA of the current HEAD + commit_sha=$(git rev-parse HEAD) + echo "Commit SHA: $commit_sha" + + # Get the commit message of the current HEAD + commit_message=$(git log -1 --pretty=format:"%B") + trimmed_commit_message=$(trim_commit_message "$commit_message") + echo "Commit Message: $trimmed_commit_message" + + # Get the commit author of the current HEAD + commit_author=$(git log -1 --pretty=format:"%an") + echo "Commit Author: $commit_author" + + # Get the commit date and time of the current HEAD including timezone + commit_datetime=$(git log -1 --pretty=format:"%cd" --date=format:"%Y-%m-%d %H:%M:%S %z") + echo "Commit Date: $commit_datetime" + + # Attempt to find branches that contain this commit + branches=$(git branch --contains "$commit_sha" | sed 's/^/ /') + + if [ -n "$branches" ]; then + echo "Contained in branches:" + echo "$branches" + else + echo "This commit is not on any known branch." + fi + + echo " Tracking: $fork_owner_and_branch" + + echo + + # Navigate back to the original directory + cd "$ROOT_DIR" +} + +trim_commit_message() { + local commit_message="$1" + + # Check if the commit message contains a newline character + if [[ "$commit_message" == *$'\n'* ]]; then + # Extract the first line of the commit message + local first_line="${commit_message%%$'\n'*}" + else + # If there's no newline, use the entire commit message + local first_line="$commit_message" + fi + + # Trim leading and trailing whitespace from the first line and echo it + echo "$first_line" | sed 's/^[ \t]*//;s/[ \t]*$//' +} + +# Print SDK root git status +get_repo_and_branch_info "." + +# Handle arguments +case "$1" in + --git-sub) + # Initialize an array to hold the directories + declare -a repo_dirs + + cd "$ROOT_DIR" + + # Find directories containing a .github folder and store them in the array, excluding the current directory + while IFS= read -r dir; do + # Check if the directory is not the current directory + if [[ "$dir" != "." ]]; then + repo_dirs+=("$dir") + fi + done < <(find . -type d -name .github | awk -F'/[^/]*$' '{print $1}') + + # Iterate through the directories and call the function for each + for dir in "${repo_dirs[@]}"; do + get_repo_and_branch_info "$dir" + done + ;; + *) ;; +esac diff --git a/scripts/sdk-doctor/_version.sh b/scripts/sdk-doctor/_version.sh new file mode 100755 index 00000000000000..c83f1804b7d46b --- /dev/null +++ b/scripts/sdk-doctor/_version.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# +# Copyright (c) 2024 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. + +ROOT_DIR=$(realpath "$(dirname "$0")"/../..) +cd "$ROOT_DIR" + +SPEC_VERSION=$(head -n 1 "$ROOT_DIR"/SPECIFICATION_VERSION) +SPEC_SHA=$(head -n 1 "$ROOT_DIR"/data_model/spec_sha) +SCRAPPER_VERSION=$(head -n 1 "$ROOT_DIR"/data_model/scraper_version) + +echo 'SPEC VERSION:' "$SPEC_VERSION" +echo 'SPEC SHA:' "$SPEC_SHA" +echo 'SCRAPER VERSION:' "$SCRAPPER_VERSION" diff --git a/scripts/sdk-doctor/sdk-doctor.sh b/scripts/sdk-doctor/sdk-doctor.sh new file mode 100755 index 00000000000000..597ec81c67fc58 --- /dev/null +++ b/scripts/sdk-doctor/sdk-doctor.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# +# Copyright (c) 2024 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. + +{ + echo " __ __ _ _ _____ _____ _ __ _____ _ " + echo " | \/ | | | | | / ____| __ \| |/ / | __ \ | | " + echo " | \ / | __ _| |_| |_ ___ _ __ | (___ | | | | ' / | | | | ___ ___| |_ ___ _ __ " + echo ' | |\/| |/ _` | __| __/ _ \ `__| \___ \| | | | < | | | |/ _ \ / __| __/ _ \| `__|' + echo " | | | | (_| | |_| || __/ | ____) | |__| | . \ | |__| | (_) | (__| || (_) | | " + echo " |_| |_|\__,_|\__|\__\___|_| |_____/|_____/|_|\_\ |_____/ \___/ \___|\__\___/|_| " + echo " " + + usage() { + echo "Displays Matter SDK setup information." + echo + echo "Usage:" + echo + echo " Show Matter SDK Version, SHA, repository details: $0" + echo " Show Matter SDK git submodule information: $0 --git-sub" + echo " Show Matter SDK host OS system information: $0 --system" + echo " Show all available Matter SDK details: $0 --complete" + echo + } + + width=104 + + # Get the date string + date_string=$(date) + + # Calculate the length of the date string + date_length=${#date_string} + + # Calculate the padding on each side + padding=$(((width - date_length) / 2)) + + # Print spaces for left padding + printf "%${padding}s" + + # Print the date string + echo "$date_string" + echo + + ROOT_DIR=$(realpath "$(dirname "$0")"/../..) + TH_DEV_SCRIPTS_DIR=$ROOT_DIR/scripts/sdk-doctor + cd "$ROOT_DIR" + + # Check for arguments + if [ "$#" -gt 1 ]; then + echo "Error: Too many arguments." + usage + exit 1 + fi + + print_framed_text() { + # Get the text and title from function arguments + input_text="$1" + title="$2" + + # Ensure 'width' is set and has a reasonable value + if [ -z "$width" ] || [ "$width" -lt 10 ]; then + echo "Error: 'width' is not set or too small." + return 1 + fi + + max_line_length=$((width - 6)) # Maximum characters in a line before wrapping + + # Word-wrap the input text + input_text_wrapped=$(echo -e "$input_text" | fold -w "$max_line_length" -s) + + # Calculate height based on the number of lines in the input text + height=$(echo -e "$input_text_wrapped" | wc -l) + height=$((height + 4)) # Add 4 to account for the top and bottom frame borders and inner padding + + # Print the top border with title + title_with_padding=" $title " + title_padding_left=$(((width - 2 - ${#title_with_padding}) / 2)) + [ "$title_padding_left" -lt 0 ] && title_padding_left=0 + title_padding_right=$((width - 2 - ${#title_with_padding} - title_padding_left)) + [ "$title_padding_right" -lt 0 ] && title_padding_right=0 + echo '+'"$(printf "%0.s-" "$(seq 1 "$title_padding_left")")$title_with_padding""$(printf "%0.s-" "$(seq 1 "$title_padding_right")")"'+' + + # Inner top padding + echo "|$(printf ' %.0s' "$(seq 1 $((width - 2)))")|" + + # Print each line of wrapped input text with frame borders and padding + echo -e "$input_text_wrapped" | while IFS= read -r line; do + padding_right=$((width - 4 - ${#line} - 2)) # Subtract 4 for the borders and 2 for the left padding + [ "$padding_right" -lt 0 ] && padding_right=0 + echo "| $line$(printf ' %.0s' "$(seq 1 "$padding_right")") |" + done + + # Inner bottom padding + echo "|$(printf ' %.0s' "$(seq 1 $((width - 2)))")|" + + # Print the bottom border + echo '+'"$(printf "%0.s-" "$(seq 1 $((width - 2)))")"'+' + echo + } + + show_system() { + # OS + os_output=$("$TH_DEV_SCRIPTS_DIR/_os.sh") + print_framed_text "$os_output" "OS" + + # Network + network_output=$("$TH_DEV_SCRIPTS_DIR/_network.sh") + print_framed_text "$network_output" "Network" + } + + # Matter SDK Version + th_version_output=$("$TH_DEV_SCRIPTS_DIR/_version.sh") + print_framed_text "$th_version_output" "Matter SDK Version" + + # Git Status + if [[ "$1" = "--git-sub" ]] || [[ "$1" = "--complete" ]]; then + repository_branches_output=$("$TH_DEV_SCRIPTS_DIR/_repo.sh" "--git-sub") + print_framed_text "$repository_branches_output" "Git Status" + else + repository_branches_output=$("$TH_DEV_SCRIPTS_DIR/_repo.sh" "--git") + print_framed_text "$repository_branches_output" "Git Status" + fi + + # Handle arguments + case "$1" in + --system) + show_system + usage + ;; + --complete) + show_system + usage + ;; + *) + usage + exit 1 + ;; + esac + +} 2>&1 | tee sdk-doctor.txt From cdfca0f1c5ee43bad4c57aa53ee70b7129043206 Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:07:57 +1300 Subject: [PATCH 53/76] Work around TSAN ASLR issue in CI (#32567) * Work around TSAN ASLR issue * Simplify action condition --------- Co-authored-by: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> --- .../action.yaml | 7 ++ .../actions/maximize-runner-disk/action.yaml | 72 +++++++++---------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.github/actions/checkout-submodules-and-bootstrap/action.yaml b/.github/actions/checkout-submodules-and-bootstrap/action.yaml index b514b8dd795509..f88be1eded25e6 100644 --- a/.github/actions/checkout-submodules-and-bootstrap/action.yaml +++ b/.github/actions/checkout-submodules-and-bootstrap/action.yaml @@ -41,3 +41,10 @@ runs: uses: ./.github/actions/upload-bootstrap-logs with: bootstrap-log-name: ${{ inputs.bootstrap-log-name }} + - name: Work around TSAN ASLR issues + if: runner.os == 'Linux' && !env.ACT + shell: bash + run: | + # See https://stackoverflow.com/a/77856955/2365113 + if [[ "$UID" == 0 ]]; then function sudo() { "$@"; }; fi + sudo sysctl vm.mmap_rnd_bits=28 diff --git a/.github/actions/maximize-runner-disk/action.yaml b/.github/actions/maximize-runner-disk/action.yaml index fe5f95352aa53e..d71ba3646d3279 100644 --- a/.github/actions/maximize-runner-disk/action.yaml +++ b/.github/actions/maximize-runner-disk/action.yaml @@ -4,47 +4,45 @@ runs: using: "composite" steps: - name: Free up disk space on the github runner - if: ${{ !env.ACT }} + if: runner.os == 'Linux' && !env.ACT shell: bash run: | # maximize-runner-disk - if [[ "$RUNNER_OS" == Linux ]]; then - # Directories to prune to free up space. Candidates: - # 1.6G /usr/share/dotnet - # 1.1G /usr/local/lib/android/sdk/platforms - # 1000M /usr/local/lib/android/sdk/build-tools - # 8.9G /usr/local/lib/android/sdk - # This list can be amended later to change the trade-off between the amount of - # disk space freed up, and how long it takes to do so (deleting many files is slow). - prune=(/usr/share/dotnet /usr/local/lib/android/sdk/platforms /usr/local/lib/android/sdk/build-tools) + # Directories to prune to free up space. Candidates: + # 1.6G /usr/share/dotnet + # 1.1G /usr/local/lib/android/sdk/platforms + # 1000M /usr/local/lib/android/sdk/build-tools + # 8.9G /usr/local/lib/android/sdk + # This list can be amended later to change the trade-off between the amount of + # disk space freed up, and how long it takes to do so (deleting many files is slow). + prune=(/usr/share/dotnet /usr/local/lib/android/sdk/platforms /usr/local/lib/android/sdk/build-tools) - if [[ "$UID" -eq 0 && -d /__w ]]; then - root=/runner-root-volume - if [[ ! -d "$root" ]]; then - echo "Unable to maximize disk space, job is running inside a container and $root is not mounted" - exit 0 - fi - function sudo() { "$@"; } # we're already root (and sudo is probably unavailable) - elif [[ "$UID" -ne 0 && "$RUNNER_ENVIRONMENT" == github-hosted ]]; then - root= - else - echo "Unable to maximize disk space, unknown runner environment" + if [[ "$UID" -eq 0 && -d /__w ]]; then + root=/runner-root-volume + if [[ ! -d "$root" ]]; then + echo "Unable to maximize disk space, job is running inside a container and $root is not mounted" exit 0 fi - - echo "Freeing up runner disk space on ${root:-/}" - function avail() { df -k --output=avail "${root:-/}" | grep '^[0-9]*$'; } - function now() { date '+%s'; } - before="$(avail)" start="$(now)" - for dir in "${prune[@]}"; do - if [[ -d "${root}${dir}" ]]; then - echo "- $dir" - # du -sh -- "${root}${dir}" - sudo rm -rf -- "${root}${dir}" - else - echo "- $dir (not found)" - fi - done - after="$(avail)" end="$(now)" - echo "Done, freed up $(( (after - before) / 1024 ))M of disk space in $(( end - start )) seconds." + function sudo() { "$@"; } # we're already root (and sudo is probably unavailable) + elif [[ "$UID" -ne 0 && "$RUNNER_ENVIRONMENT" == github-hosted ]]; then + root= + else + echo "Unable to maximize disk space, unknown runner environment" + exit 0 fi + + echo "Freeing up runner disk space on ${root:-/}" + function avail() { df -k --output=avail "${root:-/}" | grep '^[0-9]*$'; } + function now() { date '+%s'; } + before="$(avail)" start="$(now)" + for dir in "${prune[@]}"; do + if [[ -d "${root}${dir}" ]]; then + echo "- $dir" + # du -sh -- "${root}${dir}" + sudo rm -rf -- "${root}${dir}" + else + echo "- $dir (not found)" + fi + done + after="$(avail)" end="$(now)" + echo "Done, freed up $(( (after - before) / 1024 ))M of disk space in $(( end - start )) seconds." From 84970a085422d6f8088f34053cc88d06add8b0ca Mon Sep 17 00:00:00 2001 From: Terence Hampson Date: Thu, 14 Mar 2024 16:43:21 -0400 Subject: [PATCH 54/76] Fix compiler error on tip of tree (#32575) * Fix compiler error on tip of tree * Address PR comment --- src/controller/AbstractDnssdDiscoveryController.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controller/AbstractDnssdDiscoveryController.cpp b/src/controller/AbstractDnssdDiscoveryController.cpp index 2443c5fe0b1ac9..4d916e068ddbe6 100644 --- a/src/controller/AbstractDnssdDiscoveryController.cpp +++ b/src/controller/AbstractDnssdDiscoveryController.cpp @@ -34,9 +34,11 @@ void AbstractDnssdDiscoveryController::OnNodeDiscovered(const chip::Dnssd::Disco { continue; } + // TODO(#32576) Check if IP address are the same. Must account for `numIPs` in the list of `ipAddress`. + // Additionally, must NOT assume that the ordering is consistent. if (strcmp(discoveredNode.resolutionData.hostName, nodeData.resolutionData.hostName) == 0 && discoveredNode.resolutionData.port == nodeData.resolutionData.port && - discoveredNode.resolutionData.ipAddress == nodeData.resolutionData.ipAddress) + discoveredNode.resolutionData.numIPs == nodeData.resolutionData.numIPs) { discoveredNode = nodeData; if (mDeviceDiscoveryDelegate != nullptr) From 0aa09e0d99ffee39a72990250a22ee1153089f0d Mon Sep 17 00:00:00 2001 From: Justin Wood Date: Thu, 14 Mar 2024 16:56:29 -0700 Subject: [PATCH 55/76] Make MTRDevice mark itself as unreachable after 10s of waiting for a subscription (#32579) * First commit * Restyled by whitespace * Restyled by clang-format --------- Co-authored-by: Restyled.io --- src/darwin/Framework/CHIP/MTRDevice.mm | 45 +++++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 4c6b4a21e828d9..a83528b74d53e8 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -51,6 +51,8 @@ NSString * const MTRPreviousDataKey = @"previousData"; NSString * const MTRDataVersionKey = @"dataVersion"; +#define kTimeToWaitBeforeMarkingUnreachableAfterSettingUpSubscription 10 + // Consider moving utility classes to their own file #pragma mark - Utility Classes // This class is for storing weak references in a container @@ -125,6 +127,12 @@ - (id)strongObject } // anonymous namespace #pragma mark - MTRDevice +typedef NS_ENUM(NSUInteger, MTRInternalDeviceState) { + MTRInternalDeviceStateUnsubscribed = 0, + MTRInternalDeviceStateSubscribing = 1, + MTRInternalDeviceStateSubscribed = 2 +}; + typedef NS_ENUM(NSUInteger, MTRDeviceExpectedValueFieldIndex) { MTRDeviceExpectedValueFieldExpirationTimeIndex = 0, MTRDeviceExpectedValueFieldValueIndex = 1, @@ -157,18 +165,10 @@ @interface MTRDevice () @property (nonatomic) BOOL receivingPrimingReport; // TODO: instead of all the BOOL properties that are some facet of the state, move to internal state machine that has (at least): -// Unsubscribed (not attemping) -// Attempting subscription -// Subscribed (gotten subscription response / in steady state with no OnError/OnDone) // Actively receiving report // Actively receiving priming report -/** - * If subscriptionActive is true that means that either we are in the middle of - * trying to get a CASE session for the publisher or we have a live ReadClient - * right now (possibly with a lost subscription and trying to re-subscribe). - */ -@property (nonatomic) BOOL subscriptionActive; +@property (nonatomic) MTRInternalDeviceState internalDeviceState; #define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MIN_WAIT_SECONDS (1) #define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS (3600) @@ -640,6 +640,7 @@ - (void)_handleSubscriptionEstablished // reset subscription attempt wait time when subscription succeeds _lastSubscriptionAttemptWait = 0; + _internalDeviceState = MTRInternalDeviceStateSubscribed; // As subscription is established, check if the delegate needs to be informed if (!_delegateDeviceCachePrimedCalled) { @@ -663,7 +664,7 @@ - (void)_handleSubscriptionError:(NSError *)error { os_unfair_lock_lock(&self->_lock); - _subscriptionActive = NO; + _internalDeviceState = MTRInternalDeviceStateUnsubscribed; _unreportedEvents = nil; [self _changeState:MTRDeviceStateUnreachable]; @@ -757,6 +758,17 @@ - (void)_handleUnsolicitedMessageFromPublisher os_unfair_lock_unlock(&self->_lock); } +- (void)_markDeviceAsUnreachableIfNotSusbcribed +{ + os_unfair_lock_assert_owner(&self->_lock); + + if (_internalDeviceState >= MTRInternalDeviceStateSubscribed) + return; + + MTR_LOG_DEFAULT("%@ still not subscribed, marking the device as unreachable", self); + [self _changeState:MTRDeviceStateUnreachable]; +} + - (void)_handleReportBegin { os_unfair_lock_lock(&self->_lock); @@ -991,11 +1003,20 @@ - (void)_setupSubscription #endif // for now just subscribe once - if (_subscriptionActive) { + if (_internalDeviceState > MTRInternalDeviceStateUnsubscribed) { return; } - _subscriptionActive = YES; + _internalDeviceState = MTRInternalDeviceStateSubscribing; + + // Set up a timer to mark as not reachable if it takes too long to set up a subscription + MTRWeakReference * weakSelf = [MTRWeakReference weakReferenceWithObject:self]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (kTimeToWaitBeforeMarkingUnreachableAfterSettingUpSubscription * NSEC_PER_SEC)), self.queue, ^{ + MTRDevice * strongSelf = weakSelf.strongObject; + os_unfair_lock_lock(&strongSelf->_lock); + [strongSelf _markDeviceAsUnreachableIfNotSusbcribed]; + os_unfair_lock_unlock(&strongSelf->_lock); + }); [_deviceController getSessionForNode:_nodeID.unsignedLongLongValue From 320c1f098bfd68b0fdd834c7de741c79040efbf9 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 14 Mar 2024 20:38:19 -0400 Subject: [PATCH 56/76] MTR_EXTERN should not imply MTR_EXPORT. (#32580) Most things using MTR_EXTERN had availability annotations anyway, which imply MTR_EXPORT. --- src/darwin/Framework/CHIP/MTRDefines.h | 4 ++-- .../Framework/CHIP/MTRDeviceControllerLocalTestStorage.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDefines.h b/src/darwin/Framework/CHIP/MTRDefines.h index fea32d4a0e076e..fb8fed4f678ab8 100644 --- a/src/darwin/Framework/CHIP/MTRDefines.h +++ b/src/darwin/Framework/CHIP/MTRDefines.h @@ -51,9 +51,9 @@ #define MTR_EXPORT __attribute__((visibility("default"))) #ifdef __cplusplus -#define MTR_EXTERN extern "C" MTR_EXPORT +#define MTR_EXTERN extern "C" #else -#define MTR_EXTERN extern MTR_EXPORT +#define MTR_EXTERN extern #endif #if __has_attribute(__swift_attr__) diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.h b/src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.h index 915834ecabd35c..92670f1721c3e2 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerLocalTestStorage.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN -MTR_EXTERN @interface MTRDeviceControllerLocalTestStorage : NSObject +MTR_EXTERN MTR_EXPORT @interface MTRDeviceControllerLocalTestStorage : NSObject // Setting this variable only affects subsequent MTRDeviceController initializations @property (class, nonatomic, assign) BOOL localTestStorageEnabled; From 22901a1996f636c5825825bb3d0e7376a8c201f1 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 14 Mar 2024 21:45:56 -0400 Subject: [PATCH 57/76] Update ZAP to v2024.03.14-nightly. (#32564) This fixes the endpoint_config representation of "endpoint with no specific parent endpoint set" to match the SDK internal representation, so we can remove some code that was working around the representation mismatch. --- scripts/setup/zap.json | 4 ++-- scripts/setup/zap.version | 2 +- .../app-templates/endpoint_config.h | 2 +- .../lighting-app/app-templates/endpoint_config.h | 2 +- scripts/tools/zap/zap_execution.py | 2 +- src/app/util/attribute-storage.cpp | 13 +++---------- 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/scripts/setup/zap.json b/scripts/setup/zap.json index 1945fece82ed1a..173a68633f6d0e 100644 --- a/scripts/setup/zap.json +++ b/scripts/setup/zap.json @@ -8,13 +8,13 @@ "mac-amd64", "windows-amd64" ], - "tags": ["version:2@v2024.02.29-nightly.1"] + "tags": ["version:2@v2024.03.14-nightly.1"] }, { "_comment": "Always get the amd64 version on mac until usable arm64 zap build is available", "path": "fuchsia/third_party/zap/mac-amd64", "platforms": ["mac-arm64"], - "tags": ["version:2@v2024.02.29-nightly.1"] + "tags": ["version:2@v2024.03.14-nightly.1"] } ] } diff --git a/scripts/setup/zap.version b/scripts/setup/zap.version index 4a877a4050f7e2..6f551d6ac92803 100644 --- a/scripts/setup/zap.version +++ b/scripts/setup/zap.version @@ -1 +1 @@ -v2024.02.29-nightly +v2024.03.14-nightly diff --git a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h index 09b17447be9e52..ca2e15eedf5683 100644 --- a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h +++ b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h @@ -3072,5 +3072,5 @@ static_assert(ATTRIBUTE_LARGEST <= CHIP_CONFIG_MAX_ATTRIBUTE_STORE_ELEMENT_SIZE, // Array of parent endpoints for each endpoint #define FIXED_PARENT_ENDPOINTS \ { \ - 0, 0, 0, 0 \ + kInvalidEndpointId, kInvalidEndpointId, kInvalidEndpointId, kInvalidEndpointId \ } diff --git a/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h b/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h index 0e6ba62431b71e..f04deadd1610b7 100644 --- a/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h +++ b/scripts/tools/zap/tests/outputs/lighting-app/app-templates/endpoint_config.h @@ -1210,5 +1210,5 @@ static_assert(ATTRIBUTE_LARGEST <= CHIP_CONFIG_MAX_ATTRIBUTE_STORE_ELEMENT_SIZE, // Array of parent endpoints for each endpoint #define FIXED_PARENT_ENDPOINTS \ { \ - 0, 0 \ + kInvalidEndpointId, kInvalidEndpointId \ } diff --git a/scripts/tools/zap/zap_execution.py b/scripts/tools/zap/zap_execution.py index fafe3e5e29d2d9..b5880f761dd41d 100644 --- a/scripts/tools/zap/zap_execution.py +++ b/scripts/tools/zap/zap_execution.py @@ -23,7 +23,7 @@ # Use scripts/tools/zap/version_update.py to manage ZAP versioning as many # files may need updating for versions # -MIN_ZAP_VERSION = '2024.2.29' +MIN_ZAP_VERSION = '2024.3.14' class ZapTool: diff --git a/src/app/util/attribute-storage.cpp b/src/app/util/attribute-storage.cpp index 3bce63e6da254c..62e4631297e7d0 100644 --- a/src/app/util/attribute-storage.cpp +++ b/src/app/util/attribute-storage.cpp @@ -214,16 +214,9 @@ void emberAfEndpointConfigure() emAfEndpoints[ep].endpoint = fixedEndpoints[ep]; emAfEndpoints[ep].deviceTypeList = Span(&fixedDeviceTypeList[fixedDeviceTypeListOffsets[ep]], fixedDeviceTypeListLengths[ep]); - emAfEndpoints[ep].endpointType = &generatedEmberAfEndpointTypes[fixedEmberAfEndpointTypes[ep]]; - emAfEndpoints[ep].dataVersions = currentDataVersions; - if (fixedParentEndpoints[ep] == 0) - { - emAfEndpoints[ep].parentEndpointId = kInvalidEndpointId; - } - else - { - emAfEndpoints[ep].parentEndpointId = fixedParentEndpoints[ep]; - } + emAfEndpoints[ep].endpointType = &generatedEmberAfEndpointTypes[fixedEmberAfEndpointTypes[ep]]; + emAfEndpoints[ep].dataVersions = currentDataVersions; + emAfEndpoints[ep].parentEndpointId = fixedParentEndpoints[ep]; emAfEndpoints[ep].bitmask.Set(EmberAfEndpointOptions::isEnabled); emAfEndpoints[ep].bitmask.Set(EmberAfEndpointOptions::isFlatComposition); From 09fbf5cc1bfc1062aaf752cb7a9df622ca42f188 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 14 Mar 2024 22:06:53 -0400 Subject: [PATCH 58/76] Stop forcing a particular interface ID when sending response messages. (#32581) Because we were storing the PeerAddress in the session when getting a message, we effectively pinned sessions to particular interface ids at that point. This can lead to routing failures. We should only be pinning to interface IDs for link-local addresses, just like we do for initial IP resolution via DNS-SD. --- src/transport/SessionManager.cpp | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/transport/SessionManager.cpp b/src/transport/SessionManager.cpp index 085fda948629c7..78f9d98d79600f 100644 --- a/src/transport/SessionManager.cpp +++ b/src/transport/SessionManager.cpp @@ -57,6 +57,20 @@ using Transport::SecureSession; namespace { Global gGroupPeerTable; + +// Helper function that strips off the interface ID from a peer address that is +// not an IPv6 link-local address. For any other address type we should rely on +// the device's routing table to route messages sent. Forcing messages down a +// specific interface might fail with "no route to host". +void CorrectPeerAddressInterfaceID(Transport::PeerAddress & peerAddress) +{ + if (peerAddress.GetIPAddress().IsIPv6LinkLocal()) + { + return; + } + peerAddress.SetInterface(Inet::InterfaceId::Null()); +} + } // namespace uint32_t EncryptedPacketBufferHandle::GetMessageCounter() const @@ -633,7 +647,9 @@ void SessionManager::UnauthenticatedMessageDispatch(const PacketHeader & partial const SessionHandle & session = optionalSession.Value(); Transport::UnauthenticatedSession * unsecuredSession = session->AsUnauthenticatedSession(); - unsecuredSession->SetPeerAddress(peerAddress); + Transport::PeerAddress mutablePeerAddress = peerAddress; + CorrectPeerAddressInterfaceID(mutablePeerAddress); + unsecuredSession->SetPeerAddress(mutablePeerAddress); SessionMessageDelegate::DuplicateMessage isDuplicate = SessionMessageDelegate::DuplicateMessage::No; unsecuredSession->MarkActiveRx(); @@ -766,12 +782,11 @@ void SessionManager::SecureUnicastMessageDispatch(const PacketHeader & partialPa secureSession->GetSessionMessageCounter().GetPeerMessageCounter().CommitEncryptedUnicast(packetHeader.GetMessageCounter()); } - // TODO: once mDNS address resolution is available reconsider if this is required - // This updates the peer address once a packet is received from a new address - // and serves as a way to auto-detect peer changing IPs. - if (secureSession->GetPeerAddress() != peerAddress) + Transport::PeerAddress mutablePeerAddress = peerAddress; + CorrectPeerAddressInterfaceID(mutablePeerAddress); + if (secureSession->GetPeerAddress() != mutablePeerAddress) { - secureSession->SetPeerAddress(peerAddress); + secureSession->SetPeerAddress(mutablePeerAddress); } if (mCB != nullptr) From f0b9945191468dac7dfd47ffa66ae27368635664 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 14 Mar 2024 22:09:02 -0400 Subject: [PATCH 59/76] Remove redundant checks from the operational state impl in all-clusters-app. (#32573) The cluster implementation does those checks. --- .../src/operational-state-delegate-impl.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp index 2070e1b86754fe..d258b8261a1aed 100644 --- a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp +++ b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp @@ -55,15 +55,6 @@ CHIP_ERROR GenericOperationalStateDelegateImpl::GetOperationalPhaseAtIndex(size_ void GenericOperationalStateDelegateImpl::HandlePauseStateCallback(GenericOperationalError & err) { - OperationalState::OperationalStateEnum state = - static_cast(GetInstance()->GetCurrentOperationalState()); - - if (state == OperationalState::OperationalStateEnum::kStopped || state == OperationalState::OperationalStateEnum::kError) - { - err.Set(to_underlying(OperationalState::ErrorStateEnum::kCommandInvalidInState)); - return; - } - // placeholder implementation auto error = GetInstance()->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kPaused)); if (error == CHIP_NO_ERROR) @@ -78,15 +69,6 @@ void GenericOperationalStateDelegateImpl::HandlePauseStateCallback(GenericOperat void GenericOperationalStateDelegateImpl::HandleResumeStateCallback(GenericOperationalError & err) { - OperationalState::OperationalStateEnum state = - static_cast(GetInstance()->GetCurrentOperationalState()); - - if (state == OperationalState::OperationalStateEnum::kStopped || state == OperationalState::OperationalStateEnum::kError) - { - err.Set(to_underlying(OperationalState::ErrorStateEnum::kCommandInvalidInState)); - return; - } - // placeholder implementation auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning)); if (error == CHIP_NO_ERROR) From 2e13ddccd8d69b6fe22f5aac2dbe1205833a7ed9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 15 Mar 2024 03:23:16 +0100 Subject: [PATCH 60/76] [Python] Delete BLE scanner handle in an explicit way (#32574) * Normalize BT MAC letter case passed to DiscoverAsync * Remove obsolete docstrings * [Python] Delete BLE scanner handle in an explicit way * Update callback names * Get rid of obsolete comments * Unref manager client on glib thread --- src/controller/python/chip/ble/LinuxImpl.cpp | 18 ++- .../python/chip/ble/darwin/Scanning.mm | 7 +- .../python/chip/ble/library_handle.py | 6 +- .../python/chip/ble/scan_devices.py | 143 +++++++++--------- src/platform/Linux/bluez/AdapterIterator.cpp | 12 ++ src/platform/Linux/bluez/AdapterIterator.h | 5 + .../Linux/bluez/ChipDeviceScanner.cpp | 30 ++-- 7 files changed, 119 insertions(+), 102 deletions(-) diff --git a/src/controller/python/chip/ble/LinuxImpl.cpp b/src/controller/python/chip/ble/LinuxImpl.cpp index 6218d9c66d4c83..852e1f725d22b0 100644 --- a/src/controller/python/chip/ble/LinuxImpl.cpp +++ b/src/controller/python/chip/ble/LinuxImpl.cpp @@ -111,11 +111,9 @@ class ScannerDelegateImpl : public ChipDeviceScannerDelegate { mCompleteCallback(mContext); } - - delete this; } - virtual void OnScanError(CHIP_ERROR error) override + void OnScanError(CHIP_ERROR error) override { if (mErrorCallback) { @@ -133,10 +131,10 @@ class ScannerDelegateImpl : public ChipDeviceScannerDelegate } // namespace -extern "C" void * pychip_ble_start_scanning(PyObject * context, void * adapter, uint32_t timeoutMs, - ScannerDelegateImpl::DeviceScannedCallback scanCallback, - ScannerDelegateImpl::ScanCompleteCallback completeCallback, - ScannerDelegateImpl::ScanErrorCallback errorCallback) +extern "C" void * pychip_ble_scanner_start(PyObject * context, void * adapter, uint32_t timeoutMs, + ScannerDelegateImpl::DeviceScannedCallback scanCallback, + ScannerDelegateImpl::ScanCompleteCallback completeCallback, + ScannerDelegateImpl::ScanErrorCallback errorCallback) { std::unique_ptr delegate = std::make_unique(context, scanCallback, completeCallback, errorCallback); @@ -151,3 +149,9 @@ extern "C" void * pychip_ble_start_scanning(PyObject * context, void * adapter, return delegate.release(); } + +extern "C" void pychip_ble_scanner_delete(void * scanner) +{ + chip::DeviceLayer::StackLock lock; + delete static_cast(scanner); +} diff --git a/src/controller/python/chip/ble/darwin/Scanning.mm b/src/controller/python/chip/ble/darwin/Scanning.mm index caf18c658ebc9c..d3fb928790ab3e 100644 --- a/src/controller/python/chip/ble/darwin/Scanning.mm +++ b/src/controller/python/chip/ble/darwin/Scanning.mm @@ -131,7 +131,7 @@ - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPerip @end -extern "C" void * pychip_ble_start_scanning(PyObject * context, void * adapter, uint32_t timeout, +extern "C" void * pychip_ble_scanner_start(PyObject * context, void * adapter, uint32_t timeout, DeviceScannedCallback scanCallback, ScanCompleteCallback completeCallback, ScanErrorCallback errorCallback) { // NOTE: adapter is ignored as it does not apply to mac @@ -144,3 +144,8 @@ - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPerip return (__bridge_retained void *) (scanner); } + +extern "C" void pychip_ble_scanner_delete(void * scanner) +{ + CFRelease((CFTypeRef) scanner); +} diff --git a/src/controller/python/chip/ble/library_handle.py b/src/controller/python/chip/ble/library_handle.py index 2dbb52f28bf429..00f2a77eab68d6 100644 --- a/src/controller/python/chip/ble/library_handle.py +++ b/src/controller/python/chip/ble/library_handle.py @@ -57,8 +57,10 @@ def _GetBleLibraryHandle() -> ctypes.CDLL: setter.Set('pychip_ble_adapter_list_get_raw_adapter', VoidPointer, [VoidPointer]) - setter.Set('pychip_ble_start_scanning', VoidPointer, [ - py_object, VoidPointer, c_uint32, DeviceScannedCallback, ScanDoneCallback, ScanErrorCallback + setter.Set('pychip_ble_scanner_start', VoidPointer, [ + py_object, VoidPointer, c_uint32, DeviceScannedCallback, + ScanDoneCallback, ScanErrorCallback, ]) + setter.Set('pychip_ble_scanner_delete', None, [VoidPointer]) return handle diff --git a/src/controller/python/chip/ble/scan_devices.py b/src/controller/python/chip/ble/scan_devices.py index 5e9ee24dadd1c4..86cc0a321e8c7d 100644 --- a/src/controller/python/chip/ble/scan_devices.py +++ b/src/controller/python/chip/ble/scan_devices.py @@ -17,6 +17,7 @@ import ctypes from dataclasses import dataclass from queue import Queue +from threading import Thread from typing import Generator from chip.ble.library_handle import _GetBleLibraryHandle @@ -26,75 +27,17 @@ @DeviceScannedCallback def ScanFoundCallback(closure, address: str, discriminator: int, vendor: int, product: int): - closure.DeviceFound(address, discriminator, vendor, product) + closure.OnDeviceScanned(address, discriminator, vendor, product) @ScanDoneCallback def ScanDoneCallback(closure): - closure.ScanCompleted() + closure.OnScanComplete() @ScanErrorCallback def ScanErrorCallback(closure, errorCode: int): - closure.ScanErrorCallback(errorCode) - - -def DiscoverAsync(timeoutMs: int, scanCallback, doneCallback, errorCallback, adapter=None): - """Initiate a BLE discovery of devices with the given timeout. - - NOTE: devices are not guaranteed to be unique. New entries are returned - as soon as the underlying BLE manager detects changes. - - Args: - timeoutMs: scan will complete after this time - scanCallback: callback when a device is found - doneCallback: callback when the scan is complete - errorCallback: callback when error occurred during scan - adapter: what adapter to choose. Either an AdapterInfo object or - a string with the adapter address. If None, the first - adapter on the system is used. - """ - if adapter and not isinstance(adapter, str): - adapter = adapter.address - - handle = _GetBleLibraryHandle() - - nativeList = handle.pychip_ble_adapter_list_new() - if nativeList == 0: - raise Exception('Failed to list available adapters') - - try: - while handle.pychip_ble_adapter_list_next(nativeList): - if adapter and (adapter != handle.pychip_ble_adapter_list_get_address( - nativeList).decode('utf8')): - continue - - class ScannerClosure: - - def DeviceFound(self, *args): - scanCallback(*args) - - def ScanCompleted(self, *args): - doneCallback(*args) - ctypes.pythonapi.Py_DecRef(ctypes.py_object(self)) - - def ScanErrorCallback(self, *args): - errorCallback(*args) - - closure = ScannerClosure() - ctypes.pythonapi.Py_IncRef(ctypes.py_object(closure)) - - scanner = handle.pychip_ble_start_scanning( - ctypes.py_object(closure), - handle.pychip_ble_adapter_list_get_raw_adapter( - nativeList), timeoutMs, - ScanFoundCallback, ScanDoneCallback, ScanErrorCallback) - - if scanner == 0: - raise Exception('Failed to initiate scan') - break - finally: - handle.pychip_ble_adapter_list_delete(nativeList) + closure.OnScanError(errorCode) @dataclass @@ -107,7 +50,7 @@ class DeviceInfo: class _DeviceInfoReceiver: """Uses a queue to notify of objects received asynchronously - from a ble scan. + from a BLE scan. Internal queue gets filled on DeviceFound and ends with None when ScanCompleted. @@ -116,13 +59,13 @@ class _DeviceInfoReceiver: def __init__(self): self.queue = Queue() - def DeviceFound(self, address, discriminator, vendor, product): + def OnDeviceScanned(self, address, discriminator, vendor, product): self.queue.put(DeviceInfo(address, discriminator, vendor, product)) - def ScanCompleted(self): + def OnScanComplete(self): self.queue.put(None) - def ScanError(self, errorCode): + def OnScanError(self, errorCode): # TODO need to determine what we do with this error. Most of the time this # error is just a timeout introduced in PR #24873, right before we get a # ScanCompleted. @@ -132,24 +75,76 @@ def ScanError(self, errorCode): def DiscoverSync(timeoutMs: int, adapter=None) -> Generator[DeviceInfo, None, None]: """Discover BLE devices over the specified period of time. - NOTE: devices are not guaranteed to be unique. New entries are returned + NOTE: Devices are not guaranteed to be unique. New entries are returned + as soon as the underlying BLE manager detects changes. + + Args: + timeoutMs: scan will complete after this time + adapter: what adapter to choose. Either an AdapterInfo object or + a string with the adapter address. If None, the first + adapter on the system is used. + """ + if adapter: + if isinstance(adapter, str): + adapter = adapter.upper() + else: + adapter = adapter.address + + handle = _GetBleLibraryHandle() + + nativeList = handle.pychip_ble_adapter_list_new() + if nativeList == 0: + raise Exception('Failed to list available adapters') + + try: + while handle.pychip_ble_adapter_list_next(nativeList): + if adapter and (adapter != handle.pychip_ble_adapter_list_get_address( + nativeList).decode('utf8')): + continue + + receiver = _DeviceInfoReceiver() + scanner = handle.pychip_ble_scanner_start( + ctypes.py_object(receiver), + handle.pychip_ble_adapter_list_get_raw_adapter(nativeList), + timeoutMs, ScanFoundCallback, ScanDoneCallback, ScanErrorCallback) + + if scanner == 0: + raise Exception('Failed to start BLE scan') + + while True: + data = receiver.queue.get() + if not data: + break + yield data + + handle.pychip_ble_scanner_delete(scanner) + break + finally: + handle.pychip_ble_adapter_list_delete(nativeList) + + +def DiscoverAsync(timeoutMs: int, scanCallback, doneCallback, errorCallback, adapter=None): + """Discover BLE devices over the specified period of time without blocking. + + NOTE: Devices are not guaranteed to be unique. The scanCallback is called as soon as the underlying BLE manager detects changes. Args: timeoutMs: scan will complete after this time scanCallback: callback when a device is found doneCallback: callback when the scan is complete + errorCallback: callback when error occurred during scan adapter: what adapter to choose. Either an AdapterInfo object or a string with the adapter address. If None, the first adapter on the system is used. """ - receiver = _DeviceInfoReceiver() - DiscoverAsync(timeoutMs, receiver.DeviceFound, - receiver.ScanCompleted, receiver.ScanError, adapter) + def _DiscoverAsync(timeoutMs, scanCallback, doneCallback, errorCallback, adapter): + for device in DiscoverSync(timeoutMs, adapter): + scanCallback(device.address, device.discriminator, device.vendor, device.product) + doneCallback() - while True: - data = receiver.queue.get() - if not data: - break - yield data + t = Thread(target=_DiscoverAsync, + args=(timeoutMs, scanCallback, doneCallback, errorCallback, adapter), + daemon=True) + t.start() diff --git a/src/platform/Linux/bluez/AdapterIterator.cpp b/src/platform/Linux/bluez/AdapterIterator.cpp index ced172446882b8..0455359f424bfb 100644 --- a/src/platform/Linux/bluez/AdapterIterator.cpp +++ b/src/platform/Linux/bluez/AdapterIterator.cpp @@ -47,6 +47,18 @@ CHIP_ERROR AdapterIterator::Initialize() return CHIP_NO_ERROR; } +CHIP_ERROR AdapterIterator::Shutdown() +{ + // Release resources on the glib thread to synchronize with potential signal handlers + // attached to the manager client object that may run on the glib thread. + return PlatformMgrImpl().GLibMatterContextInvokeSync( + +[](AdapterIterator * self) { + self->mManager.reset(); + return CHIP_NO_ERROR; + }, + this); +} + bool AdapterIterator::Advance() { for (; mIterator != BluezObjectList::end(); ++mIterator) diff --git a/src/platform/Linux/bluez/AdapterIterator.h b/src/platform/Linux/bluez/AdapterIterator.h index 38af64f7ecc21c..c998df602f81fc 100644 --- a/src/platform/Linux/bluez/AdapterIterator.h +++ b/src/platform/Linux/bluez/AdapterIterator.h @@ -48,6 +48,9 @@ namespace Internal { class AdapterIterator { public: + AdapterIterator() = default; + ~AdapterIterator() { Shutdown(); } + /// Moves to the next DBUS interface. /// /// MUST be called before any of the 'current value' methods are @@ -66,6 +69,8 @@ class AdapterIterator private: /// Sets up the DBUS manager and loads the list CHIP_ERROR Initialize(); + /// Destroys the DBUS manager + CHIP_ERROR Shutdown(); /// Loads the next value in the list. /// diff --git a/src/platform/Linux/bluez/ChipDeviceScanner.cpp b/src/platform/Linux/bluez/ChipDeviceScanner.cpp index 5523bf87dfdf66..ce52b8482752b8 100644 --- a/src/platform/Linux/bluez/ChipDeviceScanner.cpp +++ b/src/platform/Linux/bluez/ChipDeviceScanner.cpp @@ -112,16 +112,12 @@ CHIP_ERROR ChipDeviceScanner::StartScan(System::Clock::Timeout timeout) VerifyOrReturnError(mTimerState == ScannerTimerState::TIMER_CANCELED, CHIP_ERROR_INCORRECT_STATE); mCancellable.reset(g_cancellable_new()); - if (PlatformMgrImpl().GLibMatterContextInvokeSync(MainLoopStartScan, this) != CHIP_NO_ERROR) + CHIP_ERROR err = PlatformMgrImpl().GLibMatterContextInvokeSync(MainLoopStartScan, this); + if (err != CHIP_NO_ERROR) { - ChipLogError(Ble, "Failed to schedule BLE scan start."); - - ChipDeviceScannerDelegate * delegate = this->mDelegate; - // callback is explicitly allowed to delete the scanner (hence no more - // references to 'self' here) - delegate->OnScanComplete(); - - return CHIP_ERROR_INTERNAL; + ChipLogError(Ble, "Failed to initiate BLE scan start: %" CHIP_ERROR_FORMAT, err.Format()); + mDelegate->OnScanComplete(); + return err; } // Here need to set the Bluetooth scanning status immediately. @@ -129,14 +125,14 @@ CHIP_ERROR ChipDeviceScanner::StartScan(System::Clock::Timeout timeout) // calling StopScan will be effective. mScannerState = ChipDeviceScannerState::SCANNER_SCANNING; - CHIP_ERROR err = chip::DeviceLayer::SystemLayer().StartTimer(timeout, TimerExpiredCallback, static_cast(this)); - + err = chip::DeviceLayer::SystemLayer().StartTimer(timeout, TimerExpiredCallback, static_cast(this)); if (err != CHIP_NO_ERROR) { - ChipLogError(Ble, "Failed to schedule scan timeout."); + ChipLogError(Ble, "Failed to schedule scan timeout: %" CHIP_ERROR_FORMAT, err.Format()); StopScan(); return err; } + mTimerState = ScannerTimerState::TIMER_STARTED; ChipLogDetail(Ble, "ChipDeviceScanner has started scanning!"); @@ -157,9 +153,10 @@ CHIP_ERROR ChipDeviceScanner::StopScan() assertChipStackLockedByCurrentThread(); VerifyOrReturnError(mScannerState == ChipDeviceScannerState::SCANNER_SCANNING, CHIP_NO_ERROR); - if (PlatformMgrImpl().GLibMatterContextInvokeSync(MainLoopStopScan, this) != CHIP_NO_ERROR) + CHIP_ERROR err = PlatformMgrImpl().GLibMatterContextInvokeSync(MainLoopStopScan, this); + if (err != CHIP_NO_ERROR) { - ChipLogError(Ble, "Failed to schedule BLE scan stop."); + ChipLogError(Ble, "Failed to initiate BLE scan stop: %" CHIP_ERROR_FORMAT, err.Format()); return CHIP_ERROR_INTERNAL; } @@ -176,10 +173,7 @@ CHIP_ERROR ChipDeviceScanner::StopScan() // Reset timer status mTimerState = ScannerTimerState::TIMER_CANCELED; - ChipDeviceScannerDelegate * delegate = this->mDelegate; - // callback is explicitly allowed to delete the scanner (hence no more - // references to 'self' here) - delegate->OnScanComplete(); + mDelegate->OnScanComplete(); return CHIP_NO_ERROR; } From 73d3147ba8bae560edda38e72d332595d9fd2b4c Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:23:34 +0900 Subject: [PATCH 61/76] [Android] Add StopDevicePairing method in Java (#32566) --- .../chip/devicecontroller/ChipDeviceController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index 25d300ec1cadc1..d6b31c59dabcd5 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -453,6 +453,15 @@ public void unpairDeviceCallback(long deviceId, UnpairDeviceCallback callback) { unpairDeviceCallback(deviceControllerPtr, deviceId, callback); } + /** + * This function stops a pairing or commissioning process that is in progress. + * + * @param deviceId The remote device Id. + */ + public void stopDevicePairing(long deviceId) { + stopDevicePairing(deviceControllerPtr, deviceId); + } + /** * Returns a pointer to a device currently being commissioned. This should be used before the * device is operationally available. @@ -1474,6 +1483,8 @@ private native void continueCommissioning( private native void unpairDeviceCallback( long deviceControllerPtr, long deviceId, UnpairDeviceCallback callback); + private native void stopDevicePairing(long deviceControllerPtr, long deviceId); + private native long getDeviceBeingCommissionedPointer(long deviceControllerPtr, long nodeId); private native void getConnectedDevicePointer( From dfab05d8dd1cc77b90b1568cc573360f8ca82b9c Mon Sep 17 00:00:00 2001 From: joonhaengHeo <85541460+joonhaengHeo@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:23:59 +0900 Subject: [PATCH 62/76] [Android] Implement check in delegate (#32557) * Implement Android check in delegate * Restyled by whitespace * Restyled by google-java-format * Restyled by clang-format * Update documentation * Restyled by whitespace * Restyled by google-java-format * Fix jni object reference issue, add chiptool callback * Restyled by google-java-format * Restyled by clang-format * Update kotlin codestyle * remove public * fix typo * Restyled by clang-format * remove chip * Modify from comment --------- Co-authored-by: Restyled.io --- .../com/google/chip/chiptool/ChipClient.kt | 19 ++ .../java/AndroidCheckInDelegate.cpp | 162 ++++++++++++++++++ src/controller/java/AndroidCheckInDelegate.h | 52 ++++++ .../java/AndroidDeviceControllerWrapper.cpp | 5 + .../java/AndroidDeviceControllerWrapper.h | 6 +- src/controller/java/BUILD.gn | 4 + .../java/CHIPDeviceController-JNI.cpp | 19 ++ .../ChipDeviceController.java | 8 + .../devicecontroller/ICDCheckInDelegate.java | 55 ++++++ .../ICDCheckInDelegateWrapper.java | 59 +++++++ 10 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 src/controller/java/AndroidCheckInDelegate.cpp create mode 100644 src/controller/java/AndroidCheckInDelegate.h create mode 100644 src/controller/java/src/chip/devicecontroller/ICDCheckInDelegate.java create mode 100644 src/controller/java/src/chip/devicecontroller/ICDCheckInDelegateWrapper.java diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt index f8686ce3220b70..0534f7f3ee3589 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/ChipClient.kt @@ -22,6 +22,8 @@ import android.util.Log import chip.devicecontroller.ChipDeviceController import chip.devicecontroller.ControllerParams import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback +import chip.devicecontroller.ICDCheckInDelegate +import chip.devicecontroller.ICDClientInfo import chip.platform.AndroidBleManager import chip.platform.AndroidChipPlatform import chip.platform.ChipMdnsCallbackImpl @@ -60,6 +62,23 @@ object ChipClient { chipDeviceController.setAttestationTrustStoreDelegate( ExampleAttestationTrustStoreDelegate(chipDeviceController) ) + + chipDeviceController.setICDCheckInDelegate( + object : ICDCheckInDelegate { + override fun onCheckInComplete(info: ICDClientInfo) { + Log.d(TAG, "onCheckInComplete : $info") + } + + override fun onKeyRefreshNeeded(info: ICDClientInfo): ByteArray? { + Log.d(TAG, "onKeyRefreshNeeded : $info") + return null + } + + override fun onKeyRefreshDone(errorCode: Long) { + Log.d(TAG, "onKeyRefreshDone : $errorCode") + } + } + ) } return chipDeviceController diff --git a/src/controller/java/AndroidCheckInDelegate.cpp b/src/controller/java/AndroidCheckInDelegate.cpp new file mode 100644 index 00000000000000..1091738c804bbc --- /dev/null +++ b/src/controller/java/AndroidCheckInDelegate.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024 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 "AndroidCheckInDelegate.h" + +#include +#include +#include +#include +#include + +#define PARSE_CLIENT_INFO(_clientInfo, _peerNodeId, _startCounter, _offset, _monitoredSubject, _jniICDAesKey, _jniICDHmacKey) \ + jlong _peerNodeId = static_cast(_clientInfo.peer_node.GetNodeId()); \ + jlong _startCounter = static_cast(_clientInfo.start_icd_counter); \ + jlong _offset = static_cast(_clientInfo.offset); \ + jlong _monitoredSubject = static_cast(_clientInfo.monitored_subject); \ + chip::ByteSpan aes_buf(_clientInfo.aes_key_handle.As()); \ + chip::ByteSpan hmac_buf(_clientInfo.hmac_key_handle.As()); \ + chip::ByteArray _jniICDAesKey(env, aes_buf); \ + chip::ByteArray _jniICDHmacKey(env, hmac_buf); + +namespace chip { +namespace app { + +CHIP_ERROR AndroidCheckInDelegate::Init(ICDClientStorage * storage, InteractionModelEngine * engine) +{ + VerifyOrReturnError(storage != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(mpStorage == nullptr, CHIP_ERROR_INCORRECT_STATE); + mpStorage = storage; + mpImEngine = engine; + return CHIP_NO_ERROR; +} + +CHIP_ERROR AndroidCheckInDelegate::SetDelegate(jobject checkInDelegateObj) +{ + ReturnLogErrorOnFailure(mCheckInDelegate.Init(checkInDelegateObj)); + return CHIP_NO_ERROR; +} + +void AndroidCheckInDelegate::OnCheckInComplete(const ICDClientInfo & clientInfo) +{ + ChipLogProgress( + ICD, "Check In Message processing complete: start_counter=%" PRIu32 " offset=%" PRIu32 " nodeid=" ChipLogFormatScopedNodeId, + clientInfo.start_icd_counter, clientInfo.offset, ChipLogValueScopedNodeId(clientInfo.peer_node)); + + VerifyOrReturn(mCheckInDelegate.HasValidObjectRef(), ChipLogProgress(ICD, "check-in delegate is not implemented!")); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "JNIEnv is null!")); + PARSE_CLIENT_INFO(clientInfo, peerNodeId, startCounter, offset, monitoredSubject, jniICDAesKey, jniICDHmacKey) + + jmethodID onCheckInCompleteMethodID = nullptr; + CHIP_ERROR err = chip::JniReferences::GetInstance().FindMethod(env, mCheckInDelegate.ObjectRef(), "onCheckInComplete", + "(JJJJ[B[B)V", &onCheckInCompleteMethodID); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogProgress(ICD, "onCheckInComplete - FindMethod is failed! : %" CHIP_ERROR_FORMAT, err.Format())); + + env->CallVoidMethod(mCheckInDelegate.ObjectRef(), onCheckInCompleteMethodID, peerNodeId, startCounter, offset, monitoredSubject, + jniICDAesKey.jniValue(), jniICDHmacKey.jniValue()); +} + +RefreshKeySender * AndroidCheckInDelegate::OnKeyRefreshNeeded(ICDClientInfo & clientInfo, ICDClientStorage * clientStorage) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + RefreshKeySender::RefreshKeyBuffer newKey; + + bool hasSetKey = false; + if (mCheckInDelegate.HasValidObjectRef()) + { + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(Controller, "JNIEnv is null!")); + + PARSE_CLIENT_INFO(clientInfo, peerNodeId, startCounter, offset, monitoredSubject, jniICDAesKey, jniICDHmacKey) + + jmethodID onKeyRefreshNeededMethodID = nullptr; + err = chip::JniReferences::GetInstance().FindMethod(env, mCheckInDelegate.ObjectRef(), "onKeyRefreshNeeded", "(JJJJ[B[B)V", + &onKeyRefreshNeededMethodID); + VerifyOrReturnValue(err == CHIP_NO_ERROR, nullptr, + ChipLogProgress(ICD, "onKeyRefreshNeeded - FindMethod is failed! : %" CHIP_ERROR_FORMAT, err.Format())); + + jbyteArray key = static_cast(env->CallObjectMethod(mCheckInDelegate.ObjectRef(), onKeyRefreshNeededMethodID, + peerNodeId, startCounter, offset, monitoredSubject, + jniICDAesKey.jniValue(), jniICDHmacKey.jniValue())); + + if (key != nullptr) + { + JniByteArray jniKey(env, key); + VerifyOrReturnValue(static_cast(jniKey.size()) == newKey.Capacity(), nullptr, + ChipLogProgress(ICD, "Invalid key length : %d", jniKey.size())); + memcpy(newKey.Bytes(), jniKey.data(), newKey.Capacity()); + hasSetKey = true; + } + } + else + { + ChipLogProgress(ICD, "check-in delegate is not implemented!"); + } + if (!hasSetKey) + { + err = Crypto::DRBG_get_bytes(newKey.Bytes(), newKey.Capacity()); + if (err != CHIP_NO_ERROR) + { + ChipLogError(ICD, "Generation of new key failed: %" CHIP_ERROR_FORMAT, err.Format()); + return nullptr; + } + } + + auto refreshKeySender = Platform::New(this, clientInfo, clientStorage, mpImEngine, newKey); + if (refreshKeySender == nullptr) + { + return nullptr; + } + return refreshKeySender; +} + +void AndroidCheckInDelegate::OnKeyRefreshDone(RefreshKeySender * refreshKeySender, CHIP_ERROR error) +{ + if (error == CHIP_NO_ERROR) + { + ChipLogProgress(ICD, "Re-registration with new key completed successfully"); + } + else + { + ChipLogError(ICD, "Re-registration with new key failed with error : %" CHIP_ERROR_FORMAT, error.Format()); + // The callee can take corrective action based on the error received. + } + + VerifyOrReturn(mCheckInDelegate.HasValidObjectRef(), ChipLogProgress(ICD, "check-in delegate is not implemented!")); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(Controller, "JNIEnv is null!")); + + jmethodID onKeyRefreshDoneMethodID = nullptr; + CHIP_ERROR err = chip::JniReferences::GetInstance().FindMethod(env, mCheckInDelegate.ObjectRef(), "onKeyRefreshDone", "(J)V", + &onKeyRefreshDoneMethodID); + VerifyOrReturn(err == CHIP_NO_ERROR, + ChipLogProgress(ICD, "onKeyRefreshDone - FindMethod is failed! : %" CHIP_ERROR_FORMAT, err.Format())); + + env->CallVoidMethod(mCheckInDelegate.ObjectRef(), onKeyRefreshDoneMethodID, static_cast(error.AsInteger())); + + if (refreshKeySender != nullptr) + { + Platform::Delete(refreshKeySender); + refreshKeySender = nullptr; + } +} +} // namespace app +} // namespace chip diff --git a/src/controller/java/AndroidCheckInDelegate.h b/src/controller/java/AndroidCheckInDelegate.h new file mode 100644 index 00000000000000..5616b2815ed9b2 --- /dev/null +++ b/src/controller/java/AndroidCheckInDelegate.h @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2024 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. + */ + +#pragma once + +#include +#include +#include + +namespace chip { +namespace app { + +using namespace std; + +class InteractionModelEngine; + +/// Callbacks for check in protocol +class AndroidCheckInDelegate : public CheckInDelegate +{ +public: + virtual ~AndroidCheckInDelegate() {} + CHIP_ERROR Init(ICDClientStorage * storage, InteractionModelEngine * engine); + void OnCheckInComplete(const ICDClientInfo & clientInfo) override; + RefreshKeySender * OnKeyRefreshNeeded(ICDClientInfo & clientInfo, ICDClientStorage * clientStorage) override; + void OnKeyRefreshDone(RefreshKeySender * refreshKeySender, CHIP_ERROR error) override; + + CHIP_ERROR SetDelegate(jobject checkInDelegateObj); + +private: + ICDClientStorage * mpStorage = nullptr; + InteractionModelEngine * mpImEngine = nullptr; + + JniGlobalReference mCheckInDelegate; +}; + +} // namespace app +} // namespace chip diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp index 0143d36e8d3c8e..d914bb5279f99e 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.cpp +++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp @@ -689,6 +689,11 @@ CHIP_ERROR AndroidDeviceControllerWrapper::FinishOTAProvider() #endif } +CHIP_ERROR AndroidDeviceControllerWrapper::SetICDCheckInDelegate(jobject checkInDelegate) +{ + return mCheckInDelegate.SetDelegate(checkInDelegate); +} + void AndroidDeviceControllerWrapper::OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) { chip::DeviceLayer::StackUnlock unlock; diff --git a/src/controller/java/AndroidDeviceControllerWrapper.h b/src/controller/java/AndroidDeviceControllerWrapper.h index fe56c02646942c..cc0152cdc66421 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.h +++ b/src/controller/java/AndroidDeviceControllerWrapper.h @@ -25,7 +25,6 @@ #include #include -#include #include #include #include @@ -43,6 +42,7 @@ #include #endif // JAVA_MATTER_CONTROLLER_TEST +#include "AndroidCheckInDelegate.h" #include "AndroidOperationalCredentialsIssuer.h" #include "AttestationTrustStoreBridge.h" #include "DeviceAttestationDelegateBridge.h" @@ -212,6 +212,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::app::DefaultICDClientStorage * getICDClientStorage() { return &mICDClientStorage; } + CHIP_ERROR SetICDCheckInDelegate(jobject checkInDelegate); + private: using ChipDeviceControllerPtr = std::unique_ptr; @@ -225,7 +227,7 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::Crypto::RawKeySessionKeystore mSessionKeystore; chip::app::DefaultICDClientStorage mICDClientStorage; - chip::app::DefaultCheckInDelegate mCheckInDelegate; + chip::app::AndroidCheckInDelegate mCheckInDelegate; chip::app::CheckInHandler mCheckInHandler; JavaVM * mJavaVM = nullptr; diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index ff42d1b43a42f4..9e135aa5de2f34 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -41,6 +41,8 @@ shared_library("jni") { "AndroidCallbacks-JNI.cpp", "AndroidCallbacks.cpp", "AndroidCallbacks.h", + "AndroidCheckInDelegate.cpp", + "AndroidCheckInDelegate.h", "AndroidClusterExceptions.cpp", "AndroidClusterExceptions.h", "AndroidCommissioningWindowOpener.cpp", @@ -459,6 +461,8 @@ android_library("java") { "src/chip/devicecontroller/ExtendableInvokeCallbackJni.java", "src/chip/devicecontroller/GetConnectedDeviceCallbackJni.java", "src/chip/devicecontroller/GroupKeySecurityPolicy.java", + "src/chip/devicecontroller/ICDCheckInDelegate.java", + "src/chip/devicecontroller/ICDCheckInDelegateWrapper.java", "src/chip/devicecontroller/ICDClientInfo.java", "src/chip/devicecontroller/ICDDeviceInfo.java", "src/chip/devicecontroller/ICDRegistrationInfo.java", diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 8890c881dc0c2c..f6509557650bee 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -588,6 +588,25 @@ JNI_METHOD(void, finishOTAProvider)(JNIEnv * env, jobject self, jlong handle) #endif } +JNI_METHOD(void, setICDCheckInDelegate)(JNIEnv * env, jobject self, jlong handle, jobject checkInDelegate) +{ + chip::DeviceLayer::StackLock lock; + CHIP_ERROR err = CHIP_NO_ERROR; + AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); + + VerifyOrExit(wrapper != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + + ChipLogProgress(Controller, "setICDCheckInDelegate() called"); + + err = wrapper->SetICDCheckInDelegate(checkInDelegate); +exit: + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Failed to set ICD Check-In Deleagate. : %" CHIP_ERROR_FORMAT, err.Format()); + JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); + } +} + JNI_METHOD(void, commissionDevice) (JNIEnv * env, jobject self, jlong handle, jlong deviceId, jbyteArray csrNonce, jobject networkCredentials) { diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index d6b31c59dabcd5..9064d71755ceb2 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -151,6 +151,11 @@ public void finishOTAProvider() { finishOTAProvider(deviceControllerPtr); } + /** Set the delegate of ICD check in */ + public void setICDCheckInDelegate(ICDCheckInDelegate delegate) { + setICDCheckInDelegate(deviceControllerPtr, new ICDCheckInDelegateWrapper(delegate)); + } + public void pairDevice( BluetoothGatt bleServer, int connId, @@ -1434,6 +1439,9 @@ private native void setAttestationTrustStoreDelegate( private native void finishOTAProvider(long deviceControllerPtr); + private native void setICDCheckInDelegate( + long deviceControllerPtr, ICDCheckInDelegateWrapper delegate); + private native void pairDevice( long deviceControllerPtr, long deviceId, diff --git a/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegate.java b/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegate.java new file mode 100644 index 00000000000000..10b1eaaf9c64f6 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegate.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 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 chip.devicecontroller; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Delegate for ICD check in. + * + *

See detailed in {@link ChipDeviceController#setICDCheckInDelegate(ICDCheckInDelegate)} + */ +public interface ICDCheckInDelegate { + /** + * Callback used to let the application know that a check-in message was received and validated. + * + * @param info ICDClientInfo object representing the state associated with the node that sent the + * check-in message. + */ + void onCheckInComplete(@Nonnull ICDClientInfo info); + + /** + * Callback used to let the application know that a key refresh is needed to avoid counter + * rollover problems. + * + * @param info ICDClientInfo object representing the state associated with the node that sent the + * check-in message. + * @return refreshed key + */ + @Nullable + byte[] onKeyRefreshNeeded(@Nonnull ICDClientInfo info); + + /** + * Callback used to let the application know that the re-registration process is done. + * + * @param errorCode to check for success and failure + */ + void onKeyRefreshDone(long errorCode); +} diff --git a/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegateWrapper.java b/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegateWrapper.java new file mode 100644 index 00000000000000..39ca5df1b9e766 --- /dev/null +++ b/src/controller/java/src/chip/devicecontroller/ICDCheckInDelegateWrapper.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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 chip.devicecontroller; + +class ICDCheckInDelegateWrapper { + private ICDCheckInDelegate delegate; + + ICDCheckInDelegateWrapper(ICDCheckInDelegate delegate) { + this.delegate = delegate; + } + + // For JNI call + @SuppressWarnings("unused") + private void onCheckInComplete( + long peerNodeId, + long startCounter, + long offset, + long monitoredSubject, + byte[] icdAesKey, + byte[] icdHmacKey) { + delegate.onCheckInComplete( + new ICDClientInfo( + peerNodeId, startCounter, offset, monitoredSubject, icdAesKey, icdHmacKey)); + } + + @SuppressWarnings("unused") + private byte[] onKeyRefreshNeeded( + long peerNodeId, + long startCounter, + long offset, + long monitoredSubject, + byte[] icdAesKey, + byte[] icdHmacKey) { + return delegate.onKeyRefreshNeeded( + new ICDClientInfo( + peerNodeId, startCounter, offset, monitoredSubject, icdAesKey, icdHmacKey)); + } + + @SuppressWarnings("unused") + private void onKeyRefreshDone(int errorCode) { + delegate.onKeyRefreshDone(errorCode); + } +} From 0a6626f3aee3c813eeea88c481609d82bb3e2c3b Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 15 Mar 2024 14:45:54 +0100 Subject: [PATCH 63/76] [Linux] Factor common code when connecting BLE device (#32502) --- src/platform/Linux/bluez/BluezEndpoint.cpp | 45 +++++++--------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/platform/Linux/bluez/BluezEndpoint.cpp b/src/platform/Linux/bluez/BluezEndpoint.cpp index fe7a5a25171f73..5203488a7aae82 100644 --- a/src/platform/Linux/bluez/BluezEndpoint.cpp +++ b/src/platform/Linux/bluez/BluezEndpoint.cpp @@ -307,7 +307,7 @@ CHIP_ERROR BluezEndpoint::RegisterGattApplicationImpl() /// Update the table of open BLE connections whenever a new device is spotted or its attributes have changed. void BluezEndpoint::UpdateConnectionTable(BluezDevice1 * apDevice) { - const char * objectPath = g_dbus_proxy_get_object_path(G_DBUS_PROXY(apDevice)); + const char * objectPath = g_dbus_proxy_get_object_path(reinterpret_cast(apDevice)); BluezConnection * connection = GetBluezConnection(objectPath); if (connection != nullptr && !bluez_device1_get_connected(apDevice)) @@ -321,22 +321,9 @@ void BluezEndpoint::UpdateConnectionTable(BluezDevice1 * apDevice) return; } - if (connection == nullptr && !bluez_device1_get_connected(apDevice) && mIsCentral) + if (connection == nullptr) { - return; - } - - if (connection == nullptr && bluez_device1_get_connected(apDevice) && - (!mIsCentral || bluez_device1_get_services_resolved(apDevice))) - { - connection = chip::Platform::New(*this, apDevice); - mpPeerDevicePath = g_strdup(objectPath); - mConnMap[mpPeerDevicePath] = connection; - - ChipLogDetail(DeviceLayer, "New BLE connection: conn %p, device %s, path %s", connection, connection->GetPeerAddress(), - mpPeerDevicePath); - - BLEManagerImpl::HandleNewConnection(connection); + HandleNewDevice(apDevice); } } @@ -355,28 +342,22 @@ void BluezEndpoint::BluezSignalInterfacePropertiesChanged(GDBusObjectManagerClie void BluezEndpoint::HandleNewDevice(BluezDevice1 * device) { - VerifyOrReturn(!mIsCentral); - - // We need to handle device connection both this function and BluezSignalInterfacePropertiesChanged - // When a device is connected for first time, this function will be triggered. - // The future connections for the same device will trigger ``Connect'' property change. - // TODO: Factor common code in the two function. - BluezConnection * conn; - VerifyOrExit(bluez_device1_get_connected(device), ChipLogError(DeviceLayer, "FAIL: device is not connected")); + VerifyOrReturn(bluez_device1_get_connected(device)); + VerifyOrReturn(!mIsCentral || bluez_device1_get_services_resolved(device)); - conn = GetBluezConnection(g_dbus_proxy_get_object_path(G_DBUS_PROXY(device))); - VerifyOrExit(conn == nullptr, - ChipLogError(DeviceLayer, "FAIL: connection already tracked: conn: %p new device: %s", conn, - g_dbus_proxy_get_object_path(G_DBUS_PROXY(device)))); + const char * objectPath = g_dbus_proxy_get_object_path(reinterpret_cast(device)); + BluezConnection * conn = GetBluezConnection(objectPath); + VerifyOrReturn(conn == nullptr, + ChipLogError(DeviceLayer, "FAIL: Connection already tracked: conn=%p device=%s path=%s", conn, + conn->GetPeerAddress(), objectPath)); conn = chip::Platform::New(*this, device); - mpPeerDevicePath = g_strdup(g_dbus_proxy_get_object_path(G_DBUS_PROXY(device))); + mpPeerDevicePath = g_strdup(objectPath); mConnMap[mpPeerDevicePath] = conn; - ChipLogDetail(DeviceLayer, "BLE device connected: conn %p, device %s, path %s", conn, conn->GetPeerAddress(), mpPeerDevicePath); + ChipLogDetail(DeviceLayer, "New BLE connection: conn=%p device=%s path=%s", conn, conn->GetPeerAddress(), objectPath); -exit: - return; + BLEManagerImpl::HandleNewConnection(conn); } void BluezEndpoint::BluezSignalOnObjectAdded(GDBusObjectManager * aManager, GDBusObject * aObject) From c56f3bf9e5bc7108060909d11cc078e81c2341ef Mon Sep 17 00:00:00 2001 From: Erwin Pan Date: Fri, 15 Mar 2024 22:22:05 +0800 Subject: [PATCH 64/76] [Chef] Implement DoorLock example app (#32532) * Update Chef Lock to enable user PIN and more * Remove unused clusters (Groups). Add Power Source * Refer to the latest lock-app codes * Update device PowerSource attributes * Enable PrivacyMode, Remove debug message * Fix typo * Restyled by whitespace * Restyled by clang-format * Add macro to doorlock to avoid compilation of light * [Chef] support doorlock in esp32 & nrfconnect * Update the date from 2023 to 2024 * Use CharSpan instead of "char *". Refine ifdef * Restyled by clang-format --------- Co-authored-by: Restyled.io --- .../door-lock/chef-doorlock-stubs.cpp | 133 +++ .../clusters/door-lock/chef-lock-endpoint.cpp | 680 +++++++++++++++ .../clusters/door-lock/chef-lock-endpoint.h | 162 ++++ .../clusters/door-lock/chef-lock-manager.cpp | 376 +++++++++ .../clusters/door-lock/chef-lock-manager.h | 78 ++ examples/chef/common/stubs.cpp | 232 ------ .../rootnode_doorlock_aNKYAreMXE.matter | 453 +++++++--- .../devices/rootnode_doorlock_aNKYAreMXE.zap | 786 ++++++++++++++---- examples/chef/esp32/main/CMakeLists.txt | 9 +- examples/chef/linux/BUILD.gn | 3 + examples/chef/nrfconnect/CMakeLists.txt | 11 +- 11 files changed, 2406 insertions(+), 517 deletions(-) create mode 100644 examples/chef/common/clusters/door-lock/chef-doorlock-stubs.cpp create mode 100644 examples/chef/common/clusters/door-lock/chef-lock-endpoint.cpp create mode 100644 examples/chef/common/clusters/door-lock/chef-lock-endpoint.h create mode 100644 examples/chef/common/clusters/door-lock/chef-lock-manager.cpp create mode 100644 examples/chef/common/clusters/door-lock/chef-lock-manager.h diff --git a/examples/chef/common/clusters/door-lock/chef-doorlock-stubs.cpp b/examples/chef/common/clusters/door-lock/chef-doorlock-stubs.cpp new file mode 100644 index 00000000000000..7d92300ca5659c --- /dev/null +++ b/examples/chef/common/clusters/door-lock/chef-doorlock-stubs.cpp @@ -0,0 +1,133 @@ +/* + * + * Copyright (c) 2020-2024 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 + +#ifdef MATTER_DM_PLUGIN_DOOR_LOCK_SERVER +#include "chef-lock-manager.h" +#include + +using namespace chip; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::DoorLock; +using chip::app::DataModel::Nullable; + +// ============================================================================= +// 'Default' callbacks for cluster commands +// ============================================================================= + +// App handles physical aspects of locking but not locking logic. That is it +// should wait for door to be locked on lock command and return success) but +// door lock server should check pin before even calling the lock-door +// callback. +bool emberAfPluginDoorLockOnDoorLockCommand(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pinCode, + OperationErrorEnum & err) +{ + return LockManager::Instance().Lock(endpointId, fabricIdx, nodeId, pinCode, err, OperationSourceEnum::kRemote); +} + +bool emberAfPluginDoorLockOnDoorUnlockCommand(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pinCode, + OperationErrorEnum & err) +{ + return LockManager::Instance().Unlock(endpointId, fabricIdx, nodeId, pinCode, err, OperationSourceEnum::kRemote); +} + +bool emberAfPluginDoorLockOnDoorUnboltCommand(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pinCode, + OperationErrorEnum & err) +{ + return LockManager::Instance().Unbolt(endpointId, fabricIdx, nodeId, pinCode, err, OperationSourceEnum::kRemote); +} + +bool emberAfPluginDoorLockGetUser(chip::EndpointId endpointId, uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) +{ + return LockManager::Instance().GetUser(endpointId, userIndex, user); +} + +bool emberAfPluginDoorLockSetUser(chip::EndpointId endpointId, uint16_t userIndex, chip::FabricIndex creator, + chip::FabricIndex modifier, const chip::CharSpan & userName, uint32_t uniqueId, + UserStatusEnum userStatus, UserTypeEnum usertype, CredentialRuleEnum credentialRule, + const CredentialStruct * credentials, size_t totalCredentials) +{ + + return LockManager::Instance().SetUser(endpointId, userIndex, creator, modifier, userName, uniqueId, userStatus, usertype, + credentialRule, credentials, totalCredentials); +} + +bool emberAfPluginDoorLockGetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, CredentialTypeEnum credentialType, + EmberAfPluginDoorLockCredentialInfo & credential) +{ + return LockManager::Instance().GetCredential(endpointId, credentialIndex, credentialType, credential); +} + +bool emberAfPluginDoorLockSetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator, + chip::FabricIndex modifier, DlCredentialStatus credentialStatus, + CredentialTypeEnum credentialType, const chip::ByteSpan & credentialData) +{ + return LockManager::Instance().SetCredential(endpointId, credentialIndex, creator, modifier, credentialStatus, credentialType, + credentialData); +} + +DlStatus emberAfPluginDoorLockGetSchedule(chip::EndpointId endpointId, uint8_t weekdayIndex, uint16_t userIndex, + EmberAfPluginDoorLockWeekDaySchedule & schedule) +{ + return LockManager::Instance().GetSchedule(endpointId, weekdayIndex, userIndex, schedule); +} + +DlStatus emberAfPluginDoorLockGetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, + EmberAfPluginDoorLockHolidaySchedule & schedule) +{ + return LockManager::Instance().GetSchedule(endpointId, holidayIndex, schedule); +} + +DlStatus emberAfPluginDoorLockSetSchedule(chip::EndpointId endpointId, uint8_t weekdayIndex, uint16_t userIndex, + DlScheduleStatus status, DaysMaskMap daysMask, uint8_t startHour, uint8_t startMinute, + uint8_t endHour, uint8_t endMinute) +{ + return LockManager::Instance().SetSchedule(endpointId, weekdayIndex, userIndex, status, daysMask, startHour, startMinute, + endHour, endMinute); +} + +DlStatus emberAfPluginDoorLockSetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, + DlScheduleStatus status, uint32_t localStartTime, uint32_t localEndTime) +{ + return LockManager::Instance().SetSchedule(endpointId, yearDayIndex, userIndex, status, localStartTime, localEndTime); +} + +DlStatus emberAfPluginDoorLockGetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, + EmberAfPluginDoorLockYearDaySchedule & schedule) +{ + return LockManager::Instance().GetSchedule(endpointId, yearDayIndex, userIndex, schedule); +} + +DlStatus emberAfPluginDoorLockSetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, DlScheduleStatus status, + uint32_t localStartTime, uint32_t localEndTime, OperatingModeEnum operatingMode) +{ + return LockManager::Instance().SetSchedule(endpointId, holidayIndex, status, localStartTime, localEndTime, operatingMode); +} + +void emberAfDoorLockClusterInitCallback(EndpointId endpoint) +{ + DoorLockServer::Instance().InitServer(endpoint); + LockManager::Instance().InitEndpoint(endpoint); +} +#endif // MATTER_DM_PLUGIN_DOOR_LOCK_SERVER diff --git a/examples/chef/common/clusters/door-lock/chef-lock-endpoint.cpp b/examples/chef/common/clusters/door-lock/chef-lock-endpoint.cpp new file mode 100644 index 00000000000000..7cff212dba743b --- /dev/null +++ b/examples/chef/common/clusters/door-lock/chef-lock-endpoint.cpp @@ -0,0 +1,680 @@ +/* + * + * Copyright (c) 2022-2024 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 + +#ifdef MATTER_DM_PLUGIN_DOOR_LOCK_SERVER +#include "chef-lock-endpoint.h" + +using chip::to_underlying; +using chip::app::DataModel::MakeNullable; + +struct LockActionData +{ + chip::EndpointId endpointId; + DlLockState lockState; + OperationSourceEnum opSource; + Nullable userIndex; + uint16_t credentialIndex; + Nullable fabricIdx; + Nullable nodeId; + bool moving = false; +}; + +static LockActionData gCurrentAction; + +bool LockEndpoint::Lock(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) +{ + return setLockState(fabricIdx, nodeId, DlLockState::kLocked, pin, err, opSource); +} + +bool LockEndpoint::Unlock(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) +{ + if (DoorLockServer::Instance().SupportsUnbolt(mEndpointId)) + { + // If Unbolt is supported Unlock is supposed to pull the latch + return setLockState(fabricIdx, nodeId, DlLockState::kUnlatched, pin, err, opSource); + } + + return setLockState(fabricIdx, nodeId, DlLockState::kUnlocked, pin, err, opSource); +} + +bool LockEndpoint::Unbolt(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) +{ + return setLockState(fabricIdx, nodeId, DlLockState::kUnlocked, pin, err, opSource); +} + +bool LockEndpoint::GetUser(uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) const +{ + ChipLogProgress(Zcl, "Lock App: LockEndpoint::GetUser [endpoint=%d,userIndex=%hu]", mEndpointId, userIndex); + + auto adjustedUserIndex = static_cast(userIndex - 1); + if (adjustedUserIndex > mLockUsers.size()) + { + ChipLogError(Zcl, "Cannot get user - index out of range [endpoint=%d,index=%hu,adjustedIndex=%d]", mEndpointId, userIndex, + adjustedUserIndex); + return false; + } + + const auto & userInDb = mLockUsers[adjustedUserIndex]; + user.userStatus = userInDb.userStatus; + if (UserStatusEnum::kAvailable == user.userStatus) + { + ChipLogDetail(Zcl, "Found unoccupied user [endpoint=%d,adjustedIndex=%hu]", mEndpointId, adjustedUserIndex); + return true; + } + + user.userName = userInDb.userName; + user.credentials = chip::Span(userInDb.credentials.data(), userInDb.credentials.size()); + user.userUniqueId = userInDb.userUniqueId; + user.userType = userInDb.userType; + user.credentialRule = userInDb.credentialRule; + // So far there's no way to actually create the credential outside the matter, so here we always set the creation/modification + // source to Matter + user.creationSource = DlAssetSource::kMatterIM; + user.createdBy = userInDb.createdBy; + user.modificationSource = DlAssetSource::kMatterIM; + user.lastModifiedBy = userInDb.lastModifiedBy; + + ChipLogDetail(Zcl, + "Found occupied user " + "[endpoint=%d,adjustedIndex=%hu,name=\"%.*s\",credentialsCount=%u,uniqueId=%x,type=%u,credentialRule=%u," + "createdBy=%d,lastModifiedBy=%d]", + mEndpointId, adjustedUserIndex, static_cast(user.userName.size()), user.userName.data(), + static_cast(user.credentials.size()), user.userUniqueId, to_underlying(user.userType), + to_underlying(user.credentialRule), user.createdBy, user.lastModifiedBy); + + return true; +} + +bool LockEndpoint::SetUser(uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + const chip::CharSpan & userName, uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, + CredentialRuleEnum credentialRule, const CredentialStruct * credentials, size_t totalCredentials) +{ + ChipLogProgress(Zcl, + "Lock App: LockEndpoint::SetUser " + "[endpoint=%d,userIndex=%u,creator=%d,modifier=%d,userName=\"%.*s\",uniqueId=%" PRIx32 + ",userStatus=%u,userType=%u," + "credentialRule=%u,credentials=%p,totalCredentials=%u]", + mEndpointId, userIndex, creator, modifier, static_cast(userName.size()), userName.data(), uniqueId, + to_underlying(userStatus), to_underlying(usertype), to_underlying(credentialRule), credentials, + static_cast(totalCredentials)); + + auto adjustedUserIndex = static_cast(userIndex - 1); + if (adjustedUserIndex > mLockUsers.size()) + { + ChipLogError(Zcl, "Cannot set user - index out of range [endpoint=%d,index=%d,adjustedUserIndex=%u]", mEndpointId, + userIndex, adjustedUserIndex); + return false; + } + + auto & userInStorage = mLockUsers[adjustedUserIndex]; + if (userName.size() > DOOR_LOCK_MAX_USER_NAME_SIZE) + { + ChipLogError(Zcl, "Cannot set user - user name is too long [endpoint=%d,index=%d,adjustedUserIndex=%u]", mEndpointId, + userIndex, adjustedUserIndex); + return false; + } + + if (totalCredentials > userInStorage.credentials.capacity()) + { + ChipLogError(Zcl, + "Cannot set user - total number of credentials is too big [endpoint=%d,index=%d,adjustedUserIndex=%u" + ",totalCredentials=%u,maxNumberOfCredentials=%u]", + mEndpointId, userIndex, adjustedUserIndex, static_cast(totalCredentials), + static_cast(userInStorage.credentials.capacity())); + return false; + } + + userInStorage.userName = chip::MutableCharSpan(userInStorage.userNameBuf, DOOR_LOCK_USER_NAME_BUFFER_SIZE); + CopyCharSpanToMutableCharSpan(userName, userInStorage.userName); + userInStorage.userUniqueId = uniqueId; + userInStorage.userStatus = userStatus; + userInStorage.userType = usertype; + userInStorage.credentialRule = credentialRule; + userInStorage.lastModifiedBy = modifier; + userInStorage.createdBy = creator; + + userInStorage.credentials.clear(); + for (size_t i = 0; i < totalCredentials; ++i) + { + userInStorage.credentials.push_back(credentials[i]); + } + + ChipLogProgress(Zcl, "Successfully set the user [mEndpointId=%d,index=%d,adjustedIndex=%d]", mEndpointId, userIndex, + adjustedUserIndex); + + return true; +} + +DoorStateEnum LockEndpoint::GetDoorState() const +{ + return mDoorState; +} + +bool LockEndpoint::SetDoorState(DoorStateEnum newState) +{ + if (mDoorState != newState) + { + ChipLogProgress(Zcl, "Changing the door state to: %d [endpointId=%d,previousState=%d]", to_underlying(newState), + mEndpointId, to_underlying(mDoorState)); + + mDoorState = newState; + return DoorLockServer::Instance().SetDoorState(mEndpointId, mDoorState); + } + return true; +} + +bool LockEndpoint::SendLockAlarm(AlarmCodeEnum alarmCode) const +{ + ChipLogProgress(Zcl, "Sending the LockAlarm event [endpointId=%d,alarmCode=%u]", mEndpointId, to_underlying(alarmCode)); + return DoorLockServer::Instance().SendLockAlarmEvent(mEndpointId, alarmCode); +} + +bool LockEndpoint::GetCredential(uint16_t credentialIndex, CredentialTypeEnum credentialType, + EmberAfPluginDoorLockCredentialInfo & credential) const +{ + ChipLogProgress(Zcl, "Lock App: LockEndpoint::GetCredential [endpoint=%d,credentialIndex=%u,credentialType=%u]", mEndpointId, + credentialIndex, to_underlying(credentialType)); + + if (to_underlying(credentialType) >= mLockCredentials.size()) + { + ChipLogError(Zcl, "Cannot get the credential - index out of range [endpoint=%d,index=%d]", mEndpointId, credentialIndex); + return false; + } + + if (credentialIndex >= mLockCredentials.at(to_underlying(credentialType)).size() || + (0 == credentialIndex && CredentialTypeEnum::kProgrammingPIN != credentialType)) + { + ChipLogError(Zcl, "Cannot get the credential - index out of range [endpoint=%d,index=%d]", mEndpointId, credentialIndex); + return false; + } + + const auto & credentialInStorage = mLockCredentials[to_underlying(credentialType)][credentialIndex]; + + credential.status = credentialInStorage.status; + if (DlCredentialStatus::kAvailable == credential.status) + { + ChipLogDetail(Zcl, "Found unoccupied credential [endpoint=%d,index=%u]", mEndpointId, credentialIndex); + return true; + } + credential.credentialType = credentialInStorage.credentialType; + credential.credentialData = chip::ByteSpan(credentialInStorage.credentialData, credentialInStorage.credentialDataSize); + // So far there's no way to actually create the credential outside the matter, so here we always set the creation/modification + // source to Matter + credential.creationSource = DlAssetSource::kMatterIM; + credential.createdBy = credentialInStorage.createdBy; + credential.modificationSource = DlAssetSource::kMatterIM; + credential.lastModifiedBy = credentialInStorage.modifiedBy; + + ChipLogDetail(Zcl, "Found occupied credential [endpoint=%d,index=%u,type=%u,dataSize=%u,createdBy=%u,modifiedBy=%u]", + mEndpointId, credentialIndex, to_underlying(credential.credentialType), + static_cast(credential.credentialData.size()), credential.createdBy, credential.lastModifiedBy); + + return true; +} + +bool LockEndpoint::SetCredential(uint16_t credentialIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, + const chip::ByteSpan & credentialData) +{ + ChipLogProgress( + Zcl, + "Lock App: LockEndpoint::SetCredential " + "[endpoint=%d,credentialIndex=%u,credentialStatus=%u,credentialType=%u,credentialDataSize=%u,creator=%u,modifier=%u]", + mEndpointId, credentialIndex, to_underlying(credentialStatus), to_underlying(credentialType), + static_cast(credentialData.size()), creator, modifier); + + if (to_underlying(credentialType) >= mLockCredentials.capacity()) + { + ChipLogError(Zcl, "Cannot set the credential - type out of range [endpoint=%d,type=%d]", mEndpointId, + to_underlying(credentialType)); + return false; + } + + if (credentialIndex >= mLockCredentials.at(to_underlying(credentialType)).size() || + (0 == credentialIndex && CredentialTypeEnum::kProgrammingPIN != credentialType)) + { + ChipLogError(Zcl, "Cannot set the credential - index out of range [endpoint=%d,index=%d]", mEndpointId, credentialIndex); + return false; + } + + // Assign to array by credentialIndex. Note: 0 is reserved for programmingPIN only + auto & credentialInStorage = mLockCredentials[to_underlying(credentialType)][credentialIndex]; + if (credentialData.size() > DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE) + { + ChipLogError(Zcl, + "Cannot get the credential - data size exceeds limit " + "[endpoint=%d,index=%d,dataSize=%u,maxDataSize=%u]", + mEndpointId, credentialIndex, static_cast(credentialData.size()), + static_cast(DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE)); + return false; + } + credentialInStorage.status = credentialStatus; + credentialInStorage.credentialType = credentialType; + credentialInStorage.createdBy = creator; + credentialInStorage.modifiedBy = modifier; + std::memcpy(credentialInStorage.credentialData, credentialData.data(), credentialData.size()); + credentialInStorage.credentialDataSize = credentialData.size(); + + ChipLogProgress(Zcl, "Successfully set the credential [mEndpointId=%d,index=%d,credentialType=%u,creator=%u,modifier=%u]", + mEndpointId, credentialIndex, to_underlying(credentialType), credentialInStorage.createdBy, + credentialInStorage.modifiedBy); + + return true; +} + +DlStatus LockEndpoint::GetSchedule(uint8_t weekDayIndex, uint16_t userIndex, EmberAfPluginDoorLockWeekDaySchedule & schedule) +{ + if (0 == userIndex || userIndex > mWeekDaySchedules.size()) + { + return DlStatus::kFailure; + } + + if (0 == weekDayIndex || weekDayIndex > mWeekDaySchedules.at(userIndex - 1).size()) + { + return DlStatus::kFailure; + } + + const auto & scheduleInStorage = mWeekDaySchedules.at(userIndex - 1).at(weekDayIndex - 1); + if (DlScheduleStatus::kAvailable == scheduleInStorage.status) + { + return DlStatus::kNotFound; + } + + schedule = scheduleInStorage.schedule; + + return DlStatus::kSuccess; +} + +DlStatus LockEndpoint::SetSchedule(uint8_t weekDayIndex, uint16_t userIndex, DlScheduleStatus status, DaysMaskMap daysMask, + uint8_t startHour, uint8_t startMinute, uint8_t endHour, uint8_t endMinute) +{ + if (0 == userIndex || userIndex > mWeekDaySchedules.size()) + { + return DlStatus::kFailure; + } + + if (0 == weekDayIndex || weekDayIndex > mWeekDaySchedules.at(userIndex - 1).size()) + { + return DlStatus::kFailure; + } + + auto & scheduleInStorage = mWeekDaySchedules.at(userIndex - 1).at(weekDayIndex - 1); + + scheduleInStorage.schedule.daysMask = daysMask; + scheduleInStorage.schedule.startHour = startHour; + scheduleInStorage.schedule.startMinute = startMinute; + scheduleInStorage.schedule.endHour = endHour; + scheduleInStorage.schedule.endMinute = endMinute; + scheduleInStorage.status = status; + + return DlStatus::kSuccess; +} + +DlStatus LockEndpoint::GetSchedule(uint8_t yearDayIndex, uint16_t userIndex, EmberAfPluginDoorLockYearDaySchedule & schedule) +{ + if (0 == userIndex || userIndex > mYearDaySchedules.size()) + { + return DlStatus::kFailure; + } + + if (0 == yearDayIndex || yearDayIndex > mYearDaySchedules.at(userIndex - 1).size()) + { + return DlStatus::kFailure; + } + + const auto & scheduleInStorage = mYearDaySchedules.at(userIndex - 1).at(yearDayIndex - 1); + if (DlScheduleStatus::kAvailable == scheduleInStorage.status) + { + return DlStatus::kNotFound; + } + + schedule = scheduleInStorage.schedule; + + return DlStatus::kSuccess; +} + +DlStatus LockEndpoint::SetSchedule(uint8_t yearDayIndex, uint16_t userIndex, DlScheduleStatus status, uint32_t localStartTime, + uint32_t localEndTime) +{ + if (0 == userIndex || userIndex > mYearDaySchedules.size()) + { + return DlStatus::kFailure; + } + + if (0 == yearDayIndex || yearDayIndex > mYearDaySchedules.at(userIndex - 1).size()) + { + return DlStatus::kFailure; + } + + auto & scheduleInStorage = mYearDaySchedules.at(userIndex - 1).at(yearDayIndex - 1); + scheduleInStorage.schedule.localStartTime = localStartTime; + scheduleInStorage.schedule.localEndTime = localEndTime; + scheduleInStorage.status = status; + + return DlStatus::kSuccess; +} + +DlStatus LockEndpoint::GetSchedule(uint8_t holidayIndex, EmberAfPluginDoorLockHolidaySchedule & schedule) +{ + if (0 == holidayIndex || holidayIndex > mHolidaySchedules.size()) + { + return DlStatus::kFailure; + } + + const auto & scheduleInStorage = mHolidaySchedules[holidayIndex - 1]; + if (DlScheduleStatus::kAvailable == scheduleInStorage.status) + { + return DlStatus::kNotFound; + } + + schedule = scheduleInStorage.schedule; + return DlStatus::kSuccess; +} + +DlStatus LockEndpoint::SetSchedule(uint8_t holidayIndex, DlScheduleStatus status, uint32_t localStartTime, uint32_t localEndTime, + OperatingModeEnum operatingMode) +{ + if (0 == holidayIndex || holidayIndex > mHolidaySchedules.size()) + { + return DlStatus::kFailure; + } + + auto & scheduleInStorage = mHolidaySchedules[holidayIndex - 1]; + scheduleInStorage.schedule.localStartTime = localStartTime; + scheduleInStorage.schedule.localEndTime = localEndTime; + scheduleInStorage.schedule.operatingMode = operatingMode; + scheduleInStorage.status = status; + + return DlStatus::kSuccess; +} + +bool LockEndpoint::setLockState(const Nullable & fabricIdx, const Nullable & nodeId, + DlLockState lockState, const Optional & pin, OperationErrorEnum & err, + OperationSourceEnum opSource) +{ + // Assume pin is required until told otherwise + bool requirePin = true; + chip::app::Clusters::DoorLock::Attributes::RequirePINforRemoteOperation::Get(mEndpointId, &requirePin); + + // If a pin code is not given + if (!pin.HasValue()) + { + ChipLogDetail(Zcl, "Door Lock App: PIN code is not specified [endpointId=%d]", mEndpointId); + + // If a pin code is not required + if (!requirePin) + { + ChipLogProgress(Zcl, "Door Lock App: setting door lock state to \"%s\" [endpointId=%d]", lockStateToString(lockState), + mEndpointId); + + if (gCurrentAction.moving == true) + { + ChipLogProgress(Zcl, "Lock App: not executing lock action as another lock action is already active [endpointId=%d]", + mEndpointId); + return false; + } + + gCurrentAction.moving = true; + gCurrentAction.endpointId = mEndpointId; + gCurrentAction.lockState = lockState; + gCurrentAction.opSource = opSource; + gCurrentAction.userIndex = NullNullable; + gCurrentAction.fabricIdx = fabricIdx; + gCurrentAction.nodeId = nodeId; + + // Do this async as a real lock would do too but use 0s delay to speed up CI tests + chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); + + return true; + } + + ChipLogError(Zcl, "Door Lock App: PIN code is not specified, but it is required [endpointId=%d]", mEndpointId); + + err = OperationErrorEnum::kInvalidCredential; + return false; + } + + // Find the credential so we can make sure it is not absent right away + auto & pinCredentials = mLockCredentials[to_underlying(CredentialTypeEnum::kPin)]; + auto credential = std::find_if(pinCredentials.begin(), pinCredentials.end(), [&pin](const LockCredentialInfo & c) { + return (c.status != DlCredentialStatus::kAvailable) && + chip::ByteSpan{ c.credentialData, c.credentialDataSize }.data_equal(pin.Value()); + }); + if (credential == pinCredentials.end()) + { + ChipLogDetail(Zcl, + "Lock App: specified PIN code was not found in the database, ignoring command to set lock state to \"%s\" " + "[endpointId=%d]", + lockStateToString(lockState), mEndpointId); + + err = OperationErrorEnum::kInvalidCredential; + return false; + } + + // Find a user that correspond to this credential + auto credentialIndex = static_cast(credential - pinCredentials.begin()); + auto user = std::find_if(mLockUsers.begin(), mLockUsers.end(), [credential, credentialIndex](const LockUserInfo & u) { + return std::any_of(u.credentials.begin(), u.credentials.end(), [&credential, credentialIndex](const CredentialStruct & c) { + return c.credentialIndex == credentialIndex && c.credentialType == credential->credentialType; + }); + }); + if (user == mLockUsers.end()) + { + ChipLogDetail(Zcl, + "Lock App: specified PIN code was found in the database, but the lock user is not associated with it " + "[endpointId=%d,credentialIndex=%u]", + mEndpointId, credentialIndex); + } + + auto userIndex = static_cast(user - mLockUsers.begin()); + + // Check if schedules affect the user + bool haveWeekDaySchedules = false; + bool haveYearDaySchedules = false; + if (weekDayScheduleForbidsAccess(userIndex, &haveWeekDaySchedules) || + yearDayScheduleForbidsAccess(userIndex, &haveYearDaySchedules) || + // Also disallow access for a user that's supposed to have _some_ + // schedule but doesn't have any + (user->userType == UserTypeEnum::kScheduleRestrictedUser && !haveWeekDaySchedules && !haveYearDaySchedules)) + { + ChipLogDetail(Zcl, + "Lock App: associated user is not allowed to operate the lock due to schedules" + "[endpointId=%d,userIndex=%u]", + mEndpointId, userIndex); + err = OperationErrorEnum::kRestricted; + return false; + } + ChipLogProgress( + Zcl, + "Lock App: specified PIN code was found in the database, setting door lock state to \"%s\" [endpointId=%d,userIndex=%u]", + lockStateToString(lockState), mEndpointId, userIndex); + + if (gCurrentAction.moving == true) + { + ChipLogProgress(Zcl, + "Lock App: not executing lock action as another lock action is already active [endpointId=%d,userIndex=%u]", + mEndpointId, userIndex); + return false; + } + + gCurrentAction.moving = true; + gCurrentAction.endpointId = mEndpointId; + gCurrentAction.lockState = lockState; + gCurrentAction.opSource = opSource; + gCurrentAction.userIndex = MakeNullable(static_cast(userIndex + 1)); + gCurrentAction.credentialIndex = static_cast(credentialIndex); + gCurrentAction.fabricIdx = fabricIdx; + gCurrentAction.nodeId = nodeId; + + // Do this async as a real lock would do too but use 0s delay to speed up CI tests + chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); + + return true; +} + +void LockEndpoint::OnLockActionCompleteCallback(chip::System::Layer *, void * callbackContext) +{ + if (gCurrentAction.userIndex.IsNull()) + { + DoorLockServer::Instance().SetLockState(gCurrentAction.endpointId, gCurrentAction.lockState, gCurrentAction.opSource, + NullNullable, NullNullable, gCurrentAction.fabricIdx, gCurrentAction.nodeId); + } + else + { + LockOpCredentials userCredential[] = { { CredentialTypeEnum::kPin, gCurrentAction.credentialIndex } }; + auto userCredentials = MakeNullable>(userCredential); + + DoorLockServer::Instance().SetLockState(gCurrentAction.endpointId, gCurrentAction.lockState, gCurrentAction.opSource, + gCurrentAction.userIndex, userCredentials, gCurrentAction.fabricIdx, + gCurrentAction.nodeId); + } + + // move back to Unlocked after Unlatch + if (gCurrentAction.lockState == DlLockState::kUnlatched) + { + gCurrentAction.lockState = DlLockState::kUnlocked; + + // Do this async as a real lock would do too but use 0s delay to speed up CI tests + chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); + } + else + { + gCurrentAction.moving = false; + } +} + +bool LockEndpoint::weekDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const +{ + *haveSchedule = std::any_of(mWeekDaySchedules[userIndex].begin(), mWeekDaySchedules[userIndex].end(), + [](const WeekDaysScheduleInfo & s) { return s.status == DlScheduleStatus::kOccupied; }); + + const auto & user = mLockUsers[userIndex]; + if (user.userType != UserTypeEnum::kScheduleRestrictedUser && user.userType != UserTypeEnum::kWeekDayScheduleUser) + { + // Weekday schedules don't apply to this user. + return false; + } + + if (user.userType == UserTypeEnum::kScheduleRestrictedUser && !*haveSchedule) + { + // It's valid to not have any schedules of a given type; on its own this + // does not prevent access. + return false; + } + + chip::System::Clock::Milliseconds64 cTMs; + auto chipError = chip::System::SystemClock().GetClock_RealTimeMS(cTMs); + if (chipError != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Lock App: unable to get current time to check user schedules [endpointId=%d,error=%d (%s)]", mEndpointId, + chipError.AsInteger(), chipError.AsString()); + return true; + } + time_t unixEpoch = std::chrono::duration_cast(cTMs).count(); + + tm calendarTime{}; + localtime_r(&unixEpoch, &calendarTime); + + auto currentTime = + calendarTime.tm_hour * chip::kSecondsPerHour + calendarTime.tm_min * chip::kSecondsPerMinute + calendarTime.tm_sec; + + // Now check whether any schedule allows the current time. If it does, + // access is not forbidden. + return !std::any_of( + mWeekDaySchedules[userIndex].begin(), mWeekDaySchedules[userIndex].end(), + [currentTime, calendarTime](const WeekDaysScheduleInfo & s) { + auto startTime = s.schedule.startHour * chip::kSecondsPerHour + s.schedule.startMinute * chip::kSecondsPerMinute; + auto endTime = s.schedule.endHour * chip::kSecondsPerHour + s.schedule.endMinute * chip::kSecondsPerMinute; + + return s.status == DlScheduleStatus::kOccupied && (to_underlying(s.schedule.daysMask) & (1 << calendarTime.tm_wday)) && + startTime <= currentTime && currentTime <= endTime; + }); +} + +bool LockEndpoint::yearDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const +{ + *haveSchedule = std::any_of(mYearDaySchedules[userIndex].begin(), mYearDaySchedules[userIndex].end(), + [](const YearDayScheduleInfo & sch) { return sch.status == DlScheduleStatus::kOccupied; }); + + const auto & user = mLockUsers[userIndex]; + if (user.userType != UserTypeEnum::kScheduleRestrictedUser && user.userType != UserTypeEnum::kYearDayScheduleUser) + { + return false; + } + + if (user.userType == UserTypeEnum::kScheduleRestrictedUser && !*haveSchedule) + { + // It's valid to not have any schedules of a given type; on its own this + // does not prevent access. + return false; + } + + chip::System::Clock::Milliseconds64 cTMs; + auto chipError = chip::System::SystemClock().GetClock_RealTimeMS(cTMs); + if (chipError != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Lock App: unable to get current time to check user schedules [endpointId=%d,error=%d (%s)]", mEndpointId, + chipError.AsInteger(), chipError.AsString()); + return true; + } + auto unixEpoch = std::chrono::duration_cast(cTMs).count(); + uint32_t chipEpoch = 0; + if (!chip::UnixEpochToChipEpochTime(unixEpoch, chipEpoch)) + { + ChipLogError(Zcl, + "Lock App: unable to convert Unix Epoch time to Matter Epoch Time to check user schedules " + "[endpointId=%d,userIndex=%d]", + mEndpointId, userIndex); + return false; + } + + return !std::any_of(mYearDaySchedules[userIndex].begin(), mYearDaySchedules[userIndex].end(), + [chipEpoch](const YearDayScheduleInfo & sch) { + return sch.status == DlScheduleStatus::kOccupied && sch.schedule.localStartTime <= chipEpoch && + chipEpoch <= sch.schedule.localEndTime; + }); +} + +const char * LockEndpoint::lockStateToString(DlLockState lockState) const +{ + switch (lockState) + { + case DlLockState::kNotFullyLocked: + return "Not Fully Locked"; + case DlLockState::kLocked: + return "Locked"; + case DlLockState::kUnlocked: + return "Unlocked"; + case DlLockState::kUnlatched: + return "Unlatched"; + case DlLockState::kUnknownEnumValue: + break; + } + + return "Unknown"; +} +#endif // MATTER_DM_PLUGIN_DOOR_LOCK_SERVER diff --git a/examples/chef/common/clusters/door-lock/chef-lock-endpoint.h b/examples/chef/common/clusters/door-lock/chef-lock-endpoint.h new file mode 100644 index 00000000000000..285e703dfaf73c --- /dev/null +++ b/examples/chef/common/clusters/door-lock/chef-lock-endpoint.h @@ -0,0 +1,162 @@ +/* + * + * Copyright (c) 2022-2024 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. + */ + +#pragma once + +#include +#include + +struct LockUserInfo +{ + char userNameBuf[DOOR_LOCK_USER_NAME_BUFFER_SIZE]; + chip::MutableCharSpan userName; + uint32_t userUniqueId; + UserStatusEnum userStatus; + UserTypeEnum userType; + CredentialRuleEnum credentialRule; + std::vector credentials; + chip::FabricIndex createdBy; + chip::FabricIndex lastModifiedBy; +}; + +struct LockCredentialInfo; +struct WeekDaysScheduleInfo; +struct YearDayScheduleInfo; +struct HolidayScheduleInfo; + +static constexpr size_t DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE = 20; +static constexpr size_t DOOR_LOCK_CREDENTIAL_INFO_MAX_TYPES = 6; // 0: ProgrammingPIN ~ 5: Face + +class LockEndpoint +{ +public: + LockEndpoint(chip::EndpointId endpointId, uint16_t numberOfLockUsersSupported, uint16_t numberOfCredentialsSupported, + uint8_t weekDaySchedulesPerUser, uint8_t yearDaySchedulesPerUser, uint8_t numberOfCredentialsPerUser, + uint8_t numberOfHolidaySchedules) : + mEndpointId{ endpointId }, + mLockState{ DlLockState::kLocked }, mDoorState{ DoorStateEnum::kDoorClosed }, mLockUsers(numberOfLockUsersSupported), + mLockCredentials(DOOR_LOCK_CREDENTIAL_INFO_MAX_TYPES, std::vector(numberOfCredentialsSupported + 1)), + mWeekDaySchedules(numberOfLockUsersSupported, std::vector(weekDaySchedulesPerUser)), + mYearDaySchedules(numberOfLockUsersSupported, std::vector(yearDaySchedulesPerUser)), + mHolidaySchedules(numberOfHolidaySchedules) + { + for (auto & lockUser : mLockUsers) + { + lockUser.credentials.reserve(numberOfCredentialsPerUser); + } + DoorLockServer::Instance().SetDoorState(endpointId, mDoorState); + DoorLockServer::Instance().SetLockState(endpointId, mLockState); + } + + inline chip::EndpointId GetEndpointId() const { return mEndpointId; } + + bool Lock(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + bool Unlock(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + bool Unbolt(const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + + bool GetUser(uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) const; + bool SetUser(uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, const chip::CharSpan & userName, + uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, CredentialRuleEnum credentialRule, + const CredentialStruct * credentials, size_t totalCredentials); + + bool SetDoorState(DoorStateEnum newState); + + DoorStateEnum GetDoorState() const; + + bool SendLockAlarm(AlarmCodeEnum alarmCode) const; + + bool GetCredential(uint16_t credentialIndex, CredentialTypeEnum credentialType, + EmberAfPluginDoorLockCredentialInfo & credential) const; + + bool SetCredential(uint16_t credentialIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, + const chip::ByteSpan & credentialData); + + DlStatus GetSchedule(uint8_t weekDayIndex, uint16_t userIndex, EmberAfPluginDoorLockWeekDaySchedule & schedule); + DlStatus GetSchedule(uint8_t yearDayIndex, uint16_t userIndex, EmberAfPluginDoorLockYearDaySchedule & schedule); + DlStatus GetSchedule(uint8_t holidayIndex, EmberAfPluginDoorLockHolidaySchedule & schedule); + + DlStatus SetSchedule(uint8_t weekDayIndex, uint16_t userIndex, DlScheduleStatus status, DaysMaskMap daysMask, uint8_t startHour, + uint8_t startMinute, uint8_t endHour, uint8_t endMinute); + DlStatus SetSchedule(uint8_t yearDayIndex, uint16_t userIndex, DlScheduleStatus status, uint32_t localStartTime, + uint32_t localEndTime); + DlStatus SetSchedule(uint8_t holidayIndex, DlScheduleStatus status, uint32_t localStartTime, uint32_t localEndTime, + OperatingModeEnum operatingMode); + +private: + bool setLockState(const Nullable & fabricIdx, const Nullable & nodeId, DlLockState lockState, + const Optional & pin, OperationErrorEnum & err, + OperationSourceEnum opSource = OperationSourceEnum::kUnspecified); + const char * lockStateToString(DlLockState lockState) const; + + // Returns true if week day schedules should apply to the user, there are + // schedules defined for the user, and access is not currently allowed by + // those schedules. The outparam indicates whether there were in fact any + // year day schedules defined for the user. + bool weekDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const; + // Returns true if year day schedules should apply to the user, there are + // schedules defined for the user, and access is not currently allowed by + // those schedules. The outparam indicates whether there were in fact any + // year day schedules defined for the user. + bool yearDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const; + + static void OnLockActionCompleteCallback(chip::System::Layer *, void * callbackContext); + + chip::EndpointId mEndpointId; + DlLockState mLockState; + DoorStateEnum mDoorState; + + // This is very naive implementation of users/credentials/schedules database and by no means the best practice. Proper storage + // of those items is out of scope of this example. + std::vector mLockUsers; + std::vector> mLockCredentials; + std::vector> mWeekDaySchedules; + std::vector> mYearDaySchedules; + std::vector mHolidaySchedules; +}; + +struct LockCredentialInfo +{ + DlCredentialStatus status; + CredentialTypeEnum credentialType; + chip::FabricIndex createdBy; + chip::FabricIndex modifiedBy; + uint8_t credentialData[DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE]; + size_t credentialDataSize; +}; + +struct WeekDaysScheduleInfo +{ + DlScheduleStatus status; + EmberAfPluginDoorLockWeekDaySchedule schedule; +}; + +struct YearDayScheduleInfo +{ + DlScheduleStatus status; + EmberAfPluginDoorLockYearDaySchedule schedule; +}; + +struct HolidayScheduleInfo +{ + DlScheduleStatus status; + EmberAfPluginDoorLockHolidaySchedule schedule; +}; diff --git a/examples/chef/common/clusters/door-lock/chef-lock-manager.cpp b/examples/chef/common/clusters/door-lock/chef-lock-manager.cpp new file mode 100644 index 00000000000000..0b81cce895b264 --- /dev/null +++ b/examples/chef/common/clusters/door-lock/chef-lock-manager.cpp @@ -0,0 +1,376 @@ +/* + * + * Copyright (c) 2020-2024 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 + +#ifdef MATTER_DM_PLUGIN_DOOR_LOCK_SERVER +#include "chef-lock-manager.h" + +using chip::to_underlying; + +LockManager LockManager::instance; + +LockManager & LockManager::Instance() +{ + return instance; +} + +bool LockManager::InitEndpoint(chip::EndpointId endpointId) +{ + uint16_t numberOfSupportedUsers = 0; + if (!DoorLockServer::Instance().GetNumberOfUserSupported(endpointId, numberOfSupportedUsers)) + { + ChipLogError(Zcl, + "Unable to get number of supported users when initializing lock endpoint, defaulting to 10 [endpointId=%d]", + endpointId); + numberOfSupportedUsers = 10; + } + + uint16_t numberOfSupportedCredentials = 0; + // We're planning to use shared storage for PIN and RFID users so we will have the maximum of both sizes her to simplify logic + uint16_t numberOfPINCredentialsSupported = 0; + uint16_t numberOfRFIDCredentialsSupported = 0; + if (!DoorLockServer::Instance().GetNumberOfPINCredentialsSupported(endpointId, numberOfPINCredentialsSupported) || + !DoorLockServer::Instance().GetNumberOfRFIDCredentialsSupported(endpointId, numberOfRFIDCredentialsSupported)) + { + ChipLogError( + Zcl, "Unable to get number of supported credentials when initializing lock endpoint, defaulting to 10 [endpointId=%d]", + endpointId); + numberOfSupportedCredentials = 10; + } + else + { + numberOfSupportedCredentials = std::max(numberOfPINCredentialsSupported, numberOfRFIDCredentialsSupported); + } + + uint8_t numberOfCredentialsSupportedPerUser = 0; + if (!DoorLockServer::Instance().GetNumberOfCredentialsSupportedPerUser(endpointId, numberOfCredentialsSupportedPerUser)) + { + ChipLogError(Zcl, + "Unable to get number of credentials supported per user when initializing lock endpoint, defaulting to 5 " + "[endpointId=%d]", + endpointId); + numberOfCredentialsSupportedPerUser = 5; + } + + uint8_t numberOfWeekDaySchedulesPerUser = 0; + if (!DoorLockServer::Instance().GetNumberOfWeekDaySchedulesPerUserSupported(endpointId, numberOfWeekDaySchedulesPerUser)) + { + ChipLogError(Zcl, + "Unable to get number of supported week day schedules per user when initializing lock endpoint, defaulting to " + "10 [endpointId=%d]", + endpointId); + numberOfWeekDaySchedulesPerUser = 10; + } + + uint8_t numberOfYearDaySchedulesPerUser = 0; + if (!DoorLockServer::Instance().GetNumberOfYearDaySchedulesPerUserSupported(endpointId, numberOfYearDaySchedulesPerUser)) + { + ChipLogError(Zcl, + "Unable to get number of supported year day schedules per user when initializing lock endpoint, defaulting to " + "10 [endpointId=%d]", + endpointId); + numberOfYearDaySchedulesPerUser = 10; + } + + uint8_t numberOfHolidaySchedules = 0; + if (!DoorLockServer::Instance().GetNumberOfHolidaySchedulesSupported(endpointId, numberOfHolidaySchedules)) + { + ChipLogError( + Zcl, + "Unable to get number of supported holiday schedules when initializing lock endpoint, defaulting to 10 [endpointId=%d]", + endpointId); + numberOfHolidaySchedules = 10; + } + + mEndpoints.emplace_back(endpointId, numberOfSupportedUsers, numberOfSupportedCredentials, numberOfWeekDaySchedulesPerUser, + numberOfYearDaySchedulesPerUser, numberOfCredentialsSupportedPerUser, numberOfHolidaySchedules); + + // Refer to 5.2.10.34. SetUser Command, when Creat a new user record + // - UserIndex value SHALL be set to a user record with UserType set to Available + // - UserName MAY be null causing new user record to use empty string for UserName + // otherwise UserName SHALL be set to the value provided in the new user record. + // - UserUniqueID MAY be null causing new user record to use 0xFFFFFFFF for UserUniqueID + // otherwise UserUniqueID SHALL be set to the value provided in the new user record + // - UserStatus MAY be null causing new user record to use OccupiedEnabled for UserStatus + // otherwise UserStatus SHALL be set to the value provided in the new user record + // - UserType MAY be null causing new user record to use UnrestrictedUser for UserType + // otherwise UserType SHALL be set to the value provided in the new user record. + uint16_t userIndex(1); + chip::FabricIndex creator(1); + chip::FabricIndex modifier(1); + const chip::CharSpan userName = chip::CharSpan::fromCharString("user1"); // default + // username + uint32_t uniqueId = 0xFFFFFFFF; // null + UserStatusEnum userStatus = UserStatusEnum::kOccupiedEnabled; + // Set to programming user instead of unrestrict user to perform + // priviledged function + UserTypeEnum usertype = UserTypeEnum::kProgrammingUser; + CredentialRuleEnum credentialRule = CredentialRuleEnum::kSingle; + + constexpr size_t totalCredentials(2); + // According to spec (5.2.6.26.2. CredentialIndex Field), programming PIN credential should be always indexed as 0 + uint16_t credentialIndex0(0); + // 1st non ProgrammingPIN credential should be indexed as 1 + uint16_t credentialIndex1(1); + + const CredentialStruct credentials[totalCredentials] = { + { credentialType : CredentialTypeEnum::kProgrammingPIN, credentialIndex : credentialIndex0 }, + { credentialType : CredentialTypeEnum::kPin, credentialIndex : credentialIndex1 } + }; + + if (!SetUser(endpointId, userIndex, creator, modifier, userName, uniqueId, userStatus, usertype, credentialRule, + &credentials[0], totalCredentials)) + { + ChipLogError(Zcl, "Unable to set the User [endpointId=%d]", endpointId); + return false; + } + + DlCredentialStatus credentialStatus = DlCredentialStatus::kOccupied; + + // Set the default user's ProgrammingPIN credential + uint8_t defaultProgrammingPIN[6] = { 0x39, 0x39, 0x39, 0x39, 0x39, 0x39 }; // 000000 + if (!SetCredential(endpointId, credentialIndex0, creator, modifier, credentialStatus, CredentialTypeEnum::kProgrammingPIN, + chip::ByteSpan(defaultProgrammingPIN))) + { + ChipLogError(Zcl, "Unable to set the credential - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + + // Set the default user's non ProgrammingPIN credential + uint8_t defaultPin[6] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; // 123456 + if (!SetCredential(endpointId, credentialIndex1, creator, modifier, credentialStatus, CredentialTypeEnum::kPin, + chip::ByteSpan(defaultPin))) + { + ChipLogError(Zcl, "Unable to set the credential - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + + ChipLogProgress(Zcl, + "Initialized new lock door endpoint " + "[id=%d,users=%d,credentials=%d,weekDaySchedulesPerUser=%d,yearDaySchedulesPerUser=%d," + "numberOfCredentialsSupportedPerUser=%d,holidaySchedules=%d]", + endpointId, numberOfSupportedUsers, numberOfSupportedCredentials, numberOfWeekDaySchedulesPerUser, + numberOfYearDaySchedulesPerUser, numberOfCredentialsSupportedPerUser, numberOfHolidaySchedules); + + return true; +} + +bool LockManager::SetDoorState(chip::EndpointId endpointId, DoorStateEnum doorState) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to toggle the door state - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return false; + } + return lockEndpoint->SetDoorState(doorState); +} + +bool LockManager::SendLockAlarm(chip::EndpointId endpointId, AlarmCodeEnum alarmCode) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to send lock alarm - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->SendLockAlarm(alarmCode); +} + +bool LockManager::Lock(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, + OperationSourceEnum opSource) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to lock the door - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->Lock(fabricIdx, nodeId, pin, err, opSource); +} + +bool LockManager::Unlock(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, + OperationSourceEnum opSource) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to unlock the door - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->Unlock(fabricIdx, nodeId, pin, err, opSource); +} + +bool LockManager::Unbolt(chip::EndpointId endpointId, const Nullable & fabricIdx, + const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, + OperationSourceEnum opSource) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to unbolt the door - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->Unbolt(fabricIdx, nodeId, pin, err, opSource); +} + +bool LockManager::GetUser(chip::EndpointId endpointId, uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to get the user - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->GetUser(userIndex, user); +} + +bool LockManager::SetUser(chip::EndpointId endpointId, uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + const chip::CharSpan & userName, uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, + CredentialRuleEnum credentialRule, const CredentialStruct * credentials, size_t totalCredentials) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to set the user - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->SetUser(userIndex, creator, modifier, userName, uniqueId, userStatus, usertype, credentialRule, + credentials, totalCredentials); +} + +bool LockManager::GetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, CredentialTypeEnum credentialType, + EmberAfPluginDoorLockCredentialInfo & credential) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to get the credential - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->GetCredential(credentialIndex, credentialType, credential); +} + +bool LockManager::SetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator, + chip::FabricIndex modifier, DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, + const chip::ByteSpan & credentialData) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to set the credential - endpoint does not exist or not initialized [endpointId=%d]", endpointId); + return false; + } + return lockEndpoint->SetCredential(credentialIndex, creator, modifier, credentialStatus, credentialType, credentialData); +} + +DlStatus LockManager::GetSchedule(chip::EndpointId endpointId, uint8_t weekDayIndex, uint16_t userIndex, + EmberAfPluginDoorLockWeekDaySchedule & schedule) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to get the week day schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->GetSchedule(weekDayIndex, userIndex, schedule); +} + +DlStatus LockManager::SetSchedule(chip::EndpointId endpointId, uint8_t weekDayIndex, uint16_t userIndex, DlScheduleStatus status, + DaysMaskMap daysMask, uint8_t startHour, uint8_t startMinute, uint8_t endHour, uint8_t endMinute) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to set the week day schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->SetSchedule(weekDayIndex, userIndex, status, daysMask, startHour, startMinute, endHour, endMinute); +} + +DlStatus LockManager::GetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, + EmberAfPluginDoorLockYearDaySchedule & schedule) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to get the year day schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->GetSchedule(yearDayIndex, userIndex, schedule); +} + +DlStatus LockManager::SetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, DlScheduleStatus status, + uint32_t localStartTime, uint32_t localEndTime) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to set the year day schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->SetSchedule(yearDayIndex, userIndex, status, localStartTime, localEndTime); +} + +DlStatus LockManager::GetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, + EmberAfPluginDoorLockHolidaySchedule & schedule) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to get the holiday schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->GetSchedule(holidayIndex, schedule); +} + +DlStatus LockManager::SetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, DlScheduleStatus status, + uint32_t localStartTime, uint32_t localEndTime, OperatingModeEnum operatingMode) +{ + auto lockEndpoint = getEndpoint(endpointId); + if (nullptr == lockEndpoint) + { + ChipLogError(Zcl, "Unable to set the holiday schedule - endpoint does not exist or not initialized [endpointId=%d]", + endpointId); + return DlStatus::kFailure; + } + return lockEndpoint->SetSchedule(holidayIndex, status, localStartTime, localEndTime, operatingMode); +} + +LockEndpoint * LockManager::getEndpoint(chip::EndpointId endpointId) +{ + for (auto & mEndpoint : mEndpoints) + { + if (mEndpoint.GetEndpointId() == endpointId) + { + return &mEndpoint; + } + } + return nullptr; +} +#endif // MATTER_DM_PLUGIN_DOOR_LOCK_SERVER diff --git a/examples/chef/common/clusters/door-lock/chef-lock-manager.h b/examples/chef/common/clusters/door-lock/chef-lock-manager.h new file mode 100644 index 00000000000000..709f13c1addc66 --- /dev/null +++ b/examples/chef/common/clusters/door-lock/chef-lock-manager.h @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2020-2024 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. + */ + +#pragma once + +#include "chef-lock-endpoint.h" +#include +#include + +#include + +class LockManager +{ +public: + LockManager() = default; + + bool InitEndpoint(chip::EndpointId endpointId); + + bool SetDoorState(chip::EndpointId endpointId, DoorStateEnum doorState); + + bool SendLockAlarm(chip::EndpointId endpointId, AlarmCodeEnum alarmCode); + + bool Lock(chip::EndpointId endpointId, const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + bool Unlock(chip::EndpointId endpointId, const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + bool Unbolt(chip::EndpointId endpointId, const Nullable & fabricIdx, const Nullable & nodeId, + const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource); + + bool GetUser(chip::EndpointId endpointId, uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user); + bool SetUser(chip::EndpointId endpointId, uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + const chip::CharSpan & userName, uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, + CredentialRuleEnum credentialRule, const CredentialStruct * credentials, size_t totalCredentials); + + bool GetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, CredentialTypeEnum credentialType, + EmberAfPluginDoorLockCredentialInfo & credential); + + bool SetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator, chip::FabricIndex modifier, + DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, + const chip::ByteSpan & credentialData); + + DlStatus GetSchedule(chip::EndpointId endpointId, uint8_t weekDayIndex, uint16_t userIndex, + EmberAfPluginDoorLockWeekDaySchedule & schedule); + DlStatus GetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, + EmberAfPluginDoorLockYearDaySchedule & schedule); + DlStatus GetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, EmberAfPluginDoorLockHolidaySchedule & schedule); + + DlStatus SetSchedule(chip::EndpointId endpointId, uint8_t weekDayIndex, uint16_t userIndex, DlScheduleStatus status, + DaysMaskMap daysMask, uint8_t startHour, uint8_t startMinute, uint8_t endHour, uint8_t endMinute); + DlStatus SetSchedule(chip::EndpointId endpointId, uint8_t yearDayIndex, uint16_t userIndex, DlScheduleStatus status, + uint32_t localStartTime, uint32_t localEndTime); + DlStatus SetSchedule(chip::EndpointId endpointId, uint8_t holidayIndex, DlScheduleStatus status, uint32_t localStartTime, + uint32_t localEndTime, OperatingModeEnum operatingMode); + + static LockManager & Instance(); + +private: + LockEndpoint * getEndpoint(chip::EndpointId endpointId); + + std::vector mEndpoints; + + static LockManager instance; +}; diff --git a/examples/chef/common/stubs.cpp b/examples/chef/common/stubs.cpp index 48c48620c98fb0..91a7b5a84d10e1 100644 --- a/examples/chef/common/stubs.cpp +++ b/examples/chef/common/stubs.cpp @@ -111,238 +111,6 @@ Protocols::InteractionModel::Status emberAfExternalAttributeWriteCallback(Endpoi return Protocols::InteractionModel::Status::Success; } -// Include door lock callbacks only when the server is enabled -#ifdef MATTER_DM_PLUGIN_DOOR_LOCK_SERVER -#include - -class LockManager -{ -public: - static constexpr uint32_t kNumEndpoints = 1; - static constexpr uint32_t kNumUsersPerEndpoint = 2; - static constexpr uint32_t kNumCredentialsPerEndpoint = 20; - static constexpr uint32_t kNumCredentialsPerUser = 10; - static constexpr uint32_t kMaxNameLength = 32; - static constexpr uint32_t kMaxDataLength = 16; - - struct Credential - { - bool set(DlCredentialStatus status, CredentialTypeEnum type, chip::ByteSpan newData) - { - if (newData.size() > kMaxDataLength || type != CredentialTypeEnum::kPin) - return false; - memcpy(data, newData.data(), newData.size()); - info = EmberAfPluginDoorLockCredentialInfo{ - status, - type, - chip::ByteSpan(data, newData.size()), - }; - return true; - } - - EmberAfPluginDoorLockCredentialInfo info = { DlCredentialStatus::kAvailable }; - uint8_t data[kMaxDataLength]; - }; - - struct User - { - void set(chip::CharSpan newName, uint32_t userId, UserStatusEnum userStatus, UserTypeEnum type, - CredentialRuleEnum credentialRule) - { - size_t sz = std::min(sizeof(name), newName.size()); - memcpy(name, newName.data(), sz); - info = EmberAfPluginDoorLockUserInfo{ - chip::CharSpan(name, sz), chip::Span(), userId, userStatus, type, credentialRule, - }; - } - bool addCredential(CredentialTypeEnum type, uint16_t index) - { - if (info.credentials.size() == kNumCredentialsPerUser) - return false; - auto & cr = credentialMap[info.credentials.size()]; - cr.credentialType = type; - cr.credentialIndex = index; - info.credentials = chip::Span(credentialMap, info.credentials.size() + 1); - return true; - } - - EmberAfPluginDoorLockUserInfo info = { .userStatus = UserStatusEnum::kAvailable }; - char name[kMaxNameLength]; - CredentialStruct credentialMap[kNumCredentialsPerUser]; - }; - - struct Endpoint - { - chip::EndpointId id; - User users[kNumUsersPerEndpoint]; - Credential credentials[kNumCredentialsPerEndpoint]; - }; - - static LockManager & Instance() - { - static LockManager instance; - return instance; - } - - LockManager() { defaultInitialize(); } - - bool getUser(chip::EndpointId endpointId, uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) - { - auto ep = findEndpoint(endpointId); - if (!ep) - return false; - if (userIndex >= kNumUsersPerEndpoint) - return false; - user = ep->users[userIndex].info; - return true; - } - - bool setUser(chip::EndpointId endpointId, uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, - const chip::CharSpan & userName, uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, - CredentialRuleEnum credentialRule, const CredentialStruct * credentials, size_t totalCredentials) - { - auto ep = findEndpoint(endpointId); - if (!ep) - return false; - if (userIndex >= kNumUsersPerEndpoint || totalCredentials > kNumCredentialsPerUser) - return false; - ep->users[userIndex].set(userName, uniqueId, userStatus, usertype, credentialRule); - ep->users[userIndex].info.creationSource = DlAssetSource::kMatterIM; - ep->users[userIndex].info.createdBy = creator; - ep->users[userIndex].info.modificationSource = DlAssetSource::kMatterIM; - ep->users[userIndex].info.lastModifiedBy = modifier; - for (size_t i = 0; i < totalCredentials; i++) - ep->users[userIndex].addCredential(credentials[i].credentialType, credentials[i].credentialIndex); - return true; - } - - bool getCredential(chip::EndpointId endpointId, uint16_t credentialIndex, CredentialTypeEnum credentialType, - EmberAfPluginDoorLockCredentialInfo & credential) - { - auto ep = findEndpoint(endpointId); - if (!ep) - return false; - if (credentialIndex >= kNumCredentialsPerEndpoint) - return false; - if (credentialType != CredentialTypeEnum::kPin) - return false; - credential = ep->credentials[credentialIndex].info; - return true; - } - - bool setCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator, chip::FabricIndex modifier, - DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, - const chip::ByteSpan & credentialData) - { - auto ep = findEndpoint(endpointId); - if (!ep) - return false; - if (credentialIndex >= kNumCredentialsPerEndpoint) - return false; - if (credentialType != CredentialTypeEnum::kPin) - return false; - auto & credential = ep->credentials[credentialIndex]; - if (!credential.set(credentialStatus, credentialType, credentialData)) - return false; - credential.info.creationSource = DlAssetSource::kMatterIM; - credential.info.createdBy = creator; - credential.info.modificationSource = DlAssetSource::kMatterIM; - credential.info.lastModifiedBy = modifier; - return true; - } - - bool checkPin(chip::EndpointId endpointId, const chip::Optional & pinCode, - chip::app::Clusters::DoorLock::OperationErrorEnum & err) - { - if (!pinCode.HasValue()) - { - err = OperationErrorEnum::kInvalidCredential; - return false; - } - auto ep = findEndpoint(endpointId); - if (!ep) - return false; - for (auto & pin : ep->credentials) - { - if (pin.info.status == DlCredentialStatus::kOccupied && pin.info.credentialData.data_equal(pinCode.Value())) - { - return true; - } - } - err = OperationErrorEnum::kInvalidCredential; - return false; - } - -private: - Endpoint * findEndpoint(chip::EndpointId endpointId) - { - for (auto & e : endpoints) - { - if (e.id == endpointId) - return &e; - } - return nullptr; - } - - void defaultInitialize() - { - endpoints[0].id = 1; - uint8_t pin[6] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36 }; - endpoints[0].credentials[0].set(DlCredentialStatus::kOccupied, CredentialTypeEnum::kPin, chip::ByteSpan(pin)); - endpoints[0].users[0].set("default"_span, 1, UserStatusEnum::kOccupiedEnabled, UserTypeEnum::kUnrestrictedUser, - CredentialRuleEnum::kSingle); - endpoints[0].users[0].addCredential(CredentialTypeEnum::kPin, 1); - } - - Endpoint endpoints[kNumEndpoints]; -}; - -bool emberAfPluginDoorLockOnDoorLockCommand(chip::EndpointId endpointId, const Nullable & fabricIdx, - const Nullable & nodeId, const chip::Optional & pinCode, - chip::app::Clusters::DoorLock::OperationErrorEnum & err) -{ - err = OperationErrorEnum::kUnspecified; - return DoorLockServer::Instance().SetLockState(endpointId, DlLockState::kLocked); -} - -bool emberAfPluginDoorLockOnDoorUnlockCommand(chip::EndpointId endpointId, const Nullable & fabricIdx, - const Nullable & nodeId, const chip::Optional & pinCode, - chip::app::Clusters::DoorLock::OperationErrorEnum & err) -{ - err = OperationErrorEnum::kUnspecified; - return DoorLockServer::Instance().SetLockState(endpointId, DlLockState::kUnlocked); -} - -bool emberAfPluginDoorLockGetUser(chip::EndpointId endpointId, uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) -{ - return LockManager::Instance().getUser(endpointId, userIndex - 1, user); -} - -bool emberAfPluginDoorLockSetUser(chip::EndpointId endpointId, uint16_t userIndex, chip::FabricIndex creator, - chip::FabricIndex modifier, const chip::CharSpan & userName, uint32_t uniqueId, - UserStatusEnum userStatus, UserTypeEnum usertype, CredentialRuleEnum credentialRule, - const CredentialStruct * credentials, size_t totalCredentials) -{ - return LockManager::Instance().setUser(endpointId, userIndex - 1, creator, modifier, userName, uniqueId, userStatus, usertype, - credentialRule, credentials, totalCredentials); -} - -bool emberAfPluginDoorLockGetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, CredentialTypeEnum credentialType, - EmberAfPluginDoorLockCredentialInfo & credential) -{ - return LockManager::Instance().getCredential(endpointId, credentialIndex - 1, credentialType, credential); -} - -bool emberAfPluginDoorLockSetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator, - chip::FabricIndex modifier, DlCredentialStatus credentialStatus, - CredentialTypeEnum credentialType, const chip::ByteSpan & credentialData) -{ - return LockManager::Instance().setCredential(endpointId, credentialIndex - 1, creator, modifier, credentialStatus, - credentialType, credentialData); -} - -#endif /* MATTER_DM_PLUGIN_DOOR_LOCK_SERVER */ - void emberAfPluginSmokeCoAlarmSelfTestRequestCommand(EndpointId endpointId) {} void MatterPostAttributeChangeCallback(const chip::app::ConcreteAttributePath & attributePath, uint8_t type, uint16_t size, diff --git a/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.matter b/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.matter index 5819641c778086..bceebc611c7ca3 100644 --- a/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.matter +++ b/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.matter @@ -51,83 +51,6 @@ cluster Identify = 3 { command access(invoke: manage) TriggerEffect(TriggerEffectRequest): DefaultSuccess = 64; } -/** Attributes and commands for group configuration and manipulation. */ -cluster Groups = 4 { - revision 4; - - bitmap Feature : bitmap32 { - kGroupNames = 0x1; - } - - bitmap NameSupportBitmap : bitmap8 { - kGroupNames = 0x80; - } - - readonly attribute NameSupportBitmap nameSupport = 0; - readonly attribute command_id generatedCommandList[] = 65528; - readonly attribute command_id acceptedCommandList[] = 65529; - readonly attribute event_id eventList[] = 65530; - readonly attribute attrib_id attributeList[] = 65531; - readonly attribute bitmap32 featureMap = 65532; - readonly attribute int16u clusterRevision = 65533; - - request struct AddGroupRequest { - group_id groupID = 0; - char_string<16> groupName = 1; - } - - response struct AddGroupResponse = 0 { - enum8 status = 0; - group_id groupID = 1; - } - - request struct ViewGroupRequest { - group_id groupID = 0; - } - - response struct ViewGroupResponse = 1 { - enum8 status = 0; - group_id groupID = 1; - char_string<16> groupName = 2; - } - - request struct GetGroupMembershipRequest { - group_id groupList[] = 0; - } - - response struct GetGroupMembershipResponse = 2 { - nullable int8u capacity = 0; - group_id groupList[] = 1; - } - - request struct RemoveGroupRequest { - group_id groupID = 0; - } - - response struct RemoveGroupResponse = 3 { - enum8 status = 0; - group_id groupID = 1; - } - - request struct AddGroupIfIdentifyingRequest { - group_id groupID = 0; - char_string<16> groupName = 1; - } - - /** Command description for AddGroup */ - fabric command access(invoke: manage) AddGroup(AddGroupRequest): AddGroupResponse = 0; - /** Command description for ViewGroup */ - fabric command ViewGroup(ViewGroupRequest): ViewGroupResponse = 1; - /** Command description for GetGroupMembership */ - fabric command GetGroupMembership(GetGroupMembershipRequest): GetGroupMembershipResponse = 2; - /** Command description for RemoveGroup */ - fabric command access(invoke: manage) RemoveGroup(RemoveGroupRequest): RemoveGroupResponse = 3; - /** Command description for RemoveAllGroups */ - fabric command access(invoke: manage) RemoveAllGroups(): DefaultSuccess = 4; - /** Command description for AddGroupIfIdentifying */ - fabric command access(invoke: manage) AddGroupIfIdentifying(AddGroupIfIdentifyingRequest): DefaultSuccess = 5; -} - /** The Descriptor Cluster is meant to replace the support from the Zigbee Device Object (ZDO) for describing a node, its endpoints and clusters. */ cluster Descriptor = 29 { revision 2; @@ -161,27 +84,6 @@ cluster Descriptor = 29 { readonly attribute int16u clusterRevision = 65533; } -/** The Binding Cluster is meant to replace the support from the Zigbee Device Object (ZDO) for supporting the binding table. */ -cluster Binding = 30 { - revision 1; // NOTE: Default/not specifically set - - fabric_scoped struct TargetStruct { - optional node_id node = 1; - optional group_id group = 2; - optional endpoint_no endpoint = 3; - optional cluster_id cluster = 4; - fabric_idx fabricIndex = 254; - } - - attribute access(write: manage) TargetStruct binding[] = 0; - readonly attribute command_id generatedCommandList[] = 65528; - readonly attribute command_id acceptedCommandList[] = 65529; - readonly attribute event_id eventList[] = 65530; - readonly attribute attrib_id attributeList[] = 65531; - readonly attribute bitmap32 featureMap = 65532; - readonly attribute int16u clusterRevision = 65533; -} - /** The Access Control Cluster exposes a data model view of a Node's Access Control List (ACL), which codifies the rules used to manage and enforce Access Control for the Node's endpoints and their associated @@ -569,6 +471,265 @@ cluster TimeFormatLocalization = 44 { readonly attribute int16u clusterRevision = 65533; } +/** This cluster is used to describe the configuration and capabilities of a physical power source that provides power to the Node. */ +cluster PowerSource = 47 { + revision 1; // NOTE: Default/not specifically set + + enum BatApprovedChemistryEnum : enum16 { + kUnspecified = 0; + kAlkaline = 1; + kLithiumCarbonFluoride = 2; + kLithiumChromiumOxide = 3; + kLithiumCopperOxide = 4; + kLithiumIronDisulfide = 5; + kLithiumManganeseDioxide = 6; + kLithiumThionylChloride = 7; + kMagnesium = 8; + kMercuryOxide = 9; + kNickelOxyhydride = 10; + kSilverOxide = 11; + kZincAir = 12; + kZincCarbon = 13; + kZincChloride = 14; + kZincManganeseDioxide = 15; + kLeadAcid = 16; + kLithiumCobaltOxide = 17; + kLithiumIon = 18; + kLithiumIonPolymer = 19; + kLithiumIronPhosphate = 20; + kLithiumSulfur = 21; + kLithiumTitanate = 22; + kNickelCadmium = 23; + kNickelHydrogen = 24; + kNickelIron = 25; + kNickelMetalHydride = 26; + kNickelZinc = 27; + kSilverZinc = 28; + kSodiumIon = 29; + kSodiumSulfur = 30; + kZincBromide = 31; + kZincCerium = 32; + } + + enum BatChargeFaultEnum : enum8 { + kUnspecified = 0; + kAmbientTooHot = 1; + kAmbientTooCold = 2; + kBatteryTooHot = 3; + kBatteryTooCold = 4; + kBatteryAbsent = 5; + kBatteryOverVoltage = 6; + kBatteryUnderVoltage = 7; + kChargerOverVoltage = 8; + kChargerUnderVoltage = 9; + kSafetyTimeout = 10; + } + + enum BatChargeLevelEnum : enum8 { + kOK = 0; + kWarning = 1; + kCritical = 2; + } + + enum BatChargeStateEnum : enum8 { + kUnknown = 0; + kIsCharging = 1; + kIsAtFullCharge = 2; + kIsNotCharging = 3; + } + + enum BatCommonDesignationEnum : enum16 { + kUnspecified = 0; + kAAA = 1; + kAA = 2; + kC = 3; + kD = 4; + k4v5 = 5; + k6v0 = 6; + k9v0 = 7; + k12AA = 8; + kAAAA = 9; + kA = 10; + kB = 11; + kF = 12; + kN = 13; + kNo6 = 14; + kSubC = 15; + kA23 = 16; + kA27 = 17; + kBA5800 = 18; + kDuplex = 19; + k4SR44 = 20; + k523 = 21; + k531 = 22; + k15v0 = 23; + k22v5 = 24; + k30v0 = 25; + k45v0 = 26; + k67v5 = 27; + kJ = 28; + kCR123A = 29; + kCR2 = 30; + k2CR5 = 31; + kCRP2 = 32; + kCRV3 = 33; + kSR41 = 34; + kSR43 = 35; + kSR44 = 36; + kSR45 = 37; + kSR48 = 38; + kSR54 = 39; + kSR55 = 40; + kSR57 = 41; + kSR58 = 42; + kSR59 = 43; + kSR60 = 44; + kSR63 = 45; + kSR64 = 46; + kSR65 = 47; + kSR66 = 48; + kSR67 = 49; + kSR68 = 50; + kSR69 = 51; + kSR516 = 52; + kSR731 = 53; + kSR712 = 54; + kLR932 = 55; + kA5 = 56; + kA10 = 57; + kA13 = 58; + kA312 = 59; + kA675 = 60; + kAC41E = 61; + k10180 = 62; + k10280 = 63; + k10440 = 64; + k14250 = 65; + k14430 = 66; + k14500 = 67; + k14650 = 68; + k15270 = 69; + k16340 = 70; + kRCR123A = 71; + k17500 = 72; + k17670 = 73; + k18350 = 74; + k18500 = 75; + k18650 = 76; + k19670 = 77; + k25500 = 78; + k26650 = 79; + k32600 = 80; + } + + enum BatFaultEnum : enum8 { + kUnspecified = 0; + kOverTemp = 1; + kUnderTemp = 2; + } + + enum BatReplaceabilityEnum : enum8 { + kUnspecified = 0; + kNotReplaceable = 1; + kUserReplaceable = 2; + kFactoryReplaceable = 3; + } + + enum PowerSourceStatusEnum : enum8 { + kUnspecified = 0; + kActive = 1; + kStandby = 2; + kUnavailable = 3; + } + + enum WiredCurrentTypeEnum : enum8 { + kAC = 0; + kDC = 1; + } + + enum WiredFaultEnum : enum8 { + kUnspecified = 0; + kOverVoltage = 1; + kUnderVoltage = 2; + } + + bitmap Feature : bitmap32 { + kWired = 0x1; + kBattery = 0x2; + kRechargeable = 0x4; + kReplaceable = 0x8; + } + + struct BatChargeFaultChangeType { + BatChargeFaultEnum current[] = 0; + BatChargeFaultEnum previous[] = 1; + } + + struct BatFaultChangeType { + BatFaultEnum current[] = 0; + BatFaultEnum previous[] = 1; + } + + struct WiredFaultChangeType { + WiredFaultEnum current[] = 0; + WiredFaultEnum previous[] = 1; + } + + info event WiredFaultChange = 0 { + WiredFaultEnum current[] = 0; + WiredFaultEnum previous[] = 1; + } + + info event BatFaultChange = 1 { + BatFaultEnum current[] = 0; + BatFaultEnum previous[] = 1; + } + + info event BatChargeFaultChange = 2 { + BatChargeFaultEnum current[] = 0; + BatChargeFaultEnum previous[] = 1; + } + + readonly attribute PowerSourceStatusEnum status = 0; + readonly attribute int8u order = 1; + readonly attribute char_string<60> description = 2; + readonly attribute optional nullable int32u wiredAssessedInputVoltage = 3; + readonly attribute optional nullable int16u wiredAssessedInputFrequency = 4; + readonly attribute optional WiredCurrentTypeEnum wiredCurrentType = 5; + readonly attribute optional nullable int32u wiredAssessedCurrent = 6; + readonly attribute optional int32u wiredNominalVoltage = 7; + readonly attribute optional int32u wiredMaximumCurrent = 8; + readonly attribute optional boolean wiredPresent = 9; + readonly attribute optional WiredFaultEnum activeWiredFaults[] = 10; + readonly attribute optional nullable int32u batVoltage = 11; + readonly attribute optional nullable int8u batPercentRemaining = 12; + readonly attribute optional nullable int32u batTimeRemaining = 13; + readonly attribute optional BatChargeLevelEnum batChargeLevel = 14; + readonly attribute optional boolean batReplacementNeeded = 15; + readonly attribute optional BatReplaceabilityEnum batReplaceability = 16; + readonly attribute optional boolean batPresent = 17; + readonly attribute optional BatFaultEnum activeBatFaults[] = 18; + readonly attribute optional char_string<60> batReplacementDescription = 19; + readonly attribute optional BatCommonDesignationEnum batCommonDesignation = 20; + readonly attribute optional char_string<20> batANSIDesignation = 21; + readonly attribute optional char_string<20> batIECDesignation = 22; + readonly attribute optional BatApprovedChemistryEnum batApprovedChemistry = 23; + readonly attribute optional int32u batCapacity = 24; + readonly attribute optional int8u batQuantity = 25; + readonly attribute optional BatChargeStateEnum batChargeState = 26; + readonly attribute optional nullable int32u batTimeToFullCharge = 27; + readonly attribute optional boolean batFunctionalWhileCharging = 28; + readonly attribute optional nullable int32u batChargingCurrent = 29; + readonly attribute optional BatChargeFaultEnum activeBatChargeFaults[] = 30; + readonly attribute endpoint_no endpointList[] = 31; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute event_id eventList[] = 65530; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; +} + /** This cluster is used to manage global aspects of the Commissioning flow. */ cluster GeneralCommissioning = 48 { revision 1; // NOTE: Default/not specifically set @@ -2178,40 +2339,22 @@ endpoint 0 { } } endpoint 1 { + device type ma_powersource = 17, version 1; device type ma_doorlock = 10, version 1; - binding cluster Binding; server cluster Identify { ram attribute identifyTime default = 0x0; ram attribute identifyType default = 0x0; callback attribute generatedCommandList; callback attribute acceptedCommandList; + callback attribute eventList; callback attribute attributeList; ram attribute featureMap default = 0; ram attribute clusterRevision default = 2; handle command Identify; - } - - server cluster Groups { - ram attribute nameSupport default = 0; - callback attribute generatedCommandList; - callback attribute acceptedCommandList; - callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 3; - - handle command AddGroup; - handle command AddGroupResponse; - handle command ViewGroup; - handle command ViewGroupResponse; - handle command GetGroupMembership; - handle command GetGroupMembershipResponse; - handle command RemoveGroup; - handle command RemoveGroupResponse; - handle command RemoveAllGroups; - handle command AddGroupIfIdentifying; + handle command TriggerEffect; } server cluster Descriptor { @@ -2221,39 +2364,90 @@ endpoint 1 { callback attribute partsList; callback attribute generatedCommandList; callback attribute acceptedCommandList; + callback attribute eventList; callback attribute attributeList; callback attribute featureMap; callback attribute clusterRevision; } + server cluster PowerSource { + ram attribute status default = 1; + ram attribute order default = 1; + ram attribute description default = "Battery"; + ram attribute batVoltage; + ram attribute batPercentRemaining; + ram attribute batTimeRemaining; + ram attribute batChargeLevel; + ram attribute batReplacementNeeded; + ram attribute batReplaceability; + ram attribute batPresent; + ram attribute batReplacementDescription; + ram attribute batQuantity default = 1; + callback attribute endpointList; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute eventList; + callback attribute attributeList; + ram attribute featureMap default = 0x0A; + ram attribute clusterRevision default = 2; + } + server cluster DoorLock { emits event DoorLockAlarm; + emits event DoorStateChange; emits event LockOperation; emits event LockOperationError; + emits event LockUserChange; ram attribute lockState default = 1; ram attribute lockType default = 0; ram attribute actuatorEnabled default = 0; - ram attribute numberOfTotalUsersSupported default = 2; - ram attribute numberOfPINUsersSupported default = 2; - ram attribute maxPINCodeLength default = 10; - ram attribute minPINCodeLength default = 5; + ram attribute doorState default = 1; + ram attribute doorOpenEvents; + ram attribute doorClosedEvents; + ram attribute numberOfTotalUsersSupported default = 10; + ram attribute numberOfPINUsersSupported default = 10; + ram attribute numberOfRFIDUsersSupported default = 10; + ram attribute numberOfWeekDaySchedulesSupportedPerUser default = 10; + ram attribute numberOfYearDaySchedulesSupportedPerUser default = 10; + ram attribute numberOfHolidaySchedulesSupported default = 10; + ram attribute maxPINCodeLength default = 8; + ram attribute minPINCodeLength default = 6; + ram attribute maxRFIDCodeLength default = 20; + ram attribute minRFIDCodeLength default = 10; + ram attribute credentialRulesSupport default = 1; ram attribute numberOfCredentialsSupportedPerUser default = 5; - ram attribute autoRelockTime default = 0; + ram attribute language default = "en"; + ram attribute autoRelockTime default = 5; + ram attribute soundVolume default = 0; ram attribute operatingMode default = 0; - ram attribute supportedOperatingModes default = 0xFFF6; + ram attribute supportedOperatingModes default = 0xFFFF; + ram attribute enableOneTouchLocking default = 0; + ram attribute enablePrivacyModeButton default = 0; ram attribute wrongCodeEntryLimit default = 3; ram attribute userCodeTemporaryDisableTime default = 10; - ram attribute sendPINOverTheAir default = 0; - ram attribute requirePINforRemoteOperation default = 1; + ram attribute requirePINforRemoteOperation default = 0; callback attribute generatedCommandList; callback attribute acceptedCommandList; + callback attribute eventList; callback attribute attributeList; - ram attribute featureMap default = 0x0181; - ram attribute clusterRevision default = 6; + ram attribute featureMap default = 0x1DB3; + ram attribute clusterRevision default = 7; handle command LockDoor; handle command UnlockDoor; handle command UnlockWithTimeout; + handle command SetWeekDaySchedule; + handle command GetWeekDaySchedule; + handle command GetWeekDayScheduleResponse; + handle command ClearWeekDaySchedule; + handle command SetYearDaySchedule; + handle command GetYearDaySchedule; + handle command GetYearDayScheduleResponse; + handle command ClearYearDaySchedule; + handle command SetHolidaySchedule; + handle command GetHolidaySchedule; + handle command GetHolidayScheduleResponse; + handle command ClearHolidaySchedule; handle command SetUser; handle command GetUser; handle command GetUserResponse; @@ -2263,6 +2457,7 @@ endpoint 1 { handle command GetCredentialStatus; handle command GetCredentialStatusResponse; handle command ClearCredential; + handle command UnboltDoor; } } diff --git a/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.zap b/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.zap index 66f7adbb12fd02..1e9094f9d7a96e 100644 --- a/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.zap +++ b/examples/chef/devices/rootnode_doorlock_aNKYAreMXE.zap @@ -2480,13 +2480,21 @@ "profileId": 259, "label": "MA-doorlock", "name": "MA-doorlock" + }, + { + "code": 17, + "profileId": 259, + "label": "MA-powersource", + "name": "MA-powersource" } ], "deviceVersions": [ + 1, 1 ], "deviceIdentifiers": [ - 10 + 10, + 17 ], "deviceTypeName": "MA-doorlock", "deviceTypeCode": 10, @@ -2507,6 +2515,14 @@ "source": "client", "isIncoming": 1, "isEnabled": 1 + }, + { + "name": "TriggerEffect", + "code": 64, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 } ], "attributes": [ @@ -2574,6 +2590,22 @@ "maxInterval": 65534, "reportableChange": 0 }, + { + "name": "EventList", + "code": 65530, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "AttributeList", "code": 65531, @@ -2625,106 +2657,72 @@ ] }, { - "name": "Groups", - "code": 4, + "name": "Descriptor", + "code": 29, "mfgCode": null, - "define": "GROUPS_CLUSTER", + "define": "DESCRIPTOR_CLUSTER", "side": "server", "enabled": 1, - "commands": [ - { - "name": "AddGroup", - "code": 0, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 - }, + "attributes": [ { - "name": "AddGroupResponse", + "name": "DeviceTypeList", "code": 0, "mfgCode": null, - "source": "server", - "isIncoming": 0, - "isEnabled": 1 - }, - { - "name": "ViewGroup", - "code": 1, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 }, { - "name": "ViewGroupResponse", + "name": "ServerList", "code": 1, "mfgCode": null, - "source": "server", - "isIncoming": 0, - "isEnabled": 1 - }, - { - "name": "GetGroupMembership", - "code": 2, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 }, { - "name": "GetGroupMembershipResponse", + "name": "ClientList", "code": 2, "mfgCode": null, - "source": "server", - "isIncoming": 0, - "isEnabled": 1 - }, - { - "name": "RemoveGroup", - "code": 3, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 }, { - "name": "RemoveGroupResponse", + "name": "PartsList", "code": 3, "mfgCode": null, - "source": "server", - "isIncoming": 0, - "isEnabled": 1 - }, - { - "name": "RemoveAllGroups", - "code": 4, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 - }, - { - "name": "AddGroupIfIdentifying", - "code": 5, - "mfgCode": null, - "source": "client", - "isIncoming": 1, - "isEnabled": 1 - } - ], - "attributes": [ - { - "name": "NameSupport", - "code": 0, - "mfgCode": null, "side": "server", - "type": "NameSupportBitmap", + "type": "array", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "0", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -2762,6 +2760,22 @@ "maxInterval": 65534, "reportableChange": 0 }, + { + "name": "EventList", + "code": 65530, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "AttributeList", "code": 65531, @@ -2785,10 +2799,10 @@ "side": "server", "type": "bitmap32", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "0", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -2801,10 +2815,10 @@ "side": "server", "type": "int16u", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "3", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -2813,53 +2827,69 @@ ] }, { - "name": "Descriptor", - "code": 29, + "name": "Power Source", + "code": 47, "mfgCode": null, - "define": "DESCRIPTOR_CLUSTER", + "define": "POWER_SOURCE_CLUSTER", "side": "server", "enabled": 1, "attributes": [ { - "name": "DeviceTypeList", + "name": "Status", "code": 0, "mfgCode": null, "side": "server", - "type": "array", + "type": "PowerSourceStatusEnum", "included": 1, - "storageOption": "External", + "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": null, + "defaultValue": "1", "reportable": 1, "minInterval": 1, "maxInterval": 65534, "reportableChange": 0 }, { - "name": "ServerList", + "name": "Order", "code": 1, "mfgCode": null, "side": "server", - "type": "array", + "type": "int8u", "included": 1, - "storageOption": "External", + "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": null, + "defaultValue": "1", "reportable": 1, "minInterval": 1, "maxInterval": 65534, "reportableChange": 0 }, { - "name": "ClientList", + "name": "Description", "code": 2, "mfgCode": null, "side": "server", - "type": "array", + "type": "char_string", "included": 1, - "storageOption": "External", + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "Battery", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatVoltage", + "code": 11, + "mfgCode": null, + "side": "server", + "type": "int32u", + "included": 1, + "storageOption": "RAM", "singleton": 0, "bounded": 0, "defaultValue": null, @@ -2869,8 +2899,136 @@ "reportableChange": 0 }, { - "name": "PartsList", - "code": 3, + "name": "BatPercentRemaining", + "code": 12, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatTimeRemaining", + "code": 13, + "mfgCode": null, + "side": "server", + "type": "int32u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatChargeLevel", + "code": 14, + "mfgCode": null, + "side": "server", + "type": "BatChargeLevelEnum", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatReplacementNeeded", + "code": 15, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatReplaceability", + "code": 16, + "mfgCode": null, + "side": "server", + "type": "BatReplaceabilityEnum", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatPresent", + "code": 17, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatReplacementDescription", + "code": 19, + "mfgCode": null, + "side": "server", + "type": "char_string", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "BatQuantity", + "code": 25, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EndpointList", + "code": 31, "mfgCode": null, "side": "server", "type": "array", @@ -2917,8 +3075,8 @@ "reportableChange": 0 }, { - "name": "AttributeList", - "code": 65531, + "name": "EventList", + "code": 65530, "mfgCode": null, "side": "server", "type": "array", @@ -2933,11 +3091,11 @@ "reportableChange": 0 }, { - "name": "FeatureMap", - "code": 65532, + "name": "AttributeList", + "code": 65531, "mfgCode": null, "side": "server", - "type": "bitmap32", + "type": "array", "included": 1, "storageOption": "External", "singleton": 0, @@ -2949,42 +3107,32 @@ "reportableChange": 0 }, { - "name": "ClusterRevision", - "code": 65533, + "name": "FeatureMap", + "code": 65532, "mfgCode": null, "side": "server", - "type": "int16u", + "type": "bitmap32", "included": 1, - "storageOption": "External", + "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": null, + "defaultValue": "0x0A", "reportable": 1, "minInterval": 1, "maxInterval": 65534, "reportableChange": 0 - } - ] - }, - { - "name": "Binding", - "code": 30, - "mfgCode": null, - "define": "BINDING_CLUSTER", - "side": "client", - "enabled": 1, - "attributes": [ + }, { "name": "ClusterRevision", "code": 65533, "mfgCode": null, - "side": "client", + "side": "server", "type": "int16u", "included": 1, "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "1", + "defaultValue": "2", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3009,16 +3157,112 @@ "isEnabled": 1 }, { - "name": "UnlockDoor", - "code": 1, + "name": "UnlockDoor", + "code": 1, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "UnlockWithTimeout", + "code": 3, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SetWeekDaySchedule", + "code": 11, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetWeekDaySchedule", + "code": 12, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetWeekDayScheduleResponse", + "code": 12, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "ClearWeekDaySchedule", + "code": 13, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SetYearDaySchedule", + "code": 14, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetYearDaySchedule", + "code": 15, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetYearDayScheduleResponse", + "code": 15, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "ClearYearDaySchedule", + "code": 16, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SetHolidaySchedule", + "code": 17, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetHolidaySchedule", + "code": 18, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "GetHolidayScheduleResponse", + "code": 18, "mfgCode": null, - "source": "client", - "isIncoming": 1, + "source": "server", + "isIncoming": 0, "isEnabled": 1 }, { - "name": "UnlockWithTimeout", - "code": 3, + "name": "ClearHolidaySchedule", + "code": 19, "mfgCode": null, "source": "client", "isIncoming": 1, @@ -3095,6 +3339,14 @@ "source": "client", "isIncoming": 1, "isEnabled": 1 + }, + { + "name": "UnboltDoor", + "code": 39, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 } ], "attributes": [ @@ -3146,6 +3398,54 @@ "maxInterval": 65534, "reportableChange": 0 }, + { + "name": "DoorState", + "code": 3, + "mfgCode": null, + "side": "server", + "type": "DoorStateEnum", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "DoorOpenEvents", + "code": 4, + "mfgCode": null, + "side": "server", + "type": "int32u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "DoorClosedEvents", + "code": 5, + "mfgCode": null, + "side": "server", + "type": "int32u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "NumberOfTotalUsersSupported", "code": 17, @@ -3156,7 +3456,7 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "2", + "defaultValue": "10", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3172,7 +3472,71 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "2", + "defaultValue": "10", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "NumberOfRFIDUsersSupported", + "code": 19, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "10", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "NumberOfWeekDaySchedulesSupportedPerUser", + "code": 20, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "10", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "NumberOfYearDaySchedulesSupportedPerUser", + "code": 21, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "10", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "NumberOfHolidaySchedulesSupported", + "code": 22, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "10", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3188,7 +3552,7 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "10", + "defaultValue": "8", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3204,7 +3568,55 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "5", + "defaultValue": "6", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "MaxRFIDCodeLength", + "code": 25, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "20", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "MinRFIDCodeLength", + "code": 26, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "10", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "CredentialRulesSupport", + "code": 27, + "mfgCode": null, + "side": "server", + "type": "DlCredentialRuleMask", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3226,6 +3638,22 @@ "maxInterval": 65534, "reportableChange": 0 }, + { + "name": "Language", + "code": 33, + "mfgCode": null, + "side": "server", + "type": "char_string", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "en", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "AutoRelockTime", "code": 35, @@ -3236,6 +3664,22 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, + "defaultValue": "5", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "SoundVolume", + "code": 36, + "mfgCode": null, + "side": "server", + "type": "int8u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, "defaultValue": "0", "reportable": 1, "minInterval": 1, @@ -3268,7 +3712,39 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "0xFFF6", + "defaultValue": "0xFFFF", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EnableOneTouchLocking", + "code": 41, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EnablePrivacyModeButton", + "code": 43, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3307,8 +3783,8 @@ "reportableChange": 0 }, { - "name": "SendPINOverTheAir", - "code": 50, + "name": "RequirePINforRemoteOperation", + "code": 51, "mfgCode": null, "side": "server", "type": "boolean", @@ -3323,24 +3799,24 @@ "reportableChange": 0 }, { - "name": "RequirePINforRemoteOperation", - "code": 51, + "name": "GeneratedCommandList", + "code": 65528, "mfgCode": null, "side": "server", - "type": "boolean", + "type": "array", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "1", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, "reportableChange": 0 }, { - "name": "GeneratedCommandList", - "code": 65528, + "name": "AcceptedCommandList", + "code": 65529, "mfgCode": null, "side": "server", "type": "array", @@ -3355,8 +3831,8 @@ "reportableChange": 0 }, { - "name": "AcceptedCommandList", - "code": 65529, + "name": "EventList", + "code": 65530, "mfgCode": null, "side": "server", "type": "array", @@ -3396,7 +3872,7 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "0x0181", + "defaultValue": "0x1DB3", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3412,7 +3888,7 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "6", + "defaultValue": "7", "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -3427,6 +3903,13 @@ "side": "server", "included": 1 }, + { + "name": "DoorStateChange", + "code": 1, + "mfgCode": null, + "side": "server", + "included": 1 + }, { "name": "LockOperation", "code": 2, @@ -3440,6 +3923,13 @@ "mfgCode": null, "side": "server", "included": 1 + }, + { + "name": "LockUserChange", + "code": 4, + "mfgCode": null, + "side": "server", + "included": 1 } ] } @@ -3458,10 +3948,10 @@ { "endpointTypeName": "Anonymous Endpoint Type", "endpointTypeIndex": 1, - "profileId": 260, + "profileId": 259, "endpointId": 1, "networkId": 0, "parentEndpointIdentifier": null } ] -} \ No newline at end of file +} diff --git a/examples/chef/esp32/main/CMakeLists.txt b/examples/chef/esp32/main/CMakeLists.txt index 8278cf9a735675..75cf0b85222d95 100644 --- a/examples/chef/esp32/main/CMakeLists.txt +++ b/examples/chef/esp32/main/CMakeLists.txt @@ -63,14 +63,15 @@ set(SRC_DIRS_LIST ${SRC_DIRS_LIST} "${CMAKE_CURRENT_LIST_DIR}" "${CMAKE_SOURCE_DIR}/../common" - "${CMAKE_SOURCE_DIR}/../common/clusters/media-input/" + "${CMAKE_SOURCE_DIR}/../common/clusters/audio-output/" + "${CMAKE_SOURCE_DIR}/../common/clusters/channel/" + "${CMAKE_SOURCE_DIR}/../common/clusters/door-lock/" + "${CMAKE_SOURCE_DIR}/../common/clusters/keypad-input/" "${CMAKE_SOURCE_DIR}/../common/clusters/low-power/" + "${CMAKE_SOURCE_DIR}/../common/clusters/media-input/" "${CMAKE_SOURCE_DIR}/../common/clusters/media-playback/" "${CMAKE_SOURCE_DIR}/../common/clusters/target-navigator/" "${CMAKE_SOURCE_DIR}/../common/clusters/wake-on-lan/" - "${CMAKE_SOURCE_DIR}/../common/clusters/channel/" - "${CMAKE_SOURCE_DIR}/../common/clusters/keypad-input/" - "${CMAKE_SOURCE_DIR}/../common/clusters/audio-output/" "${CMAKE_SOURCE_DIR}/third_party/connectedhomeip/zzz_generated/app-common/app-common/zap-generated/attributes" "${CMAKE_SOURCE_DIR}/third_party/connectedhomeip/src/app/server" "${CMAKE_SOURCE_DIR}/third_party/connectedhomeip/src/app/util" diff --git a/examples/chef/linux/BUILD.gn b/examples/chef/linux/BUILD.gn index 0a4e2385f28dda..3cf4bdc7dd5d98 100644 --- a/examples/chef/linux/BUILD.gn +++ b/examples/chef/linux/BUILD.gn @@ -49,6 +49,9 @@ executable("${sample_name}") { "${project_dir}/common/chef-rvc-operational-state-delegate.cpp", "${project_dir}/common/clusters/audio-output/AudioOutputManager.cpp", "${project_dir}/common/clusters/channel/ChannelManager.cpp", + "${project_dir}/common/clusters/door-lock/chef-doorlock-stubs.cpp", + "${project_dir}/common/clusters/door-lock/chef-lock-endpoint.cpp", + "${project_dir}/common/clusters/door-lock/chef-lock-manager.cpp", "${project_dir}/common/clusters/keypad-input/KeypadInputManager.cpp", "${project_dir}/common/clusters/low-power/LowPowerManager.cpp", "${project_dir}/common/clusters/media-input/MediaInputManager.cpp", diff --git a/examples/chef/nrfconnect/CMakeLists.txt b/examples/chef/nrfconnect/CMakeLists.txt index 25d663211b9f15..29b21817705afa 100644 --- a/examples/chef/nrfconnect/CMakeLists.txt +++ b/examples/chef/nrfconnect/CMakeLists.txt @@ -88,14 +88,17 @@ target_sources(app PRIVATE ${CHEF}/common/chef-resource-monitoring-delegates.cpp ${CHEF}/common/chef-rvc-mode-delegate.cpp ${CHEF}/common/chef-rvc-operational-state-delegate.cpp - ${CHEF}/common/clusters/media-input/MediaInputManager.cpp + ${CHEF}/common/clusters/audio-output/AudioOutputManager.cpp + ${CHEF}/common/clusters/channel/ChannelManager.cpp + ${CHEF}/common/clusters/door-lock/chef-doorlock-stubs.cpp + ${CHEF}/common/clusters/door-lock/chef-lock-endpoint.cpp + ${CHEF}/common/clusters/door-lock/chef-lock-manager.cpp + ${CHEF}/common/clusters/keypad-input/KeypadInputManager.cpp ${CHEF}/common/clusters/low-power/LowPowerManager.cpp + ${CHEF}/common/clusters/media-input/MediaInputManager.cpp ${CHEF}/common/clusters/media-playback/MediaPlaybackManager.cpp ${CHEF}/common/clusters/target-navigator/TargetNavigatorManager.cpp ${CHEF}/common/clusters/wake-on-lan/WakeOnLanManager.cpp - ${CHEF}/common/clusters/channel/ChannelManager.cpp - ${CHEF}/common/clusters/keypad-input/KeypadInputManager.cpp - ${CHEF}/common/clusters/audio-output/AudioOutputManager.cpp ${CHEF}/common/stubs.cpp ${CHEF}/nrfconnect/main.cpp ) From d86fa60d0fc71a8383771b85af8ab91879780dcb Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Fri, 15 Mar 2024 12:57:32 -0400 Subject: [PATCH 65/76] Set more than one DST offset in MTRDevice if the server supports that. (#32578) This way when the next DST transition happens, the device won't be confused about the local time. --- src/darwin/Framework/CHIP/MTRDevice.mm | 71 ++++++++++--------- .../Framework/CHIP/MTRDeviceController.mm | 51 ++++++------- src/darwin/Framework/CHIP/MTRTimeUtils.h | 34 +++++++++ src/darwin/Framework/CHIP/MTRTimeUtils.mm | 67 +++++++++++++++++ .../Framework/CHIPTests/MTRDSTOffsetTests.m | 57 +++++++++++++++ .../Matter.xcodeproj/project.pbxproj | 12 ++++ 6 files changed, 231 insertions(+), 61 deletions(-) create mode 100644 src/darwin/Framework/CHIP/MTRTimeUtils.h create mode 100644 src/darwin/Framework/CHIP/MTRTimeUtils.mm create mode 100644 src/darwin/Framework/CHIPTests/MTRDSTOffsetTests.m diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index a83528b74d53e8..98387bfde797fb 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -33,6 +33,7 @@ #import "MTRError_Internal.h" #import "MTREventTLVValueDecoder_Internal.h" #import "MTRLogging_Internal.h" +#import "MTRTimeUtils.h" #import "MTRUnfairLock.h" #import "zap-generated/MTRCommandPayloads_Internal.h" @@ -270,41 +271,50 @@ - (void)_setTimeOnDevice return; } - NSTimeZone * localTimeZone = [NSTimeZone localTimeZone]; - BOOL setDST = TRUE; - if (!localTimeZone) { - MTR_LOG_ERROR("%@ Could not retrieve local time zone. Unable to setDSTOffset on endpoints.", self); - setDST = FALSE; - } - uint64_t matterEpochTimeMicroseconds = 0; - uint64_t nextDSTInMatterEpochTimeMicroseconds = 0; if (!DateToMatterEpochMicroseconds(now, matterEpochTimeMicroseconds)) { MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setUTCTime on endpoints.", self, now); return; } - int32_t dstOffset = 0; - if (setDST) { - NSTimeInterval dstOffsetAsInterval = [localTimeZone daylightSavingTimeOffsetForDate:now]; - dstOffset = int32_t(dstOffsetAsInterval); - - // Calculate time to next DST. This is needed when we set the current DST. - NSDate * nextDSTTransitionDate = [localTimeZone nextDaylightSavingTimeTransition]; - if (!DateToMatterEpochMicroseconds(nextDSTTransitionDate, nextDSTInMatterEpochTimeMicroseconds)) { - MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setDSTOffset on endpoints.", self, nextDSTTransitionDate); - setDST = FALSE; - } - } - // Set Time on each Endpoint with a Time Synchronization Cluster Server NSArray * endpointsToSync = [self _endpointsWithTimeSyncClusterServer]; for (NSNumber * endpoint in endpointsToSync) { MTR_LOG_DEBUG("%@ Setting Time on Endpoint %@", self, endpoint); [self _setUTCTime:matterEpochTimeMicroseconds withGranularity:MTRTimeSynchronizationGranularityMicrosecondsGranularity forEndpoint:endpoint]; - if (setDST) { - [self _setDSTOffset:dstOffset validStarting:0 validUntil:nextDSTInMatterEpochTimeMicroseconds forEndpoint:endpoint]; + + // Check how many DST offsets this endpoint supports. + auto dstOffsetsMaxSizePath = [MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeTimeSynchronizationID) attributeID:@(MTRAttributeIDTypeClusterTimeSynchronizationAttributeDSTOffsetListMaxSizeID)]; + auto dstOffsetsMaxSize = [self readAttributeWithEndpointID:dstOffsetsMaxSizePath.endpoint clusterID:dstOffsetsMaxSizePath.cluster attributeID:dstOffsetsMaxSizePath.attribute params:nil]; + if (dstOffsetsMaxSize == nil) { + // This endpoint does not support TZ, so won't support SetDSTOffset. + MTR_LOG_DEFAULT("%@ Unable to SetDSTOffset on endpoint %@, since it does not support the TZ feature", self, endpoint); + continue; } + auto attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{ + MTRAttributePathKey : dstOffsetsMaxSizePath, + MTRDataKey : dstOffsetsMaxSize, + } + error:nil]; + uint8_t maxOffsetCount; + if (attrReport == nil) { + MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is invalid. Defaulting to 1.", self, endpoint); + maxOffsetCount = 1; + } else { + NSNumber * maxOffsetCountAsNumber = attrReport.value; + maxOffsetCount = maxOffsetCountAsNumber.unsignedCharValue; + if (maxOffsetCount == 0) { + MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is 0, which is not allowed. Defaulting to 1.", self, endpoint); + maxOffsetCount = 1; + } + } + auto * dstOffsets = MTRComputeDSTOffsets(maxOffsetCount); + if (dstOffsets == nil) { + MTR_LOG_ERROR("%@ Could not retrieve DST offset information. Unable to setDSTOffset on endpoint %@.", self, endpoint); + continue; + } + + [self _setDSTOffsets:dstOffsets forEndpoint:endpoint]; } } @@ -410,23 +420,18 @@ - (void)_setUTCTime:(UInt64)matterEpochTime withGranularity:(uint8_t)granularity completion:setUTCTimeResponseHandler]; } -- (void)_setDSTOffset:(int32_t)dstOffset validStarting:(uint64_t)validStarting validUntil:(uint64_t)validUntil forEndpoint:(NSNumber *)endpoint +- (void)_setDSTOffsets:(NSArray *)dstOffsets forEndpoint:(NSNumber *)endpoint { - MTR_LOG_DEBUG("%@ _setDSTOffset with offset: %d, validStarting: %llu, validUntil: %llu, endpoint %@", - self, - dstOffset, validStarting, validUntil, endpoint); + MTR_LOG_DEBUG("%@ _setDSTOffsets with offsets: %@, endpoint %@", + self, dstOffsets, endpoint); MTRTimeSynchronizationClusterSetDSTOffsetParams * params = [[MTRTimeSynchronizationClusterSetDSTOffsetParams alloc] init]; - MTRTimeSynchronizationClusterDSTOffsetStruct * dstOffsetStruct = [[MTRTimeSynchronizationClusterDSTOffsetStruct alloc] init]; - dstOffsetStruct.offset = @(dstOffset); - dstOffsetStruct.validStarting = @(validStarting); - dstOffsetStruct.validUntil = @(validUntil); - params.dstOffset = @[ dstOffsetStruct ]; + params.dstOffset = dstOffsets; auto setDSTOffsetResponseHandler = ^(id _Nullable response, NSError * _Nullable error) { if (error) { - MTR_LOG_ERROR("%@ _setDSTOffset failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error); + MTR_LOG_ERROR("%@ _setDSTOffsets failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error); } }; diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index fa90eb2f542831..d40e269a753d96 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -44,6 +44,7 @@ #import "MTRPersistentStorageDelegateBridge.h" #import "MTRServerEndpoint_Internal.h" #import "MTRSetupPayload.h" +#import "MTRTimeUtils.h" #import "NSDataSpanConversion.h" #import "NSStringSpanConversion.h" #import @@ -737,40 +738,34 @@ - (BOOL)commissionNodeWithID:(NSNumber *)nodeID // to add, but in practice devices likely support only 2 and // AutoCommissioner caps the list at 10. Let's do up to 4 transitions // for now. + constexpr size_t dstOffsetMaxCount = 4; using DSTOffsetType = chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type; - - DSTOffsetType dstOffsets[4]; - size_t dstOffsetCount = 0; - auto nextOffset = tz.daylightSavingTimeOffset; - uint64_t nextValidStarting = 0; - auto * nextTransition = tz.nextDaylightSavingTimeTransition; - for (auto & dstOffset : dstOffsets) { - ++dstOffsetCount; - dstOffset.offset = static_cast(nextOffset); - dstOffset.validStarting = nextValidStarting; - if (nextTransition != nil) { - uint32_t transitionEpochS; - if (DateToMatterEpochSeconds(nextTransition, transitionEpochS)) { - using Microseconds64 = chip::System::Clock::Microseconds64; - using Seconds32 = chip::System::Clock::Seconds32; - dstOffset.validUntil.SetNonNull(Microseconds64(Seconds32(transitionEpochS)).count()); + // dstOffsets needs to live long enough, so its existence is not + // conditional on having offsets. + DSTOffsetType dstOffsets[dstOffsetMaxCount]; + + auto * offsets = MTRComputeDSTOffsets(dstOffsetMaxCount); + if (offsets != nil) { + size_t dstOffsetCount = 0; + for (MTRTimeSynchronizationClusterDSTOffsetStruct * offset in offsets) { + if (dstOffsetCount >= dstOffsetMaxCount) { + // Really shouldn't happen, but let's be extra careful about + // buffer overruns. + break; + } + auto & targetOffset = dstOffsets[dstOffsetCount]; + targetOffset.offset = offset.offset.intValue; + targetOffset.validStarting = offset.validStarting.unsignedLongLongValue; + if (offset.validUntil == nil) { + targetOffset.validUntil.SetNull(); } else { - // Out of range; treat as "forever". - dstOffset.validUntil.SetNull(); + targetOffset.validUntil.SetNonNull(offset.validUntil.unsignedLongLongValue); } - } else { - dstOffset.validUntil.SetNull(); - } - - if (dstOffset.validUntil.IsNull()) { - break; + ++dstOffsetCount; } - nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition]; - nextValidStarting = dstOffset.validUntil.Value(); - nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition]; + params.SetDSTOffsets(chip::app::DataModel::List(dstOffsets, dstOffsetCount)); } - params.SetDSTOffsets(chip::app::DataModel::List(dstOffsets, dstOffsetCount)); chip::NodeId deviceId = [nodeID unsignedLongLongValue]; self->_operationalCredentialsDelegate->SetDeviceID(deviceId); diff --git a/src/darwin/Framework/CHIP/MTRTimeUtils.h b/src/darwin/Framework/CHIP/MTRTimeUtils.h new file mode 100644 index 00000000000000..6295619535f11f --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRTimeUtils.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 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. + */ + +#import +#import +#import + +#import "MTRDefines_Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Utility method to compute up to the given count of instances of + * MTRTimeSynchronizationClusterDSTOffsetStruct and return them. + * + * Returns nil on errors. + */ +MTR_EXTERN MTR_TESTABLE + NSArray * _Nullable MTRComputeDSTOffsets(size_t maxCount); + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRTimeUtils.mm b/src/darwin/Framework/CHIP/MTRTimeUtils.mm new file mode 100644 index 00000000000000..87bc39b736f4cd --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRTimeUtils.mm @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2024 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. + */ + +#import "MTRTimeUtils.h" +#import "MTRConversion.h" +#import "MTRLogging_Internal.h" + +NSArray * _Nullable MTRComputeDSTOffsets(size_t maxCount) +{ + auto * tz = [NSTimeZone localTimeZone]; + if (!tz) { + MTR_LOG_ERROR("Could not retrieve local time zone. Unable to determine DST offsets."); + return nil; + } + + NSMutableArray * retval = [NSMutableArray arrayWithCapacity:maxCount]; + + auto nextOffset = tz.daylightSavingTimeOffset; + NSNumber * nextValidStarting = @(0); + auto * nextTransition = tz.nextDaylightSavingTimeTransition; + for (size_t offsetsAdded = 0; offsetsAdded < maxCount; ++offsetsAdded) { + auto offset = [[MTRTimeSynchronizationClusterDSTOffsetStruct alloc] init]; + offset.offset = @(nextOffset); + offset.validStarting = nextValidStarting; + if (nextTransition == nil) { + // This one is valid forever. + offset.validUntil = nil; + } else { + uint64_t nextTransitionEpochUs; + if (!DateToMatterEpochMicroseconds(nextTransition, nextTransitionEpochUs)) { + // Transition is somehow before Matter epoch start. This really + // should not happen, but if it does just don't pretend like we + // know what's going on with timezones here. + MTR_LOG_ERROR("Future daylight savings transition is before Matter epoch start?"); + return nil; + } + + offset.validUntil = @(nextTransitionEpochUs); + } + + [retval addObject:offset]; + + if (offset.validUntil == nil) { + // Valid forever, so no need for more offsets. + break; + } + + nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition]; + nextValidStarting = offset.validUntil; + nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition]; + } + + return [retval copy]; +} diff --git a/src/darwin/Framework/CHIPTests/MTRDSTOffsetTests.m b/src/darwin/Framework/CHIPTests/MTRDSTOffsetTests.m new file mode 100644 index 00000000000000..18c8866380b9bd --- /dev/null +++ b/src/darwin/Framework/CHIPTests/MTRDSTOffsetTests.m @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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. + */ + +// module headers +#import "MTRTimeUtils.h" +#import + +// system dependencies +#import + +@interface MTRDSTOffsetTests : XCTestCase +@end + +@implementation MTRDSTOffsetTests + +- (void)test001_SingleOffset +{ + __auto_type * offsets = MTRComputeDSTOffsets(1); + // We should be able to get offsets. + XCTAssertNotNil(offsets); + + // And there is always at least one, even if all it says is "no offset, forever". + XCTAssertEqual(offsets.count, 1); + XCTAssertEqualObjects(offsets[0].validStarting, @(0)); +} + +- (void)test002_TryGetting2Offsets +{ + __auto_type * offsets = MTRComputeDSTOffsets(2); + // We should be able to get offsets. + XCTAssertNotNil(offsets); + + // And there is always at least one, even if all it says is "no offset, + // forever". And we should not get too many offsets. + XCTAssertTrue(offsets.count >= 1); + XCTAssertTrue(offsets.count <= 2); + NSNumber * previousValidUntil = @(0); + for (MTRTimeSynchronizationClusterDSTOffsetStruct * offset in offsets) { + XCTAssertEqualObjects(previousValidUntil, offset.validStarting); + previousValidUntil = offset.validUntil; + } +} + +@end diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index 606d0f144544a9..305be3271b0372 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -200,6 +200,8 @@ 51B22C222740CB1D008D5055 /* MTRCommandPayloadsObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 51B22C212740CB1D008D5055 /* MTRCommandPayloadsObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51B22C262740CB32008D5055 /* MTRStructsObjc.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51B22C252740CB32008D5055 /* MTRStructsObjc.mm */; }; 51B22C2A2740CB47008D5055 /* MTRCommandPayloadsObjc.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51B22C292740CB47008D5055 /* MTRCommandPayloadsObjc.mm */; }; + 51C659D92BA3787500C54922 /* MTRTimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C659D72BA3787500C54922 /* MTRTimeUtils.h */; }; + 51C659DA2BA3787500C54922 /* MTRTimeUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51C659D82BA3787500C54922 /* MTRTimeUtils.mm */; }; 51C8E3F82825CDB600D47D00 /* MTRTestKeys.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C8E3F72825CDB600D47D00 /* MTRTestKeys.m */; }; 51C984622A61CE2A00B0AD9A /* MTRFabricInfoChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C984602A61CE2A00B0AD9A /* MTRFabricInfoChecker.m */; }; 51D0B1272B617246006E3511 /* MTRServerEndpoint.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51D0B1252B617246006E3511 /* MTRServerEndpoint.mm */; }; @@ -214,6 +216,7 @@ 51D0B1402B61B3A4006E3511 /* MTRServerCluster.h in Headers */ = {isa = PBXBuildFile; fileRef = 51D0B13E2B61B3A3006E3511 /* MTRServerCluster.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51D0B1412B61B3A4006E3511 /* MTRServerCluster.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51D0B13F2B61B3A3006E3511 /* MTRServerCluster.mm */; }; 51D10D2E2808E2CA00E8CA3D /* MTRTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 51D10D2D2808E2CA00E8CA3D /* MTRTestStorage.m */; }; + 51D9CB0B2BA37DCE0049D6DB /* MTRDSTOffsetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51D9CB0A2BA37DCE0049D6DB /* MTRDSTOffsetTests.m */; }; 51E0FC102ACBBF230001E197 /* MTRSwiftDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E0FC0F2ACBBF230001E197 /* MTRSwiftDeviceTests.swift */; }; 51E24E73274E0DAC007CCF6E /* MTRErrorTestUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51E24E72274E0DAC007CCF6E /* MTRErrorTestUtils.mm */; }; 51E51FBF282AD37A00FC978D /* MTRDeviceControllerStartupParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 51E51FBC282AD37A00FC978D /* MTRDeviceControllerStartupParams.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -600,6 +603,8 @@ 51B22C252740CB32008D5055 /* MTRStructsObjc.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRStructsObjc.mm; sourceTree = ""; }; 51B22C292740CB47008D5055 /* MTRCommandPayloadsObjc.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRCommandPayloadsObjc.mm; sourceTree = ""; }; 51B6C5C72AD85B47003F4D3A /* MTRCommandPayloads_Internal.zapt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MTRCommandPayloads_Internal.zapt; sourceTree = ""; }; + 51C659D72BA3787500C54922 /* MTRTimeUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRTimeUtils.h; sourceTree = ""; }; + 51C659D82BA3787500C54922 /* MTRTimeUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRTimeUtils.mm; sourceTree = ""; }; 51C8E3F72825CDB600D47D00 /* MTRTestKeys.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRTestKeys.m; sourceTree = ""; }; 51C984602A61CE2A00B0AD9A /* MTRFabricInfoChecker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRFabricInfoChecker.m; sourceTree = ""; }; 51C984612A61CE2A00B0AD9A /* MTRFabricInfoChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFabricInfoChecker.h; sourceTree = ""; }; @@ -615,6 +620,7 @@ 51D0B13E2B61B3A3006E3511 /* MTRServerCluster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerCluster.h; sourceTree = ""; }; 51D0B13F2B61B3A3006E3511 /* MTRServerCluster.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRServerCluster.mm; sourceTree = ""; }; 51D10D2D2808E2CA00E8CA3D /* MTRTestStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRTestStorage.m; sourceTree = ""; }; + 51D9CB0A2BA37DCE0049D6DB /* MTRDSTOffsetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRDSTOffsetTests.m; sourceTree = ""; }; 51E0FC0F2ACBBF230001E197 /* MTRSwiftDeviceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTRSwiftDeviceTests.swift; sourceTree = ""; }; 51E24E72274E0DAC007CCF6E /* MTRErrorTestUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRErrorTestUtils.mm; sourceTree = ""; }; 51E51FBC282AD37A00FC978D /* MTRDeviceControllerStartupParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStartupParams.h; sourceTree = ""; }; @@ -1320,6 +1326,8 @@ 2C8C8FBE253E0C2100797F05 /* MTRStorage.h */, 997DED172695344800975E97 /* MTRThreadOperationalDataset.h */, 997DED152695343400975E97 /* MTRThreadOperationalDataset.mm */, + 51C659D72BA3787500C54922 /* MTRTimeUtils.h */, + 51C659D82BA3787500C54922 /* MTRTimeUtils.mm */, 3D843710294977000070D20A /* NSDataSpanConversion.h */, 3D84370E294977000070D20A /* NSStringSpanConversion.h */, 5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */, @@ -1342,6 +1350,7 @@ 3DFCB3282966684500332B35 /* MTRCertificateInfoTests.m */, 99C65E0F267282F1003402F6 /* MTRControllerTests.m */, 5AE6D4E327A99041001F2493 /* MTRDeviceTests.m */, + 51D9CB0A2BA37DCE0049D6DB /* MTRDSTOffsetTests.m */, 3D0C484A29DA4FA0006D811F /* MTRErrorTests.m */, 5A6FEC9C27B5E48800F25F42 /* MTRXPCProtocolTests.m */, 5A7947DD27BEC3F500434CF2 /* MTRXPCListenerSampleTests.m */, @@ -1589,6 +1598,7 @@ 1EC4CE6425CC276600D7304F /* MTRBaseClusters.h in Headers */, 3D843712294977000070D20A /* MTRCallbackBridgeBase.h in Headers */, 3DECCB742934C21B00585AEC /* MTRDefines.h in Headers */, + 51C659D92BA3787500C54922 /* MTRTimeUtils.h in Headers */, 75139A702B7FE68C00E3A919 /* MTRDeviceControllerLocalTestStorage.h in Headers */, 2C5EEEF6268A85C400CAE3D3 /* MTRDeviceConnectionBridge.h in Headers */, 2C8C8FC0253E0C2100797F05 /* MTRPersistentStorageDelegateBridge.h in Headers */, @@ -1928,6 +1938,7 @@ 3DA1A3562ABAB3B4004F0BB9 /* MTRAsyncWorkQueue.mm in Sources */, 51D0B1272B617246006E3511 /* MTRServerEndpoint.mm in Sources */, 3DECCB722934AFE200585AEC /* MTRLogging.mm in Sources */, + 51C659DA2BA3787500C54922 /* MTRTimeUtils.mm in Sources */, 7596A84528762729004DAE0E /* MTRDevice.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1970,6 +1981,7 @@ 519498322A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m in Sources */, 51A2F1322A00402A00F03298 /* MTRDataValueParserTests.m in Sources */, 51E95DF82A78110900A434F0 /* MTRPerControllerStorageTests.m in Sources */, + 51D9CB0B2BA37DCE0049D6DB /* MTRDSTOffsetTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From d166650657bd49a84fbd4efe770199f59033a310 Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Fri, 15 Mar 2024 10:06:15 -0700 Subject: [PATCH 66/76] Disable QEMU ESP32 test until it's fixed (#32588) --- .github/workflows/qemu.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/qemu.yaml b/.github/workflows/qemu.yaml index c4992953780911..714b5b8782f1ae 100644 --- a/.github/workflows/qemu.yaml +++ b/.github/workflows/qemu.yaml @@ -58,6 +58,8 @@ jobs: build \ " - name: Run all tests + # Disabled being tracked here: https://github.com/project-chip/connectedhomeip/issues/32587 + if: false run: | src/test_driver/esp32/run_qemu_image.py \ --verbose \ From 3b1c825efab7825a6a29192537caa187a1bb02ca Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 15 Mar 2024 18:25:55 +0100 Subject: [PATCH 67/76] [Python] Add ruff as Python linter (#32384) * Add ruff as Python linter * Add ruff to linter workflow * Remove flake8 * Set default line-length and explicitly select rules * Address REPL Test issue * Explicitly set line lenght for autopep8 * Fix restyled config --- .flake8 | 6 ------ .github/workflows/lint.yml | 9 ++++----- .restyled.yaml | 2 ++ .../docker/images/base/chip-build/Dockerfile | 1 - ruff.toml | 19 +++++++++++++++++++ .../matter_idl/generators/java/__init__.py | 1 - .../matter_idl/generators/kotlin/__init__.py | 1 - .../matter_yamltests/parser.py | 4 ++-- .../wildcard_response_extractor_cluster.py | 2 +- .../generate_nrfconnect_chip_factory_data.py | 2 +- scripts/tools/silabs/FactoryDataProvider.py | 2 +- src/python_testing/TestSpecParsingSupport.py | 2 +- src/python_testing/spec_parsing_support.py | 8 ++++---- .../linux-cirque/helper/CHIPTestBase.py | 4 ++-- 14 files changed, 37 insertions(+), 26 deletions(-) delete mode 100644 .flake8 create mode 100644 ruff.toml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 65b1c2a93da0e8..00000000000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 132 -exclude = third_party - .* - out/* - ./examples/common/QRCode/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 70edd539eee0bf..8e67de5878ccf9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,7 +29,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build:35 + image: ghcr.io/project-chip/chip-build:39 steps: - name: Checkout @@ -267,12 +267,11 @@ jobs: run: | git grep -I -n 'emberAfWriteAttribute' -- './*' ':(exclude).github/workflows/lint.yml' ':(exclude)src/app/util/af.h' ':(exclude)zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp' ':(exclude)src/app/zap-templates/templates/app/attributes/Accessors-src.zapt' ':(exclude)src/app/util/attribute-table.cpp' ':(exclude)examples/common/pigweed/rpc_services/Attributes.h' ':(exclude)src/app/util/attribute-table.h' ':(exclude)src/app/util/ember-compatibility-functions.cpp' && exit 1 || exit 0 - # Run python Linter (flake8) and verify python files - # ignore some style errors, restyler should do that - - name: Check for errors using flake8 Python linter + # Run ruff python linter + - name: Check for errors using ruff Python linter if: always() run: | - flake8 --extend-ignore=E501,W391 + ruff check # git grep exits with 0 if it finds a match, but we want # to fail (exit nonzero) on match. And we want to exclude this file, diff --git a/.restyled.yaml b/.restyled.yaml index 56ed3061e6e1fa..9acbe8e5134aed 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -230,6 +230,8 @@ restylers: command: - autopep8 - "--in-place" + - "--max-line-length" + - "132" arguments: [] include: - "**/*.py" diff --git a/integrations/docker/images/base/chip-build/Dockerfile b/integrations/docker/images/base/chip-build/Dockerfile index 39aada6fec7af8..a2f83fc275a4b0 100644 --- a/integrations/docker/images/base/chip-build/Dockerfile +++ b/integrations/docker/images/base/chip-build/Dockerfile @@ -144,7 +144,6 @@ RUN set -x \ click \ coloredlogs \ cxxfilt \ - flake8 \ future \ ghapi \ mobly \ diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000000000..fec7608d707987 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,19 @@ +exclude = [ + ".environment", + ".git", + ".github", + ".*", + "build", + "out", + "third_party", + "examples/common/QRCode/", +] +target-version = "py37" + +line-length = 132 + +[lint] +select = ["E4", "E7", "E9", "F"] +ignore = [ + "E721" # We use is for good reasons +] diff --git a/scripts/py_matter_idl/matter_idl/generators/java/__init__.py b/scripts/py_matter_idl/matter_idl/generators/java/__init__.py index da7eef254f9fa7..96fa37c1a26445 100644 --- a/scripts/py_matter_idl/matter_idl/generators/java/__init__.py +++ b/scripts/py_matter_idl/matter_idl/generators/java/__init__.py @@ -114,7 +114,6 @@ def FieldToGlobalName(field: Field, context: TypeLookupContext) -> Optional[str] 'event_no': 'chip::EventNumber', 'fabric_id': 'chip::FabricId', 'fabric_idx': 'chip::FabricIndex', - 'fabric_idx': 'chip::FabricIndex', 'field_id': 'chip::FieldId', 'group_id': 'chip::GroupId', 'node_id': 'chip::NodeId', diff --git a/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py b/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py index b069319d7f586c..17e5136e169b6a 100644 --- a/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py +++ b/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py @@ -114,7 +114,6 @@ def FieldToGlobalName(field: Field, context: TypeLookupContext) -> Optional[str] 'event_no': 'chip::EventNumber', 'fabric_id': 'chip::FabricId', 'fabric_idx': 'chip::FabricIndex', - 'fabric_idx': 'chip::FabricIndex', 'field_id': 'chip::FieldId', 'group_id': 'chip::GroupId', 'node_id': 'chip::NodeId', diff --git a/scripts/py_matter_yamltests/matter_yamltests/parser.py b/scripts/py_matter_yamltests/matter_yamltests/parser.py index cb0a89c6418756..9612d5749d085b 100644 --- a/scripts/py_matter_yamltests/matter_yamltests/parser.py +++ b/scripts/py_matter_yamltests/matter_yamltests/parser.py @@ -1086,7 +1086,7 @@ def _response_values_validation(self, expected_response, received_response, resu break received_value = received_response.get('value') - if not self.is_attribute and not self.is_event and not (self.command in ANY_COMMANDS_LIST): + if not self.is_attribute and not self.is_event and self.command not in ANY_COMMANDS_LIST: expected_name = value.get('name') if expected_name not in received_value: result.error(check_type, error_name_does_not_exist.format( @@ -1173,7 +1173,7 @@ def _maybe_save_as(self, key: str, default_target: str, expected_response, recei continue received_value = received_response.get(default_target) - if not self.is_attribute and not self.is_event and not (self.command in ANY_COMMANDS_LIST): + if not self.is_attribute and not self.is_event and self.command not in ANY_COMMANDS_LIST: expected_name = value.get('name') if received_value is None or expected_name not in received_value: result.error(check_type, error_name_does_not_exist.format( diff --git a/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py b/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py index a0e34cc148d8ac..2f60a526bb6b16 100644 --- a/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py +++ b/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py @@ -113,7 +113,7 @@ def __get_argument(self, request, argument_name): if arguments is None: return None - if not type(arguments) is list: + if type(arguments) is not list: return None for argument in arguments: diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py index 7cddb766a90cca..724484058029d1 100644 --- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py +++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py @@ -246,7 +246,7 @@ def _validate_args(self): "Cannot find paths to DAC or PAI certificates .der files. To generate a new ones please provide a path to chip-cert executable (--chip_cert_path) and add --gen_certs argument" assert self._args.output.endswith(".json"), \ "Output path doesn't contain .json file path. ({})".format(self._args.output) - assert not (self._args.passcode in INVALID_PASSCODES), \ + assert self._args.passcode not in INVALID_PASSCODES, \ "Provided invalid passcode!" def generate_json(self): diff --git a/scripts/tools/silabs/FactoryDataProvider.py b/scripts/tools/silabs/FactoryDataProvider.py index 8cb489587cb122..3ff8ece3bf735f 100644 --- a/scripts/tools/silabs/FactoryDataProvider.py +++ b/scripts/tools/silabs/FactoryDataProvider.py @@ -140,7 +140,7 @@ def __init__(self, arguments) -> None: assert (bool(arguments.gen_spake2p_path) != bool(arguments.spake2_verifier) ), "Provide either the spake2_verifier string or the path to the spake2 generator" - assert not (arguments.passcode in INVALID_PASSCODES), "The provided passcode is invalid" + assert arguments.passcode not in INVALID_PASSCODES, "The provided passcode is invalid" self._args = arguments diff --git a/src/python_testing/TestSpecParsingSupport.py b/src/python_testing/TestSpecParsingSupport.py index 2ff80e55244648..2fdfcdcd40ef1f 100644 --- a/src/python_testing/TestSpecParsingSupport.py +++ b/src/python_testing/TestSpecParsingSupport.py @@ -262,7 +262,7 @@ def test_derived_clusters(self): 0, "Unexpected number of unknown commands in base") asserts.assert_equal(len(clusters[0xFFFF].unknown_commands), 2, "Unexpected number of unknown commands in derived cluster") - combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name) + combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name, problems) # Ensure the base-only attribute (1) was added to the derived cluster asserts.assert_equal(set(clusters[0xFFFF].attributes.keys()), set( [0, 1, 2, 3] + expected_global_attrs), "Unexpected attribute list") diff --git a/src/python_testing/spec_parsing_support.py b/src/python_testing/spec_parsing_support.py index 6a9d1f87ae3442..ac344638c0cdd1 100644 --- a/src/python_testing/spec_parsing_support.py +++ b/src/python_testing/spec_parsing_support.py @@ -487,7 +487,7 @@ def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocati clusters[action_id].accepted_commands[c].conformance = optional() remove_problem(CommandPathLocation(endpoint_id=0, cluster_id=action_id, command_id=c)) - combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name) + combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name, problems) for alias_base_name, aliased_clusters in CLUSTER_ALIASES.items(): for id, (alias_name, pics) in aliased_clusters.items(): @@ -547,10 +547,10 @@ def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocati return clusters, problems -def combine_derived_clusters_with_base(xml_clusters: dict[int, XmlCluster], pure_base_clusters: dict[str, XmlCluster], ids_by_name: dict[str, int]) -> None: +def combine_derived_clusters_with_base(xml_clusters: dict[int, XmlCluster], pure_base_clusters: dict[str, XmlCluster], ids_by_name: dict[str, int], problems: list[ProblemNotice]) -> None: ''' Overrides base elements with the derived cluster values for derived clusters. ''' - def combine_attributes(base: dict[uint, XmlAttribute], derived: dict[uint, XmlAttribute], cluster_id: uint) -> dict[uint, XmlAttribute]: + def combine_attributes(base: dict[uint, XmlAttribute], derived: dict[uint, XmlAttribute], cluster_id: uint, problems: list[ProblemNotice]) -> dict[uint, XmlAttribute]: ret = deepcopy(base) extras = {k: v for k, v in derived.items() if k not in base.keys()} overrides = {k: v for k, v in derived.items() if k in base.keys()} @@ -590,7 +590,7 @@ def combine_attributes(base: dict[uint, XmlAttribute], derived: dict[uint, XmlAt command_map.update(c.command_map) features = deepcopy(base.features) features.update(c.features) - attributes = combine_attributes(base.attributes, c.attributes, id) + attributes = combine_attributes(base.attributes, c.attributes, id, problems) accepted_commands = deepcopy(base.accepted_commands) accepted_commands.update(c.accepted_commands) generated_commands = deepcopy(base.generated_commands) diff --git a/src/test_driver/linux-cirque/helper/CHIPTestBase.py b/src/test_driver/linux-cirque/helper/CHIPTestBase.py index 036ba25a8538ff..7fd0c81e86f3ca 100644 --- a/src/test_driver/linux-cirque/helper/CHIPTestBase.py +++ b/src/test_driver/linux-cirque/helper/CHIPTestBase.py @@ -299,13 +299,13 @@ def assertTrue(self, exp, note=None): assert(Not)Equal python unittest style functions that raise exceptions when condition not met ''' - if not (exp is True): + if exp is not True: if note: self.logger.error(note) raise AssertionError def assertFalse(self, exp, note=None): - if not (exp is False): + if exp is not False: if note: self.logger.error(note) raise AssertionError From 1d6b13e54efe5a344c90b8ada8824f3e1178929b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 15 Mar 2024 18:27:06 +0100 Subject: [PATCH 68/76] Update RelativeHumidityMeasurement in Matter Linux Air Quality Example README.md (#32254) * Update Matter Linux Air Quality Example README.md * restyled * Update README.md * Update README.md * restyled --- examples/air-quality-sensor-app/linux/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/air-quality-sensor-app/linux/README.md b/examples/air-quality-sensor-app/linux/README.md index 651afa4c6297ee..b1c2e92461d8f0 100644 --- a/examples/air-quality-sensor-app/linux/README.md +++ b/examples/air-quality-sensor-app/linux/README.md @@ -136,10 +136,11 @@ $ echo '{"Name":"TemperatureMeasurement","NewValue":1800}' > /tmp/chip_air_quali ### Trigger Humidity change event -Generate event `RelativeHumidityMeasurement`, to change the temperate value. +Generate event `RelativeHumidityMeasurement`, to change the relative humidity +value (6000 for 60,0 %). ``` -$ echo '{"Name":"RelativeHumidityMeasurement","NewValue":60}' > /tmp/chip_air_quality_fifo_ +$ echo '{"Name":"RelativeHumidityMeasurement","NewValue":6000}' > /tmp/chip_air_quality_fifo_ ``` ### Trigger concentration change event From a0b2fe2d0b2e291d82238d37f678ca207716f080 Mon Sep 17 00:00:00 2001 From: Sharad Binjola <31142146+sharadb-amazon@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:13:31 -0700 Subject: [PATCH 69/76] Android tv-casting-app: simplified Endpoint API (#32552) --- examples/tv-casting-app/APIs.md | 142 +++++-- .../com/chip/casting/app/MainActivity.java | 20 +- .../util/PreferencesConfigurationManager.java | 153 -------- .../casting/ActionSelectorFragment.java | 97 +++++ .../casting/ConnectionExampleFragment.java | 101 ++--- ...ntentLauncherLaunchURLExampleFragment.java | 105 ++++++ .../casting/DiscoveryExampleFragment.java | 13 +- .../matter/casting/InitializationExample.java | 3 +- .../PreferencesConfigurationManager.java | 265 ++++++++++++++ .../com/matter/casting/core/CastingApp.java | 17 +- .../matter/casting/core/CastingAppState.java | 16 + .../matter/casting/core/CastingPlayer.java | 38 +- .../jni/com/matter/casting/core/Endpoint.java | 33 ++ .../casting/core/MatterCastingPlayer.java | 24 +- .../matter/casting/core/MatterEndpoint.java | 59 +++ .../casting/support/EndpointFilter.java | 2 + .../casting/support/MatterCallback.java | 34 ++ .../src/main/jni/cpp/core/CastingApp-JNI.cpp | 36 +- .../main/jni/cpp/core/CastingPlayer-JNI.cpp | 171 --------- .../cpp/core/CastingPlayerDiscovery-JNI.cpp | 39 +- .../jni/cpp/core/MatterCastingPlayer-JNI.cpp | 141 +++++++ .../jni/cpp/core/MatterCastingPlayer-JNI.h | 48 +++ .../main/jni/cpp/core/MatterEndpoint-JNI.cpp | 91 +++++ ...stingPlayer-JNI.h => MatterEndpoint-JNI.h} | 17 +- .../support/CastingPlayerConverter-JNI.cpp | 96 ----- .../cpp/support/CastingPlayerConverter-JNI.h | 41 --- .../main/jni/cpp/support/Converters-JNI.cpp | 346 ++++++++++++++++++ .../src/main/jni/cpp/support/Converters-JNI.h | 77 ++++ .../jni/cpp/support/ErrorConverter-JNI.cpp | 43 --- .../main/jni/cpp/support/ErrorConverter-JNI.h | 31 -- .../main/jni/cpp/support/MatterCallback-JNI.h | 95 +++++ .../RotatingDeviceIdUniqueIdProvider-JNI.cpp | 2 +- .../fragment_matter_action_selector.xml | 39 ++ ...ent_matter_content_launcher_launch_url.xml | 88 +++++ .../App/app/src/main/res/values/strings.xml | 3 + examples/tv-casting-app/android/BUILD.gn | 15 +- examples/tv-casting-app/android/args.gni | 2 + .../tv-casting-common/core/CastingPlayer.cpp | 10 +- .../support/CastingStore.cpp | 1 + .../support/EndpointListLoader.cpp | 24 +- 40 files changed, 1881 insertions(+), 697 deletions(-) delete mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/PreferencesConfigurationManager.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/PreferencesConfigurationManager.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/MatterCallback.java delete mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp rename examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/{CastingPlayer-JNI.h => MatterEndpoint-JNI.h} (66%) delete mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp delete mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h delete mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp delete mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.h create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_content_launcher_launch_url.xml diff --git a/examples/tv-casting-app/APIs.md b/examples/tv-casting-app/APIs.md index f254df1d7e2d14..d4a51dcc3146bf 100644 --- a/examples/tv-casting-app/APIs.md +++ b/examples/tv-casting-app/APIs.md @@ -34,15 +34,11 @@ samples so you can see the experience end to end. A Casting Client (e.g. a mobile phone app) is expected to be a Matter Commissionable Node and a `CastingPlayer` (i.e. a TV) is expected to be a Matter -Commissioner. In the context of the -[Matter Video Player architecture](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc), -a `CastingPlayer` would map to -[Casting "Video" Player](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc#1-introduction). -The `CastingPlayer` is expected to be hosting one or more `Endpoints` (some of -which can represent -[Content Apps](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc#1-introduction) -in the Matter Video Player architecture) that support one or more Matter Media -`Clusters`. +Commissioner. In the context of the Matter Video Player architecture, a +`CastingPlayer` would map to Casting "Video" Player. The `CastingPlayer` is +expected to be hosting one or more `Endpoints` (some of which can represent +Content Apps in the Matter Video Player architecture) that support one or more +Matter Media `Clusters`. The steps to start a casting session are: @@ -80,6 +76,8 @@ consume each platform's specific libraries. The libraries MUST be built with the client's specific values for `CHIP_DEVICE_CONFIG_DEVICE_VENDOR_ID` and `CHIP_DEVICE_CONFIG_DEVICE_PRODUCT_ID` updated in the [CHIPProjectAppConfig.h](tv-casting-common/include/CHIPProjectAppConfig.h) file. +Other values like the `CHIP_DEVICE_CONFIG_DEVICE_NAME` may be updated as well to +correspond to the client being built. ### Initialize the Casting Client @@ -91,10 +89,10 @@ A Casting Client must first initialize the Matter SDK and define the following `DataProvider` objects for the the Matter Casting library to use throughout the client's lifecycle: -1. **Rotating Device Identifier** - Refer to the Matter specification for - details on how to generate the - [Rotating Device Identifier](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/rendezvous/DeviceDiscovery.adoc#245-rotating-device-identifier)). - Then, instantiate a `DataProvider` object as described below. +1. **Rotating Device Identifier** - "This unique per-device identifier SHALL + consist of a randomly-generated 128-bit or longer octet string." Refer to + the Matter specification for more details. Instantiate a `DataProvider` + object as described below to provide this identifier. On Linux, define a `RotatingDeviceIdUniqueIdProvider` to provide the Casting Client's `RotatingDeviceIdUniqueId`, by implementing a @@ -152,10 +150,13 @@ client's lifecycle: ``` 2. **Commissioning Data** - This object contains the passcode, discriminator, - etc which identify the app and are provided to the `CastingPlayer` during - the commissioning process. Refer to the Matter specification's - [Onboarding Payload](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/qr_code/OnboardingPayload.adoc#ref_OnboardingPayload) - section for details on commissioning data. + etc. which identify the app and are provided to the `CastingPlayer` during + the commissioning process. "A Passcode SHALL be included as a 27-bit + unsigned integer, which serves as proof of possession during commissioning." + "A Discriminator SHALL be included as a 12-bit unsigned integer, which SHALL + match the value which a device advertises during commissioning." Refer to + the Matter specification's "Onboarding Payload" section for more details on + commissioning data. On Linux, define a function `InitCommissionableDataProvider` to initialize initialize a `LinuxCommissionableDataProvider` that can provide the required @@ -217,9 +218,8 @@ client's lifecycle: 3. **Device Attestation Credentials** - This object contains the `DeviceAttestationCertificate`, `ProductAttestationIntermediateCertificate`, etc. and implements a way to sign messages when called upon by the Matter TV - Casting Library as part of the - [Device Attestation process](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/device_attestation/Device_Attestation_Specification.adoc) - during commissioning. + Casting Library as part of the Matter Device Attestation process during + commissioning. On Linux, implement a define a `dacProvider` to provide the Casting Client's Device Attestation Credentials, by implementing a @@ -487,8 +487,8 @@ potentially skipping the longer commissioning process and instead, simply re-establishing the CASE session. This cache can be cleared by calling the `ClearCache` API on the `CastingApp`, say when the user signs out of the app. See API and its documentation for [Linux](tv-casting-common/core/CastingApp.h), -Android and -[iOS](darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h). +[Android](android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java) +and [iOS](darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h). ### Discover Casting Players @@ -702,10 +702,9 @@ Each `CastingPlayer` object created during [Discovery](#discover-casting-players) contains information such as `deviceName`, `vendorId`, `productId`, etc. which can help the user pick the right `CastingPlayer`. A Casting Client can attempt to connect to the -`selectedCastingPlayer` using -[Matter User Directed Commissioning (UDC)](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/rendezvous/UserDirectedCommissioning.adoc). -The Matter TV Casting library locally caches information required to reconnect -to a `CastingPlayer`, once the Casting client has been commissioned by it. After +`selectedCastingPlayer` using Matter User Directed Commissioning (UDC). The +Matter TV Casting library locally caches information required to reconnect to a +`CastingPlayer`, once the Casting client has been commissioned by it. After that, the Casting client is able to skip the full UDC process by establishing CASE with the `CastingPlayer` directly. Once connected, the `CastingPlayer` object will contain the list of available Endpoints on that `CastingPlayer`. @@ -743,6 +742,60 @@ targetCastingPlayer->VerifyOrEstablishConnection(ConnectionHandler, ... ``` +On Android, the Casting Client may call `verifyOrEstablishConnection` on the +`CastingPlayer` object it wants to connect to. + +```java +private static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; + +EndpointFilter desiredEndpointFilter = new EndpointFilter(); +desiredEndpointFilter.vendorId = DESIRED_ENDPOINT_VENDOR_ID; + +MatterError err = targetCastingPlayer.verifyOrEstablishConnection( + MIN_CONNECTION_TIMEOUT_SEC, + desiredEndpointFilter, + new MatterCallback() { + @Override + public void handle(Void v) { + Log.i( + TAG, + "Connected to CastingPlayer with deviceId: " + + targetCastingPlayer.getDeviceId()); + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Connected to Casting Player with device name: " + + targetCastingPlayer.getDeviceName() + + "\n\n"); + connectionFragmentNextButton.setEnabled(true); + }); + } + }, + new MatterCallback() { + @Override + public void handle(MatterError err) { + Log.e(TAG, "CastingPLayer connection failed: " + err); + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Casting Player connection failed due to: " + err + "\n\n"); + }); + } + }); + +if (err.hasError()) +{ + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Casting Player connection failed due to: " + err + "\n\n"); + }); +} +``` + On iOS, the Casting Client may call `verifyOrEstablishConnection` on the `MCCastingPlayer` object it wants to connect to and handle any `NSErrors` that may happen in the process. @@ -777,6 +830,8 @@ func connect(selectedCastingPlayer: MCCastingPlayer?) { ### Select an Endpoint on the Casting Player _{Complete Endpoint selection examples: [Linux](linux/simple-app-helper.cpp) | +[Android](android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java) +| [iOS](darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift)}_ On a successful connection with a `CastingPlayer`, a Casting Client may select @@ -803,6 +858,34 @@ if (it != endpoints.end()) } ``` +On Android, it can select an `Endpoint` as shown below. + +```java +private static final Integer SAMPLE_ENDPOINT_VID = 65521; + +private Endpoint selectEndpoint() +{ + Endpoint endpoint = null; + if(selectedCastingPlayer != null) + { + List endpoints = selectedCastingPlayer.getEndpoints(); + if (endpoints == null) + { + Log.e(TAG, "No Endpoints found on CastingPlayer"); + } + else + { + endpoint = endpoints + .stream() + .filter(e -> SAMPLE_ENDPOINT_VID.equals(e.getVendorId())) + .findFirst() + .get(); + } + } + return endpoint; +} +``` + On iOS, it can select an `MCEndpoint` similarly and as shown below. ```swift @@ -1045,11 +1128,8 @@ vendorIDAttribute!.read(nil) { context, before, after, err in ### Subscriptions -_{Complete Attribute subscription examples: -[Linux](linux/simple-app-helper.cpp)}_ - -_{Complete Attribute Read examples: [Linux](linux/simple-app-helper.cpp) | -[iOS](darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift)}_ +_{Complete Attribute subscription examples: [Linux](linux/simple-app-helper.cpp) +|[iOS](darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift)}_ A Casting Client may subscribe to an attribute on an `Endpoint` of the `CastingPlayer` to get data reports when the attributes change. diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java index 9db82781d855b8..6e54ff61b01c7e 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java @@ -10,10 +10,12 @@ import com.chip.casting.DiscoveredNodeData; import com.chip.casting.TvCastingApp; import com.chip.casting.util.GlobalCastingConstants; -import com.chip.casting.util.PreferencesConfigurationManager; +import com.matter.casting.ActionSelectorFragment; import com.matter.casting.ConnectionExampleFragment; +import com.matter.casting.ContentLauncherLaunchURLExampleFragment; import com.matter.casting.DiscoveryExampleFragment; import com.matter.casting.InitializationExample; +import com.matter.casting.PreferencesConfigurationManager; import com.matter.casting.core.CastingPlayer; import java.util.Random; @@ -22,7 +24,8 @@ public class MainActivity extends AppCompatActivity ConnectionFragment.Callback, SelectClusterFragment.Callback, DiscoveryExampleFragment.Callback, - ConnectionExampleFragment.Callback { + ConnectionExampleFragment.Callback, + ActionSelectorFragment.Callback { private static final String TAG = MainActivity.class.getSimpleName(); @@ -33,6 +36,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Log.i(TAG, "ChipCastingSimplified = " + GlobalCastingConstants.ChipCastingSimplified); boolean ret = GlobalCastingConstants.ChipCastingSimplified ? InitializationExample.initAndStart(this.getApplicationContext()).hasNoError() @@ -73,9 +77,12 @@ public void handleCommissioningComplete() { @Override public void handleConnectionComplete(CastingPlayer castingPlayer) { Log.i(TAG, "MainActivity.handleConnectionComplete() called "); + showFragment(ActionSelectorFragment.newInstance(castingPlayer)); + } - // TODO: Implement in following PRs. Select Cluster Fragment. - // showFragment(SelectClusterFragment.newInstance(tvCastingApp)); + @Override + public void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCastingPlayer) { + showFragment(ContentLauncherLaunchURLExampleFragment.newInstance(selectedCastingPlayer)); } @Override @@ -95,7 +102,10 @@ public void handleMediaPlaybackSelected() { @Override public void handleDisconnect() { - showFragment(CommissionerDiscoveryFragment.newInstance(tvCastingApp)); + showFragment( + GlobalCastingConstants.ChipCastingSimplified + ? DiscoveryExampleFragment.newInstance() + : CommissionerDiscoveryFragment.newInstance(tvCastingApp)); } /** diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/PreferencesConfigurationManager.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/PreferencesConfigurationManager.java deleted file mode 100644 index 5f7cb1cb7a2a16..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/util/PreferencesConfigurationManager.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2021-2022 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.chip.casting.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import chip.platform.AndroidChipPlatformException; -import chip.platform.ConfigurationManager; -import chip.platform.KeyValueStoreManager; -import java.util.Base64; -import java.util.Map; -import java.util.UUID; - -/** Java interface for ConfigurationManager */ -public class PreferencesConfigurationManager implements ConfigurationManager { - - private final String TAG = KeyValueStoreManager.class.getSimpleName(); - private SharedPreferences preferences; - - public PreferencesConfigurationManager(Context context, String preferenceFileKey) { - preferences = context.getSharedPreferences(preferenceFileKey, Context.MODE_PRIVATE); - - try { - String keyUniqueId = getKey(kConfigNamespace_ChipFactory, kConfigKey_UniqueId); - if (!preferences.contains(keyUniqueId)) { - preferences - .edit() - .putString(keyUniqueId, UUID.randomUUID().toString().replaceAll("-", "")) - .apply(); - } - } catch (AndroidChipPlatformException e) { - e.printStackTrace(); - } - } - - @Override - public long readConfigValueLong(String namespace, String name) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - if (preferences.contains(key)) { - long value = preferences.getLong(key, Long.MAX_VALUE); - return value; - } else { - Log.d(TAG, "Key '" + key + "' not found in shared preferences"); - throw new AndroidChipPlatformException(); - } - } - - @Override - public String readConfigValueStr(String namespace, String name) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - if (preferences.contains(key)) { - String value = preferences.getString(key, null); - return value; - } else { - Log.d(TAG, "Key '" + key + "' not found in shared preferences"); - throw new AndroidChipPlatformException(); - } - } - - @Override - public byte[] readConfigValueBin(String namespace, String name) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - if (preferences.contains(key)) { - String value = preferences.getString(key, null); - byte[] byteValue = Base64.getDecoder().decode(value); - return byteValue; - } else { - Log.d(TAG, "Key '" + key + "' not found in shared preferences"); - throw new AndroidChipPlatformException(); - } - } - - @Override - public void writeConfigValueLong(String namespace, String name, long val) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - preferences.edit().putLong(key, val).apply(); - } - - @Override - public void writeConfigValueStr(String namespace, String name, String val) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - preferences.edit().putString(key, val).apply(); - } - - @Override - public void writeConfigValueBin(String namespace, String name, byte[] val) - throws AndroidChipPlatformException { - String key = getKey(namespace, name); - if (val != null) { - String valStr = Base64.getEncoder().encodeToString(val); - preferences.edit().putString(key, valStr).apply(); - } else { - preferences.edit().remove(key).apply(); - } - } - - @Override - public void clearConfigValue(String namespace, String name) throws AndroidChipPlatformException { - if (namespace != null && name != null) { - preferences.edit().remove(getKey(namespace, name)).apply(); - } else if (namespace != null && name == null) { - String pre = getKey(namespace, null); - SharedPreferences.Editor editor = preferences.edit(); - Map allEntries = preferences.getAll(); - for (Map.Entry entry : allEntries.entrySet()) { - String key = entry.getKey(); - if (key.startsWith(pre)) { - editor.remove(key); - } - } - editor.apply(); - } else if (namespace == null && name == null) { - preferences.edit().clear().apply(); - } - } - - @Override - public boolean configValueExists(String namespace, String name) - throws AndroidChipPlatformException { - return preferences.contains(getKey(namespace, name)); - } - - private String getKey(String namespace, String name) throws AndroidChipPlatformException { - if (namespace != null && name != null) { - return namespace + ":" + name; - } else if (namespace != null && name == null) { - return namespace + ":"; - } - - throw new AndroidChipPlatformException(); - } -} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java new file mode 100644 index 00000000000000..2906ad186d6054 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ActionSelectorFragment.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 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.matter.casting; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.R; +import com.matter.casting.core.CastingPlayer; + +/** An interstitial {@link Fragment} to select one of the supported media actions to perform */ +public class ActionSelectorFragment extends Fragment { + private static final String TAG = ActionSelectorFragment.class.getSimpleName(); + + private final CastingPlayer selectedCastingPlayer; + + private View.OnClickListener selectContentLauncherButtonClickListener; + private View.OnClickListener disconnectButtonClickListener; + + public ActionSelectorFragment(CastingPlayer selectedCastingPlayer) { + this.selectedCastingPlayer = selectedCastingPlayer; + } + + /** + * Use this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param selectedCastingPlayer CastingPlayer that the casting app connected to + * @return A new instance of fragment SelectActionFragment. + */ + public static ActionSelectorFragment newInstance(CastingPlayer selectedCastingPlayer) { + return new ActionSelectorFragment(selectedCastingPlayer); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + ActionSelectorFragment.Callback callback = (ActionSelectorFragment.Callback) this.getActivity(); + this.selectContentLauncherButtonClickListener = + v -> { + Log.d(TAG, "handle() called on selectContentLauncherButtonClickListener"); + callback.handleContentLauncherLaunchURLSelected(selectedCastingPlayer); + }; + + this.disconnectButtonClickListener = + v -> { + Log.d(TAG, "Disconnecting from current casting player"); + selectedCastingPlayer.disconnect(); + callback.handleDisconnect(); + }; + + return inflater.inflate(R.layout.fragment_matter_action_selector, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Log.d(TAG, "ActionSelectorFragment.onViewCreated called"); + getView() + .findViewById(R.id.selectContentLauncherLaunchURLButton) + .setOnClickListener(selectContentLauncherButtonClickListener); + + getView().findViewById(R.id.disconnectButton).setOnClickListener(disconnectButtonClickListener); + } + + /** Interface for notifying the host. */ + public interface Callback { + /** Notifies listener to trigger transition on selection of Content Launcher cluster */ + void handleContentLauncherLaunchURLSelected(CastingPlayer selectedCastingPlayer); + + /** Notifies listener to trigger transition on click of the Disconnect button */ + void handleDisconnect(); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java index c6462cd52690e5..690a9b02e840bc 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java @@ -27,10 +27,9 @@ import androidx.fragment.app.Fragment; import com.R; import com.matter.casting.core.CastingPlayer; -import com.matter.casting.support.DeviceTypeStruct; import com.matter.casting.support.EndpointFilter; -import java.util.ArrayList; -import java.util.concurrent.CompletableFuture; +import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.MatterError; import java.util.concurrent.Executors; /** A {@link Fragment} to Verify or establish a connection with a selected Casting Player. */ @@ -39,12 +38,16 @@ public class ConnectionExampleFragment extends Fragment { // Time (in sec) to keep the commissioning window open, if commissioning is required. // Must be >= 3 minutes. private static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60; + private static final Integer DESIRED_ENDPOINT_VENDOR_ID = 65521; private final CastingPlayer targetCastingPlayer; private TextView connectionFragmentStatusTextView; private Button connectionFragmentNextButton; public ConnectionExampleFragment(CastingPlayer targetCastingPlayer) { - Log.i(TAG, "ConnectionExampleFragment() called with target CastingPlayer"); + Log.i( + TAG, + "ConnectionExampleFragment() called with target CastingPlayer ID: " + + targetCastingPlayer.getDeviceId()); this.targetCastingPlayer = targetCastingPlayer; } @@ -81,7 +84,11 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { connectionFragmentStatusTextView = getView().findViewById(R.id.connectionFragmentStatusText); connectionFragmentStatusTextView.setText( "Verifying or establishing connection with Casting Player with device name: " - + targetCastingPlayer.getDeviceName()); + + targetCastingPlayer.getDeviceName() + + "\nSetup Passcode: " + + InitializationExample.commissionableDataProvider.get().getSetupPasscode() + + "\nDiscriminator: " + + InitializationExample.commissionableDataProvider.get().getDiscriminator()); connectionFragmentNextButton = getView().findViewById(R.id.connectionFragmentNextButton); Callback callback = (ConnectionExampleFragment.Callback) this.getActivity(); @@ -94,48 +101,54 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { Executors.newSingleThreadExecutor() .submit( () -> { - Log.d(TAG, "onViewCreated() calling verifyOrEstablishConnection()"); + Log.d(TAG, "onViewCreated() calling CastingPlayer.verifyOrEstablishConnection()"); - EndpointFilter desiredEndpointFilter = - new EndpointFilter(null, 65521, new ArrayList()); - // The desired commissioning window timeout and EndpointFilter are optional. - CompletableFuture completableFuture = - targetCastingPlayer.VerifyOrEstablishConnection( - MIN_CONNECTION_TIMEOUT_SEC, desiredEndpointFilter); + EndpointFilter desiredEndpointFilter = new EndpointFilter(); + desiredEndpointFilter.vendorId = DESIRED_ENDPOINT_VENDOR_ID; - Log.d(TAG, "onViewCreated() verifyOrEstablishConnection() called"); - - completableFuture - .thenRun( - () -> { - Log.i( - TAG, - "CompletableFuture.thenRun(), connected to CastingPlayer with deviceId: " - + targetCastingPlayer.getDeviceId()); - getActivity() - .runOnUiThread( - () -> { - connectionFragmentStatusTextView.setText( - "Connected to Casting Player with device name: " - + targetCastingPlayer.getDeviceName()); - connectionFragmentNextButton.setEnabled(true); - }); - }) - .exceptionally( - exc -> { - Log.e( - TAG, - "CompletableFuture.exceptionally(), CastingPLayer connection failed: " - + exc.getMessage()); - getActivity() - .runOnUiThread( - () -> { - connectionFragmentStatusTextView.setText( - "Casting Player connection failed due to: " - + exc.getMessage()); - }); - return null; + MatterError err = + targetCastingPlayer.verifyOrEstablishConnection( + MIN_CONNECTION_TIMEOUT_SEC, + desiredEndpointFilter, + new MatterCallback() { + @Override + public void handle(Void v) { + Log.i( + TAG, + "Connected to CastingPlayer with deviceId: " + + targetCastingPlayer.getDeviceId()); + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Connected to Casting Player with device name: " + + targetCastingPlayer.getDeviceName() + + "\n\n"); + connectionFragmentNextButton.setEnabled(true); + }); + } + }, + new MatterCallback() { + @Override + public void handle(MatterError err) { + Log.e(TAG, "CastingPlayer connection failed: " + err); + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Casting Player connection failed due to: " + err + "\n\n"); + }); + } }); + + if (err.hasError()) { + getActivity() + .runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText( + "Casting Player connection failed due to: " + err + "\n\n"); + }); + } }); } diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java new file mode 100644 index 00000000000000..c1bde0f9bb0323 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ContentLauncherLaunchURLExampleFragment.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 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.matter.casting; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.R; +import com.matter.casting.core.CastingPlayer; +import com.matter.casting.core.Endpoint; +import java.util.List; + +/** A {@link Fragment} to send Content Launcher LaunchURL command from the TV Casting App. */ +public class ContentLauncherLaunchURLExampleFragment extends Fragment { + private static final String TAG = ContentLauncherLaunchURLExampleFragment.class.getSimpleName(); + private static final Integer SAMPLE_ENDPOINT_VID = 65521; + + private final CastingPlayer selectedCastingPlayer; + + private View.OnClickListener launchUrlButtonClickListener; + + public ContentLauncherLaunchURLExampleFragment(CastingPlayer selectedCastingPlayer) { + this.selectedCastingPlayer = selectedCastingPlayer; + } + + /** + * Use this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param selectedCastingPlayer CastingPlayer that the casting app connected to + * @return A new instance of fragment ContentLauncherLaunchURLExampleFragment. + */ + public static ContentLauncherLaunchURLExampleFragment newInstance( + CastingPlayer selectedCastingPlayer) { + return new ContentLauncherLaunchURLExampleFragment(selectedCastingPlayer); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + this.launchUrlButtonClickListener = + v -> { + Endpoint endpoint = selectEndpoint(); + if (endpoint == null) { + Log.e( + TAG, + "No Endpoint with chosen vendorID: " + + SAMPLE_ENDPOINT_VID + + " found on CastingPlayer"); + return; + } + + // TODO: add command invocation API call + }; + return inflater.inflate(R.layout.fragment_content_launcher, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Log.d(TAG, "ContentLauncherLaunchURLExampleFragment.onViewCreated called"); + getView().findViewById(R.id.launchUrlButton).setOnClickListener(launchUrlButtonClickListener); + } + + private Endpoint selectEndpoint() { + Endpoint endpoint = null; + if (selectedCastingPlayer != null) { + List endpoints = selectedCastingPlayer.getEndpoints(); + if (endpoints == null) { + Log.e(TAG, "No Endpoints found on CastingPlayer"); + } else { + endpoint = + endpoints + .stream() + .filter(e -> SAMPLE_ENDPOINT_VID.equals(e.getVendorId())) + .findFirst() + .get(); + } + } + return endpoint; + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java index 67db95be2637df..b847e17f5b7026 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java @@ -203,8 +203,13 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { @Override public void onResume() { + Log.i(TAG, "onResume() called"); super.onResume(); - Log.i(TAG, "onResume() called. Calling startDiscovery()"); + MatterError err = + matterCastingPlayerDiscovery.removeCastingPlayerChangeListener(castingPlayerChangeListener); + if (err.hasError()) { + Log.e(TAG, "onResume() removeCastingPlayerChangeListener() err: " + err); + } if (!startDiscovery()) { Log.e(TAG, "onResume() Warning: startDiscovery() call Failed"); } @@ -253,13 +258,9 @@ private boolean startDiscovery() { matterDiscoveryMessageTextView.setText( getString(R.string.matter_discovery_message_discovering_text)); - Log.d( - TAG, - "startDiscovery() text set to: " - + getString(R.string.matter_discovery_message_discovering_text)); // TODO: In following PRs. Enable this to auto-stop discovery after stopDiscovery is - // implemented in the core Matter SKD DNS-SD API. + // implemented in the core Matter SDK DNS-SD API. // Schedule a service to stop discovery and remove the CastingPlayerChangeListener // Safe to call if discovery is not running // scheduledFutureTask = diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java index 0d2135e1a7bc66..aa602c79a66a94 100644 --- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/InitializationExample.java @@ -19,7 +19,6 @@ import android.content.Context; import android.util.Log; import chip.platform.ConfigurationManager; -import com.chip.casting.util.PreferencesConfigurationManager; import com.matter.casting.core.CastingApp; import com.matter.casting.support.AppParameters; import com.matter.casting.support.CommissionableData; @@ -49,7 +48,7 @@ public byte[] get() { * DataProvider implementation for the Commissioning Data used by the SDK when the CastingApp goes * through commissioning */ - private static final DataProvider commissionableDataProvider = + static final DataProvider commissionableDataProvider = new DataProvider() { @Override public CommissionableData get() { diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/PreferencesConfigurationManager.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/PreferencesConfigurationManager.java new file mode 100644 index 00000000000000..1b6c4dfffe6e72 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/PreferencesConfigurationManager.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2021-2024 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.matter.casting; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import chip.platform.AndroidChipPlatformException; +import chip.platform.ConfigurationManager; +import chip.platform.KeyValueStoreManager; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; + +/** Java interface for ConfigurationManager */ +public class PreferencesConfigurationManager implements ConfigurationManager { + + private final String TAG = KeyValueStoreManager.class.getSimpleName(); + private SharedPreferences preferences; + + public PreferencesConfigurationManager(Context context, String preferenceFileKey) { + preferences = context.getSharedPreferences(preferenceFileKey, Context.MODE_PRIVATE); + + try { + String keyUniqueId = getKey(kConfigNamespace_ChipFactory, kConfigKey_UniqueId); + if (!preferences.contains(keyUniqueId)) { + preferences + .edit() + .putString(keyUniqueId, UUID.randomUUID().toString().replaceAll("-", "")) + .apply(); + } + } catch (AndroidChipPlatformException e) { + e.printStackTrace(); + } + } + + @Override + public long readConfigValueLong(String namespace, String name) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + switch (key) { + /** + * The unique id assigned by the device vendor to identify the product or device type. This + * number is scoped to the device vendor id. return a different value than + * src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_ProductId: + return 0x8003; + + /** + * The default hardware version number assigned to the device or product by the device + * vendor. + * + *

Hardware versions are specific to a particular device vendor and product id, and + * typically correspond to a revision of the physical device, a change to its packaging, + * and/or a change to its marketing presentation. This value is generally *not* incremented + * for device software revisions. + * + *

This is a default value which is used when a hardware version has not been stored in + * device persistent storage (e.g. by a factory provisioning process). + * + *

return a different value than src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_HardwareVersion: + return 1; + + /** + * A monothonic number identifying the software version running on the device. + * + *

return a different value than src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_SoftwareVersion: + return 1; + + /** Matter Casting Video Client has device type ID 41 (i.e. 0x0029) */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_DeviceTypeId: + return 41; + } + + if (preferences.contains(key)) { + long value = preferences.getLong(key, Long.MAX_VALUE); + return value; + } else { + Log.d(TAG, "Key '" + key + "' not found in shared preferences"); + throw new AndroidChipPlatformException(); + } + } + + @Override + public String readConfigValueStr(String namespace, String name) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + + switch (key) { + /** + * Human readable name of the device model. return a different value than + * src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_ProductName: + return "TEST_ANDROID_PRODUCT"; + + /** + * Human readable string identifying version of the product assigned by the device vendor. + * + *

return a different value than src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_HardwareVersionString: + return "TEST_ANDROID_VERSION"; + + /** + * A string identifying the software version running on the device. + * + *

return a different value than src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_SoftwareVersionString: + return "prerelease(android)"; + + /** + * The ManufacturingDate attribute SHALL specify the date that the Node was manufactured. + * The first 8 characters SHALL specify the date of manufacture of the Node in international + * date notation according to ISO 8601, i.e., YYYYMMDD, e.g., 20060814. The final 8 + * characters MAY include country, factory, line, shift or other related information at the + * option of the vendor. The format of this information is vendor defined. + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_ManufacturingDate: + return "2021-12-06"; + + /** + * Enables the use of a hard-coded default serial number if none * is found in Chip NV + * storage. + * + *

return a different value than src/include/platform/CHIPDeviceConfig.h for debug + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_SerialNum: + return "TEST_ANDROID_SN"; + + /** + * The PartNumber attribute SHALL specify a human-readable (displayable) vendor assigned + * part number for the Node whose meaning and numbering scheme is vendor defined. Multiple + * products (and hence PartNumbers) can share a ProductID. For instance, there may be + * different packaging (with different PartNumbers) for different regions; also different + * colors of a product might share the ProductID but may have a different PartNumber. + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_PartNumber: + return "TEST_ANDROID_PRODUCT_BLUE"; + + /** + * The ProductURL attribute SHALL specify a link to a product specific web page. The syntax + * of the ProductURL attribute SHALL follow the syntax as specified in RFC 3986. The + * specified URL SHOULD resolve to a maintained web page available for the lifetime of the + * product. The maximum length of the ProductUrl attribute is 256 ASCII characters. + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_ProductURL: + return "https://buildwithmatter.com/"; + + /** + * The ProductLabel attribute SHALL specify a vendor specific human readable (displayable) + * product label. The ProductLabel attribute MAY be used to provide a more user-friendly + * value than that represented by the ProductName attribute. The ProductLabel attribute + * SHOULD NOT include the name of the vendor as defined within the VendorName attribute. + */ + case kConfigNamespace_ChipFactory + ":" + kConfigKey_ProductLabel: + return "X10"; + } + + if (preferences.contains(key)) { + String value = preferences.getString(key, null); + return value; + } else { + Log.d(TAG, "Key '" + key + "' not found in shared preferences"); + throw new AndroidChipPlatformException(); + } + } + + @Override + public byte[] readConfigValueBin(String namespace, String name) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + if (preferences.contains(key)) { + String value = preferences.getString(key, null); + byte[] byteValue = Base64.getDecoder().decode(value); + return byteValue; + } else { + Log.d(TAG, "Key '" + key + "' not found in shared preferences"); + throw new AndroidChipPlatformException(); + } + } + + @Override + public void writeConfigValueLong(String namespace, String name, long val) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + preferences.edit().putLong(key, val).apply(); + } + + @Override + public void writeConfigValueStr(String namespace, String name, String val) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + preferences.edit().putString(key, val).apply(); + } + + @Override + public void writeConfigValueBin(String namespace, String name, byte[] val) + throws AndroidChipPlatformException { + String key = getKey(namespace, name); + if (val != null) { + String valStr = Base64.getEncoder().encodeToString(val); + preferences.edit().putString(key, valStr).apply(); + } else { + preferences.edit().remove(key).apply(); + } + } + + @Override + public void clearConfigValue(String namespace, String name) throws AndroidChipPlatformException { + if (namespace != null && name != null) { + preferences.edit().remove(getKey(namespace, name)).apply(); + } else if (namespace != null && name == null) { + String pre = getKey(namespace, null); + SharedPreferences.Editor editor = preferences.edit(); + Map allEntries = preferences.getAll(); + for (Map.Entry entry : allEntries.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(pre)) { + editor.remove(key); + } + } + editor.apply(); + } else if (namespace == null && name == null) { + preferences.edit().clear().apply(); + } + } + + @Override + public boolean configValueExists(String namespace, String name) + throws AndroidChipPlatformException { + return preferences.contains(getKey(namespace, name)); + } + + private String getKey(String namespace, String name) throws AndroidChipPlatformException { + if (namespace != null && name != null) { + return namespace + ":" + name; + } else if (namespace != null && name == null) { + return namespace + ":"; + } + + throw new AndroidChipPlatformException(); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java index 0823920503d9db..f79eab859a8ce3 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingApp.java @@ -37,6 +37,8 @@ */ public final class CastingApp { private static final String TAG = CastingApp.class.getSimpleName(); + private static final long BROWSE_SERVICE_TIMEOUT = 2500; + private static final long RESOLVE_SERVICE_TIMEOUT = 3000; private static CastingApp sInstance; @@ -75,8 +77,9 @@ public MatterError initialize(AppParameters appParameters) { new AndroidBleManager(), new PreferencesKeyValueStoreManager(appParameters.getApplicationContext()), appParameters.getConfigurationManagerProvider().get(), - new NsdManagerServiceResolver(applicationContext, nsdManagerResolverAvailState), - new NsdManagerServiceBrowser(applicationContext), + new NsdManagerServiceResolver( + applicationContext, nsdManagerResolverAvailState, RESOLVE_SERVICE_TIMEOUT), + new NsdManagerServiceBrowser(applicationContext, BROWSE_SERVICE_TIMEOUT), new ChipMdnsCallbackImpl(), new DiagnosticDataProviderImpl(applicationContext)); @@ -147,6 +150,16 @@ public MatterError stop() { return MatterError.NO_ERROR; } + /** @brief Tears down all active subscriptions. */ + public native MatterError shutdownAllSubscriptions(); + + /** + * Clears app cache that contains the information about CastingPlayers previously connected to + * + * @return + */ + public native MatterError clearCache(); + /** * Sets DeviceAttestationCrdentials provider and RotatingDeviceIdUniqueId * diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingAppState.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingAppState.java index 89ddef7aa0dbf6..9f2e952216f067 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingAppState.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingAppState.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024 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.matter.casting.core; /** Represents the state of the CastingApp */ diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java index 723f1b8e93b1a5..39db6488fa8ed8 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java @@ -17,9 +17,10 @@ package com.matter.casting.core; import com.matter.casting.support.EndpointFilter; +import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.MatterError; import java.net.InetAddress; import java.util.List; -import java.util.concurrent.CompletableFuture; /** * The CastingPlayer interface defines a Matter commissioner that is able to play media to a @@ -48,6 +49,8 @@ public interface CastingPlayer { long getDeviceType(); + List getEndpoints(); + @Override String toString(); @@ -68,27 +71,30 @@ public interface CastingPlayer { * that the client wants to interact with after commissioning. If this value is passed in, the * VerifyOrEstablishConnection will force User Directed Commissioning, in case the desired * Endpoint is not found in the on device CastingStore. - * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed. - * The CompletableFuture will be completed with a Void value if the - * VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be - * completed with an Exception. The Exception will be of type - * com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the - * CastingException will contain the error code and message from the CastingApp. + * @param successCallback called when the connection is established successfully + * @param failureCallback called with MatterError when the connection is fails to establish + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error */ - CompletableFuture VerifyOrEstablishConnection( - long commissioningWindowTimeoutSec, EndpointFilter desiredEndpointFilter); + MatterError verifyOrEstablishConnection( + long commissioningWindowTimeoutSec, + EndpointFilter desiredEndpointFilter, + MatterCallback successCallback, + MatterCallback failureCallback); /** * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on * disk, this will execute the user directed commissioning process. * - * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed. - * The CompletableFuture will be completed with a Void value if the - * VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be - * completed with an Exception. The Exception will be of type - * com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the - * CastingException will contain the error code and message from the CastingApp. + * @param successCallback called when the connection is established successfully + * @param failureCallback called with MatterError when the connection is fails to establish + * @return MatterError - Matter.NO_ERROR if request submitted successfully, otherwise a + * MatterError object corresponding to the error */ - CompletableFuture VerifyOrEstablishConnection(); + MatterError verifyOrEstablishConnection( + MatterCallback successCallback, MatterCallback failureCallback); + + /** @brief Sets the internal connection state of this CastingPlayer to "disconnected" */ + void disconnect(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java new file mode 100644 index 00000000000000..6d1b63555aad08 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/Endpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 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.matter.casting.core; + +import com.matter.casting.support.DeviceTypeStruct; +import java.util.List; + +public interface Endpoint { + int getId(); + + int getVendorId(); + + int getProductId(); + + List getDeviceTypeList(); + + CastingPlayer getCastingPlayer(); +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java index d5d93c3204ec34..dd4bd0ba6531c6 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,11 @@ package com.matter.casting.core; import com.matter.casting.support.EndpointFilter; +import com.matter.casting.support.MatterCallback; +import com.matter.casting.support.MatterError; import java.net.InetAddress; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; /** * A Matter Casting Player represents a Matter commissioner that is able to play media to a physical @@ -130,6 +131,9 @@ public long getDeviceType() { return this.deviceType; } + @Override + public native List getEndpoints(); + @Override public String toString() { return this.deviceId; @@ -167,8 +171,11 @@ public boolean equals(Object o) { * CastingException will contain the error code and message from the CastingApp. */ @Override - public native CompletableFuture VerifyOrEstablishConnection( - long commissioningWindowTimeoutSec, EndpointFilter desiredEndpointFilter); + public native MatterError verifyOrEstablishConnection( + long commissioningWindowTimeoutSec, + EndpointFilter desiredEndpointFilter, + MatterCallback successCallback, + MatterCallback failureCallback); /** * Verifies that a connection exists with this CastingPlayer, or triggers a new session request. @@ -183,7 +190,12 @@ public native CompletableFuture VerifyOrEstablishConnection( * CastingException will contain the error code and message from the CastingApp. */ @Override - public CompletableFuture VerifyOrEstablishConnection() { - return VerifyOrEstablishConnection(MIN_CONNECTION_TIMEOUT_SEC, null); + public MatterError verifyOrEstablishConnection( + MatterCallback successCallback, MatterCallback failureCallback) { + return verifyOrEstablishConnection( + MIN_CONNECTION_TIMEOUT_SEC, null, successCallback, failureCallback); } + + @Override + public native void disconnect(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java new file mode 100644 index 00000000000000..b9dd564d6ff95f --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterEndpoint.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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.matter.casting.core; + +import com.matter.casting.support.DeviceTypeStruct; +import java.util.List; +import java.util.Objects; + +public class MatterEndpoint implements Endpoint { + private static final String TAG = MatterEndpoint.class.getSimpleName(); + protected long _cppEndpoint; + + @Override + public native int getId(); + + @Override + public native int getVendorId(); + + @Override + public native int getProductId(); + + @Override + public native List getDeviceTypeList(); + + @Override + public native CastingPlayer getCastingPlayer(); + + @Override + public String toString() { + return "MatterEndpoint{" + "id=" + getId() + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MatterEndpoint that = (MatterEndpoint) o; + return getId() == that.getId(); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java index 1152e48b95890c..833c24fdc57544 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java @@ -26,6 +26,8 @@ public class EndpointFilter { public Integer vendorId; public List requiredDeviceTypes; + public EndpointFilter() {} + public EndpointFilter( Integer productId, Integer vendorId, List requiredDeviceTypes) { this.productId = productId; diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/MatterCallback.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/MatterCallback.java new file mode 100644 index 00000000000000..2d1f01dedb262d --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/MatterCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.matter.casting.support; + +import android.util.Log; + +public abstract class MatterCallback { + private static final String TAG = MatterCallback.class.getSimpleName(); + + public abstract void handle(R response); + + protected final void handleInternal(R response) { + try { + handle(response); + } catch (Throwable t) { + Log.e(TAG, "MatterCallback::Caught an unhandled Throwable from the client: " + t); + } + } +} diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp index e3a4c79b956822..81a42115070da7 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp @@ -19,7 +19,7 @@ #include "CastingApp-JNI.h" #include "../JNIDACProvider.h" -#include "../support/ErrorConverter-JNI.h" +#include "../support/Converters-JNI.h" #include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" // from tv-casting-common @@ -48,15 +48,15 @@ JNI_METHOD(jobject, finishInitialization)(JNIEnv *, jobject, jobject jAppParamet { chip::DeviceLayer::StackLock lock; ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::finishInitialization() called"); - VerifyOrReturnValue(jAppParameters != nullptr, support::createJMatterError(CHIP_ERROR_INVALID_ARGUMENT)); + VerifyOrReturnValue(jAppParameters != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); CHIP_ERROR err = CHIP_NO_ERROR; jobject jUniqueIdProvider = extractJAppParameter(jAppParameters, "getRotatingDeviceIdUniqueIdProvider", "()Lcom/matter/casting/support/DataProvider;"); - VerifyOrReturnValue(jUniqueIdProvider != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(jUniqueIdProvider != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); support::RotatingDeviceIdUniqueIdProviderJNI * uniqueIdProvider = new support::RotatingDeviceIdUniqueIdProviderJNI(); err = uniqueIdProvider->Initialize(jUniqueIdProvider); - VerifyOrReturnValue(err == CHIP_NO_ERROR, support::createJMatterError(CHIP_ERROR_INVALID_ARGUMENT)); + VerifyOrReturnValue(err == CHIP_NO_ERROR, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); // set the RotatingDeviceIdUniqueId #if CHIP_ENABLE_ROTATING_DEVICE_ID @@ -69,13 +69,13 @@ JNI_METHOD(jobject, finishInitialization)(JNIEnv *, jobject, jobject jAppParamet // get the DACProvider jobject jDACProvider = extractJAppParameter(jAppParameters, "getDacProvider", "()Lcom/matter/casting/support/DACProvider;"); - VerifyOrReturnValue(jDACProvider != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(jDACProvider != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); // set the DACProvider JNIDACProvider * dacProvider = new JNIDACProvider(jDACProvider); chip::Credentials::SetDeviceAttestationCredentialsProvider(dacProvider); - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } JNI_METHOD(jobject, finishStartup)(JNIEnv *, jobject) @@ -92,17 +92,35 @@ JNI_METHOD(jobject, finishStartup)(JNIEnv *, jobject) // Initialize binding handlers err = chip::BindingManager::GetInstance().Init( { &server.GetFabricTable(), server.GetCASESessionManager(), &server.GetPersistentStorage() }); - VerifyOrReturnValue(err == CHIP_NO_ERROR, support::createJMatterError(err), + VerifyOrReturnValue(err == CHIP_NO_ERROR, support::convertMatterErrorFromCppToJava(err), ChipLogError(AppServer, "Failed to init BindingManager %" CHIP_ERROR_FORMAT, err.Format())); // TODO: Set FabricDelegate // chip::Server::GetInstance().GetFabricTable().AddFabricDelegate(&mPersistenceManager); err = chip::DeviceLayer::PlatformMgrImpl().AddEventHandler(support::ChipDeviceEventHandler::Handle, 0); - VerifyOrReturnValue(err == CHIP_NO_ERROR, support::createJMatterError(err), + VerifyOrReturnValue(err == CHIP_NO_ERROR, support::convertMatterErrorFromCppToJava(err), ChipLogError(AppServer, "Failed to register ChipDeviceEventHandler %" CHIP_ERROR_FORMAT, err.Format())); - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); +} + +JNI_METHOD(jobject, shutdownAllSubscriptions)(JNIEnv * env, jobject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::shutdownAllSubscriptions called"); + + CHIP_ERROR err = matter::casting::core::CastingApp::GetInstance()->ShutdownAllSubscriptions(); + return support::convertMatterErrorFromCppToJava(err); +} + +JNI_METHOD(jobject, clearCache)(JNIEnv * env, jobject) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "JNI_METHOD CastingApp-JNI::clearCache called"); + + CHIP_ERROR err = matter::casting::core::CastingApp::GetInstance()->ClearCache(); + return support::convertMatterErrorFromCppToJava(err); } jobject extractJAppParameter(jobject jAppParameters, const char * methodName, const char * methodSig) diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp deleted file mode 100644 index ba5a8e765908f1..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2024 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 "CastingPlayer-JNI.h" - -#include "../JNIDACProvider.h" -#include "../support/CastingPlayerConverter-JNI.h" -#include "../support/ErrorConverter-JNI.h" -#include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" -#include "core/CastingApp.h" // from tv-casting-common -#include "core/CastingPlayer.h" // from tv-casting-common -#include "core/CastingPlayerDiscovery.h" // from tv-casting-common - -#include -#include -#include -#include -#include - -using namespace chip; - -#define JNI_METHOD(RETURN, METHOD_NAME) \ - extern "C" JNIEXPORT RETURN JNICALL Java_com_matter_casting_core_MatterCastingPlayer_##METHOD_NAME - -namespace matter { -namespace casting { -namespace core { - -JNI_METHOD(jobject, VerifyOrEstablishConnection) -(JNIEnv * env, jobject thiz, jlong commissioningWindowTimeoutSec, jobject desiredEndpointFilterJavaObject) -{ - chip::DeviceLayer::StackLock lock; - ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() called with a timeout of: %ld seconds", - static_cast(commissioningWindowTimeoutSec)); - - // Convert the CastingPlayer jlong to a CastingPlayer pointer - jclass castingPlayerClass = env->GetObjectClass(thiz); - jfieldID _cppCastingPlayerFieldId = env->GetFieldID(castingPlayerClass, "_cppCastingPlayer", "J"); - VerifyOrReturnValue( - _cppCastingPlayerFieldId != nullptr, nullptr, - ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() _cppCastingPlayerFieldId == nullptr")); - - jlong _cppCastingPlayerValue = env->GetLongField(thiz, _cppCastingPlayerFieldId); - CastingPlayer * castingPlayer = reinterpret_cast(_cppCastingPlayerValue); - VerifyOrReturnValue(castingPlayer != nullptr, nullptr, - ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() castingPlayer == nullptr")); - - // Create a new Java CompletableFuture - jclass completableFutureClass = env->FindClass("java/util/concurrent/CompletableFuture"); - jmethodID completableFutureConstructor = env->GetMethodID(completableFutureClass, "", "()V"); - jobject completableFutureObj = env->NewObject(completableFutureClass, completableFutureConstructor); - jobject completableFutureObjGlobalRef = env->NewGlobalRef(completableFutureObj); - VerifyOrReturnValue( - completableFutureObjGlobalRef != nullptr, nullptr, - ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() completableFutureObjGlobalRef == nullptr")); - - ConnectCallback callback = [completableFutureObjGlobalRef](CHIP_ERROR err, CastingPlayer * playerPtr) { - ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback called"); - VerifyOrReturn(completableFutureObjGlobalRef != nullptr, - ChipLogError(AppServer, "ConnectCallback, completableFutureObjGlobalRef == nullptr")); - - JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); - VerifyOrReturn(env != nullptr, ChipLogError(AppServer, "ConnectCallback, env == nullptr")); - // Ensures proper cleanup of local references to Java objects. - JniLocalReferenceScope scope(env); - // Ensures proper cleanup of global references to Java objects. - JniGlobalRefWrapper globalRefWrapper(completableFutureObjGlobalRef); - - jclass completableFutureClass = env->FindClass("java/util/concurrent/CompletableFuture"); - VerifyOrReturn(completableFutureClass != nullptr, - ChipLogError(AppServer, "ConnectCallback, completableFutureClass == nullptr"); - env->DeleteGlobalRef(completableFutureObjGlobalRef);); - - if (err == CHIP_NO_ERROR) - { - ChipLogProgress(AppServer, "ConnectCallback, Connected to Casting Player with device ID: %s", playerPtr->GetId()); - jmethodID completeMethod = env->GetMethodID(completableFutureClass, "complete", "(Ljava/lang/Object;)Z"); - VerifyOrReturn(completeMethod != nullptr, ChipLogError(AppServer, "ConnectCallback, completeMethod == nullptr")); - - chip::DeviceLayer::StackUnlock unlock; - env->CallBooleanMethod(completableFutureObjGlobalRef, completeMethod, nullptr); - } - else - { - ChipLogError(AppServer, "ConnectCallback, connection error: %" CHIP_ERROR_FORMAT, err.Format()); - jmethodID completeExceptionallyMethod = - env->GetMethodID(completableFutureClass, "completeExceptionally", "(Ljava/lang/Throwable;)Z"); - VerifyOrReturn(completeExceptionallyMethod != nullptr, - ChipLogError(AppServer, "ConnectCallback, completeExceptionallyMethod == nullptr")); - - // Create a Throwable object (e.g., RuntimeException) to pass to completeExceptionallyMethod - jclass throwableClass = env->FindClass("java/lang/RuntimeException"); - VerifyOrReturn(throwableClass != nullptr, ChipLogError(AppServer, "ConnectCallback, throwableClass == nullptr")); - jmethodID throwableConstructor = env->GetMethodID(throwableClass, "", "(Ljava/lang/String;)V"); - VerifyOrReturn(throwableConstructor != nullptr, - ChipLogError(AppServer, "ConnectCallback, throwableConstructor == nullptr")); - jstring errorMessage = env->NewStringUTF(err.Format()); - VerifyOrReturn(errorMessage != nullptr, ChipLogError(AppServer, "ConnectCallback, errorMessage == nullptr")); - jobject throwableObject = env->NewObject(throwableClass, throwableConstructor, errorMessage); - VerifyOrReturn(throwableObject != nullptr, ChipLogError(AppServer, "ConnectCallback, throwableObject == nullptr")); - - chip::DeviceLayer::StackUnlock unlock; - env->CallBooleanMethod(completableFutureObjGlobalRef, completeExceptionallyMethod, throwableObject); - } - }; - - if (desiredEndpointFilterJavaObject == nullptr) - { - ChipLogProgress(AppServer, - "CastingPlayer-JNI::VerifyOrEstablishConnection() calling CastingPlayer::VerifyOrEstablishConnection() on " - "Casting Player with device ID: %s", - castingPlayer->GetId()); - castingPlayer->VerifyOrEstablishConnection(callback, static_cast(commissioningWindowTimeoutSec)); - } - else - { - // Convert the EndpointFilter Java class to a C++ EndpointFilter - jclass endpointFilterJavaClass = env->GetObjectClass(desiredEndpointFilterJavaObject); - jfieldID vendorIdFieldId = env->GetFieldID(endpointFilterJavaClass, "vendorId", "Ljava/lang/Integer;"); - jfieldID productIdFieldId = env->GetFieldID(endpointFilterJavaClass, "productId", "Ljava/lang/Integer;"); - jobject vendorIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, vendorIdFieldId); - jobject productIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, productIdFieldId); - // jfieldID requiredDeviceTypesFieldId = env->GetFieldID(endpointFilterJavaClass, "requiredDeviceTypes", - // "Ljava/util/List;"); - - matter::casting::core::EndpointFilter desiredEndpointFilter; - // Value of 0 means unspecified - desiredEndpointFilter.vendorId = vendorIdIntegerObject != nullptr - ? static_cast(env->CallIntMethod( - vendorIdIntegerObject, env->GetMethodID(env->GetObjectClass(vendorIdIntegerObject), "intValue", "()I"))) - : 0; - desiredEndpointFilter.productId = productIdIntegerObject != nullptr - ? static_cast(env->CallIntMethod( - productIdIntegerObject, env->GetMethodID(env->GetObjectClass(productIdIntegerObject), "intValue", "()I"))) - : 0; - ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() desiredEndpointFilter.vendorId: %d", - desiredEndpointFilter.vendorId); - ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() desiredEndpointFilter.productId: %d", - desiredEndpointFilter.productId); - // TODO: In following PRs. Translate the Java requiredDeviceTypes list to a C++ requiredDeviceTypes vector. For now we're - // passing an empty list of DeviceTypeStruct. - - ChipLogProgress(AppServer, - "CastingPlayer-JNI::VerifyOrEstablishConnection() calling " - "CastingPlayer::VerifyOrEstablishConnection() on Casting Player with device ID: %s", - castingPlayer->GetId()); - castingPlayer->VerifyOrEstablishConnection(callback, static_cast(commissioningWindowTimeoutSec), - desiredEndpointFilter); - } - - return completableFutureObjGlobalRef; -} - -}; // namespace core -}; // namespace casting -}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp index 5838d4039ae6a3..5981ab80bbd52e 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp @@ -19,8 +19,7 @@ #include "CastingPlayerDiscovery-JNI.h" #include "../JNIDACProvider.h" -#include "../support/CastingPlayerConverter-JNI.h" -#include "../support/ErrorConverter-JNI.h" +#include "../support/Converters-JNI.h" #include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" #include "core/CastingApp.h" // from tv-casting-common #include "core/CastingPlayerDiscovery.h" // from tv-casting-common @@ -81,7 +80,7 @@ class DiscoveryDelegateImpl : public DiscoveryDelegate "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Not set, " "onAddedCallbackJavaMethodID == nullptr")); - jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + jobject matterCastingPlayerJavaObject = support::convertCastingPlayerFromCppToJava(player); VerifyOrReturn(matterCastingPlayerJavaObject != nullptr, ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Could not create " @@ -108,7 +107,7 @@ class DiscoveryDelegateImpl : public DiscoveryDelegate "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Not set, " "onChangedCallbackJavaMethodID == nullptr")); - jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + jobject matterCastingPlayerJavaObject = support::convertCastingPlayerFromCppToJava(player); VerifyOrReturn(matterCastingPlayerJavaObject != nullptr, ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Could not " @@ -160,10 +159,10 @@ JNI_METHOD(jobject, startDiscovery)(JNIEnv * env, jobject, jobject targetDeviceT if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "CastingPlayerDiscovery-JNI startDiscovery() err: %" CHIP_ERROR_FORMAT, err.Format()); - return support::createJMatterError(err); + return support::convertMatterErrorFromCppToJava(err); } - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } JNI_METHOD(jobject, stopDiscovery)(JNIEnv * env, jobject) @@ -176,53 +175,55 @@ JNI_METHOD(jobject, stopDiscovery)(JNIEnv * env, jobject) if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::StopDiscovery() err: %" CHIP_ERROR_FORMAT, err.Format()); - return support::createJMatterError(err); + return support::convertMatterErrorFromCppToJava(err); } - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } JNI_METHOD(jobject, addCastingPlayerChangeListener)(JNIEnv * env, jobject, jobject castingPlayerChangeListenerJavaObject) { chip::DeviceLayer::StackLock lock; ChipLogProgress(AppServer, "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() called"); - VerifyOrReturnValue(castingPlayerChangeListenerJavaObject != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(castingPlayerChangeListenerJavaObject != nullptr, + support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); if (DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject.HasValidObjectRef()) { ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() Warning: Call removeCastingPlayerChangeListener " "before adding a new one"); - return support::createJMatterError(CHIP_ERROR_INCORRECT_STATE); + return support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE); } // Get the class and method IDs for the CastingPlayerChangeListener Java class jclass castingPlayerChangeListenerJavaClass = env->GetObjectClass(castingPlayerChangeListenerJavaObject); - VerifyOrReturnValue(castingPlayerChangeListenerJavaClass != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(castingPlayerChangeListenerJavaClass != nullptr, + support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); jmethodID onAddedJavaMethodID = env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onAdded", "(Lcom/matter/casting/core/CastingPlayer;)V"); - VerifyOrReturnValue(onAddedJavaMethodID != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(onAddedJavaMethodID != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); jmethodID onChangedJavaMethodID = env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onChanged", "(Lcom/matter/casting/core/CastingPlayer;)V"); - VerifyOrReturnValue(onChangedJavaMethodID != nullptr, support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + VerifyOrReturnValue(onChangedJavaMethodID != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); // jmethodID onRemovedJavaMethodID = env->GetMethodID(castingPlayerChangeListenerJavaClass, "_onRemoved", // "(Lcom/matter/casting/core/CastingPlayer;)V"); VerifyOrReturnValue(onRemovedJavaMethodID != nullptr, - // support::createJMatterError(CHIP_ERROR_INCORRECT_STATE)); + // support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE)); // Set Java callbacks in the DiscoveryDelegateImpl Singleton CHIP_ERROR err = DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject.Init(castingPlayerChangeListenerJavaObject); if (err != CHIP_NO_ERROR) { - return support::createJMatterError(err); + return support::convertMatterErrorFromCppToJava(err); } DiscoveryDelegateImpl::GetInstance()->onAddedCallbackJavaMethodID = onAddedJavaMethodID; DiscoveryDelegateImpl::GetInstance()->onChangedCallbackJavaMethodID = onChangedJavaMethodID; // DiscoveryDelegateImpl::GetInstance()->onRemovedCallbackJavaMethodID = onRemovedJavaMethodID; - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } JNI_METHOD(jobject, removeCastingPlayerChangeListener)(JNIEnv * env, jobject, jobject castingPlayerChangeListenerJavaObject) @@ -243,14 +244,14 @@ JNI_METHOD(jobject, removeCastingPlayerChangeListener)(JNIEnv * env, jobject, jo DiscoveryDelegateImpl::GetInstance()->onChangedCallbackJavaMethodID = nullptr; // DiscoveryDelegateImpl::GetInstance()->onRemovedCallbackJavaMethodID = nullptr; - return support::createJMatterError(CHIP_NO_ERROR); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); } else { ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() Warning: Cannot remove listener. Received a " "different CastingPlayerChangeListener object"); - return support::createJMatterError(CHIP_ERROR_INCORRECT_STATE); + return support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE); } } @@ -269,7 +270,7 @@ JNI_METHOD(jobject, getCastingPlayers)(JNIEnv * env, jobject) for (const auto & player : castingPlayersList) { - jobject matterCastingPlayerJavaObject = support::createJCastingPlayer(player); + jobject matterCastingPlayerJavaObject = support::convertCastingPlayerFromCppToJava(player); if (matterCastingPlayerJavaObject != nullptr) { jboolean added = env->CallBooleanMethod(arrayList, addMethod, matterCastingPlayerJavaObject); diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp new file mode 100644 index 00000000000000..caa64b4ac8d717 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 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 "MatterCastingPlayer-JNI.h" + +#include "../JNIDACProvider.h" +#include "../support/Converters-JNI.h" +#include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" +#include "core/CastingApp.h" // from tv-casting-common +#include "core/CastingPlayer.h" // from tv-casting-common +#include "core/CastingPlayerDiscovery.h" // from tv-casting-common + +#include +#include +#include +#include +#include + +using namespace chip; + +#define JNI_METHOD(RETURN, METHOD_NAME) \ + extern "C" JNIEXPORT RETURN JNICALL Java_com_matter_casting_core_MatterCastingPlayer_##METHOD_NAME + +namespace matter { +namespace casting { +namespace core { + +MatterCastingPlayerJNI MatterCastingPlayerJNI::sInstance; + +JNI_METHOD(jobject, verifyOrEstablishConnection) +(JNIEnv * env, jobject thiz, jlong commissioningWindowTimeoutSec, jobject desiredEndpointFilterJavaObject, jobject jSuccessCallback, + jobject jFailureCallback) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() called with a timeout of: %ld seconds", + static_cast(commissioningWindowTimeoutSec)); + + CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturnValue(castingPlayer != nullptr, support::convertMatterErrorFromCppToJava(CHIP_ERROR_INVALID_ARGUMENT)); + + matter::casting::core::EndpointFilter desiredEndpointFilter; + if (desiredEndpointFilterJavaObject != nullptr) + { + // Convert the EndpointFilter Java class to a C++ EndpointFilter + jclass endpointFilterJavaClass = env->GetObjectClass(desiredEndpointFilterJavaObject); + jfieldID vendorIdFieldId = env->GetFieldID(endpointFilterJavaClass, "vendorId", "Ljava/lang/Integer;"); + jfieldID productIdFieldId = env->GetFieldID(endpointFilterJavaClass, "productId", "Ljava/lang/Integer;"); + jobject vendorIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, vendorIdFieldId); + jobject productIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, productIdFieldId); + // jfieldID requiredDeviceTypesFieldId = env->GetFieldID(endpointFilterJavaClass, "requiredDeviceTypes", + // "Ljava/util/List;"); + + // Value of 0 means unspecified + desiredEndpointFilter.vendorId = vendorIdIntegerObject != nullptr + ? static_cast(env->CallIntMethod( + vendorIdIntegerObject, env->GetMethodID(env->GetObjectClass(vendorIdIntegerObject), "intValue", "()I"))) + : 0; + desiredEndpointFilter.productId = productIdIntegerObject != nullptr + ? static_cast(env->CallIntMethod( + productIdIntegerObject, env->GetMethodID(env->GetObjectClass(productIdIntegerObject), "intValue", "()I"))) + : 0; + // TODO: In following PRs. Translate the Java requiredDeviceTypes list to a C++ requiredDeviceTypes vector. For now we're + // passing an empty list of DeviceTypeStruct. + } + + MatterCastingPlayerJNIMgr().mConnectionSuccessHandler.SetUp(env, jSuccessCallback); + MatterCastingPlayerJNIMgr().mConnectionFailureHandler.SetUp(env, jFailureCallback); + castingPlayer->VerifyOrEstablishConnection( + [](CHIP_ERROR err, CastingPlayer * playerPtr) { + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::verifyOrEstablishConnection() ConnectCallback called"); + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI:: Connected to Casting Player with device ID: %s", + playerPtr->GetId()); + MatterCastingPlayerJNIMgr().mConnectionSuccessHandler.Handle(nullptr); + } + else + { + ChipLogError(AppServer, "MatterCastingPlayer-JNI:: ConnectCallback, connection error: %" CHIP_ERROR_FORMAT, + err.Format()); + MatterCastingPlayerJNIMgr().mConnectionFailureHandler.Handle(err); + } + }, + static_cast(commissioningWindowTimeoutSec), desiredEndpointFilter); + return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR); +} + +JNI_METHOD(void, disconnect) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::disconnect()"); + + core::CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturn(castingPlayer != nullptr, + ChipLogError(AppServer, "MatterCastingPlayer-JNI::disconnect() castingPlayer == nullptr")); + + castingPlayer->Disconnect(); +} + +JNI_METHOD(jobject, getEndpoints) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::getEndpoints() called"); + + CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturnValue(castingPlayer != nullptr, nullptr, + ChipLogError(AppServer, "MatterCastingPlayer-JNI::getEndpoints() castingPlayer == nullptr")); + + const std::vector> endpoints = castingPlayer->GetEndpoints(); + jobject jEndpointList = nullptr; + chip::JniReferences::GetInstance().CreateArrayList(jEndpointList); + for (memory::Strong endpoint : endpoints) + { + jobject matterEndpointJavaObject = support::convertEndpointFromCppToJava(endpoint); + VerifyOrReturnValue(matterEndpointJavaObject != nullptr, jEndpointList, + ChipLogError(AppServer, "MatterCastingPlayer-JNI::getEndpoints(): Could not create Endpoint jobject")); + chip::JniReferences::GetInstance().AddToList(jEndpointList, matterEndpointJavaObject); + } + return jEndpointList; +} + +}; // namespace core +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h new file mode 100644 index 00000000000000..7f58162fff52b3 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.h @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2020-2024 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. + */ + +#pragma once + +#include "../support/MatterCallback-JNI.h" +#include "core/CastingPlayer.h" // from tv-casting-common + +#include + +namespace matter { +namespace casting { +namespace core { + +class MatterCastingPlayerJNI +{ +public: + MatterCastingPlayerJNI() : mConnectionSuccessHandler([](void *) { return nullptr; }) {} + support::MatterCallbackJNI mConnectionSuccessHandler; + support::MatterFailureCallbackJNI mConnectionFailureHandler; + +private: + friend MatterCastingPlayerJNI & MatterCastingPlayerJNIMgr(); + static MatterCastingPlayerJNI sInstance; +}; + +inline class MatterCastingPlayerJNI & MatterCastingPlayerJNIMgr() +{ + return MatterCastingPlayerJNI::sInstance; +} +}; // namespace core +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp new file mode 100644 index 00000000000000..2e28b873599a9a --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020-2024 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 "MatterEndpoint-JNI.h" + +#include "../JNIDACProvider.h" +#include "../support/Converters-JNI.h" +#include "../support/MatterCallback-JNI.h" +#include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h" +#include "clusters/Clusters.h" // from tv-casting-common +#include "core/CastingApp.h" // from tv-casting-common +#include "core/CastingPlayer.h" // from tv-casting-common +#include "core/CastingPlayerDiscovery.h" // from tv-casting-common +#include "core/Endpoint.h" // from tv-casting-common + +#include +#include +#include +#include +#include + +using namespace chip; + +#define JNI_METHOD(RETURN, METHOD_NAME) \ + extern "C" JNIEXPORT RETURN JNICALL Java_com_matter_casting_core_MatterEndpoint_##METHOD_NAME + +namespace matter { +namespace casting { +namespace core { + +MatterEndpointJNI MatterEndpointJNI::sInstance; + +JNI_METHOD(jint, getId) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterEndpoint-JNI::getId() called"); + Endpoint * endpoint = support::convertEndpointFromJavaToCpp(thiz); + VerifyOrReturnValue(endpoint != nullptr, -1, ChipLogError(AppServer, "MatterEndpoint-JNI::getId() endpoint == nullptr")); + return endpoint->GetId(); +} + +JNI_METHOD(jint, getProductId) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterEndpoint-JNI::getProductId() called"); + Endpoint * endpoint = support::convertEndpointFromJavaToCpp(thiz); + VerifyOrReturnValue(endpoint != nullptr, -1, ChipLogError(AppServer, "MatterEndpoint-JNI::getProductId() endpoint == nullptr")); + return endpoint->GetProductId(); +} + +JNI_METHOD(jint, getVendorId) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterEndpoint-JNI::getVendorId() called"); + Endpoint * endpoint = support::convertEndpointFromJavaToCpp(thiz); + VerifyOrReturnValue(endpoint != nullptr, -1, ChipLogError(AppServer, "MatterEndpoint-JNI::getVendorId() endpoint == nullptr")); + return endpoint->GetVendorId(); +} + +JNI_METHOD(jobject, getCastingPlayer) +(JNIEnv * env, jobject thiz) +{ + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterEndpoint-JNI::getCastingPlayer() called"); + Endpoint * endpoint = support::convertEndpointFromJavaToCpp(thiz); + VerifyOrReturnValue(endpoint != nullptr, nullptr, + ChipLogError(AppServer, "MatterEndpoint-JNI::getCastingPlayer() endpoint == nullptr")); + return support::convertCastingPlayerFromCppToJava(std::shared_ptr(endpoint->GetCastingPlayer())); +} + +}; // namespace core +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h similarity index 66% rename from examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h rename to examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h index 2870866895c868..f9534435ab1903 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterEndpoint-JNI.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2024 Project CHIP Authors + * Copyright (c) 2020-24 Project CHIP Authors * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,23 +18,26 @@ #pragma once +#include "core/Endpoint.h" // from tv-casting-common + #include +#include +#include namespace matter { namespace casting { namespace core { -class CastingPlayerJNI +class MatterEndpointJNI { -public: private: - friend CastingPlayerJNI & CastingAppJNIMgr(); - static CastingPlayerJNI sInstance; + friend MatterEndpointJNI & MatterEndpointJNIMgr(); + static MatterEndpointJNI sInstance; }; -inline class CastingPlayerJNI & CastingAppJNIMgr() +inline class MatterEndpointJNI & MatterEndpointJNIMgr() { - return CastingPlayerJNI::sInstance; + return MatterEndpointJNI::sInstance; } }; // namespace core }; // namespace casting diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp deleted file mode 100644 index 72c3677f357707..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2023 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 "CastingPlayerConverter-JNI.h" -#include - -namespace matter { -namespace casting { -namespace support { - -using namespace chip; - -jobject createJCastingPlayer(matter::casting::memory::Strong player) -{ - ChipLogProgress(AppServer, "CastingPlayerConverter-JNI.createJCastingPlayer() called"); - JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); - - // Get a reference to the MatterCastingPlayer Java class - jclass matterCastingPlayerJavaClass = env->FindClass("com/matter/casting/core/MatterCastingPlayer"); - if (matterCastingPlayerJavaClass == nullptr) - { - ChipLogError(AppServer, - "CastingPlayerConverter-JNI.createJCastingPlayer() could not locate MatterCastingPlayer Java class"); - return nullptr; - } - - // Get the constructor for the com/matter/casting/core/MatterCastingPlayer Java class - jmethodID constructor = - env->GetMethodID(matterCastingPlayerJavaClass, "", - "(ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;IIIJ)V"); - if (constructor == nullptr) - { - ChipLogError( - AppServer, - "CastingPlayerConverter-JNI.createJCastingPlayer() could not locate MatterCastingPlayer Java class constructor"); - return nullptr; - } - - // Convert the CastingPlayer fields to MatterCastingPlayer Java types - jobject jIpAddressList = nullptr; - const chip::Inet::IPAddress * ipAddresses = player->GetIPAddresses(); - if (ipAddresses != nullptr) - { - chip::JniReferences::GetInstance().CreateArrayList(jIpAddressList); - for (size_t i = 0; i < player->GetNumIPs() && i < chip::Dnssd::CommonResolutionData::kMaxIPAddresses; i++) - { - char addrCString[chip::Inet::IPAddress::kMaxStringLength]; - ipAddresses[i].ToString(addrCString, chip::Inet::IPAddress::kMaxStringLength); - jstring jIPAddressStr = env->NewStringUTF(addrCString); - - jclass jIPAddressClass = env->FindClass("java/net/InetAddress"); - jmethodID jGetByNameMid = - env->GetStaticMethodID(jIPAddressClass, "getByName", "(Ljava/lang/String;)Ljava/net/InetAddress;"); - jobject jIPAddress = env->CallStaticObjectMethod(jIPAddressClass, jGetByNameMid, jIPAddressStr); - - chip::JniReferences::GetInstance().AddToList(jIpAddressList, jIPAddress); - } - } - - // Create a new instance of the MatterCastingPlayer Java class - jobject jMatterCastingPlayer = nullptr; - jMatterCastingPlayer = env->NewObject(matterCastingPlayerJavaClass, constructor, static_cast(player->IsConnected()), - env->NewStringUTF(player->GetId()), env->NewStringUTF(player->GetHostName()), - env->NewStringUTF(player->GetDeviceName()), env->NewStringUTF(player->GetInstanceName()), - jIpAddressList, (jint) (player->GetPort()), (jint) (player->GetProductId()), - (jint) (player->GetVendorId()), (jlong) (player->GetDeviceType())); - if (jMatterCastingPlayer == nullptr) - { - ChipLogError(AppServer, - "CastingPlayerConverter-JNI.createJCastingPlayer() Warning: Could not create MatterCastingPlayer Java object"); - return jMatterCastingPlayer; - } - // Set the value of the _cppCastingPlayer field in the Java object to the C++ CastingPlayer pointer. - jfieldID longFieldId = env->GetFieldID(matterCastingPlayerJavaClass, "_cppCastingPlayer", "J"); - env->SetLongField(jMatterCastingPlayer, longFieldId, reinterpret_cast(player.get())); - return jMatterCastingPlayer; -} - -}; // namespace support -}; // namespace casting -}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h deleted file mode 100644 index 91b0ac6c79b92f..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * - * Copyright (c) 2023-2024 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 "core/CastingPlayer.h" - -#include - -#include - -namespace matter { -namespace casting { -namespace support { - -/** - * @brief Convertes a native CastingPlayer into a MatterCastingPlayer jobject - * - * @param CastingPlayer represents a Matter commissioner that is able to play media to a physical - * output or to a display screen which is part of the device. - * - * @return pointer to the CastingPlayer jobject if created successfully, nullptr otherwise. - */ -jobject createJCastingPlayer(matter::casting::memory::Strong player); - -}; // namespace support -}; // namespace casting -}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp new file mode 100644 index 00000000000000..9798f2b48b9359 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2020-24 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 "Converters-JNI.h" +#include + +namespace matter { +namespace casting { +namespace support { + +using namespace chip; + +jobject convertMatterErrorFromCppToJava(CHIP_ERROR inErr) +{ + ChipLogProgress(AppServer, "convertMatterErrorFromCppToJava() called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass jMatterErrorClass; + CHIP_ERROR err = + chip::JniReferences::GetInstance().GetLocalClassRef(env, "com/matter/casting/support/MatterError", jMatterErrorClass); + VerifyOrReturnValue(err == CHIP_NO_ERROR, nullptr); + + jmethodID jMatterErrorConstructor = env->GetMethodID(jMatterErrorClass, "", "(JLjava/lang/String;)V"); + if (jMatterErrorConstructor == nullptr) + { + ChipLogError(AppServer, "Failed to access MatterError constructor"); + env->ExceptionClear(); + return nullptr; + } + + return env->NewObject(jMatterErrorClass, jMatterErrorConstructor, inErr.AsInteger(), nullptr); +} + +jobject convertEndpointFromCppToJava(matter::casting::memory::Strong endpoint) +{ + ChipLogProgress(AppServer, "convertEndpointFromCppToJava() called"); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + // Get a reference to the MatterEndpoint Java class + jclass matterEndpointJavaClass = env->FindClass("com/matter/casting/core/MatterEndpoint"); + if (matterEndpointJavaClass == nullptr) + { + ChipLogError(AppServer, "convertEndpointFromCppToJava() could not locate MatterEndpoint Java class"); + env->ExceptionClear(); + return nullptr; + } + + // Get the constructor for the com/matter/casting/core/MatterEndpoint Java class + jmethodID constructor = env->GetMethodID(matterEndpointJavaClass, "", "()V"); + if (constructor == nullptr) + { + ChipLogError(AppServer, "convertEndpointFromCppToJava() could not locate MatterEndpoint Java class constructor"); + env->ExceptionClear(); + return nullptr; + } + + // Create a new instance of the MatterEndpoint Java class + jobject jMatterEndpoint = nullptr; + jMatterEndpoint = env->NewObject(matterEndpointJavaClass, constructor); + if (jMatterEndpoint == nullptr) + { + ChipLogError(AppServer, "convertEndpointFromCppToJava(): Could not create MatterEndpoint Java object"); + return jMatterEndpoint; + } + // Set the value of the _cppEndpoint field in the Java object to the C++ Endpoint pointer. + jfieldID longFieldId = env->GetFieldID(matterEndpointJavaClass, "_cppEndpoint", "J"); + env->SetLongField(jMatterEndpoint, longFieldId, reinterpret_cast(endpoint.get())); + return jMatterEndpoint; +} + +/** + * @brief Get the matter::casting::core::Endpoint object from the jobject jEndpointObject + */ +core::Endpoint * convertEndpointFromJavaToCpp(jobject jEndpointObject) +{ + ChipLogProgress(AppServer, "convertEndpointFromJavaToCpp() called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass endpointClass = env->GetObjectClass(jEndpointObject); + if (endpointClass == nullptr) + { + ChipLogError(AppServer, "convertEndpointFromJavaToCpp() could not locate Endpoint Java class"); + env->ExceptionClear(); + return nullptr; + } + + jfieldID _cppEndpointFieldId = env->GetFieldID(endpointClass, "_cppEndpoint", "J"); + VerifyOrReturnValue(_cppEndpointFieldId != nullptr, nullptr, + ChipLogError(AppServer, "convertEndpointFromJavaToCpp _cppEndpointFieldId == nullptr")); + jlong _cppEndpointValue = env->GetLongField(jEndpointObject, _cppEndpointFieldId); + return reinterpret_cast(_cppEndpointValue); +} + +jobject convertCastingPlayerFromCppToJava(matter::casting::memory::Strong player) +{ + ChipLogProgress(AppServer, "convertCastingPlayerFromCppToJava() called"); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + // Get a reference to the MatterCastingPlayer Java class + jclass matterCastingPlayerJavaClass = env->FindClass("com/matter/casting/core/MatterCastingPlayer"); + if (matterCastingPlayerJavaClass == nullptr) + { + ChipLogError(AppServer, "convertCastingPlayerFromCppToJava() could not locate MatterCastingPlayer Java class"); + env->ExceptionClear(); + return nullptr; + } + + // Get the constructor for the com/matter/casting/core/MatterCastingPlayer Java class + jmethodID constructor = + env->GetMethodID(matterCastingPlayerJavaClass, "", + "(ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;IIIJ)V"); + if (constructor == nullptr) + { + ChipLogError(AppServer, "convertCastingPlayerFromCppToJava() could not locate MatterCastingPlayer Java class constructor"); + env->ExceptionClear(); + return nullptr; + } + + // Convert the CastingPlayer fields to MatterCastingPlayer Java types + jobject jIpAddressList = nullptr; + const chip::Inet::IPAddress * ipAddresses = player->GetIPAddresses(); + if (ipAddresses != nullptr) + { + chip::JniReferences::GetInstance().CreateArrayList(jIpAddressList); + for (size_t i = 0; i < player->GetNumIPs() && i < chip::Dnssd::CommonResolutionData::kMaxIPAddresses; i++) + { + char addrCString[chip::Inet::IPAddress::kMaxStringLength]; + ipAddresses[i].ToString(addrCString, chip::Inet::IPAddress::kMaxStringLength); + jstring jIPAddressStr = env->NewStringUTF(addrCString); + + jclass jIPAddressClass = env->FindClass("java/net/InetAddress"); + jmethodID jGetByNameMid = + env->GetStaticMethodID(jIPAddressClass, "getByName", "(Ljava/lang/String;)Ljava/net/InetAddress;"); + jobject jIPAddress = env->CallStaticObjectMethod(jIPAddressClass, jGetByNameMid, jIPAddressStr); + + chip::JniReferences::GetInstance().AddToList(jIpAddressList, jIPAddress); + } + } + + // Create a new instance of the MatterCastingPlayer Java class + jobject jMatterCastingPlayer = nullptr; + jMatterCastingPlayer = env->NewObject(matterCastingPlayerJavaClass, constructor, static_cast(player->IsConnected()), + env->NewStringUTF(player->GetId()), env->NewStringUTF(player->GetHostName()), + env->NewStringUTF(player->GetDeviceName()), env->NewStringUTF(player->GetInstanceName()), + jIpAddressList, (jint) (player->GetPort()), (jint) (player->GetProductId()), + (jint) (player->GetVendorId()), (jlong) (player->GetDeviceType())); + if (jMatterCastingPlayer == nullptr) + { + ChipLogError(AppServer, "convertCastingPlayerFromCppToJava(): Could not create MatterCastingPlayer Java object"); + env->ExceptionClear(); + return jMatterCastingPlayer; + } + // Set the value of the _cppCastingPlayer field in the Java object to the C++ CastingPlayer pointer. + jfieldID longFieldId = env->GetFieldID(matterCastingPlayerJavaClass, "_cppCastingPlayer", "J"); + env->SetLongField(jMatterCastingPlayer, longFieldId, reinterpret_cast(player.get())); + return jMatterCastingPlayer; +} + +core::CastingPlayer * convertCastingPlayerFromJavaToCpp(jobject jCastingPlayerObject) +{ + ChipLogProgress(AppServer, "convertCastingPlayerFromJavaToCpp() called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass castingPlayerClass = env->GetObjectClass(jCastingPlayerObject); + if (castingPlayerClass == nullptr) + { + ChipLogError(AppServer, "convertCastingPlayerFromJavaToCpp() could not locate CastingPlayer Java class"); + env->ExceptionClear(); + return nullptr; + } + + jfieldID _cppCastingPlayerFieldId = env->GetFieldID(castingPlayerClass, "_cppCastingPlayer", "J"); + VerifyOrReturnValue(_cppCastingPlayerFieldId != nullptr, nullptr, + ChipLogError(AppServer, "convertCastingPlayerFromJavaToCpp _cppCastingPlayerFieldId == nullptr")); + jlong _cppCastingPlayerValue = env->GetLongField(jCastingPlayerObject, _cppCastingPlayerFieldId); + return reinterpret_cast(_cppCastingPlayerValue); +} + +jobject convertClusterFromCppToJava(matter::casting::memory::Strong cluster, const char * className) +{ + ChipLogProgress(AppServer, "convertClusterFromCppToJava() called"); + VerifyOrReturnValue(cluster.get() != nullptr, nullptr, + ChipLogError(AppServer, "convertClusterFromCppToJava() cluster.get() == nullptr")); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + // Get a reference to the cluster's Java class + jclass clusterJavaClass = env->FindClass(className); + if (clusterJavaClass == nullptr) + { + ChipLogError(AppServer, "convertClusterFromCppToJava could not locate cluster's Java class"); + env->ExceptionClear(); + return nullptr; + } + + // Get the constructor for the cluster's Java class + jmethodID constructor = env->GetMethodID(clusterJavaClass, "", "()V"); + if (constructor == nullptr) + { + ChipLogError(AppServer, "convertClusterFromCppToJava could not locate cluster's Java class constructor"); + env->ExceptionClear(); + return nullptr; + } + + // Create a new instance of the cluster's Java class + jobject jMatterCluster = nullptr; + jMatterCluster = env->NewObject(clusterJavaClass, constructor); + if (jMatterCluster == nullptr) + { + ChipLogError(AppServer, "convertClusterFromCppToJava: Could not create cluster's Java object"); + return jMatterCluster; + } + // Set the value of the _cppEndpoint field in the Java object to the C++ Endpoint pointer. + jfieldID longFieldId = env->GetFieldID(clusterJavaClass, "_cppCluster", "J"); + env->SetLongField(jMatterCluster, longFieldId, reinterpret_cast(cluster.get())); + return jMatterCluster; +} + +/** + * @brief Get the matter::casting::core::Cluster object from the jobject jClusterObject + */ +core::BaseCluster * convertClusterFromJavaToCpp(jobject jClusterObject) +{ + ChipLogProgress(AppServer, "convertClusterFromJavaToCpp() called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass clusterClass = env->GetObjectClass(jClusterObject); + if (clusterClass == nullptr) + { + ChipLogError(AppServer, "convertClusterFromJavaToCpp could not locate cluster's Java class"); + env->ExceptionClear(); + return nullptr; + } + + jfieldID _cppClusterFieldId = env->GetFieldID(clusterClass, "_cppCluster", "J"); + VerifyOrReturnValue(_cppClusterFieldId != nullptr, nullptr, + ChipLogError(AppServer, "convertClusterFromJavaToCpp() _cppCluster == nullptr")); + jlong _cppClusterValue = env->GetLongField(jClusterObject, _cppClusterFieldId); + return reinterpret_cast(_cppClusterValue); +} + +jobject convertCommandFromCppToJava(void * command, const char * className) +{ + ChipLogProgress(AppServer, "convertCommandFromCppToJava() called"); + VerifyOrReturnValue(command != nullptr, nullptr, ChipLogError(AppServer, "convertCommandFromCppToJava() command == nullptr")); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + // Get a reference to the command's Java class + jclass commandJavaClass = env->FindClass(className); + if (commandJavaClass == nullptr) + { + ChipLogError(AppServer, "convertCommandFromCppToJava() could not locate command's Java class"); + env->ExceptionClear(); + return nullptr; + } + + // Get the constructor for the command's Java class + jmethodID constructor = env->GetMethodID(commandJavaClass, "", "()V"); + if (constructor == nullptr) + { + ChipLogError(AppServer, "convertCommandFromCppToJava() could not locate command's Java class constructor"); + env->ExceptionClear(); + return nullptr; + } + + // Create a new instance of the command's Java class + jobject jMatterCommand = env->NewObject(commandJavaClass, constructor); + if (jMatterCommand == nullptr) + { + ChipLogError(AppServer, "convertCommandFromCppToJava(): Could not create command's Java object"); + env->ExceptionClear(); + return jMatterCommand; + } + // Set the value of the _cppEndpoint field in the Java object to the C++ Endpoint pointer. + jfieldID longFieldId = env->GetFieldID(commandJavaClass, "_cppCommand", "J"); + env->SetLongField(jMatterCommand, longFieldId, reinterpret_cast(command)); + return jMatterCommand; +} + +/** + * @brief Get the matter::casting::core::Command object from the jobject jCommandObject + */ +void * convertCommandFromJavaToCpp(jobject jCommandObject) +{ + ChipLogProgress(AppServer, "convertCommandFromJavaToCpp() called"); + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass commandClass = env->GetObjectClass(jCommandObject); + if (commandClass == nullptr) + { + ChipLogError(AppServer, "convertCommandFromJavaToCpp() could not locate command's Java class"); + env->ExceptionClear(); + return nullptr; + } + + jfieldID _cppCommandFieldId = env->GetFieldID(commandClass, "_cppCommand", "J"); + VerifyOrReturnValue(_cppCommandFieldId != nullptr, nullptr, + ChipLogError(AppServer, "convertCommandFromJavaToCpp() _cppCommand == nullptr")); + jlong _cppCommandValue = env->GetLongField(jCommandObject, _cppCommandFieldId); + return reinterpret_cast(_cppCommandValue); +} + +jobject convertLongFromCppToJava(uint64_t responseData) +{ + ChipLogProgress(AppServer, "convertLongFromCppToJava() called"); + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturnValue(env != nullptr, nullptr, ChipLogError(AppServer, "Could not get JNIEnv for current thread")); + + jclass responseTypeClass = env->FindClass("java/lang/Long"); + if (responseTypeClass == nullptr) + { + ChipLogError(AppServer, "convertLongFromCppToJava: Class for Response Type not found!"); + env->ExceptionClear(); + return nullptr; + } + + jmethodID constructor = env->GetMethodID(responseTypeClass, "", "(J)V"); + return env->NewObject(responseTypeClass, constructor, responseData); +} + +}; // namespace support +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h new file mode 100644 index 00000000000000..ecc3a95d15bd74 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/Converters-JNI.h @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2020-24 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 "core/BaseCluster.h" +#include "core/CastingPlayer.h" +#include "core/Command.h" +#include "core/Endpoint.h" + +#include + +#include + +namespace matter { +namespace casting { +namespace support { + +jobject convertMatterErrorFromCppToJava(CHIP_ERROR inErr); + +/** + * @brief Converts a native Endpoint into a MatterEndpoint jobject + * + * @return pointer to the Endpoint jobject if created successfully, nullptr otherwise. + */ +jobject convertEndpointFromCppToJava(matter::casting::memory::Strong endpoint); + +core::Endpoint * convertEndpointFromJavaToCpp(jobject jEndpointObject); + +/** + * @brief Convertes a native CastingPlayer into a MatterCastingPlayer jobject + * + * @param CastingPlayer represents a Matter commissioner that is able to play media to a physical + * output or to a display screen which is part of the device. + * + * @return pointer to the CastingPlayer jobject if created successfully, nullptr otherwise. + */ +jobject convertCastingPlayerFromCppToJava(matter::casting::memory::Strong player); + +core::CastingPlayer * convertCastingPlayerFromJavaToCpp(jobject jCastingPlayerObject); + +/** + * @brief Converts a native Cluster into a MatterCluster jobject + * + * @return pointer to the Cluster jobject if created successfully, nullptr otherwise. + */ +jobject convertClusterFromCppToJava(matter::casting::memory::Strong cluster, const char * className); + +core::BaseCluster * convertClusterFromJavaToCpp(jobject jClusterObject); + +/** + * @brief Converts a native Command into a MatterCommand jobject + * + * @return pointer to the Command jobject if created successfully, nullptr otherwise. + */ +jobject convertCommandFromCppToJava(void * command, const char * className); + +void * convertCommandFromJavaToCpp(jobject jCommandObject); + +jobject convertLongFromCppToJava(uint64_t responseData); + +}; // namespace support +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp deleted file mode 100644 index 1dce6f19d74776..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 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 "ErrorConverter-JNI.h" -#include - -namespace matter { -namespace casting { -namespace support { - -using namespace chip; - -jobject createJMatterError(CHIP_ERROR inErr) -{ - JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); - jclass jMatterErrorClass; - CHIP_ERROR err = - chip::JniReferences::GetInstance().GetLocalClassRef(env, "com/matter/casting/support/MatterError", jMatterErrorClass); - VerifyOrReturnValue(err == CHIP_NO_ERROR, nullptr); - - jmethodID jMatterErrorConstructor = env->GetMethodID(jMatterErrorClass, "", "(JLjava/lang/String;)V"); - - return env->NewObject(jMatterErrorClass, jMatterErrorConstructor, inErr.AsInteger(), nullptr); -} - -}; // namespace support -}; // namespace casting -}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.h deleted file mode 100644 index e11523397db4f3..00000000000000 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/ErrorConverter-JNI.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * - * Copyright (c) 2023 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 matter { -namespace casting { -namespace support { - -jobject createJMatterError(CHIP_ERROR inErr); - -}; // namespace support -}; // namespace casting -}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h new file mode 100644 index 00000000000000..3c56c426359d5c --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/MatterCallback-JNI.h @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2020-24 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 "Converters-JNI.h" + +#include +#include +#include + +namespace matter { +namespace casting { +namespace support { + +template +class MatterCallbackJNI +{ +public: + MatterCallbackJNI(std::function conversionFn) { mConversionFn = conversionFn; } + + MatterCallbackJNI(const char * methodSignature, std::function conversionFn) + { + mMethodSignature = methodSignature; + mConversionFn = conversionFn; + } + + CHIP_ERROR SetUp(JNIEnv * env, jobject inCallback) + { + ChipLogProgress(AppServer, "MatterCallbackJNI::SetUp called"); + VerifyOrReturnError(env != nullptr, CHIP_JNI_ERROR_NO_ENV, ChipLogError(AppServer, "JNIEnv was null!")); + + ReturnErrorOnFailure(mCallbackObject.Init(inCallback)); + + jclass mClazz = env->GetObjectClass(mCallbackObject.ObjectRef()); + VerifyOrReturnError(mClazz != nullptr, CHIP_JNI_ERROR_TYPE_NOT_FOUND, + ChipLogError(AppServer, "Failed to get callback Java class")); + + jclass mSuperClazz = env->GetSuperclass(mClazz); + VerifyOrReturnError(mSuperClazz != nullptr, CHIP_JNI_ERROR_TYPE_NOT_FOUND, + ChipLogError(AppServer, "Failed to get callback's parent's Java class")); + + mMethod = env->GetMethodID(mClazz, "handleInternal", mMethodSignature); + VerifyOrReturnError( + mMethod != nullptr, CHIP_JNI_ERROR_METHOD_NOT_FOUND, + ChipLogError(AppServer, "Failed to access 'handleInternal' method with signature %s", mMethodSignature)); + + return CHIP_NO_ERROR; + } + + void Handle(T responseData) + { + ChipLogProgress(AppServer, "MatterCallbackJNI::Handle called"); + + JNIEnv * env = chip::JniReferences::GetInstance().GetEnvForCurrentThread(); + VerifyOrReturn(env != nullptr, ChipLogError(AppServer, "Failed to get JNIEnv")); + + jobject jResponseData = mConversionFn(responseData); + + chip::DeviceLayer::StackUnlock unlock; + VerifyOrReturn(mCallbackObject.HasValidObjectRef(), + ChipLogError(AppServer, "MatterCallbackJNI::Handle mCallbackObject has no valid ObjectRef")); + VerifyOrReturn(mMethod != nullptr, ChipLogError(AppServer, "MatterCallbackJNI::Handle mMethod is nullptr")); + env->CallVoidMethod(mCallbackObject.ObjectRef(), mMethod, jResponseData); + } + +protected: + chip::JniGlobalReference mCallbackObject; + jmethodID mMethod = nullptr; + const char * mMethodSignature = "(Ljava/lang/Object;)V"; + std::function mConversionFn = nullptr; +}; + +class MatterFailureCallbackJNI : public MatterCallbackJNI +{ +public: + MatterFailureCallbackJNI() : MatterCallbackJNI(matter::casting::support::convertMatterErrorFromCppToJava) {} +}; + +}; // namespace support +}; // namespace casting +}; // namespace matter diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/RotatingDeviceIdUniqueIdProvider-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/RotatingDeviceIdUniqueIdProvider-JNI.cpp index f1d29cd063c450..2af0e1000d6aaf 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/RotatingDeviceIdUniqueIdProvider-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/RotatingDeviceIdUniqueIdProvider-JNI.cpp @@ -82,7 +82,7 @@ MutableByteSpan * RotatingDeviceIdUniqueIdProviderJNI::Get() ChipLogProgress(AppServer, "RotatingDeviceIdUniqueIdProviderJNI.Get() called"); mRotatingDeviceIdUniqueIdSpan = MutableByteSpan(mRotatingDeviceIdUniqueId); CHIP_ERROR err = GetJavaByteByMethod(mGetMethod, mRotatingDeviceIdUniqueIdSpan); - VerifyOrReturnValue(err != CHIP_NO_ERROR, nullptr, + VerifyOrReturnValue(err == CHIP_NO_ERROR, nullptr, ChipLogError(AppServer, "Error calling GetJavaByteByMethod %" CHIP_ERROR_FORMAT, err.Format())); return &mRotatingDeviceIdUniqueIdSpan; } diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml new file mode 100644 index 00000000000000..ae61681a0bbf2f --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_action_selector.xml @@ -0,0 +1,39 @@ + + + + + + + +

uIp%u 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