From 0c592029bab6aa06b9a1c4d4c4ac74bb4c49767f Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 9 Dec 2025 10:34:06 -0800 Subject: [PATCH] [history-tracker] add client/server for remote query (#11757) This commit introduces a client/server mechanism to the History Tracker module. This allows a device to query history information from another device over the Thread network using TMF messages. The new functionality is composed of three main parts: - Server (`HistoryTracker::Server`): This component is responsible for handling incoming TMF query requests (`h/qy`). It collects the requested local history entries (e.g., Network Info), formats them into TLVs, and sends them back to the requester in one or more TMF answer messages (`h/an`). It can fragment large responses into multiple messages. - Client (`HistoryTracker::Client`): This provides a new public API (`otHistoryTrackerQueryNetInfo`) to send a query to a remote device. It handles sending the request and processing the received answer(s), passing the retrieved history entries to the user via a callback. A function to cancel an ongoing query (`otHistoryTrackerCancelQuery`) is also added. - TLVs (`history_tracker_tlvs`): New TLVs are defined for the query/answer protocol, including `RequestTlv` to specify the query parameters, `AnswerTlv` to manage multi-message responses, `NetworkInfoTlv` to carry the data, and `QueryIdTlv` to correlate requests and responses. A new CLI command, `history query netinfo`, is added to use the new client API. The existing `history netinfo` output logic is refactored into helper methods to be shared by both the local and remote history commands. The new feature can be enabled/disabled using two new configuration flags: - `OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE` - `OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE` --- include/openthread/history_tracker.h | 64 ++++ include/openthread/instance.h | 2 +- src/cli/cli.hpp | 1 + src/cli/cli_history.cpp | 169 ++++++++- src/cli/cli_history.hpp | 24 ++ src/core/BUILD.gn | 6 + src/core/CMakeLists.txt | 3 + src/core/api/history_tracker_api.cpp | 20 + src/core/config/history_tracker.h | 19 + src/core/instance/instance.cpp | 6 + src/core/instance/instance.hpp | 17 + src/core/thread/tmf.cpp | 7 + src/core/thread/uri_paths.cpp | 6 + src/core/thread/uri_paths.hpp | 2 + src/core/utils/history_tracker.hpp | 11 +- src/core/utils/history_tracker_client.cpp | 232 ++++++++++++ src/core/utils/history_tracker_client.hpp | 137 +++++++ src/core/utils/history_tracker_server.cpp | 343 ++++++++++++++++++ src/core/utils/history_tracker_server.hpp | 116 ++++++ src/core/utils/history_tracker_tlvs.cpp | 90 +++++ src/core/utils/history_tracker_tlvs.hpp | 215 +++++++++++ .../cli/test-040-history-tracker-query.py | 116 ++++++ .../openthread-core-toranj-config-posix.h | 11 +- ...openthread-core-toranj-config-simulation.h | 4 + tests/toranj/start.sh | 1 + 25 files changed, 1599 insertions(+), 23 deletions(-) create mode 100644 src/core/utils/history_tracker_client.cpp create mode 100644 src/core/utils/history_tracker_client.hpp create mode 100644 src/core/utils/history_tracker_server.cpp create mode 100644 src/core/utils/history_tracker_server.hpp create mode 100644 src/core/utils/history_tracker_tlvs.cpp create mode 100644 src/core/utils/history_tracker_tlvs.hpp create mode 100755 tests/toranj/cli/test-040-history-tracker-query.py diff --git a/include/openthread/history_tracker.h b/include/openthread/history_tracker.h index 26da7c9db..e6ac907ab 100644 --- a/include/openthread/history_tracker.h +++ b/include/openthread/history_tracker.h @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -651,6 +652,69 @@ const otHistoryTrackerAilRouter *otHistoryTrackerIterateAilRoutersHistory(otInst */ void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize); +//---------------------------------------------------------------------------------------------------------------------- +// History Tracker Client function (requires `OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE`) + +/** + * Callback function pointer type to report the retrieved Network Info entries from a query to another device. + * + * Used when `OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE` is enabled. + * + * @param[in] aError Indicates the status of the query and entries being reported: + * - `OT_ERROR_PENDING`: There are more entries to be reported. + * - `OT_ERROR_NONE`: This is the last entry, and the query is complete. + * - `OT_ERROR_RESPONSE_TIMEOUT`: Timed out waiting for a response. + * - `OT_ERROR_PARSE`: The received query answer does not follow the expected format. + * @param[in] aNetworkInfo The network information entry. This may be `NULL` if `aError` is `OT_ERROR_NONE` + * (indicating the end of the list) or on certain error conditions. + * @param[in] aEntryAge The entry age in milliseconds. Applicable only when @p aNetworkInfo is not `NULL`. + * @param[in] aContext An arbitrary callback context provided by the caller during the query. + */ +typedef void (*otHistoryTrackerNetInfoCallback)(otError aError, + const otHistoryTrackerNetworkInfo *aNetworkInfo, + uint32_t aEntryAge, + void *aContext); + +/** + * Queries for Network Info history entries from a specified RLOC16. + * + * Requires `OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE`. + * + * Upon successful initiation of the query, the provided @p aCallback will be invoked to report the requested entries. + * + * The callback parameter `aError` indicates if any error occurs. If there are more entries to be provided, `aError` + * will be set to `OT_ERROR_PENDING`. The end of the list is indicated by `aError` being set to `OT_ERROR_NONE` with a + * null entry pointer. Any other errors, such as `OT_ERROR_RESPONSE_TIMEOUT` or `OT_ERROR_PARSE` (if the received + * response has an invalid format), will also be indicated by `aError`. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aRloc16 The RLOC16 of the device to query. + * @param[in] aMaxEntries The maximum number of entries to request (0 indicates all available entries). + * @param[in] aMaxEntryAge The maximum age (in milliseconds) of entries to request (0 indicates no age limit). + * @param[in] aCallback A pointer to a callback function to be called when the query response is received. + * @param[in] aContext A user-defined context pointer to be passed to the callback function. + * + * @retval OT_ERROR_NONE If the query was successfully sent. + * @retval OT_ERROR_BUSY If a query is already in progress. + * @retval OT_ERROR_NO_BUFS If there are insufficient message buffers to send the query. + * @retval OT_ERROR_INVALID_STATE If device is not attached. + */ +otError otHistoryTrackerQueryNetInfo(otInstance *aInstance, + uint16_t aRloc16, + uint16_t aMaxEntries, + uint32_t aMaxEntryAge, + otHistoryTrackerNetInfoCallback aCallback, + void *aContext); + +/** + * Cancels any ongoing query. + * + * Requires `OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE`. + * + * @param[in] aInstance The OpenThread instance. + */ +void otHistoryTrackerCancelQuery(otInstance *aInstance); + /** * @} */ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index f1ae1679c..c724f8f4f 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (557) +#define OPENTHREAD_API_VERSION (558) /** * @addtogroup api-instance diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index dfa4fef37..bdd2c4971 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -111,6 +111,7 @@ class Interpreter : public OutputImplementer, public Utils friend class Commissioner; friend class Dns; friend class Joiner; + friend class History; friend class LinkMetrics; friend class Mdns; friend class MeshDiag; diff --git a/src/cli/cli_history.cpp b/src/cli/cli_history.cpp index 0a2726fc2..20dfa09aa 100644 --- a/src/cli/cli_history.cpp +++ b/src/cli/cli_history.cpp @@ -647,20 +647,12 @@ template <> otError History::Process(Arg aArgs[]) otHistoryTrackerIterator iterator; const otHistoryTrackerNetworkInfo *info; uint32_t entryAge; - char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE]; - char linkModeString[Interpreter::kLinkModeStringSize]; SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries)); if (!isList) { - // | Age | Role | Mode | RLOC16 | Partition ID | - // +----------------------+----------+------+--------+--------------+ - - static const char *const kNetInfoTitles[] = {"Age", "Role", "Mode", "RLOC16", "Partition ID"}; - static const uint8_t kNetInfoColumnWidths[] = {22, 10, 6, 8, 14}; - - OutputTableHeader(kNetInfoTitles, kNetInfoColumnWidths); + OutputNetInfoTableHeader(); } otHistoryTrackerInitIterator(&iterator); @@ -670,18 +662,37 @@ template <> otError History::Process(Arg aArgs[]) info = otHistoryTrackerIterateNetInfoHistory(GetInstancePtr(), &iterator, &entryAge); VerifyOrExit(info != nullptr); - otHistoryTrackerEntryAgeToString(entryAge, ageString, sizeof(ageString)); - - OutputLine( - isList ? "%s -> role:%s mode:%s rloc16:0x%04x partition-id:%lu" : "| %20s | %-8s | %-4s | 0x%04x | %12lu |", - ageString, otThreadDeviceRoleToString(info->mRole), - Interpreter::LinkModeToString(info->mMode, linkModeString), info->mRloc16, ToUlong(info->mPartitionId)); + OutputNetInfoEntry(isList, *info, entryAge); } exit: return error; } +void History::OutputNetInfoTableHeader(void) +{ + // | Age | Role | Mode | RLOC16 | Partition ID | + // +----------------------+----------+------+--------+--------------+ + + static const char *const kNetInfoTitles[] = {"Age", "Role", "Mode", "RLOC16", "Partition ID"}; + static const uint8_t kNetInfoColumnWidths[] = {22, 10, 6, 8, 14}; + + OutputTableHeader(kNetInfoTitles, kNetInfoColumnWidths); +} + +void History::OutputNetInfoEntry(bool aIsList, const otHistoryTrackerNetworkInfo &aInfo, uint32_t aEntryAge) +{ + char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE]; + char linkModeString[Interpreter::kLinkModeStringSize]; + + otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString)); + + OutputLine(aIsList ? "%s -> role:%s mode:%s rloc16:0x%04x partition-id:%lu" + : "| %20s | %-8s | %-4s | 0x%04x | %12lu |", + ageString, otThreadDeviceRoleToString(aInfo.mRole), + Interpreter::LinkModeToString(aInfo.mMode, linkModeString), aInfo.mRloc16, ToUlong(aInfo.mPartitionId)); +} + /** * @cli history rx * @code @@ -1899,6 +1910,126 @@ const char *History::DnsSrpAddrTypeToString(otHistoryTrackerDnsSrpAddrType aType return Stringify(aType, kAddrTypeStrings, "--"); } +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + +otError History::ParseQueryArgs(Arg aArgs[], + bool &aIsList, + uint16_t &aRloc16, + uint16_t &aNumEntries, + uint32_t &aMaxEntryAge) const +{ + otError error = OT_ERROR_NONE; + + if (*aArgs == "list") + { + aArgs++; + aIsList = true; + } + else + { + aIsList = false; + } + + SuccessOrExit(error = aArgs->ParseAsUint16(aRloc16)); + aArgs++; + + if (aArgs->ParseAsUint16(aNumEntries) == OT_ERROR_NONE) + { + aArgs++; + } + else + { + aNumEntries = 0; + } + + if (aArgs->ParseAsUint32(aMaxEntryAge) == OT_ERROR_NONE) + { + aArgs++; + } + else + { + aMaxEntryAge = 0; + } + + error = aArgs[0].IsEmpty() ? OT_ERROR_NONE : OT_ERROR_INVALID_ARGS; + +exit: + return error; +} + +template <> otError History::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + uint16_t rloc16; + uint16_t maxEntries; + uint32_t maxEntryAge; + + /** + * @cli history query netinfo + * @code + * history query netinfo 0xac00 + * | Age | Role | Mode | RLOC16 | Partition ID | + * +----------------------+----------+------+--------+--------------+ + * | 00:00:36.786 | detached | rdn | 0xac00 | 807291876 | + * | 00:00:43.966 | detached | rdn | 0xfffe | 0 | + * Done + * @endcode + * @cparam history query netinfo [@ca{list}] @ca{rloc16} [@ca{num-entries}] [@ca{max-entry-age}] + * * Use the `list` option to display the output in list format. Otherwise, the output is shown in table format. + * * The `rloc16` indicates the RLOC16 of the device to query. + * * Use the `num-entries` option to limit the output to the number of most-recent entries specified. If this option + * is not used or set to zero, all stored entries are shown in the output. + * * Use the `max-entry-age` option to limit maximum age of entries. If this option is not used or set to zero, all + * stored entries are shown in the output. + * @par + * Queries the "netinfo" history entries from another device and outputs them in a table or list format. For details + * on the table format and its entries, please refer to the `history netinfo` command. + * @sa otHistoryTrackerQueryNetInfo + */ + if (aArgs[0] == "netinfo") + { + SuccessOrExit(error = ParseQueryArgs(&aArgs[1], mQueryUseListFormat, rloc16, maxEntries, maxEntryAge)); + SuccessOrExit(error = otHistoryTrackerQueryNetInfo(GetInstancePtr(), rloc16, maxEntries, maxEntryAge, + HandleNetInfo, this)); + + if (!mQueryUseListFormat) + { + OutputNetInfoTableHeader(); + } + + error = OT_ERROR_PENDING; + } + else + { + error = OT_ERROR_INVALID_ARGS; + } + +exit: + return error; +} + +void History::HandleNetInfo(otError aError, + const otHistoryTrackerNetworkInfo *aNetworkInfo, + uint32_t aEntryAge, + void *aContext) +{ + static_cast(aContext)->HandleNetInfo(aError, aNetworkInfo, aEntryAge); +} + +void History::HandleNetInfo(otError aError, const otHistoryTrackerNetworkInfo *aNetworkInfo, uint32_t aEntryAge) +{ + if (aNetworkInfo != nullptr) + { + OutputNetInfoEntry(mQueryUseListFormat, *aNetworkInfo, aEntryAge); + } + + OutputResult(aError); +} + +void History::OutputResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); } + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + otError History::Process(Arg aArgs[]) { #define CmdEntry(aCommandString) {aCommandString, &History::Process} @@ -1915,8 +2046,12 @@ otError History::Process(Arg aArgs[]) #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE CmdEntry("omrprefix"), CmdEntry("onlinkprefix"), #endif - CmdEntry("prefix"), CmdEntry("route"), CmdEntry("router"), - CmdEntry("rx"), CmdEntry("rxtx"), CmdEntry("tx"), + CmdEntry("prefix"), +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + CmdEntry("query"), +#endif + CmdEntry("route"), CmdEntry("router"), CmdEntry("rx"), + CmdEntry("rxtx"), CmdEntry("tx"), }; #undef CmdEntry diff --git a/src/cli/cli_history.hpp b/src/cli/cli_history.hpp index a37918e10..760a44b32 100644 --- a/src/cli/cli_history.hpp +++ b/src/cli/cli_history.hpp @@ -97,11 +97,35 @@ private: void OutputRxTxEntryListFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx); void OutputRxTxEntryTableFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx); + void OutputNetInfoTableHeader(void); + void OutputNetInfoEntry(bool aIsList, const otHistoryTrackerNetworkInfo &aInfo, uint32_t aEntryAge); + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + void OutputResult(otError aError); + otError ParseQueryArgs(Arg aArgs[], + bool &aIsList, + uint16_t &aRloc16, + uint16_t &aNumEntries, + uint32_t &aMaxEntryAge) const; + + void HandleNetInfo(otError aError, const otHistoryTrackerNetworkInfo *aNetworkInfo, uint32_t aEntryAge); + + static void HandleNetInfo(otError aError, + const otHistoryTrackerNetworkInfo *aNetworkInfo, + uint32_t aEntryAge, + void *aContext); + +#endif + static const char *MessagePriorityToString(uint8_t aPriority); static const char *RadioTypeToString(const otHistoryTrackerMessageInfo &aInfo); static const char *MessageTypeToString(const otHistoryTrackerMessageInfo &aInfo); static const char *DnsSrpAddrTypeToString(otHistoryTrackerDnsSrpAddrType aType); static const char *AilRouterEventToString(otHistoryTrackerAilRouterEvent aEvent); + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + bool mQueryUseListFormat; +#endif }; } // namespace Cli diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 9bed5e494..7a8440cea 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -621,6 +621,12 @@ openthread_core_files = [ "utils/heap.hpp", "utils/history_tracker.cpp", "utils/history_tracker.hpp", + "utils/history_tracker_client.cpp", + "utils/history_tracker_client.hpp", + "utils/history_tracker_server.cpp", + "utils/history_tracker_server.hpp", + "utils/history_tracker_tlvs.cpp", + "utils/history_tracker_tlvs.hpp", "utils/jam_detector.cpp", "utils/jam_detector.hpp", "utils/link_metrics_manager.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6679264ae..1707d4436 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -280,6 +280,9 @@ set(COMMON_SOURCES utils/flash.cpp utils/heap.cpp utils/history_tracker.cpp + utils/history_tracker_client.cpp + utils/history_tracker_server.cpp + utils/history_tracker_tlvs.cpp utils/jam_detector.cpp utils/link_metrics_manager.cpp utils/mesh_diag.cpp diff --git a/src/core/api/history_tracker_api.cpp b/src/core/api/history_tracker_api.cpp index b328f9bef..de58388bf 100644 --- a/src/core/api/history_tracker_api.cpp +++ b/src/core/api/history_tracker_api.cpp @@ -204,4 +204,24 @@ void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_ HistoryTracker::Local::EntryAgeToString(aEntryAge, aBuffer, aSize); } +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + +otError otHistoryTrackerQueryNetInfo(otInstance *aInstance, + uint16_t aRloc16, + uint16_t aMaxEntries, + uint32_t aMaxEntryAge, + otHistoryTrackerNetInfoCallback aCallback, + void *aContext) +{ + return AsCoreType(aInstance).Get().QueryNetInfo(aRloc16, aMaxEntries, aMaxEntryAge, + aCallback, aContext); +} + +void otHistoryTrackerCancelQuery(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().CancelQuery(); +} + +#endif + #endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE diff --git a/src/core/config/history_tracker.h b/src/core/config/history_tracker.h index a2af226c4..29c7afbcf 100644 --- a/src/core/config/history_tracker.h +++ b/src/core/config/history_tracker.h @@ -52,6 +52,25 @@ #define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + * + * Define as 1 to enable History Tracker Server module (answering received TMF history queries). + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#endif + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + * + * Define as 1 to enable History Tracker Client module (using TMF history query to retrieve history info from other + * devices). + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#endif + /** * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE * diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index c5e424f16..44192eb62 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -260,6 +260,12 @@ Instance::Instance(void) #endif #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE , mHistoryTrackerLocal(*this) +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + , mHistoryTrackerServer(*this) +#endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + , mHistoryTrackerClient(*this) +#endif #endif #if OPENTHREAD_CONFIG_LINK_METRICS_MANAGER_ENABLE , mLinkMetricsManager(*this) diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 4ee781ced..3974855f0 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -147,6 +147,8 @@ #include "utils/channel_monitor.hpp" #include "utils/heap.hpp" #include "utils/history_tracker.hpp" +#include "utils/history_tracker_client.hpp" +#include "utils/history_tracker_server.hpp" #include "utils/jam_detector.hpp" #include "utils/link_metrics_manager.hpp" #include "utils/mesh_diag.hpp" @@ -706,6 +708,12 @@ private: #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE HistoryTracker::Local mHistoryTrackerLocal; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + HistoryTracker::Server mHistoryTrackerServer; +#endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + HistoryTracker::Client mHistoryTrackerClient; +#endif #endif #if OPENTHREAD_CONFIG_LINK_METRICS_MANAGER_ENABLE @@ -1030,7 +1038,16 @@ template <> inline Utils::MeshDiag &Instance::Get(void) { return mMeshDiag; } #endif #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + template <> inline HistoryTracker::Local &Instance::Get(void) { return mHistoryTrackerLocal; } + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE +template <> inline HistoryTracker::Server &Instance::Get(void) { return mHistoryTrackerServer; } +#endif + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE +template <> inline HistoryTracker::Client &Instance::Get(void) { return mHistoryTrackerClient; } +#endif #endif #if OPENTHREAD_CONFIG_LINK_METRICS_MANAGER_ENABLE diff --git a/src/core/thread/tmf.cpp b/src/core/thread/tmf.cpp index bffbb6342..b385a49c3 100644 --- a/src/core/thread/tmf.cpp +++ b/src/core/thread/tmf.cpp @@ -172,6 +172,13 @@ bool Agent::HandleResource(const char *aUriPath, Message &aMessage, const Ip6::M Case(kUriDiagnosticGetAnswer, NetworkDiagnostic::Client); #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + Case(kUriHistoryQuery, HistoryTracker::Server); +#endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + Case(kUriHistoryAnswer, HistoryTracker::Client); +#endif + #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE Case(kUriMlr, BackboneRouter::Manager); diff --git a/src/core/thread/uri_paths.cpp b/src/core/thread/uri_paths.cpp index 85c1f654c..aafdab496 100644 --- a/src/core/thread/uri_paths.cpp +++ b/src/core/thread/uri_paths.cpp @@ -91,6 +91,8 @@ static constexpr Entry kEntries[] = { {"d/dg"}, // kUriDiagnosticGetRequest {"d/dq"}, // kUriDiagnosticGetQuery {"d/dr"}, // kUriDiagnosticReset + {"h/an"}, // kUriHistoryAnswer + {"h/qy"}, // kUriHistoryQuery {"n/dn"}, // kUriDuaRegistrationNotify {"n/dr"}, // kUriDuaRegistrationRequest {"n/mr"}, // kUriMlr @@ -137,6 +139,8 @@ static_assert(AreConstStringsEqual(kEntries[kUriDiagnosticGetAnswer].mPath, "d/d static_assert(AreConstStringsEqual(kEntries[kUriDiagnosticGetRequest].mPath, "d/dg"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriDiagnosticGetQuery].mPath, "d/dq"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriDiagnosticReset].mPath, "d/dr"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriHistoryAnswer].mPath, "h/an"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriHistoryQuery].mPath, "h/qy"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriDuaRegistrationNotify].mPath, "n/dn"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriDuaRegistrationRequest].mPath, "n/dr"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriMlr].mPath, "n/mr"), "kEntries is invalid"); @@ -200,6 +204,8 @@ template <> const char *UriToString(void) { return "Dia template <> const char *UriToString(void) { return "DiagGetRequest"; } template <> const char *UriToString(void) { return "DiagGetQuery"; } template <> const char *UriToString(void) { return "DiagReset"; } +template <> const char *UriToString(void) { return "HistAnswer"; } +template <> const char *UriToString(void) { return "HistQuery"; } template <> const char *UriToString(void) { return "DuaRegNotify"; } template <> const char *UriToString(void) { return "DuaRegRequest"; } template <> const char *UriToString(void) { return "Mlr"; } diff --git a/src/core/thread/uri_paths.hpp b/src/core/thread/uri_paths.hpp index 175a414d9..ed19b90b4 100644 --- a/src/core/thread/uri_paths.hpp +++ b/src/core/thread/uri_paths.hpp @@ -83,6 +83,8 @@ enum Uri : uint8_t kUriDiagnosticGetRequest, ///< Network Diagnostic Get Request ("d/dg") kUriDiagnosticGetQuery, ///< Network Diagnostic Get Query ("d/dq") kUriDiagnosticReset, ///< Network Diagnostic Reset ("d/dr") + kUriHistoryAnswer, ///< History Answer ("h/an") + kUriHistoryQuery, ///< History Query ("h/qy") kUriDuaRegistrationNotify, ///< DUA Registration Notification ("n/dn") kUriDuaRegistrationRequest, ///< DUA Registration Request ("n/dr") kUriMlr, ///< Multicast Listener Registration ("n/mr") diff --git a/src/core/utils/history_tracker.hpp b/src/core/utils/history_tracker.hpp index c6f50a696..175f8b231 100644 --- a/src/core/utils/history_tracker.hpp +++ b/src/core/utils/history_tracker.hpp @@ -89,14 +89,21 @@ public: * An iterator MUST be initialized before it is used. An iterator can be initialized again to start from * the beginning of the list. */ - void Init(void) { ResetEntryNumber(), SetInitTime(); } + void Init(void) { Init(TimerMilli::GetNow()); } + + /** + * Initializes an `Iterator` + * + * @param[in] aNow The now time. + */ + void Init(TimeMilli aNow) { ResetEntryNumber(), SetInitTime(aNow); } private: uint16_t GetEntryNumber(void) const { return mData16; } void ResetEntryNumber(void) { mData16 = 0; } void IncrementEntryNumber(void) { mData16++; } TimeMilli GetInitTime(void) const { return TimeMilli(mData32); } - void SetInitTime(void) { mData32 = TimerMilli::GetNow().GetValue(); } + void SetInitTime(TimeMilli aNow) { mData32 = aNow.GetValue(); } }; typedef otHistoryTrackerNetworkInfo NetworkInfo; ///< Thread network info. diff --git a/src/core/utils/history_tracker_client.cpp b/src/core/utils/history_tracker_client.cpp new file mode 100644 index 000000000..839066ddd --- /dev/null +++ b/src/core/utils/history_tracker_client.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the History Tracker Client. + */ + +#include "history_tracker_client.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace HistoryTracker { + +RegisterLogModule("HistoryClient"); + +Client::Client(Instance &aInstance) + : InstanceLocator(aInstance) + , mActive(false) + , mQueryRloc16(0) + , mQueryId(0) + , mAnswerIndex(0) + , mTimer(aInstance) +{ +} + +void Client::CancelQuery(void) +{ + mActive = false; + mTimer.Stop(); +} + +Error Client::QueryNetInfo(uint16_t aRloc16, + uint16_t aMaxEntries, + uint32_t aMaxEntryAge, + NetInfoCallback aCallback, + void *aContext) +{ + Error error = kErrorNone; + + VerifyOrExit(!mActive, error = kErrorBusy); + + mCallbacks.mNetInfo.Set(aCallback, aContext); + error = SendQuery(Tlv::kNetworkInfo, aMaxEntries, aMaxEntryAge, aRloc16); + +exit: + return error; +} + +Error Client::SendQuery(Tlv::Type aTlvType, uint16_t aMaxEntries, uint32_t aMaxEntryAge, uint16_t aRloc16) +{ + Error error = kErrorNone; + OwnedPtr message; + Tmf::MessageInfo messageInfo(GetInstance()); + RequestTlv requestTlv; + + VerifyOrExit(Get().IsAttached(), error = kErrorInvalidState); + + message.Reset(Get().NewNonConfirmablePostMessage(kUriHistoryQuery)); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + IgnoreError(message->SetPriority(Message::kPriorityLow)); + + mQueryId++; + SuccessOrExit(error = Tlv::Append(*message, mQueryId)); + + requestTlv.Init(aTlvType, aMaxEntries, aMaxEntryAge); + SuccessOrExit(error = message->Append(requestTlv)); + + messageInfo.SetSockAddrToRloc(); + messageInfo.GetPeerAddr().SetToRoutingLocator(Get().GetMeshLocalPrefix(), aRloc16); + + SuccessOrExit(error = Get().SendMessage(*message, messageInfo)); + message.Release(); + + LogInfo("Sent %s for TLV %u to 0x%04x", UriToString(), aTlvType, aRloc16); + + mActive = true; + mTlvType = aTlvType; + mQueryRloc16 = aRloc16; + mAnswerIndex = 0; + mTimer.Start(kResponseTimeout); + +exit: + return error; +} + +template <> void Client::HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + VerifyOrExit(aMessage.IsConfirmablePostRequest()); + IgnoreError(Get().SendEmptyAck(aMessage, aMessageInfo)); + + LogInfo("Received %s from %s", ot::UriToString(), + aMessageInfo.GetPeerAddr().ToString().AsCString()); + + SuccessOrExit(ProcessAnswer(aMessage, aMessageInfo)); + + switch (mTlvType) + { + case Tlv::kNetworkInfo: + ProcessNetInfoAnswer(aMessage); + break; + default: + ExitNow(); + } + +exit: + return; +} + +Error Client::ProcessAnswer(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Error error = kErrorFailed; + AnswerTlv answerTlv; + uint16_t queryId; + + VerifyOrExit(mActive); + VerifyOrExit(Get().IsRoutingLocator(aMessageInfo.GetPeerAddr())); + VerifyOrExit(aMessageInfo.GetPeerAddr().GetIid().GetLocator() == mQueryRloc16); + + SuccessOrExit(Tlv::Find(aMessage, queryId)); + VerifyOrExit(queryId == mQueryId); + + SuccessOrExit(Tlv::FindTlv(aMessage, answerTlv)); + + if (answerTlv.GetIndex() != mAnswerIndex) + { + Finalize(kErrorResponseTimeout); + ExitNow(); + } + + mAnswerIndex++; + error = kErrorNone; + +exit: + return error; +} + +void Client::ProcessNetInfoAnswer(const Coap::Message &aMessage) +{ + Error error = kErrorNone; + OffsetRange offsetRange; + Tlv tlv; + NetworkInfoTlv netInfoTlv; + NetworkInfo netInfo; + + offsetRange.InitFromMessageOffsetToEnd(aMessage); + + for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlv.GetSize())) + { + SuccessOrExit(error = aMessage.Read(offsetRange, tlv)); + VerifyOrExit(offsetRange.Contains(tlv.GetSize()), error = kErrorParse); + + if (tlv.GetType() != Tlv::kNetworkInfo) + { + continue; + } + + if (tlv.GetLength() == 0) + { + Finalize(kErrorNone); + ExitNow(); + } + + SuccessOrExit(error = aMessage.Read(offsetRange, netInfoTlv)); + VerifyOrExit(netInfoTlv.IsValid(), error = kErrorParse); + + netInfoTlv.CopyTo(netInfo); + + mCallbacks.mNetInfo.InvokeIfSet(kErrorPending, &netInfo, netInfoTlv.GetEntryAge()); + VerifyOrExit(mActive); + } + +exit: + if (error != kErrorNone) + { + Finalize(error); + } +} + +void Client::Finalize(Error aError) +{ + VerifyOrExit(mActive); + + CancelQuery(); + + switch (mTlvType) + { + case Tlv::kNetworkInfo: + mCallbacks.mNetInfo.InvokeIfSet(aError, nullptr, 0); + break; + default: + break; + } + +exit: + return; +} + +void Client::HandleTimer(void) { Finalize(kErrorResponseTimeout); } + +} // namespace HistoryTracker +} // namespace ot + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE diff --git a/src/core/utils/history_tracker_client.hpp b/src/core/utils/history_tracker_client.hpp new file mode 100644 index 000000000..75bd85e98 --- /dev/null +++ b/src/core/utils/history_tracker_client.hpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions to support History Tracker Client. + */ + +#ifndef HISTORY_TRACKER_CLIENT_HPP_ +#define HISTORY_TRACKER_CLIENT_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + +#include + +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/timer.hpp" +#include "thread/tmf.hpp" +#include "utils/history_tracker.hpp" +#include "utils/history_tracker_tlvs.hpp" + +namespace ot { +namespace HistoryTracker { + +/** + * Implements a History Tracker Client + */ +class Client : public InstanceLocator +{ + friend class Tmf::Agent; + +public: + typedef otHistoryTrackerNetInfoCallback NetInfoCallback; ///< Callback function type for Network Info queries. + + /** + * Constructor for the Client. + * + * @param[in] aInstance The OpenThread instance. + */ + explicit Client(Instance &aInstance); + + /** + * Cancels any ongoing query. + */ + void CancelQuery(void); + + /** + * Queries for Network Info entries from a specified RLOC16. + * + * Upon successful initiation of the query, the provided @p aCallback will be invoked to report the requested + * retrieved entries (parsing the answer). The callback parameter `aError` indicates if any error occurs. If there + * are more entries to be provided, `aError` will be set to `kErrorPending`. The end of the list is indicated by + * `aError` being set to `kErrorNone` with a null entry pointer. Any other errors, such as `kErrorResponseTimeout` + * or `kErrorParse` (if the received response has an invalid format), will also be indicated by `aError`. + * + * @param[in] aRloc16 The RLOC16 of the device to query. + * @param[in] aMaxEntries The maximum number of entries to request (0 indicates all available entries). + * @param[in] aMaxEntryAge The maximum age (in milliseconds) of entries to request (0 indicates no age limit). + * @param[in] aCallback A pointer to a callback function to be called when the query response is received. + * @param[in] aContext A user-defined context pointer to be passed to the callback function. + * + * @retval kErrorNone If the query was successfully sent. + * @retval kErrorBusy If a query is already in progress. + * @retval kErrorNoBufs If there are insufficient message buffers to send the query. + * @retval kErrorInvalidState Device is not attached. + */ + Error QueryNetInfo(uint16_t aRloc16, + uint16_t aMaxEntries, + uint32_t aMaxEntryAge, + NetInfoCallback aCallback, + void *aContext); + +private: + static constexpr uint16_t kResponseTimeout = 5000; + + Error SendQuery(Tlv::Type aTlvType, uint16_t aMaxEntries, uint32_t aMaxEntryAge, uint16_t aRloc16); + Error ProcessAnswer(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + void ProcessNetInfoAnswer(const Coap::Message &aMessage); + void Finalize(Error aError); + void HandleTimer(void); + + template void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + + union Callbacks + { + Callbacks(void) {} + + Callback mNetInfo; + }; + + using TimeoutTimer = TimerMilliIn; + + bool mActive; + Tlv::Type mTlvType; + Callbacks mCallbacks; + uint16_t mQueryRloc16; + uint16_t mQueryId; + uint16_t mAnswerIndex; + TimeoutTimer mTimer; +}; + +DeclareTmfHandler(Client, kUriHistoryAnswer); + +} // namespace HistoryTracker +} // namespace ot + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE + +#endif // HISTORY_TRACKER_CLIENT_HPP_ diff --git a/src/core/utils/history_tracker_server.cpp b/src/core/utils/history_tracker_server.cpp new file mode 100644 index 000000000..e42a5c099 --- /dev/null +++ b/src/core/utils/history_tracker_server.cpp @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the History Tracker Server. + */ + +#include "history_tracker_server.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace HistoryTracker { + +RegisterLogModule("HistoryServer"); + +Server::Server(Instance &aInstance) + : InstanceLocator(aInstance) +{ +} + +template <> void Server::HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + VerifyOrExit(aMessage.IsPostRequest()); + + LogInfo("Received %s from %s", UriToString(), aMessageInfo.GetPeerAddr().ToString().AsCString()); + + if (aMessage.IsConfirmable()) + { + IgnoreError(Get().SendEmptyAck(aMessage, aMessageInfo)); + } + + PrepareAndSendAnswers(aMessageInfo.GetPeerAddr(), aMessage); + +exit: + return; +} + +Error Server::AllocateAnswer(Coap::Message *&aAnswer, AnswerInfo &aInfo) +{ + // Allocates an `Answer` message, adds it to `mAnswerQueue`, + // updates the `aInfo.mFirstAnswer` if it is the first allocated + // messages, and appends `QueryIdTlv` to the message (if needed). + + Error error = kErrorNone; + + aAnswer = Get().NewConfirmablePostMessage(kUriHistoryAnswer); + VerifyOrExit(aAnswer != nullptr, error = kErrorNoBufs); + IgnoreError(aAnswer->SetPriority(aInfo.mPriority)); + + mAnswerQueue.Enqueue(*aAnswer); + + if (aInfo.mFirstAnswer == nullptr) + { + aInfo.mFirstAnswer = aAnswer; + } + + if (aInfo.mHasQueryId) + { + SuccessOrExit(error = Tlv::Append(*aAnswer, aInfo.mQueryId)); + } + +exit: + return error; +} + +bool Server::IsLastAnswer(const Coap::Message &aAnswer) const +{ + // Indicates whether `aAnswer` is the last one associated with + // the same query. + + bool isLast = true; + AnswerTlv answerTlv; + + // If there is no Answer TLV, we assume it is the last answer. + + SuccessOrExit(Tlv::FindTlv(aAnswer, answerTlv)); + isLast = answerTlv.IsLast(); + +exit: + return isLast; +} + +void Server::FreeAllRelatedAnswers(Coap::Message &aFirstAnswer) +{ + // Dequeues and frees all answer messages related to the same query + // as `aFirstAnswer`. Note that related answers are enqueued in + // order. + + Coap::Message *answer = &aFirstAnswer; + + while (answer != nullptr) + { + Coap::Message *next = IsLastAnswer(*answer) ? nullptr : answer->GetNextCoapMessage(); + + mAnswerQueue.DequeueAndFree(*answer); + answer = next; + } +} + +void Server::PrepareAndSendAnswers(const Ip6::Address &aDestination, const Message &aRequest) +{ + Coap::Message *answer; + Error error; + AnswerInfo info; + OffsetRange offsetRange; + Tlv tlv; + RequestTlv requestTlv; + AnswerTlv answerTlv; + + if (Tlv::Find(aRequest, info.mQueryId) == kErrorNone) + { + info.mHasQueryId = true; + } + + info.mPriority = aRequest.GetPriority(); + + SuccessOrExit(error = AllocateAnswer(answer, info)); + + offsetRange.InitFromMessageOffsetToEnd(aRequest); + + for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlv.GetSize())) + { + SuccessOrExit(error = aRequest.Read(offsetRange, tlv)); + VerifyOrExit(offsetRange.Contains(tlv.GetSize()), error = kErrorParse); + + if (tlv.GetType() == Tlv::kRequest) + { + SuccessOrExit(error = aRequest.Read(offsetRange, requestTlv)); + VerifyOrExit(requestTlv.IsValid(), error = kErrorParse); + + switch (requestTlv.GetTlvType()) + { + case Tlv::kNetworkInfo: + SuccessOrExit(error = AppendNetworkInfo(answer, info, requestTlv)); + break; + + default: + break; + } + + SuccessOrExit(error = CheckAnswerLength(answer, info)); + } + } + + answerTlv.Init(info.mAnswerIndex, /* aIsLast */ true); + SuccessOrExit(error = answer->Append(answerTlv)); + + SendNextAnswer(*info.mFirstAnswer, aDestination); + +exit: + if ((error != kErrorNone) && (info.mFirstAnswer != nullptr)) + { + FreeAllRelatedAnswers(*info.mFirstAnswer); + } +} + +Error Server::CheckAnswerLength(Coap::Message *&aAnswer, AnswerInfo &aInfo) +{ + // Checks the length of the `aAnswer` message and if it is above + // the threshold, it enqueues the message for transmission after + // appending an Answer TLV with the current index to the message. + // In this case, it will also allocate a new answer message. + + Error error = kErrorNone; + AnswerTlv answerTlv; + + VerifyOrExit(aAnswer->GetLength() >= kAnswerMessageLengthThreshold); + + answerTlv.Init(aInfo.mAnswerIndex++, /* aIsLast */ false); + SuccessOrExit(error = aAnswer->Append(answerTlv)); + + error = AllocateAnswer(aAnswer, aInfo); + +exit: + return error; +} + +void Server::SendNextAnswer(Coap::Message &aAnswer, const Ip6::Address &aDestination) +{ + Error error = kErrorNone; + Coap::Message *nextAnswer = IsLastAnswer(aAnswer) ? nullptr : aAnswer.GetNextCoapMessage(); + Tmf::MessageInfo messageInfo(GetInstance()); + + mAnswerQueue.Dequeue(aAnswer); + + PrepareMessageInfoForDest(aDestination, messageInfo); + + // When sending the message, we pass `nextAnswer` as `aContext` + // to be used when invoking callback `HandleAnswerResponse()`. + + error = Get().SendMessage(aAnswer, messageInfo, HandleAnswerResponse, nextAnswer); + + if (error != kErrorNone) + { + // If the `SendMessage()` fails, we `Free` the dequeued + // `aAnswer` and all the related next answers in the queue. + + aAnswer.Free(); + + if (nextAnswer != nullptr) + { + FreeAllRelatedAnswers(*nextAnswer); + } + } +} + +void Server::PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const +{ + if (aDestination.IsMulticast()) + { + aMessageInfo.SetMulticastLoop(true); + } + + if (aDestination.IsLinkLocalUnicastOrMulticast()) + { + aMessageInfo.SetSockAddr(Get().GetLinkLocalAddress()); + } + else + { + aMessageInfo.SetSockAddrToRloc(); + } + + aMessageInfo.SetPeerAddr(aDestination); +} + +void Server::HandleAnswerResponse(void *aContext, + otMessage *aMessage, + const otMessageInfo *aMessageInfo, + otError aResult) +{ + Coap::Message *nextAnswer = static_cast(aContext); + + VerifyOrExit(nextAnswer != nullptr); + + nextAnswer->Get().HandleAnswerResponse(*nextAnswer, AsCoapMessagePtr(aMessage), AsCoreTypePtr(aMessageInfo), + aResult); + +exit: + return; +} + +void Server::HandleAnswerResponse(Coap::Message &aNextAnswer, + Coap::Message *aResponse, + const Ip6::MessageInfo *aMessageInfo, + Error aResult) +{ + Error error = aResult; + + SuccessOrExit(error); + VerifyOrExit(aResponse != nullptr && aMessageInfo != nullptr, error = kErrorDrop); + VerifyOrExit(aResponse->GetCode() == Coap::kCodeChanged, error = kErrorDrop); + + SendNextAnswer(aNextAnswer, aMessageInfo->GetPeerAddr()); + +exit: + if (error != kErrorNone) + { + FreeAllRelatedAnswers(aNextAnswer); + } +} + +Error Server::AppendNetworkInfo(Coap::Message *&aAnswer, AnswerInfo &aInfo, const RequestTlv &aRequestTlv) +{ + Error error = kErrorNone; + Iterator iterator; + uint32_t maxEntryAge = aRequestTlv.GetMaxEntryAge(); + uint16_t maxCount = aRequestTlv.GetNumEntries(); + + iterator.Init(aInfo.mNow); + + for (uint16_t count = 0; (maxCount == 0) || (count < maxCount); count++) + { + const NetworkInfo *networkInfo; + uint32_t entryAge; + NetworkInfoTlv networkInfoTlv; + + networkInfo = Get().IterateNetInfoHistory(iterator, entryAge); + + if (networkInfo == nullptr) + { + break; + } + + if ((maxEntryAge != 0) && (entryAge > maxEntryAge)) + { + break; + } + + networkInfoTlv.InitFrom(*networkInfo, entryAge); + + SuccessOrExit(error = aAnswer->Append(networkInfoTlv)); + SuccessOrExit(error = CheckAnswerLength(aAnswer, aInfo)); + } + + SuccessOrExit(error = AppendEmptyTlv(*aAnswer, Tlv::kNetworkInfo)); + +exit: + return error; +} + +Error Server::AppendEmptyTlv(Coap::Message &aAnswer, Tlv::Type aTlvType) +{ + Tlv tlv; + + tlv.SetType(aTlvType); + tlv.SetLength(0); + + return aAnswer.Append(tlv); +} + +} // namespace HistoryTracker +} // namespace ot + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE diff --git a/src/core/utils/history_tracker_server.hpp b/src/core/utils/history_tracker_server.hpp new file mode 100644 index 000000000..9de68d35b --- /dev/null +++ b/src/core/utils/history_tracker_server.hpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions to support History Tracker Server (TMF). + */ + +#ifndef HISTORY_TRACKER_SERVER_HPP_ +#define HISTORY_TRACKER_SERVER_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + +#include + +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/timer.hpp" +#include "thread/tmf.hpp" +#include "utils/history_tracker.hpp" +#include "utils/history_tracker_tlvs.hpp" + +namespace ot { +namespace HistoryTracker { + +/** + * Represents History Tracker Server. + */ +class Server : public InstanceLocator +{ + friend class Tmf::Agent; + +public: + explicit Server(Instance &aInstance); + +private: + static constexpr uint16_t kAnswerMessageLengthThreshold = 800; + + struct AnswerInfo + { + AnswerInfo(void) + : mNow(TimerMilli::GetNow()) + , mAnswerIndex(0) + , mQueryId(0) + , mHasQueryId(false) + , mFirstAnswer(nullptr) + { + } + + TimeMilli mNow; + uint16_t mAnswerIndex; + uint16_t mQueryId; + bool mHasQueryId; + Message::Priority mPriority; + Coap::Message *mFirstAnswer; + }; + + Error AllocateAnswer(Coap::Message *&aAnswer, AnswerInfo &aInfo); + bool IsLastAnswer(const Coap::Message &aAnswer) const; + void FreeAllRelatedAnswers(Coap::Message &aFirstAnswer); + void PrepareAndSendAnswers(const Ip6::Address &aDestination, const Message &aRequest); + Error CheckAnswerLength(Coap::Message *&aAnswer, AnswerInfo &aInfo); + void SendNextAnswer(Coap::Message &aAnswer, const Ip6::Address &aDestination); + void PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const; + Error AppendNetworkInfo(Coap::Message *&aAnswer, AnswerInfo &aInfo, const RequestTlv &aRequestTlv); + Error AppendEmptyTlv(Coap::Message &aAnswer, Tlv::Type aTlvType); + + static void HandleAnswerResponse(void *aContext, + otMessage *aMessage, + const otMessageInfo *aMessageInfo, + otError aResult); + void HandleAnswerResponse(Coap::Message &aNextAnswer, + Coap::Message *aResponse, + const Ip6::MessageInfo *aMessageInfo, + Error aResult); + + template void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + + Coap::MessageQueue mAnswerQueue; +}; + +DeclareTmfHandler(Server, kUriHistoryQuery); + +} // namespace HistoryTracker +} // namespace ot + +#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE + +#endif // HISTORY_TRACKER_SERVER_HPP_ diff --git a/src/core/utils/history_tracker_tlvs.cpp b/src/core/utils/history_tracker_tlvs.cpp new file mode 100644 index 000000000..8f8745c7e --- /dev/null +++ b/src/core/utils/history_tracker_tlvs.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions to support History Tracker TLVs. + */ + +#include "utils/history_tracker_tlvs.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +namespace ot { +namespace HistoryTracker { + +void AnswerTlv::Init(uint16_t aIndex, bool aIsLast) +{ + SetType(kAnswer); + SetLength(sizeof(*this) - sizeof(Tlv)); + + SetFlagsIndex((aIndex & kIndexMask) | (aIsLast ? kIsLastFlag : 0)); +} + +void RequestTlv::Init(uint8_t aTlvType, uint16_t aNumEntries, uint32_t aMaxEntryAge) +{ + SetType(kRequest); + SetLength(sizeof(*this) - sizeof(Tlv)); + + mTlvType = aTlvType; + mNumEntries = BigEndian::HostSwap16(aNumEntries); + mMaxEntryAge = BigEndian::HostSwap32(aMaxEntryAge); +} + +void NetworkInfoTlv::InitFrom(const NetworkInfo &aNetworkInfo, uint32_t aEntryAge) +{ + Mle::DeviceMode deviceMode; + + SetType(kNetworkInfo); + SetLength(sizeof(*this) - sizeof(Tlv)); + + deviceMode.Set(aNetworkInfo.mMode); + + mEntryAge = BigEndian::HostSwap32(aEntryAge); + mRole = aNetworkInfo.mRole; + mMode = deviceMode.Get(); + mRloc16 = BigEndian::HostSwap16(aNetworkInfo.mRloc16); + mPartitionId = BigEndian::HostSwap32(aNetworkInfo.mPartitionId); +} + +void NetworkInfoTlv::CopyTo(NetworkInfo &aNetworkInfo) const +{ + Mle::DeviceMode deviceMode; + + deviceMode.Set(mMode); + + aNetworkInfo.mRole = static_cast(mRole); + aNetworkInfo.mRloc16 = BigEndian::HostSwap16(mRloc16); + aNetworkInfo.mPartitionId = BigEndian::HostSwap32(mPartitionId); + deviceMode.Get(aNetworkInfo.mMode); +} + +} // namespace HistoryTracker +} // namespace ot + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE diff --git a/src/core/utils/history_tracker_tlvs.hpp b/src/core/utils/history_tracker_tlvs.hpp new file mode 100644 index 000000000..064b89b8c --- /dev/null +++ b/src/core/utils/history_tracker_tlvs.hpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions to support History Tracker TLVs. + */ + +#ifndef HISTORY_TRACKER_TLVS_HPP_ +#define HISTORY_TRACKER_TLVS_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#include + +#include "common/encoding.hpp" +#include "common/tlvs.hpp" +#include "utils/history_tracker.hpp" + +namespace ot { +namespace HistoryTracker { + +/** + * Implements History Tracker TLV generation and parsing. + */ +OT_TOOL_PACKED_BEGIN +class Tlv : public ot::Tlv +{ +public: + /** + * History Tracker TLV Types. + */ + enum Type : uint8_t + { + kQueryId = 0, + kAnswer = 1, + kRequest = 2, + kNetworkInfo = 3, + }; +} OT_TOOL_PACKED_END; + +/** + * Defines Query ID TLV constants and types. + */ +typedef UintTlvInfo QueryIdTlv; + +/** + * Implements Answer TLV generation and parsing. + */ +OT_TOOL_PACKED_BEGIN +class AnswerTlv : public Tlv, public TlvInfo +{ +public: + /** + * Initializes the TLV. + * + * @param[in] aIndex The index value. + * @param[in] aIsLast The "IsLast" flag value. + */ + void Init(uint16_t aIndex, bool aIsLast); + + /** + * Indicates whether or not the "IsLast" flag is set + * + * @retval TRUE "IsLast" flag is set (this is the last answer for this query). + * @retval FALSE "IsLast" flag is not set (more answer messages are expected for this query). + */ + bool IsLast(void) const { return GetFlagsIndex() & kIsLastFlag; } + + /** + * Gets the index. + * + * @returns The index. + */ + uint16_t GetIndex(void) const { return GetFlagsIndex() & kIndexMask; } + +private: + static constexpr uint16_t kIsLastFlag = 1 << 15; + static constexpr uint16_t kIndexMask = 0x7fff; + + uint16_t GetFlagsIndex(void) const { return BigEndian::HostSwap16(mFlagsIndex); } + void SetFlagsIndex(uint16_t aFlagsIndex) { mFlagsIndex = BigEndian::HostSwap16(aFlagsIndex); } + + uint16_t mFlagsIndex; +} OT_TOOL_PACKED_END; + +/** + * Implements Request TLV generation and parsing. + */ +OT_TOOL_PACKED_BEGIN +class RequestTlv : public Tlv, public TlvInfo +{ +public: + /** + * Initializes the TLV. + * + * @param[in] aTlvType The TLV type to request. + * @param[in] aNumEntries Maximum number of entries to include in the reply (zero indicates all). + * @param[in] aMaxEntryAge Maximum entry age to include in the reply (zero indicates no limit). + */ + void Init(uint8_t aTlvType, uint16_t aNumEntries, uint32_t aMaxEntryAge); + + /** + * Indicates whether or not the TLV appears to be well-formed. + * + * @retval TRUE If the TLV appears to be well-formed. + * @retval FALSE If the TLV does not appear to be well-formed. + */ + bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); } + + /** + * Gets the requested TLV type. + * + * @returns The requested TLV type. + */ + uint8_t GetTlvType(void) const { return mTlvType; } + + /** + * Gets the maximum number of entries to include in the reply. + * + * @returns The maximum number of entries (zero indicates all). + */ + uint16_t GetNumEntries(void) const { return BigEndian::HostSwap16(mNumEntries); } + + /** + * Gets the maximum entry age to include in the reply. + * + * @returns The maximum entry age in milliseconds (zero indicates no age limit). + */ + uint32_t GetMaxEntryAge(void) const { return BigEndian::HostSwap32(mMaxEntryAge); } + +private: + uint8_t mTlvType; + uint16_t mNumEntries; + uint32_t mMaxEntryAge; +} OT_TOOL_PACKED_END; + +/** + * Implements Network Info TLV generation and parsing. + */ +OT_TOOL_PACKED_BEGIN +class NetworkInfoTlv : public Tlv, public TlvInfo +{ +public: + /** + * Initializes the TLV from a `NetworkInfo` object and entry age. + * + * @param[in] aNetworkInfo The NetworkInfo object to initialize from. + * @param[in] aEntryAge The age of the entry in milliseconds. + */ + void InitFrom(const NetworkInfo &aNetworkInfo, uint32_t aEntryAge); + + /** + * Indicates whether or not the TLV appears to be well-formed. + * + * @retval TRUE If the TLV appears to be well-formed. + * @retval FALSE If the TLV does not appear to be well-formed. + */ + bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); } + + /** + * Copies the TLV data to a `NetworkInfo` object. + * + * @param[out] aNetworkInfo The `NetworkInfo` object to copy data to. + */ + void CopyTo(NetworkInfo &aNetworkInfo) const; + + /** + * Gets the entry age. + * + * @returns The entry age in milliseconds. + */ + uint32_t GetEntryAge(void) const { return BigEndian::HostSwap32(mEntryAge); } + +private: + uint32_t mEntryAge; + uint8_t mRole; + uint8_t mMode; + uint16_t mRloc16; + uint32_t mPartitionId; +} OT_TOOL_PACKED_END; + +} // namespace HistoryTracker +} // namespace ot + +#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#endif // HISTORY_TRACKER_TLVS_HPP_ diff --git a/tests/toranj/cli/test-040-history-tracker-query.py b/tests/toranj/cli/test-040-history-tracker-query.py new file mode 100755 index 000000000..6f421b2a5 --- /dev/null +++ b/tests/toranj/cli/test-040-history-tracker-query.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: History Tracker client/server behavior +# +# Network topology +# +# r1 ---- r2 +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 40 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.form('hist-srv') +r2.join(r1) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +r2_rlco16 = int(r2.get_rloc16(), 16) + +local_table = r2.cli('history netinfo') + +verify(len(local_table) == 5) +local_table = local_table[2:] + +# Remove the age field from the table +for i in range(3): + start_index = local_table[i].find(' | ') + local_table[i] = local_table[i][start_index:] + +# Check that the result of query matches the local table + +table = r1.cli('history query netinfo', r2_rlco16) + +verify(len(table) == 5) +table = table[2:] +for i in range(3): + verify(table[i].endswith(local_table[i])) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Validate different CLI command variations + +# Test query with the 'list' format +result = r1.cli('history query netinfo list', r2_rlco16) +verify(len(result) == 3) + +# Test specifying max entries as 2 +table = r1.cli('history query netinfo', r2_rlco16, 2) +verify(len(table) == 4) + +# Test specifying max entries as 10 +table = r1.cli('history query netinfo', r2_rlco16, 10) +verify(len(table) == 5) + +# Test specifying max entry age as zero (no age limit) +table = r1.cli('history query netinfo', r2_rlco16, 0, 0) +verify(len(table) == 5) + +# Test specifying max entry age as 1 millisecond +table = r1.cli('history query netinfo', r2_rlco16, 0, 1) +verify(len(table) == 2) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/openthread-core-toranj-config-posix.h b/tests/toranj/openthread-core-toranj-config-posix.h index 3e8294d94..1a0d91578 100644 --- a/tests/toranj/openthread-core-toranj-config-posix.h +++ b/tests/toranj/openthread-core-toranj-config-posix.h @@ -72,9 +72,10 @@ #define OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE 1 -// The following features are set explicitly on posix `toranj` -// to validate the build with these config. The `toranj` build -// under simulation platform covers the opposite configs. +// The following features (e.g., `USE_HEAP`) are enabled or disabled +// explicitly on POSIX `toranj` to validate the build with or without +// them. The `toranj` build under the simulation platform covers the +// opposite/alternative configurations (e.g., allows `USE_HEAP`). #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 0 @@ -82,4 +83,8 @@ #define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 0 +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE 1 + +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE 0 + #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_POSIX_H_ */ diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index 39c8384f6..16f723ef0 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -43,6 +43,10 @@ #define OPENTHREAD_CONFIG_PLATFORM_INFO "SIMULATION-toranj" #endif +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_SERVER_ENABLE 1 + +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_CLIENT_ENABLE 1 + #define OPENTHREAD_CONFIG_SRP_SERVER_FAST_START_MODE_ENABLE 1 #define OPENTHREAD_CONFIG_COAP_API_ENABLE 1 diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 46b1d71e1..5a1dcb08b 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -205,6 +205,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-037-mtd-annc-join-older-timestamp.py run cli/test-038-simultaneous-parent-and-child-reset.py run cli/test-039-border-agent-evict-active-commissioner.py + run cli/test-040-history-tracker-query.py run cli/test-400-srp-client-server.py run cli/test-401-srp-server-address-cache-snoop.py run cli/test-500-two-brs-two-networks.py