diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0be17862507fa0..effbef0ca439fd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -458,6 +458,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCCLEANM_1_2.py" --script-args "--int-arg PIXIT_ENDPOINT:1 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCRUNM_1_2.py" --script-args "--int-arg PIXIT_ENDPOINT:1 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"' + scripts/run_in_python_env.sh out/venv './scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py' - name: Uploading core files uses: actions/upload-artifact@v3 if: ${{ failure() && !env.ACT }} diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index ea52a49e79634a..84bb1135b99089 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -41,6 +41,7 @@ #include #include +#include #include @@ -542,6 +543,9 @@ void ChipLinuxAppMainLoop(AppMainLoopImplementation * impl) // We need to set DeviceInfoProvider before Server::Init to setup the storage of DeviceInfoProvider properly. DeviceLayer::SetDeviceInfoProvider(&gExampleDeviceInfoProvider); + chip::app::RuntimeOptionsProvider::Instance().SetSimulateNoInternalTime( + LinuxDeviceOptions::GetInstance().mSimulateNoInternalTime); + // Init ZCL Data Model and CHIP App Server Server::GetInstance().Init(initParams); diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp index 824a39fb737b57..d97566213a579e 100644 --- a/examples/platform/linux/Options.cpp +++ b/examples/platform/linux/Options.cpp @@ -83,6 +83,7 @@ enum kDeviceOption_TestEventTriggerEnableKey = 0x101f, kCommissionerOption_FabricID = 0x1020, kTraceTo = 0x1021, + kOptionSimulateNoInternalTime = 0x1022, }; constexpr unsigned kAppUsageLength = 64; @@ -136,6 +137,7 @@ OptionDef sDeviceOptionDefs[] = { #if ENABLE_TRACING { "trace-to", kArgumentRequired, kTraceTo }, #endif + { "simulate-no-internal-time", kNoArgument, kOptionSimulateNoInternalTime }, {} }; @@ -250,6 +252,8 @@ const char * sDeviceOptionHelp = " --trace-to \n" " Trace destinations, comma separated (" SUPPORTED_COMMAND_LINE_TRACING_TARGETS ")\n" #endif + " --simulate-no-internal-time\n" + " Time cluster does not use internal platform time\n" "\n"; bool Base64ArgToVector(const char * arg, size_t maxSize, std::vector & outVector) @@ -500,6 +504,9 @@ bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, LinuxDeviceOptions::GetInstance().traceTo.push_back(aValue); break; #endif + case kOptionSimulateNoInternalTime: + LinuxDeviceOptions::GetInstance().mSimulateNoInternalTime = true; + break; default: PrintArgError("%s: INTERNAL ERROR: Unhandled option: %s\n", aProgram, aName); retval = false; diff --git a/examples/platform/linux/Options.h b/examples/platform/linux/Options.h index c87c713fab9f32..b03da07cb5b929 100644 --- a/examples/platform/linux/Options.h +++ b/examples/platform/linux/Options.h @@ -66,6 +66,7 @@ struct LinuxDeviceOptions uint8_t testEventTriggerEnableKey[16] = { 0 }; chip::FabricId commissionerFabricId = chip::kUndefinedFabricId; std::vector traceTo; + bool mSimulateNoInternalTime = false; static LinuxDeviceOptions & GetInstance(); }; diff --git a/scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py b/scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py new file mode 100755 index 00000000000000..32f0b9bf61a2cf --- /dev/null +++ b/scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py @@ -0,0 +1,140 @@ +#!/usr/bin/env -S python3 -B + +# 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. + +import logging +import os +import signal +import subprocess +import sys +import time + +DEFAULT_CHIP_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..')) + + +class TestDriver: + def __init__(self): + self.app_path = os.path.abspath(os.path.join(DEFAULT_CHIP_ROOT, 'out', + 'linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test', 'chip-all-clusters-app')) + self.run_python_test_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'run_python_test.py')) + + self.script_path = os.path.abspath(os.path.join( + DEFAULT_CHIP_ROOT, 'src', 'python_testing', 'TestTimeSyncTrustedTimeSource.py')) + if not os.path.exists(self.app_path): + msg = 'chip-all-clusters-app not found' + logging.error(msg) + raise FileNotFoundError(msg) + if not os.path.exists(self.run_python_test_path): + msg = 'run_python_test.py script not found' + logging.error(msg) + raise FileNotFoundError(msg) + if not os.path.exists(self.script_path): + msg = 'TestTimeSyncTrustedTimeSource.py script not found' + logging.error(msg) + raise FileNotFoundError(msg) + + def get_base_run_python_cmd(self, run_python_test_path, app_path, app_args, script_path, script_args): + return f'{str(run_python_test_path)} --app {str(app_path)} --app-args "{app_args}" --script {str(script_path)} --script-args "{script_args}"' + + def run_test_section(self, app_args: str, script_args: str, factory_reset_all: bool = False, factory_reset_app: bool = False) -> int: + # quotes are required here + cmd = self.get_base_run_python_cmd(self.run_python_test_path, self.app_path, app_args, + self.script_path, script_args) + if factory_reset_all: + cmd = cmd + ' --factoryreset' + if factory_reset_app: + cmd = cmd + ' --factoryreset-app-only' + + logging.info(f'Running cmd {cmd}') + + process = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, shell=True, bufsize=1) + + return process.wait() + + +def kill_process(app2_process): + logging.warning("Stopping app with SIGINT") + app2_process.send_signal(signal.SIGINT.value) + app2_process.wait() + + +def main(): + # in the first round, we're just going to commission the device + base_app_args = '--discriminator 1234 --KVS kvs1' + app_args = base_app_args + base_script_args = '--storage-path admin_storage.json --discriminator 1234 --passcode 20202021' + script_args = base_script_args + ' --commissioning-method on-network --commission-only' + + driver = TestDriver() + ret = driver.run_test_section(app_args, script_args, factory_reset_all=True) + if ret != 0: + return ret + + # For this test, we need to have a time source set up already for the simulated no-internal-time source to query. + # This means it needs to be commissioned onto the same fabric, and the ACLs need to be set up to allow + # access to the time source cluster. + # This simulates the second device, so its using a different KVS and nodeid, which will allow both apps to run simultaneously + app2_args = '--discriminator 1235 --KVS kvs2 --secured-device-port 5580' + script_args = '--storage-path admin_storage.json --discriminator 1235 --passcode 20202021 --commissioning-method on-network --dut-node-id 2 --tests test_SetupTimeSourceACL' + + ret = driver.run_test_section(app2_args, script_args, factory_reset_app=True) + if ret != 0: + return ret + + # Now we've got something commissioned, we're going to test what happens when it resets, but we're simulating no time. + # In this case, the commissioner hasn't set the time after the reboot, so there should be no time returned (checked in test) + app_args = base_app_args + ' --simulate-no-internal-time' + script_args = base_script_args + ' --tests test_SimulateNoInternalTime' + + ret = driver.run_test_section(app_args, script_args) + if ret != 0: + return ret + + # Make sure we come up with internal time correctly if we don't set that flag + app_args = base_app_args + script_args = base_script_args + ' --tests test_HaveInternalTime' + + ret = driver.run_test_section(app_args, script_args) + if ret != 0: + return ret + + # Bring up app2 again, it needs to run for the duration of the next test so app1 has a place to query time + # App1 will come up, it is simulating having no internal time (confirmed in previous test), but we have + # set up app2 as the trusted time source, so it should query out to app2 for the time. + app2_cmd = str(driver.app_path) + ' ' + app2_args + app2_process = subprocess.Popen(app2_cmd.split(), stdout=sys.stdout, stderr=sys.stderr, bufsize=0) + + # Give app2 a second to come up and start advertising + time.sleep(1) + + # This first test ensures that we read from the trusted time source right after it is set. + app_args = base_app_args + ' --simulate-no-internal-time --trace_decode 1' + script_args = base_script_args + ' --tests test_SetAndReadFromTrustedTimeSource --int-arg trusted_time_source:2' + ret = driver.run_test_section(app_args, script_args) + if ret != 0: + kill_process(app2_process) + return ret + + # This next test ensures the trusted time source is saved during a reboot + script_args = base_script_args + ' --tests test_ReadFromTrustedTimeSource' + ret = driver.run_test_section(app_args, script_args) + + kill_process(app2_process) + sys.exit(ret) + + +if __name__ == '__main__': + main() diff --git a/scripts/tests/run_python_test.py b/scripts/tests/run_python_test.py index 33931accb909f9..fdea7767037aa6 100755 --- a/scripts/tests/run_python_test.py +++ b/scripts/tests/run_python_test.py @@ -72,6 +72,8 @@ def DumpProgramOutputToQueue(thread_list: typing.List[threading.Thread], tag: st help='Path to local application to use, omit to use external apps.') @click.option("--factoryreset", is_flag=True, help='Remove app config and repl configs (/tmp/chip* and /tmp/repl*) before running the tests.') +@click.option("--factoryreset-app-only", is_flag=True, + help='Remove app config and repl configs (/tmp/chip* and /tmp/repl*) before running the tests, but not the controller config') @click.option("--app-args", type=str, default='', help='The extra arguments passed to the device. Can use placholders like {SCRIPT_BASE_NAME}') @click.option("--script", type=click.Path(exists=True), default=os.path.join(DEFAULT_CHIP_ROOT, @@ -85,11 +87,11 @@ def DumpProgramOutputToQueue(thread_list: typing.List[threading.Thread], tag: st help='Script arguments, can use placeholders like {SCRIPT_BASE_NAME}.') @click.option("--script-gdb", is_flag=True, help='Run script through gdb') -def main(app: str, factoryreset: bool, app_args: str, script: str, script_args: str, script_gdb: bool): +def main(app: str, factoryreset: bool, factoryreset_app_only: bool, app_args: str, script: str, script_args: str, script_gdb: bool): app_args = app_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0]) script_args = script_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0]) - if factoryreset: + if factoryreset or factoryreset_app_only: # Remove native app config retcode = subprocess.call("rm -rf /tmp/chip* /tmp/repl*", shell=True) if retcode != 0: @@ -107,6 +109,7 @@ def main(app: str, factoryreset: bool, app_args: str, script: str, script_args: if retcode != 0: raise Exception("Failed to remove %s for factory reset." % kvs_path_to_remove) + if factoryreset: # Remove Python test admin storage if provided storage_match = re.search(r"--storage-path (?P[^ ]+)", script_args) if storage_match: diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index d20e28a83bbee8..62176c6530b4b8 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -42,6 +42,7 @@ template("chip_data_model") { # Allow building ota-requestor-app with a non-spec-compliant floor # (i.e. smaller than 2 minutes) for action delays. non_spec_compliant_ota_action_delay_floor = -1 + time_sync_enable_tsc_feature = 1 } if (defined(invoker.idl)) { @@ -198,6 +199,10 @@ template("chip_data_model") { deps = [] } + if (!defined(cflags)) { + cflags = [] + } + foreach(cluster, _cluster_sources) { if (cluster == "door-lock-server") { sources += [ @@ -257,6 +262,8 @@ template("chip_data_model") { "${_app_root}/clusters/${cluster}/DefaultTimeSyncDelegate.cpp", "${_app_root}/clusters/${cluster}/TimeSyncDataProvider.cpp", ] + cflags += + [ "-DTIME_SYNC_ENABLE_TSC_FEATURE=${time_sync_enable_tsc_feature}" ] } else if (cluster == "scenes-server") { sources += [ "${_app_root}/clusters/${cluster}/${cluster}.cpp", @@ -318,10 +325,6 @@ template("chip_data_model") { public_deps += [ "${chip_root}/src/app/server" ] } - if (!defined(cflags)) { - cflags = [] - } - cflags += [ "-Wconversion" ] if (non_spec_compliant_ota_action_delay_floor >= 0) { diff --git a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp index 0d115593e1f9d9..6b00f661eea412 100644 --- a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp +++ b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp @@ -18,6 +18,8 @@ #include "DefaultTimeSyncDelegate.h" #include "inet/IPAddress.h" +#include +#include using chip::TimeSyncDataProvider; using namespace chip::app::Clusters::TimeSynchronization; @@ -45,3 +47,27 @@ bool DefaultTimeSyncDelegate::IsNTPAddressDomain(chip::CharSpan ntp) // placeholder implementation return false; } + +CHIP_ERROR DefaultTimeSyncDelegate::UpdateTimeFromPlatformSource(chip::Callback::Callback * callback) +{ + System::Clock::Microseconds64 utcTime; + if (chip::app::RuntimeOptionsProvider::Instance().GetSimulateNoInternalTime()) + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } + if (System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR) + { + // Default assumes the time came from NTP. Platforms using other sources should overwrite this + // with their own delegates + // Call the callback right away from within this function + callback->mCall(callback->mContext, TimeSourceEnum::kMixedNTP, GranularityEnum::kMillisecondsGranularity); + return CHIP_NO_ERROR; + } + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +CHIP_ERROR DefaultTimeSyncDelegate::UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP, + chip::Callback::Callback * callback) +{ + return CHIP_ERROR_NOT_IMPLEMENTED; +} diff --git a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h index bac1f14fb28e93..a954f51954cb3c 100644 --- a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h +++ b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h @@ -33,6 +33,9 @@ class DefaultTimeSyncDelegate : public Delegate bool HandleUpdateDSTOffset(CharSpan name) override; bool IsNTPAddressValid(CharSpan ntp) override; bool IsNTPAddressDomain(CharSpan ntp) override; + CHIP_ERROR UpdateTimeFromPlatformSource(chip::Callback::Callback * callback) override; + CHIP_ERROR UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP, + chip::Callback::Callback * callback) override; }; } // namespace TimeSynchronization diff --git a/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h b/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h index 6701015205dd29..d04f3018a01f02 100644 --- a/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h +++ b/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h @@ -29,6 +29,9 @@ namespace app { namespace Clusters { namespace TimeSynchronization { +typedef void (*OnTimeSyncCompletion)(void * context, TimeSourceEnum timeSource, GranularityEnum granularity); +typedef void (*OnFallbackNTPCompletion)(void * context, bool timeSyncSuccessful); + /** @brief * Defines methods for implementing application-specific logic for the Time Synchronization Cluster. */ @@ -74,6 +77,30 @@ class Delegate */ virtual bool IsNTPAddressDomain(const CharSpan ntp) = 0; + /** + * @brief Delegate should attempt to get time from a platform-defined source using the ordering defined in the + * Time source prioritization spec section. Delegate may skip any unsupported sources + * Order: GNSS -> trusted high-resolution external source (PTP, trusted network NTP, cloud) -> + * local network defined NTP (DHCPv6 -> DHCP -> DNS-SD sources) + * If the delegate is unable to support any source, it may return an error immediately. If the delegate is going + * to attempt to obtain time from any source, it returns CHIP_NO_ERROR and calls the callback on completion. + * If the delegate successfully obtains the time, it sets the time using the platform time API (SetClock_RealTime) + * and calls the callback with the time source and granularity set as appropriate. + * If the delegate is unsuccessful in obtaining the time, it calls the callback with timeSource set to kNone and + * granularity set to kNoTimeGranularity. + */ + virtual CHIP_ERROR UpdateTimeFromPlatformSource(chip::Callback::Callback * callback) = 0; + + /** + * @brief If the delegate supports NTP, it should attempt to update its time using the provided fallbackNTP source. + * If the delegate is successful in obtaining a time from the fallbackNTP, it updates the system time (ex using + * System::SystemClock().SetClock_RealTime) and calls the callback. If the delegate is going to attempt to update + * the time and call the callback, it returns CHIP_NO_ERROR. If the delegate does not support NTP, it may return + * a CHIP_ERROR. + */ + virtual CHIP_ERROR UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP, + chip::Callback::Callback * callback) = 0; + virtual ~Delegate() = default; private: diff --git a/src/app/clusters/time-synchronization-server/time-synchronization-server.cpp b/src/app/clusters/time-synchronization-server/time-synchronization-server.cpp index f3a2d203c824fe..b34c0f157c905c 100644 --- a/src/app/clusters/time-synchronization-server/time-synchronization-server.cpp +++ b/src/app/clusters/time-synchronization-server/time-synchronization-server.cpp @@ -18,6 +18,10 @@ #include "DefaultTimeSyncDelegate.h" #include "time-synchronization-delegate.h" +#if TIME_SYNC_ENABLE_TSC_FEATURE +#include +#endif + #include #include #include @@ -31,6 +35,7 @@ #include #include #include +#include #include @@ -61,6 +66,40 @@ Delegate * GetDelegate() } return gDelegate; } + +#if TIME_SYNC_ENABLE_TSC_FEATURE +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + TimeSynchronizationServer * server = reinterpret_cast(context); + server->OnDeviceConnectedFn(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + TimeSynchronizationServer * server = reinterpret_cast(context); + server->OnDeviceConnectionFailureFn(); +} + +#endif + +void OnPlatformEventWrapper(const DeviceLayer::ChipDeviceEvent * event, intptr_t ptr) +{ + TimeSynchronizationServer * server = reinterpret_cast(ptr); + server->OnPlatformEventFn(*event); +} + +void OnTimeSyncCompletionWrapper(void * context, TimeSourceEnum timeSource, GranularityEnum granularity) +{ + TimeSynchronizationServer * server = reinterpret_cast(context); + server->OnTimeSyncCompletionFn(timeSource, granularity); +} + +void OnFallbackNTPCompletionWrapper(void * context, bool timeSyncSuccessful) +{ + TimeSynchronizationServer * server = reinterpret_cast(context); + server->OnFallbackNTPCompletionFn(timeSyncSuccessful); +} + } // namespace namespace chip { @@ -228,6 +267,186 @@ TimeSynchronizationServer & TimeSynchronizationServer::Instance() return sTimeSyncInstance; } +TimeSynchronizationServer::TimeSynchronizationServer() : +#if TIME_SYNC_ENABLE_TSC_FEATURE + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this), +#endif + mOnTimeSyncCompletion(OnTimeSyncCompletionWrapper, this), mOnFallbackNTPCompletion(OnFallbackNTPCompletionWrapper, this) +{} + +void TimeSynchronizationServer::AttemptToGetFallbackNTPTimeFromDelegate() +{ + // Sent as a char-string to the delegate so they can read it easily + char defaultNTP[kMaxDefaultNTPSize]; + MutableCharSpan span(defaultNTP); + if (GetDefaultNtp(span) != CHIP_NO_ERROR) + { + emitTimeFailureEvent(kRootEndpointId); + return; + } + if (span.size() > kMaxDefaultNTPSize) + { + emitTimeFailureEvent(kRootEndpointId); + return; + } + if (GetDelegate()->UpdateTimeUsingNTPFallback(span, &mOnFallbackNTPCompletion) != CHIP_NO_ERROR) + { + emitTimeFailureEvent(kRootEndpointId); + } +} + +#if TIME_SYNC_ENABLE_TSC_FEATURE +void TimeSynchronizationServer::OnDeviceConnectedFn(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + // Connected to our trusted time source, let's read the time. + app::AttributePathParams readPaths[2]; + readPaths[0] = app::AttributePathParams(kRootEndpointId, app::Clusters::TimeSynchronization::Id, + app::Clusters::TimeSynchronization::Attributes::UTCTime::Id); + readPaths[1] = app::AttributePathParams(kRootEndpointId, app::Clusters::TimeSynchronization::Id, + app::Clusters::TimeSynchronization::Attributes::Granularity::Id); + + app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); + app::ReadPrepareParams readParams(sessionHandle); + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 2; + + auto attributeCache = Platform::MakeUnique(*this); + if (attributeCache == nullptr) + { + // This is unlikely to work if we don't have memory, but let's try + OnDeviceConnectionFailureFn(); + return; + } + auto readClient = chip::Platform::MakeUnique(engine, &exchangeMgr, attributeCache->GetBufferedCallback(), + app::ReadClient::InteractionType::Read); + if (readClient == nullptr) + { + // This is unlikely to work if we don't have memory, but let's try + OnDeviceConnectionFailureFn(); + return; + } + CHIP_ERROR err = readClient->SendRequest(readParams); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Failed to read UTC time from trusted source"); + OnDeviceConnectionFailureFn(); + return; + } + mAttributeCache = std::move(attributeCache); + mReadClient = std::move(readClient); +} + +void TimeSynchronizationServer::OnDeviceConnectionFailureFn() +{ + // No way to read from the TrustedTimeSource, fall back to default NTP + AttemptToGetFallbackNTPTimeFromDelegate(); +} + +void TimeSynchronizationServer::OnDone(ReadClient * apReadClient) +{ + using namespace chip::app::Clusters::TimeSynchronization::Attributes; + + Granularity::TypeInfo::Type granularity = GranularityEnum::kNoTimeGranularity; + mAttributeCache->Get(kRootEndpointId, granularity); + + UTCTime::TypeInfo::Type time; + CHIP_ERROR err = mAttributeCache->Get(kRootEndpointId, time); + if (err == CHIP_NO_ERROR && !time.IsNull() && granularity != GranularityEnum::kNoTimeGranularity) + { + GranularityEnum ourGranularity; + // Being conservative with granularity - nothing smaller than seconds because of network delay + switch (granularity) + { + case GranularityEnum::kMinutesGranularity: + case GranularityEnum::kSecondsGranularity: + ourGranularity = GranularityEnum::kMinutesGranularity; + break; + default: + ourGranularity = GranularityEnum::kSecondsGranularity; + break; + } + + err = SetUTCTime(kRootEndpointId, time.Value(), ourGranularity, TimeSourceEnum::kNodeTimeCluster); + if (err == CHIP_NO_ERROR) + { + return; + } + } + // We get here if we didn't get a time, or failed to set the time source + // If we failed to set the UTC time, it doesn't hurt to try the backup - NTP system might have different permissions on the + // system clock + AttemptToGetFallbackNTPTimeFromDelegate(); +} +#endif + +void TimeSynchronizationServer::OnTimeSyncCompletionFn(TimeSourceEnum timeSource, GranularityEnum granularity) +{ + if (timeSource != TimeSourceEnum::kNone && granularity == GranularityEnum::kNoTimeGranularity) + { + // Unable to get time from the delegate. Try remaining sources. + CHIP_ERROR err = AttemptToGetTimeFromTrustedNode(); + if (err != CHIP_NO_ERROR) + { + AttemptToGetFallbackNTPTimeFromDelegate(); + } + return; + } + mGranularity = granularity; + if (EMBER_ZCL_STATUS_SUCCESS != TimeSource::Set(kRootEndpointId, timeSource)) + { + ChipLogError(Zcl, "Writing TimeSource failed."); + } +} + +void TimeSynchronizationServer::OnFallbackNTPCompletionFn(bool timeSyncSuccessful) +{ + if (timeSyncSuccessful) + { + mGranularity = GranularityEnum::kMillisecondsGranularity; + // Non-matter SNTP because we know it's external and there's only one source + if (EMBER_ZCL_STATUS_SUCCESS != TimeSource::Set(kRootEndpointId, TimeSourceEnum::kNonMatterSNTP)) + { + ChipLogError(Zcl, "Writing TimeSource failed."); + } + } + else + { + emitTimeFailureEvent(kRootEndpointId); + } +} + +CHIP_ERROR TimeSynchronizationServer::AttemptToGetTimeFromTrustedNode() +{ +#if TIME_SYNC_ENABLE_TSC_FEATURE + if (!mTrustedTimeSource.IsNull()) + { + CASESessionManager * caseSessionManager = Server::GetInstance().GetCASESessionManager(); + ScopedNodeId nodeId(mTrustedTimeSource.Value().nodeID, mTrustedTimeSource.Value().fabricIndex); + caseSessionManager->FindOrEstablishSession(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); + return CHIP_NO_ERROR; + } + return CHIP_ERROR_NOT_FOUND; +#else + return CHIP_ERROR_NOT_IMPLEMENTED; +#endif +} + +void TimeSynchronizationServer::AttemptToGetTime() +{ + // Let's check the delegate and see if can get us a time. Even if the time is already set, we want to ask the delegate so we can + // set the time source as appropriate. + CHIP_ERROR err = GetDelegate()->UpdateTimeFromPlatformSource(&mOnTimeSyncCompletion); + if (err != CHIP_NO_ERROR) + { + err = AttemptToGetTimeFromTrustedNode(); + } + if (err != CHIP_NO_ERROR) + { + AttemptToGetFallbackNTPTimeFromDelegate(); + } +} + void TimeSynchronizationServer::Init() { mTimeSyncDataProvider.Init(Server::GetInstance().GetPersistentStorage()); @@ -245,25 +464,41 @@ void TimeSynchronizationServer::Init() { ClearDSTOffset(); } - if (!mTrustedTimeSource.IsNull()) - { - // TODO: trusted time source is available, schedule a time read https://github.com/project-chip/connectedhomeip/issues/27201 - } System::Clock::Microseconds64 utcTime; - if (System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR) + + if (System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR && + !RuntimeOptionsProvider::Instance().GetSimulateNoInternalTime()) { mGranularity = GranularityEnum::kMinutesGranularity; } - else - { - mGranularity = GranularityEnum::kNoTimeGranularity; - } + // This can error, but it's not clear what should happen in this case. For now, just ignore it because we still // want time sync even if we can't register the deletgate here. CHIP_ERROR err = chip::Server::GetInstance().GetFabricTable().AddFabricDelegate(this); if (err != CHIP_NO_ERROR) { - ChipLogError(DeviceLayer, "Unable to register Fabric table delegate for time sync"); + ChipLogError(Zcl, "Unable to register Fabric table delegate for time sync"); + } + PlatformMgr().AddEventHandler(OnPlatformEventWrapper, reinterpret_cast(this)); +} + +void TimeSynchronizationServer::Shutdown() +{ + PlatformMgr().RemoveEventHandler(OnPlatformEventWrapper, 0); +} + +void TimeSynchronizationServer::OnPlatformEventFn(const DeviceLayer::ChipDeviceEvent & event) +{ + switch (event.Type) + { + case DeviceEventType::kServerReady: + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + AttemptToGetTime(); + } + break; + default: + break; } } @@ -279,6 +514,10 @@ CHIP_ERROR TimeSynchronizationServer::SetTrustedTimeSource(const DataModel::Null { err = mTimeSyncDataProvider.ClearTrustedTimeSource(); } + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + AttemptToGetTime(); + } return err; } @@ -544,7 +783,12 @@ void TimeSynchronizationServer::ScheduleDelayedAction(System::Clock::Seconds32 d CHIP_ERROR TimeSynchronizationServer::SetUTCTime(EndpointId ep, uint64_t utcTime, GranularityEnum granularity, TimeSourceEnum source) { - ReturnErrorOnFailure(UpdateUTCTime(utcTime)); + CHIP_ERROR err = UpdateUTCTime(utcTime); + if (err != CHIP_NO_ERROR && !RuntimeOptionsProvider::Instance().GetSimulateNoInternalTime()) + { + ChipLogError(Zcl, "Error setting UTC time on the device"); + return err; + } mGranularity = granularity; if (EMBER_ZCL_STATUS_SUCCESS != TimeSource::Set(ep, source)) { @@ -559,6 +803,10 @@ CHIP_ERROR TimeSynchronizationServer::GetLocalTime(EndpointId ep, DataModel::Nul int64_t timeZoneOffset = 0, dstOffset = 0; System::Clock::Microseconds64 utcTime; uint64_t chipEpochTime; + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + return CHIP_ERROR_INVALID_TIME; + } VerifyOrReturnError(TimeState::kInvalid != UpdateDSTOffsetState(), CHIP_ERROR_INVALID_TIME); ReturnErrorOnFailure(System::SystemClock().GetClock_RealTime(utcTime)); VerifyOrReturnError(UnixEpochToChipEpochMicro(utcTime.count(), chipEpochTime), CHIP_ERROR_INVALID_TIME); @@ -591,6 +839,13 @@ TimeState TimeSynchronizationServer::UpdateTimeZoneState() size_t activeTzIndex = 0; uint64_t chipEpochTime; + // This return allows us to simulate no internal time for testing purposes + // This will be set once we receive a good time either from the delegate or via a command + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + return TimeState::kInvalid; + } + VerifyOrReturnValue(System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(tzList.size() != 0, TimeState::kInvalid); VerifyOrReturnValue(UnixEpochToChipEpochMicro(utcTime.count(), chipEpochTime), TimeState::kInvalid); @@ -623,6 +878,13 @@ TimeState TimeSynchronizationServer::UpdateDSTOffsetState() uint64_t chipEpochTime; bool dstStopped = true; + // This return allows us to simulate no internal time for testing purposes + // This will be set once we receive a good time either from the delegate or via a command + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + return TimeState::kInvalid; + } + VerifyOrReturnValue(System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(dstList.size() != 0, TimeState::kInvalid); VerifyOrReturnValue(UnixEpochToChipEpochMicro(utcTime.count(), chipEpochTime), TimeState::kInvalid); @@ -778,7 +1040,12 @@ CHIP_ERROR TimeSynchronizationAttrAccess::Read(const ConcreteReadAttributePath & case UTCTime::Id: { System::Clock::Microseconds64 utcTimeUnix; uint64_t chipEpochTime; - + // This return allows us to simulate no internal time for testing purposes + // This will be set once we receive a good time either from the delegate or via a command + if (TimeSynchronizationServer::Instance().GetGranularity() == GranularityEnum::kNoTimeGranularity) + { + return aEncoder.EncodeNull(); + } VerifyOrReturnError(System::SystemClock().GetClock_RealTime(utcTimeUnix) == CHIP_NO_ERROR, aEncoder.EncodeNull()); VerifyOrReturnError(UnixEpochToChipEpochMicro(utcTimeUnix.count(), chipEpochTime), aEncoder.EncodeNull()); return aEncoder.Encode(chipEpochTime); diff --git a/src/app/clusters/time-synchronization-server/time-synchronization-server.h b/src/app/clusters/time-synchronization-server/time-synchronization-server.h index 5b64f613e7a1eb..b17fd0f880e1e7 100644 --- a/src/app/clusters/time-synchronization-server/time-synchronization-server.h +++ b/src/app/clusters/time-synchronization-server/time-synchronization-server.h @@ -21,8 +21,16 @@ #pragma once +#ifndef TIME_SYNC_ENABLE_TSC_FEATURE +#define TIME_SYNC_ENABLE_TSC_FEATURE 1 +#endif + #include "TimeSyncDataProvider.h" +#include "time-synchronization-delegate.h" +#if TIME_SYNC_ENABLE_TSC_FEATURE +#include +#endif #include #include #include @@ -62,9 +70,15 @@ enum class TimeSyncEventFlag : uint8_t }; class TimeSynchronizationServer : public FabricTable::Delegate +#if TIME_SYNC_ENABLE_TSC_FEATURE + , + public ClusterStateCache::Callback +#endif { public: + TimeSynchronizationServer(); void Init(); + void Shutdown(); static TimeSynchronizationServer & Instance(void); TimeSyncDataProvider & GetDataProvider(void) { return mTimeSyncDataProvider; } @@ -96,13 +110,30 @@ class TimeSynchronizationServer : public FabricTable::Delegate void ClearEventFlag(TimeSyncEventFlag flag); // Fabric Table delegate functions - void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex); + void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) override; + +#if TIME_SYNC_ENABLE_TSC_FEATURE + // CASE connection functions + void OnDeviceConnectedFn(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle); + void OnDeviceConnectionFailureFn(); + + // AttributeCache::Callback functions + void OnAttributeChanged(ClusterStateCache * cache, const ConcreteAttributePath & path) override {} + void OnDone(ReadClient * apReadClient) override; +#endif + + // Platform event handler functions + void OnPlatformEventFn(const DeviceLayer::ChipDeviceEvent & event); + + void OnTimeSyncCompletionFn(TimeSourceEnum timeSource, GranularityEnum granularity); + void OnFallbackNTPCompletionFn(bool timeSyncSuccessful); private: + static constexpr size_t kMaxDefaultNTPSize = 128; DataModel::Nullable mTrustedTimeSource; TimeSyncDataProvider::TimeZoneObj mTimeZoneObj{ Span(mTz), 0 }; TimeSyncDataProvider::DSTOffsetObj mDstOffsetObj{ DataModel::List(mDst), 0 }; - GranularityEnum mGranularity; + GranularityEnum mGranularity = GranularityEnum::kNoTimeGranularity; TimeSyncDataProvider::TimeZoneStore mTz[CHIP_CONFIG_TIME_ZONE_LIST_MAX_SIZE]; Structs::DSTOffsetStruct::Type mDst[CHIP_CONFIG_DST_OFFSET_LIST_MAX_SIZE]; @@ -110,6 +141,22 @@ class TimeSynchronizationServer : public FabricTable::Delegate TimeSyncDataProvider mTimeSyncDataProvider; static TimeSynchronizationServer sTimeSyncInstance; TimeSyncEventFlag mEventFlag = TimeSyncEventFlag::kNone; +#if TIME_SYNC_ENABLE_TSC_FEATURE + Platform::UniquePtr mAttributeCache; + Platform::UniquePtr mReadClient; + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; +#endif + chip::Callback::Callback mOnTimeSyncCompletion; + chip::Callback::Callback mOnFallbackNTPCompletion; + + // Called when the platform is set up - attempts to get time using the recommended source list in the spec. + void AttemptToGetTime(); + CHIP_ERROR AttemptToGetTimeFromTrustedNode(); + // Attempts to get fallback NTP from the delegate (last available source) + // If successful, the function will set mGranulatiry and the time source + // If unsuccessful, it will emit a TimeFailure event. + void AttemptToGetFallbackNTPTimeFromDelegate(); }; } // namespace TimeSynchronization diff --git a/src/include/platform/RuntimeOptionsProvider.h b/src/include/platform/RuntimeOptionsProvider.h new file mode 100644 index 00000000000000..d3a8fc291a991f --- /dev/null +++ b/src/include/platform/RuntimeOptionsProvider.h @@ -0,0 +1,35 @@ +/* + * + * 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 + +namespace chip { +namespace app { +/** + * @brief This class provides a mechanism for clusters to access runtime options set for the app. + */ +class RuntimeOptionsProvider +{ +public: + static RuntimeOptionsProvider & Instance(); + void SetSimulateNoInternalTime(bool simulateNoInternalTime) { mSimulateNoInternalTime = simulateNoInternalTime; } + bool GetSimulateNoInternalTime() { return mSimulateNoInternalTime; } + +private: + bool mSimulateNoInternalTime = false; +}; +} // namespace app +} // namespace chip diff --git a/src/platform/BUILD.gn b/src/platform/BUILD.gn index 04c4d522b5abe7..5f906139170b29 100644 --- a/src/platform/BUILD.gn +++ b/src/platform/BUILD.gn @@ -390,6 +390,7 @@ if (chip_device_platform != "none") { "../include/platform/KvsPersistentStorageDelegate.h", "../include/platform/PersistedStorage.h", "../include/platform/PlatformManager.h", + "../include/platform/RuntimeOptionsProvider.h", "../include/platform/TestOnlyCommissionableDataProvider.h", "../include/platform/ThreadStackManager.h", "../include/platform/internal/BLEManager.h", @@ -433,6 +434,7 @@ if (chip_device_platform != "none") { "LockTracker.cpp", "PersistedStorage.cpp", "PlatformEventSupport.cpp", + "RuntimeOptionsProvider.cpp", ] # Linux has its own NetworkCommissioningThreadDriver diff --git a/src/platform/RuntimeOptionsProvider.cpp b/src/platform/RuntimeOptionsProvider.cpp new file mode 100644 index 00000000000000..c8159c9966d067 --- /dev/null +++ b/src/platform/RuntimeOptionsProvider.cpp @@ -0,0 +1,29 @@ +/* + * + * 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. + */ +#include + +namespace chip { +namespace app { +namespace { +RuntimeOptionsProvider sRuntimeOptionsProvider; +} // namespace +RuntimeOptionsProvider & RuntimeOptionsProvider::Instance() +{ + return sRuntimeOptionsProvider; +} +} // namespace app +} // namespace chip diff --git a/src/python_testing/TestTimeSyncTrustedTimeSource.py b/src/python_testing/TestTimeSyncTrustedTimeSource.py new file mode 100644 index 00000000000000..2f4c9a1f55b8d1 --- /dev/null +++ b/src/python_testing/TestTimeSyncTrustedTimeSource.py @@ -0,0 +1,97 @@ +# +# 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. +# +import time + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from mobly import asserts + +# We don't have a good pipe between the c++ enums in CommissioningDelegate and python +# so this is hardcoded. +# I realize this is dodgy, not sure how to cross the enum from c++ to python cleanly +kConfigureUTCTime = 6 +kConfigureTimeZone = 7 +kConfigureDSTOffset = 8 +kConfigureDefaultNTP = 9 +kConfigureTrustedTimeSource = 19 + +# NOTE: all of these tests require a specific app setup. Please see TestTimeSyncTrustedTimeSourceRunner.py + + +class TestTestTimeSyncTrustedTimeSource(MatterBaseTest): + # This test needs to be run against an app that has previously been commissioned, has been reset + # but not factory reset, and which has been started with the --simulate-no-internal-time flag. + # This test should be run using the provided "TestTimeSyncTrustedTimeSourceRunner.py" script + @async_test_body + async def test_SimulateNoInternalTime(self): + ret = await self.read_single_attribute_check_success( + cluster=Clusters.TimeSynchronization, attribute=Clusters.TimeSynchronization.Attributes.UTCTime) + asserts.assert_equal(ret, NullValue, "Non-null value returned for time") + + @async_test_body + async def test_HaveInternalTime(self): + ret = await self.read_single_attribute_check_success( + cluster=Clusters.TimeSynchronization, attribute=Clusters.TimeSynchronization.Attributes.UTCTime) + asserts.assert_not_equal(ret, NullValue, "Null value returned for time") + + @async_test_body + async def test_SetupTimeSourceACL(self): + # We just want to append to this list + ac = Clusters.AccessControl + acl = await self.read_single_attribute_check_success(cluster=ac, attribute=ac.Attributes.Acl) + new_acl_entry = ac.Structs.AccessControlEntryStruct(privilege=ac.Enums.AccessControlEntryPrivilegeEnum.kView, + authMode=ac.Enums.AccessControlEntryAuthModeEnum.kCase, + subjects=NullValue, targets=[ac.Structs.AccessControlTargetStruct( + cluster=Clusters.TimeSynchronization.id)] + ) + acl.append(new_acl_entry) + await self.default_controller.WriteAttribute(nodeid=self.dut_node_id, attributes=[(0, ac.Attributes.Acl(acl))]) + + async def ReadFromTrustedTimeSource(self): + # Give the node a couple of seconds to reach out and set itself up + # TODO: Subscribe to granularity instead. + time.sleep(6) + ret = await self.read_single_attribute_check_success(cluster=Clusters.TimeSynchronization, attribute=Clusters.TimeSynchronization.Attributes.UTCTime) + asserts.assert_not_equal(ret, NullValue, "Returned time is null") + ret = await self.read_single_attribute_check_success(cluster=Clusters.TimeSynchronization, attribute=Clusters.TimeSynchronization.Attributes.Granularity) + asserts.assert_not_equal(ret, Clusters.TimeSynchronization.Enums.GranularityEnum.kNoTimeGranularity, + "Returned Granularity is kNoTimeGranularity") + # TODO: needs to be gated on the optional attribute + ret = await self.read_single_attribute_check_success(cluster=Clusters.TimeSynchronization, attribute=Clusters.TimeSynchronization.Attributes.TimeSource) + asserts.assert_equal(ret, Clusters.TimeSynchronization.Enums.TimeSourceEnum.kNodeTimeCluster, + "Returned time source is incorrect") + + @async_test_body + async def test_SetAndReadFromTrustedTimeSource(self): + asserts.assert_true('trusted_time_source' in self.matter_test_config.global_test_params, + "trusted_time_source must be included on the command line in " + "the --int-arg flag as trusted_time_source:") + trusted_time_source = Clusters.TimeSynchronization.Structs.FabricScopedTrustedTimeSourceStruct( + nodeID=self.matter_test_config.global_test_params["trusted_time_source"], endpoint=0) + cmd = Clusters.TimeSynchronization.Commands.SetTrustedTimeSource(trustedTimeSource=trusted_time_source) + await self.send_single_cmd(cmd) + + await self.ReadFromTrustedTimeSource() + + @async_test_body + async def test_ReadFromTrustedTimeSource(self): + await self.ReadFromTrustedTimeSource() + + +if __name__ == "__main__": + default_matter_test_main()