1 /*
2 * Copyright (c) 2023, 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 Link Metrics function.
32 */
33
34 #include "cli_link_metrics.hpp"
35
36 #include <openthread/link_metrics.h>
37
38 #include "cli/cli.hpp"
39 #include "cli/cli_utils.hpp"
40 #include "common/code_utils.hpp"
41
42 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
43
44 namespace ot {
45 namespace Cli {
46
LinkMetrics(otInstance * aInstance,OutputImplementer & aOutputImplementer)47 LinkMetrics::LinkMetrics(otInstance *aInstance, OutputImplementer &aOutputImplementer)
48 : Utils(aInstance, aOutputImplementer)
49 , mLinkMetricsQueryInProgress(false)
50 {
51 }
52
Process(Arg aArgs[])53 template <> otError LinkMetrics::Process<Cmd("query")>(Arg aArgs[])
54 {
55 otError error = OT_ERROR_NONE;
56 otIp6Address address;
57 bool isSingle;
58 bool blocking;
59 uint8_t seriesId;
60 otLinkMetrics linkMetrics;
61
62 SuccessOrExit(error = aArgs[0].ParseAsIp6Address(address));
63
64 /**
65 * @cli linkmetrics query single
66 * @code
67 * linkmetrics query fe80:0:0:0:3092:f334:1455:1ad2 single qmr
68 * Done
69 * > Received Link Metrics Report from: fe80:0:0:0:3092:f334:1455:1ad2
70 * - LQI: 76 (Exponential Moving Average)
71 * - Margin: 82 (dB) (Exponential Moving Average)
72 * - RSSI: -18 (dBm) (Exponential Moving Average)
73 * @endcode
74 * @cparam linkmetrics query @ca{peer-ipaddr} single [@ca{pqmr}]
75 * - `peer-ipaddr`: Peer address.
76 * - [`p`, `q`, `m`, and `r`] map to #otLinkMetrics.
77 * - `p`: Layer 2 Number of PDUs received.
78 * - `q`: Layer 2 LQI.
79 * - `m`: Link Margin.
80 * - `r`: RSSI.
81 * @par
82 * Perform a Link Metrics query (Single Probe).
83 * @sa otLinkMetricsQuery
84 */
85 if (aArgs[1] == "single")
86 {
87 isSingle = true;
88 SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[2]));
89 }
90 /**
91 * @cli linkmetrics query forward
92 * @code
93 * linkmetrics query fe80:0:0:0:3092:f334:1455:1ad2 forward 1
94 * Done
95 * > Received Link Metrics Report from: fe80:0:0:0:3092:f334:1455:1ad2
96 * - PDU Counter: 2 (Count/Summation)
97 * - LQI: 76 (Exponential Moving Average)
98 * - Margin: 82 (dB) (Exponential Moving Average)
99 * - RSSI: -18 (dBm) (Exponential Moving Average)
100 * @endcode
101 * @cparam linkmetrics query @ca{peer-ipaddr} forward @ca{series-id}
102 * - `peer-ipaddr`: Peer address.
103 * - `series-id`: The Series ID.
104 * @par
105 * Perform a Link Metrics query (Forward Tracking Series).
106 * @sa otLinkMetricsQuery
107 */
108 else if (aArgs[1] == "forward")
109 {
110 isSingle = false;
111 SuccessOrExit(error = aArgs[2].ParseAsUint8(seriesId));
112 }
113 else
114 {
115 ExitNow(error = OT_ERROR_INVALID_ARGS);
116 }
117
118 blocking = (aArgs[3] == "block");
119
120 SuccessOrExit(error = otLinkMetricsQuery(GetInstancePtr(), &address, isSingle ? 0 : seriesId,
121 isSingle ? &linkMetrics : nullptr, HandleLinkMetricsReport, this));
122
123 if (blocking)
124 {
125 mLinkMetricsQueryInProgress = true;
126 error = OT_ERROR_PENDING;
127 }
128 exit:
129 return error;
130 }
131
Process(Arg aArgs[])132 template <> otError LinkMetrics::Process<Cmd("mgmt")>(Arg aArgs[])
133 {
134 otError error;
135 otIp6Address address;
136 otLinkMetricsSeriesFlags seriesFlags;
137 bool clear = false;
138
139 SuccessOrExit(error = aArgs[0].ParseAsIp6Address(address));
140
141 ClearAllBytes(seriesFlags);
142
143 /**
144 * @cli linkmetrics mgmt forward
145 * @code
146 * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 forward 1 dra pqmr
147 * Done
148 * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
149 * Status: SUCCESS
150 * @endcode
151 * @cparam linkmetrics mgmt @ca{peer-ipaddr} forward @ca{series-id} [@ca{ldraX}][@ca{pqmr}]
152 * - `peer-ipaddr`: Peer address.
153 * - `series-id`: The Series ID.
154 * - [`l`, `d`, `r`, and `a`] map to #otLinkMetricsSeriesFlags. `X` represents none of the
155 * `otLinkMetricsSeriesFlags`, and stops the accounting and removes the series.
156 * - `l`: MLE Link Probe.
157 * - `d`: MAC Data.
158 * - `r`: MAC Data Request.
159 * - `a`: MAC Ack.
160 * - `X`: Can only be used without any other flags.
161 * - [`p`, `q`, `m`, and `r`] map to #otLinkMetricsValues.
162 * - `p`: Layer 2 Number of PDUs received.
163 * - `q`: Layer 2 LQI.
164 * - `m`: Link Margin.
165 * - `r`: RSSI.
166 * @par api_copy
167 * #otLinkMetricsConfigForwardTrackingSeries
168 */
169 if (aArgs[1] == "forward")
170 {
171 uint8_t seriesId;
172 otLinkMetrics linkMetrics;
173
174 ClearAllBytes(linkMetrics);
175 SuccessOrExit(error = aArgs[2].ParseAsUint8(seriesId));
176 VerifyOrExit(!aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
177
178 for (const char *arg = aArgs[3].GetCString(); *arg != '\0'; arg++)
179 {
180 switch (*arg)
181 {
182 case 'l':
183 seriesFlags.mLinkProbe = true;
184 break;
185
186 case 'd':
187 seriesFlags.mMacData = true;
188 break;
189
190 case 'r':
191 seriesFlags.mMacDataRequest = true;
192 break;
193
194 case 'a':
195 seriesFlags.mMacAck = true;
196 break;
197
198 case 'X':
199 VerifyOrExit(arg == aArgs[3].GetCString() && *(arg + 1) == '\0' && aArgs[4].IsEmpty(),
200 error = OT_ERROR_INVALID_ARGS); // Ensure the flags only contain 'X'
201 clear = true;
202 break;
203
204 default:
205 ExitNow(error = OT_ERROR_INVALID_ARGS);
206 }
207 }
208
209 if (!clear)
210 {
211 SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[4]));
212 VerifyOrExit(aArgs[5].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
213 }
214
215 error = otLinkMetricsConfigForwardTrackingSeries(GetInstancePtr(), &address, seriesId, seriesFlags,
216 clear ? nullptr : &linkMetrics,
217 &LinkMetrics::HandleLinkMetricsMgmtResponse, this);
218 }
219 else if (aArgs[1] == "enhanced-ack")
220 {
221 otLinkMetricsEnhAckFlags enhAckFlags;
222 otLinkMetrics linkMetrics;
223 otLinkMetrics *pLinkMetrics = &linkMetrics;
224
225 /**
226 * @cli linkmetrics mgmt enhanced-ack clear
227 * @code
228 * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack clear
229 * Done
230 * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
231 * Status: Success
232 * @endcode
233 * @cparam linkmetrics mgmt @ca{peer-ipaddr} enhanced-ack clear
234 * `peer-ipaddr` should be the Link Local address of the neighboring device.
235 * @par
236 * Sends a Link Metrics Management Request to clear an Enhanced-ACK Based Probing.
237 * @sa otLinkMetricsConfigEnhAckProbing
238 */
239 if (aArgs[2] == "clear")
240 {
241 enhAckFlags = OT_LINK_METRICS_ENH_ACK_CLEAR;
242 pLinkMetrics = nullptr;
243 }
244 /**
245 * @cli linkmetrics mgmt enhanced-ack register
246 * @code
247 * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack register qm
248 * Done
249 * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
250 * Status: Success
251 * @endcode
252 * @code
253 * > linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack register qm r
254 * Done
255 * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
256 * Status: Cannot support new series
257 * @endcode
258 * @cparam linkmetrics mgmt @ca{peer-ipaddr} enhanced-ack register [@ca{qmr}][@ca{r}]
259 * [`q`, `m`, and `r`] map to #otLinkMetricsValues. Per spec 4.11.3.4.4.6, you can
260 * only use a maximum of two options at once, for example `q`, or `qm`.
261 * - `q`: Layer 2 LQI.
262 * - `m`: Link Margin.
263 * - `r`: RSSI.
264 * .
265 * The additional `r` is optional and only used for reference devices. When this option
266 * is specified, Type/Average Enum of each Type Id Flags is set to reserved. This is
267 * used to verify that the Probing Subject correctly handles invalid Type Id Flags, and
268 * only available when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
269 * @par
270 * Sends a Link Metrics Management Request to register an Enhanced-ACK Based Probing.
271 * @sa otLinkMetricsConfigEnhAckProbing
272 */
273 else if (aArgs[2] == "register")
274 {
275 enhAckFlags = OT_LINK_METRICS_ENH_ACK_REGISTER;
276 SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[3]));
277 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
278 if (aArgs[4] == "r")
279 {
280 linkMetrics.mReserved = true;
281 }
282 #endif
283 }
284 else
285 {
286 ExitNow(error = OT_ERROR_INVALID_ARGS);
287 }
288
289 error = otLinkMetricsConfigEnhAckProbing(GetInstancePtr(), &address, enhAckFlags, pLinkMetrics,
290 &LinkMetrics::HandleLinkMetricsMgmtResponse, this,
291 &LinkMetrics::HandleLinkMetricsEnhAckProbingIe, this);
292 }
293 else
294 {
295 error = OT_ERROR_INVALID_ARGS;
296 }
297
298 exit:
299 return error;
300 }
301
Process(Arg aArgs[])302 template <> otError LinkMetrics::Process<Cmd("probe")>(Arg aArgs[])
303 {
304 /**
305 * @cli linkmetrics probe
306 * @code
307 * linkmetrics probe fe80:0:0:0:3092:f334:1455:1ad2 1 10
308 * Done
309 * @endcode
310 * @cparam linkmetrics probe @ca{peer-ipaddr} @ca{series-id} @ca{length}
311 * - `peer-ipaddr`: Peer address.
312 * - `series-id`: The Series ID for which this Probe message targets.
313 * - `length`: The length of the Probe message. A valid range is [0, 64].
314 * @par api_copy
315 * #otLinkMetricsSendLinkProbe
316 */
317 otError error = OT_ERROR_NONE;
318
319 otIp6Address address;
320 uint8_t seriesId;
321 uint8_t length;
322
323 SuccessOrExit(error = aArgs[0].ParseAsIp6Address(address));
324 SuccessOrExit(error = aArgs[1].ParseAsUint8(seriesId));
325 SuccessOrExit(error = aArgs[2].ParseAsUint8(length));
326
327 error = otLinkMetricsSendLinkProbe(GetInstancePtr(), &address, seriesId, length);
328 exit:
329 return error;
330 }
331
Process(Arg aArgs[])332 otError LinkMetrics::Process(Arg aArgs[])
333 {
334 #define CmdEntry(aCommandString) \
335 { \
336 aCommandString, &LinkMetrics::Process<Cmd(aCommandString)> \
337 }
338
339 static constexpr Command kCommands[] = {
340 CmdEntry("mgmt"),
341 CmdEntry("probe"),
342 CmdEntry("query"),
343 };
344
345 static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
346
347 otError error = OT_ERROR_INVALID_COMMAND;
348 const Command *command;
349
350 if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
351 {
352 OutputCommandTable(kCommands);
353 ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
354 }
355
356 command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
357 VerifyOrExit(command != nullptr);
358
359 error = (this->*command->mHandler)(aArgs + 1);
360
361 exit:
362 return error;
363 }
364
ParseLinkMetricsFlags(otLinkMetrics & aLinkMetrics,const Arg & aFlags)365 otError LinkMetrics::ParseLinkMetricsFlags(otLinkMetrics &aLinkMetrics, const Arg &aFlags)
366 {
367 otError error = OT_ERROR_NONE;
368
369 VerifyOrExit(!aFlags.IsEmpty(), error = OT_ERROR_INVALID_ARGS);
370
371 ClearAllBytes(aLinkMetrics);
372
373 for (const char *arg = aFlags.GetCString(); *arg != '\0'; arg++)
374 {
375 switch (*arg)
376 {
377 case 'p':
378 aLinkMetrics.mPduCount = true;
379 break;
380
381 case 'q':
382 aLinkMetrics.mLqi = true;
383 break;
384
385 case 'm':
386 aLinkMetrics.mLinkMargin = true;
387 break;
388
389 case 'r':
390 aLinkMetrics.mRssi = true;
391 break;
392
393 default:
394 ExitNow(error = OT_ERROR_INVALID_ARGS);
395 }
396 }
397
398 exit:
399 return error;
400 }
401
HandleLinkMetricsReport(const otIp6Address * aAddress,const otLinkMetricsValues * aMetricsValues,otLinkMetricsStatus aStatus,void * aContext)402 void LinkMetrics::HandleLinkMetricsReport(const otIp6Address *aAddress,
403 const otLinkMetricsValues *aMetricsValues,
404 otLinkMetricsStatus aStatus,
405 void *aContext)
406 {
407 static_cast<LinkMetrics *>(aContext)->HandleLinkMetricsReport(aAddress, aMetricsValues, aStatus);
408 }
409
PrintLinkMetricsValue(const otLinkMetricsValues * aMetricsValues)410 void LinkMetrics::PrintLinkMetricsValue(const otLinkMetricsValues *aMetricsValues)
411 {
412 static const char kLinkMetricsTypeAverage[] = "(Exponential Moving Average)";
413
414 if (aMetricsValues->mMetrics.mPduCount)
415 {
416 OutputLine(" - PDU Counter: %lu (Count/Summation)", ToUlong(aMetricsValues->mPduCountValue));
417 }
418
419 if (aMetricsValues->mMetrics.mLqi)
420 {
421 OutputLine(" - LQI: %u %s", aMetricsValues->mLqiValue, kLinkMetricsTypeAverage);
422 }
423
424 if (aMetricsValues->mMetrics.mLinkMargin)
425 {
426 OutputLine(" - Margin: %u (dB) %s", aMetricsValues->mLinkMarginValue, kLinkMetricsTypeAverage);
427 }
428
429 if (aMetricsValues->mMetrics.mRssi)
430 {
431 OutputLine(" - RSSI: %d (dBm) %s", aMetricsValues->mRssiValue, kLinkMetricsTypeAverage);
432 }
433 }
434
HandleLinkMetricsReport(const otIp6Address * aAddress,const otLinkMetricsValues * aMetricsValues,otLinkMetricsStatus aStatus)435 void LinkMetrics::HandleLinkMetricsReport(const otIp6Address *aAddress,
436 const otLinkMetricsValues *aMetricsValues,
437 otLinkMetricsStatus aStatus)
438 {
439 OutputFormat("Received Link Metrics Report from: ");
440 OutputIp6AddressLine(*aAddress);
441
442 if (aMetricsValues != nullptr)
443 {
444 PrintLinkMetricsValue(aMetricsValues);
445 }
446 else
447 {
448 OutputLine("Link Metrics Report, status: %s", LinkMetricsStatusToStr(aStatus));
449 }
450
451 if (mLinkMetricsQueryInProgress)
452 {
453 mLinkMetricsQueryInProgress = false;
454 OutputResult(OT_ERROR_NONE);
455 }
456 }
457
HandleLinkMetricsMgmtResponse(const otIp6Address * aAddress,otLinkMetricsStatus aStatus,void * aContext)458 void LinkMetrics::HandleLinkMetricsMgmtResponse(const otIp6Address *aAddress,
459 otLinkMetricsStatus aStatus,
460 void *aContext)
461 {
462 static_cast<LinkMetrics *>(aContext)->HandleLinkMetricsMgmtResponse(aAddress, aStatus);
463 }
464
HandleLinkMetricsMgmtResponse(const otIp6Address * aAddress,otLinkMetricsStatus aStatus)465 void LinkMetrics::HandleLinkMetricsMgmtResponse(const otIp6Address *aAddress, otLinkMetricsStatus aStatus)
466 {
467 OutputFormat("Received Link Metrics Management Response from: ");
468 OutputIp6AddressLine(*aAddress);
469
470 OutputLine("Status: %s", LinkMetricsStatusToStr(aStatus));
471 }
472
HandleLinkMetricsEnhAckProbingIe(otShortAddress aShortAddress,const otExtAddress * aExtAddress,const otLinkMetricsValues * aMetricsValues,void * aContext)473 void LinkMetrics::HandleLinkMetricsEnhAckProbingIe(otShortAddress aShortAddress,
474 const otExtAddress *aExtAddress,
475 const otLinkMetricsValues *aMetricsValues,
476 void *aContext)
477 {
478 static_cast<LinkMetrics *>(aContext)->HandleLinkMetricsEnhAckProbingIe(aShortAddress, aExtAddress, aMetricsValues);
479 }
480
HandleLinkMetricsEnhAckProbingIe(otShortAddress aShortAddress,const otExtAddress * aExtAddress,const otLinkMetricsValues * aMetricsValues)481 void LinkMetrics::HandleLinkMetricsEnhAckProbingIe(otShortAddress aShortAddress,
482 const otExtAddress *aExtAddress,
483 const otLinkMetricsValues *aMetricsValues)
484 {
485 OutputFormat("Received Link Metrics data in Enh Ack from neighbor, short address:0x%02x , extended address:",
486 aShortAddress);
487 OutputExtAddressLine(*aExtAddress);
488
489 if (aMetricsValues != nullptr)
490 {
491 PrintLinkMetricsValue(aMetricsValues);
492 }
493 }
494
LinkMetricsStatusToStr(otLinkMetricsStatus aStatus)495 const char *LinkMetrics::LinkMetricsStatusToStr(otLinkMetricsStatus aStatus)
496 {
497 static const char *const kStatusStrings[] = {
498 "Success", // (0) OT_LINK_METRICS_STATUS_SUCCESS
499 "Cannot support new series", // (1) OT_LINK_METRICS_STATUS_CANNOT_SUPPORT_NEW_SERIES
500 "Series ID already registered", // (2) OT_LINK_METRICS_STATUS_SERIESID_ALREADY_REGISTERED
501 "Series ID not recognized", // (3) OT_LINK_METRICS_STATUS_SERIESID_NOT_RECOGNIZED
502 "No matching series ID", // (4) OT_LINK_METRICS_STATUS_NO_MATCHING_FRAMES_RECEIVED
503 };
504
505 const char *str = "Unknown error";
506
507 static_assert(0 == OT_LINK_METRICS_STATUS_SUCCESS, "STATUS_SUCCESS is incorrect");
508 static_assert(1 == OT_LINK_METRICS_STATUS_CANNOT_SUPPORT_NEW_SERIES, "CANNOT_SUPPORT_NEW_SERIES is incorrect");
509 static_assert(2 == OT_LINK_METRICS_STATUS_SERIESID_ALREADY_REGISTERED, "SERIESID_ALREADY_REGISTERED is incorrect");
510 static_assert(3 == OT_LINK_METRICS_STATUS_SERIESID_NOT_RECOGNIZED, "SERIESID_NOT_RECOGNIZED is incorrect");
511 static_assert(4 == OT_LINK_METRICS_STATUS_NO_MATCHING_FRAMES_RECEIVED, "NO_MATCHING_FRAMES_RECEIVED is incorrect");
512
513 if (aStatus < OT_ARRAY_LENGTH(kStatusStrings))
514 {
515 str = kStatusStrings[aStatus];
516 }
517 else if (aStatus == OT_LINK_METRICS_STATUS_OTHER_ERROR)
518 {
519 str = "Other error";
520 }
521
522 return str;
523 }
524
OutputResult(otError aError)525 void LinkMetrics::OutputResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); }
526
527 } // namespace Cli
528 } // namespace ot
529
530 #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
531