1 /*
2 * Copyright (c) 2025, The OpenThread Authors.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * 3. Neither the name of the copyright holder nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 */
28
29 /**
30 * @file
31 * This file implements the CLI interpreter for Mesh Diagnostics function.
32 */
33
34 #include "cli_mesh_diag.hpp"
35
36 #include <openthread/mesh_diag.h>
37
38 #include "cli/cli.hpp"
39 #include "cli/cli_utils.hpp"
40 #include "common/code_utils.hpp"
41
42 #if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
43
44 namespace ot {
45 namespace Cli {
46
MeshDiag(otInstance * aInstance,OutputImplementer & aOutputImplementer)47 MeshDiag::MeshDiag(otInstance *aInstance, OutputImplementer &aOutputImplementer)
48 : Utils(aInstance, aOutputImplementer)
49 {
50 }
51
Process(Arg aArgs[])52 template <> otError MeshDiag::Process<Cmd("topology")>(Arg aArgs[])
53 {
54 /**
55 * @cli meshdiag topology
56 * @code
57 * meshdiag topology
58 * id:02 rloc16:0x0800 ext-addr:8aa57d2c603fe16c ver:4 - me - leader
59 * 3-links:{ 46 }
60 * id:46 rloc16:0xb800 ext-addr:fe109d277e0175cc ver:4
61 * 3-links:{ 02 51 57 }
62 * id:33 rloc16:0x8400 ext-addr:d2e511a146b9e54d ver:4
63 * 3-links:{ 51 57 }
64 * id:51 rloc16:0xcc00 ext-addr:9aab43ababf05352 ver:4
65 * 3-links:{ 33 57 }
66 * 2-links:{ 46 }
67 * id:57 rloc16:0xe400 ext-addr:dae9c4c0e9da55ff ver:4
68 * 3-links:{ 46 51 }
69 * 1-links:{ 33 }
70 * Done
71 * @endcode
72 * @par
73 * Discover network topology (list of routers and their connections).
74 * Parameters are optional and indicate additional items to discover. Can be added in any order.
75 * * `ip6-addrs` to discover the list of IPv6 addresses of every router.
76 * * `children` to discover the child table of every router.
77 * @par
78 * Information per router:
79 * * Router ID
80 * * RLOC16
81 * * Extended MAC address
82 * * Thread Version (if known)
83 * * Whether the router is this device is itself (`me`)
84 * * Whether the router is the parent of this device when device is a child (`parent`)
85 * * Whether the router is `leader`
86 * * Whether the router acts as a border router providing external connectivity (`br`)
87 * * List of routers to which this router has a link:
88 * * `3-links`: Router IDs to which this router has a incoming link with link quality 3
89 * * `2-links`: Router IDs to which this router has a incoming link with link quality 2
90 * * `1-links`: Router IDs to which this router has a incoming link with link quality 1
91 * * If a list if empty, it is omitted in the out.
92 * * If `ip6-addrs`, list of IPv6 addresses of the router
93 * * If `children`, list of all children of the router. Information per child:
94 * * RLOC16
95 * * Incoming Link Quality from perspective of parent to child (zero indicates unknown)
96 * * Child Device mode (`r` rx-on-when-idle, `d` Full Thread Device, `n` Full Network Data, `-` no flags set)
97 * * Whether the child is this device itself (`me`)
98 * * Whether the child acts as a border router providing external connectivity (`br`)
99 * @cparam meshdiag topology [@ca{ip6-addrs}] [@ca{children}]
100 * @sa otMeshDiagDiscoverTopology
101 */
102 otError error = OT_ERROR_NONE;
103 otMeshDiagDiscoverConfig config;
104
105 config.mDiscoverIp6Addresses = false;
106 config.mDiscoverChildTable = false;
107
108 for (; !aArgs->IsEmpty(); aArgs++)
109 {
110 if (*aArgs == "ip6-addrs")
111 {
112 config.mDiscoverIp6Addresses = true;
113 }
114 else if (*aArgs == "children")
115 {
116 config.mDiscoverChildTable = true;
117 }
118 else
119 {
120 ExitNow(error = OT_ERROR_INVALID_ARGS);
121 }
122 }
123
124 SuccessOrExit(error = otMeshDiagDiscoverTopology(GetInstancePtr(), &config, HandleMeshDiagDiscoverDone, this));
125 error = OT_ERROR_PENDING;
126
127 exit:
128 return error;
129 }
130
Process(Arg aArgs[])131 template <> otError MeshDiag::Process<Cmd("childtable")>(Arg aArgs[])
132 {
133 /**
134 * @cli meshdiag childtable
135 * @code
136 * meshdiag childtable 0x6400
137 * rloc16:0x6402 ext-addr:8e6f4d323bbed1fe ver:4
138 * timeout:120 age:36 supvn:129 q-msg:0
139 * rx-on:yes type:ftd full-net:yes
140 * rss - ave:-20 last:-20 margin:80
141 * err-rate - frame:11.51% msg:0.76%
142 * conn-time:00:11:07
143 * csl - sync:no period:0 timeout:0 channel:0
144 * rloc16:0x6403 ext-addr:ee24e64ecf8c079a ver:4
145 * timeout:120 age:19 supvn:129 q-msg:0
146 * rx-on:no type:mtd full-net:no
147 * rss - ave:-20 last:-20 margin:80
148 * err-rate - frame:0.73% msg:0.00%
149 * conn-time:01:08:53
150 * csl - sync:no period:0 timeout:0 channel:0
151 * Done
152 * @endcode
153 * @par
154 * Start a query for child table of a router with a given RLOC16.
155 * Output lists all child entries. Information per child:
156 * - RLOC16
157 * - Extended MAC address
158 * - Thread Version
159 * - Timeout (in seconds)
160 * - Age (seconds since last heard)
161 * - Supervision interval (in seconds)
162 * - Number of queued messages (in case child is sleepy)
163 * - Device Mode
164 * - RSS (average and last)
165 * - Error rates: frame tx (at MAC layer), IPv6 message tx (above MAC)
166 * - Connection time (seconds since link establishment `{dd}d.{hh}:{mm}:{ss}` format)
167 * - CSL info:
168 * - If synchronized
169 * - Period (in unit of 10-symbols-time)
170 * - Timeout (in seconds)
171 *
172 * @cparam meshdiag childtable @ca{router-rloc16}
173 * @sa otMeshDiagQueryChildTable
174 */
175 otError error = OT_ERROR_NONE;
176 uint16_t routerRloc16;
177
178 SuccessOrExit(error = aArgs[0].ParseAsUint16(routerRloc16));
179 VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
180
181 SuccessOrExit(
182 error = otMeshDiagQueryChildTable(GetInstancePtr(), routerRloc16, HandleMeshDiagQueryChildTableResult, this));
183
184 error = OT_ERROR_PENDING;
185
186 exit:
187 return error;
188 }
189
Process(Arg aArgs[])190 template <> otError MeshDiag::Process<Cmd("childip6")>(Arg aArgs[])
191 {
192 /**
193 * @cli meshdiag childip6
194 * @code
195 * meshdiag childip6 0xdc00
196 * child-rloc16: 0xdc02
197 * fdde:ad00:beef:0:ded8:cd58:b73:2c21
198 * fd00:2:0:0:c24a:456:3b6b:c597
199 * fd00:1:0:0:120b:95fe:3ecc:d238
200 * child-rloc16: 0xdc03
201 * fdde:ad00:beef:0:3aa6:b8bf:e7d6:eefe
202 * fd00:2:0:0:8ff8:a188:7436:6720
203 * fd00:1:0:0:1fcf:5495:790a:370f
204 * Done
205 * @endcode
206 * @par
207 * Send a query to a parent to retrieve the IPv6 addresses of all its MTD children.
208 * @cparam meshdiag childip6 @ca{parent-rloc16}
209 * @sa otMeshDiagQueryChildrenIp6Addrs
210 */
211 otError error = OT_ERROR_NONE;
212 uint16_t parentRloc16;
213
214 SuccessOrExit(error = aArgs[0].ParseAsUint16(parentRloc16));
215 VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
216
217 SuccessOrExit(error = otMeshDiagQueryChildrenIp6Addrs(GetInstancePtr(), parentRloc16,
218 HandleMeshDiagQueryChildIp6Addrs, this));
219
220 error = OT_ERROR_PENDING;
221
222 exit:
223 return error;
224 }
225
Process(Arg aArgs[])226 template <> otError MeshDiag::Process<Cmd("routerneighbortable")>(Arg aArgs[])
227 {
228 /**
229 * @cli meshdiag routerneighbortable
230 * @code
231 * meshdiag routerneighbortable 0x7400
232 * rloc16:0x9c00 ext-addr:764788cf6e57a4d2 ver:4
233 * rss - ave:-20 last:-20 margin:80
234 * err-rate - frame:1.38% msg:0.00%
235 * conn-time:01:54:02
236 * rloc16:0x7c00 ext-addr:4ed24fceec9bf6d3 ver:4
237 * rss - ave:-20 last:-20 margin:80
238 * err-rate - frame:0.72% msg:0.00%
239 * conn-time:00:11:27
240 * Done
241 * @endcode
242 * @par
243 * Start a query for router neighbor table of a router with a given RLOC16.
244 * Output lists all router neighbor entries. Information per entry:
245 * - RLOC16
246 * - Extended MAC address
247 * - Thread Version
248 * - RSS (average and last) and link margin
249 * - Error rates, frame tx (at MAC layer), IPv6 message tx (above MAC)
250 * - Connection time (seconds since link establishment `{dd}d.{hh}:{mm}:{ss}` format)
251 * @cparam meshdiag routerneighbortable @ca{router-rloc16}
252 * @sa otMeshDiagQueryRouterNeighborTable
253 */
254 otError error = OT_ERROR_NONE;
255 uint16_t routerRloc16;
256
257 SuccessOrExit(error = aArgs[0].ParseAsUint16(routerRloc16));
258 VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
259
260 SuccessOrExit(error = otMeshDiagQueryRouterNeighborTable(GetInstancePtr(), routerRloc16,
261 HandleMeshDiagQueryRouterNeighborTableResult, this));
262
263 error = OT_ERROR_PENDING;
264
265 exit:
266 return error;
267 }
268
Process(Arg aArgs[])269 otError MeshDiag::Process(Arg aArgs[])
270 {
271 #define CmdEntry(aCommandString) \
272 { \
273 aCommandString, &MeshDiag::Process<Cmd(aCommandString)> \
274 }
275
276 static constexpr Command kCommands[] = {
277 CmdEntry("childip6"),
278 CmdEntry("childtable"),
279 CmdEntry("routerneighbortable"),
280 CmdEntry("topology"),
281 };
282
283 static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
284
285 otError error = OT_ERROR_INVALID_COMMAND;
286 const Command *command;
287
288 if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
289 {
290 OutputCommandTable(kCommands);
291 ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
292 }
293
294 command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
295 VerifyOrExit(command != nullptr);
296
297 error = (this->*command->mHandler)(aArgs + 1);
298
299 exit:
300 return error;
301 }
302
HandleMeshDiagDiscoverDone(otError aError,otMeshDiagRouterInfo * aRouterInfo,void * aContext)303 void MeshDiag::HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo, void *aContext)
304 {
305 reinterpret_cast<MeshDiag *>(aContext)->HandleMeshDiagDiscoverDone(aError, aRouterInfo);
306 }
307
HandleMeshDiagDiscoverDone(otError aError,otMeshDiagRouterInfo * aRouterInfo)308 void MeshDiag::HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo)
309 {
310 VerifyOrExit(aRouterInfo != nullptr);
311
312 OutputFormat("id:%02u rloc16:0x%04x ext-addr:", aRouterInfo->mRouterId, aRouterInfo->mRloc16);
313 OutputExtAddress(aRouterInfo->mExtAddress);
314
315 if (aRouterInfo->mVersion != OT_MESH_DIAG_VERSION_UNKNOWN)
316 {
317 OutputFormat(" ver:%u", aRouterInfo->mVersion);
318 }
319
320 if (aRouterInfo->mIsThisDevice)
321 {
322 OutputFormat(" - me");
323 }
324
325 if (aRouterInfo->mIsThisDeviceParent)
326 {
327 OutputFormat(" - parent");
328 }
329
330 if (aRouterInfo->mIsLeader)
331 {
332 OutputFormat(" - leader");
333 }
334
335 if (aRouterInfo->mIsBorderRouter)
336 {
337 OutputFormat(" - br");
338 }
339
340 OutputNewLine();
341
342 for (uint8_t linkQuality = 3; linkQuality > 0; linkQuality--)
343 {
344 bool hasLinkQuality = false;
345
346 for (uint8_t entryQuality : aRouterInfo->mLinkQualities)
347 {
348 if (entryQuality == linkQuality)
349 {
350 hasLinkQuality = true;
351 break;
352 }
353 }
354
355 if (hasLinkQuality)
356 {
357 OutputFormat(kIndentSize, "%u-links:{ ", linkQuality);
358
359 for (uint8_t id = 0; id < static_cast<uint8_t>(OT_ARRAY_LENGTH(aRouterInfo->mLinkQualities)); id++)
360 {
361 if (aRouterInfo->mLinkQualities[id] == linkQuality)
362 {
363 OutputFormat("%02u ", id);
364 }
365 }
366
367 OutputLine("}");
368 }
369 }
370
371 if (aRouterInfo->mIp6AddrIterator != nullptr)
372 {
373 otIp6Address ip6Address;
374
375 OutputLine(kIndentSize, "ip6-addrs:");
376
377 while (otMeshDiagGetNextIp6Address(aRouterInfo->mIp6AddrIterator, &ip6Address) == OT_ERROR_NONE)
378 {
379 OutputSpaces(kIndentSize * 2);
380 OutputIp6AddressLine(ip6Address);
381 }
382 }
383
384 if (aRouterInfo->mChildIterator != nullptr)
385 {
386 otMeshDiagChildInfo childInfo;
387 char linkModeString[kLinkModeStringSize];
388 bool isFirst = true;
389
390 while (otMeshDiagGetNextChildInfo(aRouterInfo->mChildIterator, &childInfo) == OT_ERROR_NONE)
391 {
392 if (isFirst)
393 {
394 OutputLine(kIndentSize, "children:");
395 isFirst = false;
396 }
397
398 OutputFormat(kIndentSize * 2, "rloc16:0x%04x lq:%u, mode:%s", childInfo.mRloc16, childInfo.mLinkQuality,
399 LinkModeToString(childInfo.mMode, linkModeString));
400
401 if (childInfo.mIsThisDevice)
402 {
403 OutputFormat(" - me");
404 }
405
406 if (childInfo.mIsBorderRouter)
407 {
408 OutputFormat(" - br");
409 }
410
411 OutputNewLine();
412 }
413
414 if (isFirst)
415 {
416 OutputLine(kIndentSize, "children: none");
417 }
418 }
419
420 exit:
421 OutputResult(aError);
422 }
423
HandleMeshDiagQueryChildTableResult(otError aError,const otMeshDiagChildEntry * aChildEntry,void * aContext)424 void MeshDiag::HandleMeshDiagQueryChildTableResult(otError aError,
425 const otMeshDiagChildEntry *aChildEntry,
426 void *aContext)
427 {
428 reinterpret_cast<MeshDiag *>(aContext)->HandleMeshDiagQueryChildTableResult(aError, aChildEntry);
429 }
430
HandleMeshDiagQueryChildTableResult(otError aError,const otMeshDiagChildEntry * aChildEntry)431 void MeshDiag::HandleMeshDiagQueryChildTableResult(otError aError, const otMeshDiagChildEntry *aChildEntry)
432 {
433 PercentageStringBuffer stringBuffer;
434 char string[OT_DURATION_STRING_SIZE];
435
436 VerifyOrExit(aChildEntry != nullptr);
437
438 OutputFormat("rloc16:0x%04x ext-addr:", aChildEntry->mRloc16);
439 OutputExtAddress(aChildEntry->mExtAddress);
440 OutputLine(" ver:%u", aChildEntry->mVersion);
441
442 OutputLine(kIndentSize, "timeout:%lu age:%lu supvn:%u q-msg:%u", ToUlong(aChildEntry->mTimeout),
443 ToUlong(aChildEntry->mAge), aChildEntry->mSupervisionInterval, aChildEntry->mQueuedMessageCount);
444
445 OutputLine(kIndentSize, "rx-on:%s type:%s full-net:%s", aChildEntry->mRxOnWhenIdle ? "yes" : "no",
446 aChildEntry->mDeviceTypeFtd ? "ftd" : "mtd", aChildEntry->mFullNetData ? "yes" : "no");
447
448 OutputLine(kIndentSize, "rss - ave:%d last:%d margin:%d", aChildEntry->mAverageRssi, aChildEntry->mLastRssi,
449 aChildEntry->mLinkMargin);
450
451 if (aChildEntry->mSupportsErrRate)
452 {
453 OutputFormat(kIndentSize, "err-rate - frame:%s%% ",
454 PercentageToString(aChildEntry->mFrameErrorRate, stringBuffer));
455 OutputLine("msg:%s%% ", PercentageToString(aChildEntry->mMessageErrorRate, stringBuffer));
456 }
457
458 otConvertDurationInSecondsToString(aChildEntry->mConnectionTime, string, sizeof(string));
459 OutputLine(kIndentSize, "conn-time:%s", string);
460
461 OutputLine(kIndentSize, "csl - sync:%s period:%u timeout:%lu channel:%u",
462 aChildEntry->mCslSynchronized ? "yes" : "no", aChildEntry->mCslPeriod, ToUlong(aChildEntry->mCslTimeout),
463 aChildEntry->mCslChannel);
464
465 exit:
466 OutputResult(aError);
467 }
468
HandleMeshDiagQueryRouterNeighborTableResult(otError aError,const otMeshDiagRouterNeighborEntry * aNeighborEntry,void * aContext)469 void MeshDiag::HandleMeshDiagQueryRouterNeighborTableResult(otError aError,
470 const otMeshDiagRouterNeighborEntry *aNeighborEntry,
471 void *aContext)
472 {
473 reinterpret_cast<MeshDiag *>(aContext)->HandleMeshDiagQueryRouterNeighborTableResult(aError, aNeighborEntry);
474 }
475
HandleMeshDiagQueryRouterNeighborTableResult(otError aError,const otMeshDiagRouterNeighborEntry * aNeighborEntry)476 void MeshDiag::HandleMeshDiagQueryRouterNeighborTableResult(otError aError,
477 const otMeshDiagRouterNeighborEntry *aNeighborEntry)
478 {
479 PercentageStringBuffer stringBuffer;
480 char string[OT_DURATION_STRING_SIZE];
481
482 VerifyOrExit(aNeighborEntry != nullptr);
483
484 OutputFormat("rloc16:0x%04x ext-addr:", aNeighborEntry->mRloc16);
485 OutputExtAddress(aNeighborEntry->mExtAddress);
486 OutputLine(" ver:%u", aNeighborEntry->mVersion);
487
488 OutputLine(kIndentSize, "rss - ave:%d last:%d margin:%d", aNeighborEntry->mAverageRssi, aNeighborEntry->mLastRssi,
489 aNeighborEntry->mLinkMargin);
490
491 if (aNeighborEntry->mSupportsErrRate)
492 {
493 OutputFormat(kIndentSize, "err-rate - frame:%s%% ",
494 PercentageToString(aNeighborEntry->mFrameErrorRate, stringBuffer));
495 OutputLine("msg:%s%% ", PercentageToString(aNeighborEntry->mMessageErrorRate, stringBuffer));
496 }
497
498 otConvertDurationInSecondsToString(aNeighborEntry->mConnectionTime, string, sizeof(string));
499 OutputLine(kIndentSize, "conn-time:%s", string);
500
501 exit:
502 OutputResult(aError);
503 }
504
HandleMeshDiagQueryChildIp6Addrs(otError aError,uint16_t aChildRloc16,otMeshDiagIp6AddrIterator * aIp6AddrIterator,void * aContext)505 void MeshDiag::HandleMeshDiagQueryChildIp6Addrs(otError aError,
506 uint16_t aChildRloc16,
507 otMeshDiagIp6AddrIterator *aIp6AddrIterator,
508 void *aContext)
509 {
510 reinterpret_cast<MeshDiag *>(aContext)->HandleMeshDiagQueryChildIp6Addrs(aError, aChildRloc16, aIp6AddrIterator);
511 }
512
HandleMeshDiagQueryChildIp6Addrs(otError aError,uint16_t aChildRloc16,otMeshDiagIp6AddrIterator * aIp6AddrIterator)513 void MeshDiag::HandleMeshDiagQueryChildIp6Addrs(otError aError,
514 uint16_t aChildRloc16,
515 otMeshDiagIp6AddrIterator *aIp6AddrIterator)
516 {
517 otIp6Address ip6Address;
518
519 VerifyOrExit(aError == OT_ERROR_NONE || aError == OT_ERROR_PENDING);
520 VerifyOrExit(aIp6AddrIterator != nullptr);
521
522 OutputLine("child-rloc16: 0x%04x", aChildRloc16);
523
524 while (otMeshDiagGetNextIp6Address(aIp6AddrIterator, &ip6Address) == OT_ERROR_NONE)
525 {
526 OutputSpaces(kIndentSize);
527 OutputIp6AddressLine(ip6Address);
528 }
529
530 exit:
531 OutputResult(aError);
532 }
533
OutputResult(otError aError)534 void MeshDiag::OutputResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); }
535
536 } // namespace Cli
537 } // namespace ot
538
539 #endif // OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
540