diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 22ff4d2db71cb6..34306653270b2a 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -41,6 +41,7 @@ #include #include +#include #include @@ -533,6 +534,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..870ebaf8e026ae --- /dev/null +++ b/scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py @@ -0,0 +1,144 @@ +#!/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 subprocess +import signal +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 = f'chip-all-clusters-app not found' + logging.error(msg) + raise FileNotFoundError(msg) + if not os.path.exists(self.run_python_test_path): + msg = f'run_python_test.py script not found' + logging.error(msg) + raise FileNotFoundError(msg) + if not os.path.exists(self.script_path): + msg = f'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, stdout=sys.stdout, stderr=sys.stderr, shell=True, bufsize=1) + + # 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) + if ret != 0: + kill_process(app2_process) + return ret + + logging.warning("Stopping app with SIGINT") + app2_process.send_signal(signal.SIGINT.value) + app2_process.wait() + + +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/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp index 0d115593e1f9d9..d7739197211eb0 100644 --- a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp +++ b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.cpp @@ -45,3 +45,25 @@ bool DefaultTimeSyncDelegate::IsNTPAddressDomain(chip::CharSpan ntp) // placeholder implementation return false; } + +bool DefaultTimeSyncDelegate::UpdateTimeUsingGNSS() +{ + return false; +} +bool DefaultTimeSyncDelegate::UpdateTimeUsingPTP() +{ + return false; +} +bool DefaultTimeSyncDelegate::UpdateTimeUsingExternalSource() +{ + return false; +} +bool DefaultTimeSyncDelegate::UpdateTimeUsingNTP(bool & usedFullNTP, bool & usedNTS, bool & allSourcesFromMatterNetwork) +{ + // TODO: For some platforms, this is probably already implemented. Ex. Linux. We need an override here. + return false; +} +bool DefaultTimeSyncDelegate::UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP) +{ + return false; +} diff --git a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h index bac1f14fb28e93..91a44e59d0b6b4 100644 --- a/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h +++ b/src/app/clusters/time-synchronization-server/DefaultTimeSyncDelegate.h @@ -33,6 +33,11 @@ class DefaultTimeSyncDelegate : public Delegate bool HandleUpdateDSTOffset(CharSpan name) override; bool IsNTPAddressValid(CharSpan ntp) override; bool IsNTPAddressDomain(CharSpan ntp) override; + bool UpdateTimeUsingGNSS() override; + bool UpdateTimeUsingPTP() override; + bool UpdateTimeUsingExternalSource() override; + bool UpdateTimeUsingNTP(bool & usedFullNTP, bool & usedNTS, bool & allSourcesFromMatterNetwork) override; + bool UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP) 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..d39a37229ac910 100644 --- a/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h +++ b/src/app/clusters/time-synchronization-server/time-synchronization-delegate.h @@ -74,6 +74,42 @@ class Delegate */ virtual bool IsNTPAddressDomain(const CharSpan ntp) = 0; + // TODO: All of these will need to be async. That's going to be a LOT more code, so leaving that until the end since + // none of them return anything useful right now anyway. + /** + * @brief If the delegate supports GNSS and is able to get a good time, it updates the system time if required (ex using + * System::SystemClock().SetClock_RealTime) and returns true. Otherwise, it returns false. + */ + virtual bool UpdateTimeUsingGNSS() = 0; + /** + * @brief If the delegate supports PTP and is able to get a good time, it updates the system time if required (ex using + * System::SystemClock().SetClock_RealTime) and returns true. Otherwise, it returns false. + */ + virtual bool UpdateTimeUsingPTP() = 0; + /** + * @brief If the delegate supports trusted external time source (ex network NTP, cloud-based) and is able to get a good time, it + * updates the system time if required (ex using System::SystemClock().SetClock_RealTime) and returns true. Otherwise, it + * returns false. + */ + virtual bool UpdateTimeUsingExternalSource() = 0; + /** + * @brief This covers delegate-discovered NTP sources. If the delegate supports NTP, it should attempt to update first using + * the DHCPv6 defined NTP server option, falling back to the DHCP server option if ipv4 is supported, followed by servers + * given by _ntp._udp DNS-SD query, if DNS-SD is supported. If the delegate is successful in updating the time, it updates the + * system time as required (ex using System::SystemClock().SetClock_RealTime) and sets the parameters according to selected + * source. If the delegate is uncertain of any values, it should set the parameters to false. The delegate returns true if the + * time was successfully set using NTP, false otherwise. + */ + virtual bool UpdateTimeUsingNTP(bool & usedFullNTP, bool & usedNTS, bool & allSourcesFromMatterNetwork) = 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). The delegate returns true if it was successful in updating the time, false + * otherwise. + */ + virtual bool UpdateTimeUsingNTPFallback(const CharSpan & fallbackNTP) = 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 9a1b2aa408bd59..ca16b596832850 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,11 @@ #include "DefaultTimeSyncDelegate.h" #include "time-synchronization-delegate.h" +// TODO: Set define in build files, check define against flag using static assert on feature map +#if TIME_SYNC_ENABLE_TSC_FEATURE +#include +#endif + #include #include #include @@ -31,6 +36,7 @@ #include #include #include +#include #include @@ -61,6 +67,27 @@ 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(); +} + +void OnPlatformEventWrapper(const DeviceLayer::ChipDeviceEvent * event, intptr_t ptr) +{ + TimeSynchronizationServer * server = reinterpret_cast(ptr); + server->OnPlatformEventFn(*event); +} +#endif + } // namespace namespace chip { @@ -228,6 +255,213 @@ TimeSynchronizationServer & TimeSynchronizationServer::Instance() return sTimeSyncInstance; } +TimeSynchronizationServer::TimeSynchronizationServer() +#if TIME_SYNC_ENABLE_TSC_FEATURE + : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +#endif +{} + +TimeSourceEnum TimeSynchronizationServer::GetTimeFromDelegate() +{ + if (GetDelegate()->UpdateTimeUsingGNSS()) + { + return TimeSourceEnum::kGnss; + } + if (GetDelegate()->UpdateTimeUsingPTP()) + { + return TimeSourceEnum::kPtp; + } + if (GetDelegate()->UpdateTimeUsingExternalSource()) + { + return TimeSourceEnum::kCloudSource; + } + bool full_ntp = false; + bool nts = false; + bool matter = false; + if (GetDelegate()->UpdateTimeUsingNTP(full_ntp, nts, matter)) + { + if (matter) + { + if (full_ntp) + { + if (nts) + { + return TimeSourceEnum::kMatterNTPNTS; + } + return TimeSourceEnum::kMatterNTP; + } + else + { + if (nts) + { + return TimeSourceEnum::kMatterSNTPNTS; + } + return TimeSourceEnum::kMatterSNTP; + } + // non-matter + } + else + { + if (full_ntp) + { + if (nts) + { + return TimeSourceEnum::kNonMatterNTPNTS; + } + return TimeSourceEnum::kNonMatterNTP; + } + else + { + if (nts) + { + return TimeSourceEnum::kNonMatterSNTPNTS; + } + return TimeSourceEnum::kNonMatterSNTP; + } + } + } + return TimeSourceEnum::kNone; +} + +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)) + { + // This is SNTP equivalent even if the delegate has a full NTP stack since there's a single source + mGranularity = GranularityEnum::kSecondsGranularity; + TimeSource::Set(kRootEndpointId, TimeSourceEnum::kNonMatterSNTP); + } + else + { + emitTimeFailureEvent(kRootEndpointId); + } +} + +void TimeSynchronizationServer::OnDeviceConnectedFn(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + // Connected to our trusted time source, let's read the time. + app::AttributePathParams readPaths[1]; + // Read all the feature maps for all the networking clusters on any endpoint to determine what is supported + readPaths[0] = app::AttributePathParams(kRootEndpointId, app::Clusters::TimeSynchronization::Id, + app::Clusters::TimeSynchronization::Attributes::UTCTime::Id); + app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); + app::ReadPrepareParams readParams(sessionHandle); + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + + // TODO: check nulls. + auto attributeCache = Platform::MakeUnique(*this); + auto readClient = chip::Platform::MakeUnique(engine, &exchangeMgr, attributeCache->GetBufferedCallback(), + app::ReadClient::InteractionType::Read); + 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; + UTCTime::TypeInfo::Type time; + CHIP_ERROR err = mAttributeCache->Get(kRootEndpointId, time); + if (err == CHIP_NO_ERROR && !time.IsNull()) + { + // Being conservative with the granularity here + // TODO: ask for granularity in the read path and set accordingly. + // TODO: if we fail to set the time here, maybe we should try the backup? + SetUTCTime(kRootEndpointId, time.Value(), GranularityEnum::kMinutesGranularity, TimeSourceEnum::kNodeTimeCluster); + } + else + { + AttemptToGetFallbackNTPTimeFromDelegate(); + } +} + +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. + bool have_internal_time = false; + if (!RuntimeOptionsProvider::Instance().GetSimulateNoInternalTime()) + { + TimeSourceEnum source = GetTimeFromDelegate(); + if (source != TimeSourceEnum::kNone) + { + // There's not much we can do if we fail to set this, so I guess just ignore the error. + TimeSource::Set(kRootEndpointId, source); + // Estimate the granularity based on the source + // TODO: Maybe get this from the delegate? + switch (source) + { + case TimeSourceEnum::kGnss: + case TimeSourceEnum::kPtp: + mGranularity = GranularityEnum::kMicrosecondsGranularity; + break; + case TimeSourceEnum::kCloudSource: + case TimeSourceEnum::kMatterNTP: + case TimeSourceEnum::kMatterNTPNTS: + case TimeSourceEnum::kNonMatterNTP: + case TimeSourceEnum::kNonMatterNTPNTS: + mGranularity = GranularityEnum::kMillisecondsGranularity; + break; + case TimeSourceEnum::kMatterSNTP: + case TimeSourceEnum::kMatterSNTPNTS: + case TimeSourceEnum::kNonMatterSNTP: + case TimeSourceEnum::kNonMatterSNTPNTS: + // Let's be a bit more conservative with SNTP + mGranularity = GranularityEnum::kSecondsGranularity; + break; + default: + mGranularity = GranularityEnum::kMinutesGranularity; + } + have_internal_time = true; + } + } + if (!have_internal_time) + { + // Delegate couldn't get us a time, next up is to check the trusted time source, if we have one. + // TODO: ifdef this with a client flag, and check the define flag against the feature map using a static config +#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); + } + else +#endif + { + AttemptToGetFallbackNTPTimeFromDelegate(); + } + } +} + void TimeSynchronizationServer::Init() { mTimeSyncDataProvider.Init(Server::GetInstance().GetPersistentStorage()); @@ -245,25 +479,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 +529,10 @@ CHIP_ERROR TimeSynchronizationServer::SetTrustedTimeSource(const DataModel::Null { err = mTimeSyncDataProvider.ClearTrustedTimeSource(); } + if (mGranularity == GranularityEnum::kNoTimeGranularity) + { + AttemptToGetTime(); + } return err; } @@ -544,7 +798,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 +818,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 +854,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 +893,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 +1055,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..19ece61595530e 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 +// TODO: Move this into the build file +#ifndef TIME_SYNC_ENABLE_TSC_FEATURE +#define TIME_SYNC_ENABLE_TSC_FEATURE 1 +#endif + #include "TimeSyncDataProvider.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,27 @@ 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(); + + // Platform event handler functions + void OnPlatformEventFn(const DeviceLayer::ChipDeviceEvent & event); + + // AttributeCache::Callback functions + void OnAttributeChanged(ClusterStateCache * cache, const ConcreteAttributePath & path) override {} + void OnDone(ReadClient * apReadClient) override; +#endif 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 +138,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 + + // Called when the platform is set up - attempts to get time using the recommended source list in the spec. + void AttemptToGetTime(); + // Attempts to set time through the delegate using gnss -> ptp -> cloud -> external ntp. Returns + // TimeSourceEnum of the method used to set the time. If it is unable to get a time kNone is returned. + TimeSourceEnum GetTimeFromDelegate(); + // 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..5e8155a9df31ed --- /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 \ No newline at end of file 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..cc4c2c1e60f6a8 --- /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 \ No newline at end of file diff --git a/src/python_testing/TestTimeSyncTrustedTimeSource.py b/src/python_testing/TestTimeSyncTrustedTimeSource.py new file mode 100644 index 00000000000000..c5f0a4eba63ade --- /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, utc_time_in_matter_epoch +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()