1 /*
2 * Copyright (c) 2016, 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 Joiner role.
32 */
33
34 #include "joiner.hpp"
35
36 #if OPENTHREAD_CONFIG_JOINER_ENABLE
37
38 #include "instance/instance.hpp"
39
40 namespace ot {
41 namespace MeshCoP {
42
43 RegisterLogModule("Joiner");
44
Joiner(Instance & aInstance)45 Joiner::Joiner(Instance &aInstance)
46 : InstanceLocator(aInstance)
47 , mId()
48 , mDiscerner()
49 , mState(kStateIdle)
50 , mJoinerRouterIndex(0)
51 , mFinalizeMessage(nullptr)
52 , mTimer(aInstance)
53 {
54 SetIdFromIeeeEui64();
55 mDiscerner.Clear();
56 ClearAllBytes(mJoinerRouters);
57 }
58
SetIdFromIeeeEui64(void)59 void Joiner::SetIdFromIeeeEui64(void)
60 {
61 Mac::ExtAddress eui64;
62
63 Get<Radio>().GetIeeeEui64(eui64);
64 ComputeJoinerId(eui64, mId);
65 }
66
GetDiscerner(void) const67 const JoinerDiscerner *Joiner::GetDiscerner(void) const { return mDiscerner.IsEmpty() ? nullptr : &mDiscerner; }
68
SetDiscerner(const JoinerDiscerner & aDiscerner)69 Error Joiner::SetDiscerner(const JoinerDiscerner &aDiscerner)
70 {
71 Error error = kErrorNone;
72
73 VerifyOrExit(aDiscerner.IsValid(), error = kErrorInvalidArgs);
74 VerifyOrExit(mState == kStateIdle, error = kErrorInvalidState);
75
76 mDiscerner = aDiscerner;
77 mDiscerner.GenerateJoinerId(mId);
78
79 exit:
80 return error;
81 }
82
ClearDiscerner(void)83 Error Joiner::ClearDiscerner(void)
84 {
85 Error error = kErrorNone;
86
87 VerifyOrExit(mState == kStateIdle, error = kErrorInvalidState);
88 VerifyOrExit(!mDiscerner.IsEmpty());
89
90 mDiscerner.Clear();
91 SetIdFromIeeeEui64();
92
93 exit:
94 return error;
95 }
96
SetState(State aState)97 void Joiner::SetState(State aState)
98 {
99 State oldState = mState;
100 OT_UNUSED_VARIABLE(oldState);
101
102 SuccessOrExit(Get<Notifier>().Update(mState, aState, kEventJoinerStateChanged));
103
104 LogInfo("JoinerState: %s -> %s", StateToString(oldState), StateToString(aState));
105 exit:
106 return;
107 }
108
Start(const char * aPskd,const char * aProvisioningUrl,const char * aVendorName,const char * aVendorModel,const char * aVendorSwVersion,const char * aVendorData,otJoinerCallback aCallback,void * aContext)109 Error Joiner::Start(const char *aPskd,
110 const char *aProvisioningUrl,
111 const char *aVendorName,
112 const char *aVendorModel,
113 const char *aVendorSwVersion,
114 const char *aVendorData,
115 otJoinerCallback aCallback,
116 void *aContext)
117 {
118 Error error;
119 JoinerPskd joinerPskd;
120 Mac::ExtAddress randomAddress;
121 SteeringData::HashBitIndexes filterIndexes;
122
123 LogInfo("Joiner starting");
124
125 VerifyOrExit(aProvisioningUrl == nullptr || IsValidUtf8String(aProvisioningUrl), error = kErrorInvalidArgs);
126 VerifyOrExit(aVendorName == nullptr || IsValidUtf8String(aVendorName), error = kErrorInvalidArgs);
127 VerifyOrExit(aVendorSwVersion == nullptr || IsValidUtf8String(aVendorSwVersion), error = kErrorInvalidArgs);
128
129 VerifyOrExit(mState == kStateIdle, error = kErrorBusy);
130 VerifyOrExit(Get<ThreadNetif>().IsUp() && Get<Mle::Mle>().GetRole() == Mle::kRoleDisabled,
131 error = kErrorInvalidState);
132
133 SuccessOrExit(error = joinerPskd.SetFrom(aPskd));
134
135 // Use random-generated extended address.
136 randomAddress.GenerateRandom();
137 Get<Mac::Mac>().SetExtAddress(randomAddress);
138 Get<Mle::MleRouter>().UpdateLinkLocalAddress();
139
140 SuccessOrExit(error = Get<Tmf::SecureAgent>().Open(Ip6::NetifIdentifier::kNetifThreadInternal));
141 SuccessOrExit(error = Get<Tmf::SecureAgent>().Bind(kJoinerUdpPort));
142 Get<Tmf::SecureAgent>().SetConnectCallback(HandleSecureCoapClientConnect, this);
143 Get<Tmf::SecureAgent>().SetPsk(joinerPskd);
144
145 for (JoinerRouter &router : mJoinerRouters)
146 {
147 router.mPriority = 0; // Priority zero means entry is not in-use.
148 }
149
150 SuccessOrExit(error = PrepareJoinerFinalizeMessage(aProvisioningUrl, aVendorName, aVendorModel, aVendorSwVersion,
151 aVendorData));
152
153 if (!mDiscerner.IsEmpty())
154 {
155 SteeringData::CalculateHashBitIndexes(mDiscerner, filterIndexes);
156 }
157 else
158 {
159 SetIdFromIeeeEui64();
160 SteeringData::CalculateHashBitIndexes(mId, filterIndexes);
161 }
162
163 SuccessOrExit(error = Get<Mle::DiscoverScanner>().Discover(Mac::ChannelMask(0), Get<Mac::Mac>().GetPanId(),
164 /* aJoiner */ true, /* aEnableFiltering */ true,
165 &filterIndexes, HandleDiscoverResult, this));
166
167 mCallback.Set(aCallback, aContext);
168 SetState(kStateDiscover);
169
170 exit:
171 if (error != kErrorNone)
172 {
173 FreeJoinerFinalizeMessage();
174 }
175
176 LogWarnOnError(error, "start joiner");
177 return error;
178 }
179
Stop(void)180 void Joiner::Stop(void)
181 {
182 LogInfo("Joiner stopped");
183
184 // Callback is set to `nullptr` to skip calling it from `Finish()`
185 mCallback.Clear();
186 Finish(kErrorAbort);
187 }
188
Finish(Error aError)189 void Joiner::Finish(Error aError)
190 {
191 switch (mState)
192 {
193 case kStateIdle:
194 ExitNow();
195
196 case kStateConnect:
197 case kStateConnected:
198 case kStateEntrust:
199 case kStateJoined:
200 Get<Tmf::SecureAgent>().Disconnect();
201 IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
202 mTimer.Stop();
203
204 OT_FALL_THROUGH;
205
206 case kStateDiscover:
207 Get<Tmf::SecureAgent>().Close();
208 break;
209 }
210
211 SetState(kStateIdle);
212 FreeJoinerFinalizeMessage();
213
214 mCallback.InvokeIfSet(aError);
215
216 exit:
217 return;
218 }
219
CalculatePriority(int8_t aRssi,bool aSteeringDataAllowsAny)220 uint8_t Joiner::CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny)
221 {
222 int16_t priority;
223
224 if (aRssi == Radio::kInvalidRssi)
225 {
226 aRssi = -127;
227 }
228
229 // Clamp the RSSI to range [-127, -1]
230 priority = Clamp<int8_t>(aRssi, -127, -1);
231
232 // Assign higher priority to networks with an exact match of Joiner
233 // ID in the Steering Data (128 < priority < 256) compared to ones
234 // that allow all Joiners (0 < priority < 128). Sub-prioritize
235 // based on signal strength. Priority 0 is reserved for unused
236 // entry.
237
238 priority += aSteeringDataAllowsAny ? 128 : 256;
239
240 return static_cast<uint8_t>(priority);
241 }
242
HandleDiscoverResult(Mle::DiscoverScanner::ScanResult * aResult,void * aContext)243 void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext)
244 {
245 static_cast<Joiner *>(aContext)->HandleDiscoverResult(aResult);
246 }
247
HandleDiscoverResult(Mle::DiscoverScanner::ScanResult * aResult)248 void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult)
249 {
250 VerifyOrExit(mState == kStateDiscover);
251
252 if (aResult != nullptr && aResult->mJoinerUdpPort > 0)
253 {
254 SaveDiscoveredJoinerRouter(*aResult);
255 }
256 else
257 {
258 Get<Mac::Mac>().SetExtAddress(mId);
259 Get<Mle::MleRouter>().UpdateLinkLocalAddress();
260
261 mJoinerRouterIndex = 0;
262 TryNextJoinerRouter(kErrorNone);
263 }
264
265 exit:
266 return;
267 }
268
SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult & aResult)269 void Joiner::SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult)
270 {
271 uint8_t priority;
272 bool doesAllowAny;
273 JoinerRouter *end = GetArrayEnd(mJoinerRouters);
274 JoinerRouter *entry;
275
276 doesAllowAny = AsCoreType(&aResult.mSteeringData).PermitsAllJoiners();
277
278 LogInfo("Joiner discover network: %s, pan:0x%04x, port:%d, chan:%d, rssi:%d, allow-any:%s",
279 AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort,
280 aResult.mChannel, aResult.mRssi, ToYesNo(doesAllowAny));
281
282 priority = CalculatePriority(aResult.mRssi, doesAllowAny);
283
284 // We keep the list sorted based on priority. Find the place to
285 // add the new result.
286
287 for (entry = &mJoinerRouters[0]; entry < end; entry++)
288 {
289 if (priority > entry->mPriority)
290 {
291 break;
292 }
293 }
294
295 VerifyOrExit(entry < end);
296
297 // Shift elements in array to make room for the new one.
298 memmove(entry + 1, entry,
299 static_cast<size_t>(reinterpret_cast<uint8_t *>(end - 1) - reinterpret_cast<uint8_t *>(entry)));
300
301 entry->mExtAddr = AsCoreType(&aResult.mExtAddress);
302 entry->mPanId = aResult.mPanId;
303 entry->mJoinerUdpPort = aResult.mJoinerUdpPort;
304 entry->mChannel = aResult.mChannel;
305 entry->mPriority = priority;
306
307 exit:
308 return;
309 }
310
TryNextJoinerRouter(Error aPrevError)311 void Joiner::TryNextJoinerRouter(Error aPrevError)
312 {
313 for (; mJoinerRouterIndex < GetArrayLength(mJoinerRouters); mJoinerRouterIndex++)
314 {
315 JoinerRouter &router = mJoinerRouters[mJoinerRouterIndex];
316 Error error;
317
318 if (router.mPriority == 0)
319 {
320 break;
321 }
322
323 error = Connect(router);
324 VerifyOrExit(error != kErrorNone, mJoinerRouterIndex++);
325
326 // Save the error from `Connect` only if there is no previous
327 // error from earlier attempts. This ensures that if there has
328 // been a previous Joiner Router connect attempt where
329 // `Connect()` call itself was successful, the error status
330 // emitted from `Finish()` call corresponds to the error from
331 // that attempt.
332
333 if (aPrevError == kErrorNone)
334 {
335 aPrevError = error;
336 }
337 }
338
339 if (aPrevError == kErrorNone)
340 {
341 aPrevError = kErrorNotFound;
342 }
343
344 Finish(aPrevError);
345
346 exit:
347 return;
348 }
349
Connect(JoinerRouter & aRouter)350 Error Joiner::Connect(JoinerRouter &aRouter)
351 {
352 Error error = kErrorNotFound;
353 Ip6::SockAddr sockAddr(aRouter.mJoinerUdpPort);
354
355 LogInfo("Joiner connecting to %s, pan:0x%04x, chan:%d", aRouter.mExtAddr.ToString().AsCString(), aRouter.mPanId,
356 aRouter.mChannel);
357
358 Get<Mac::Mac>().SetPanId(aRouter.mPanId);
359 SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(aRouter.mChannel));
360 SuccessOrExit(error = Get<Ip6::Filter>().AddUnsecurePort(kJoinerUdpPort));
361
362 sockAddr.GetAddress().SetToLinkLocalAddress(aRouter.mExtAddr);
363
364 SuccessOrExit(error = Get<Tmf::SecureAgent>().Connect(sockAddr));
365
366 SetState(kStateConnect);
367
368 exit:
369 LogWarnOnError(error, "start secure joiner connection");
370 return error;
371 }
372
HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent,void * aContext)373 void Joiner::HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent, void *aContext)
374 {
375 static_cast<Joiner *>(aContext)->HandleSecureCoapClientConnect(aEvent);
376 }
377
HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent)378 void Joiner::HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent)
379 {
380 VerifyOrExit(mState == kStateConnect);
381
382 if (aEvent == Dtls::Session::kConnected)
383 {
384 SetState(kStateConnected);
385 SendJoinerFinalize();
386 mTimer.Start(kResponseTimeout);
387 }
388 else
389 {
390 TryNextJoinerRouter(kErrorSecurity);
391 }
392
393 exit:
394 return;
395 }
396
PrepareJoinerFinalizeMessage(const char * aProvisioningUrl,const char * aVendorName,const char * aVendorModel,const char * aVendorSwVersion,const char * aVendorData)397 Error Joiner::PrepareJoinerFinalizeMessage(const char *aProvisioningUrl,
398 const char *aVendorName,
399 const char *aVendorModel,
400 const char *aVendorSwVersion,
401 const char *aVendorData)
402 {
403 Error error = kErrorNone;
404 VendorStackVersionTlv vendorStackVersionTlv;
405
406 mFinalizeMessage = Get<Tmf::SecureAgent>().NewPriorityConfirmablePostMessage(kUriJoinerFinalize);
407 VerifyOrExit(mFinalizeMessage != nullptr, error = kErrorNoBufs);
408
409 mFinalizeMessage->SetOffset(mFinalizeMessage->GetLength());
410
411 SuccessOrExit(error = Tlv::Append<StateTlv>(*mFinalizeMessage, StateTlv::kAccept));
412
413 SuccessOrExit(error = Tlv::Append<VendorNameTlv>(*mFinalizeMessage, aVendorName));
414 SuccessOrExit(error = Tlv::Append<VendorModelTlv>(*mFinalizeMessage, aVendorModel));
415 SuccessOrExit(error = Tlv::Append<VendorSwVersionTlv>(*mFinalizeMessage, aVendorSwVersion));
416
417 vendorStackVersionTlv.Init();
418 vendorStackVersionTlv.SetOui(OPENTHREAD_CONFIG_STACK_VENDOR_OUI);
419 vendorStackVersionTlv.SetMajor(OPENTHREAD_CONFIG_STACK_VERSION_MAJOR);
420 vendorStackVersionTlv.SetMinor(OPENTHREAD_CONFIG_STACK_VERSION_MINOR);
421 vendorStackVersionTlv.SetRevision(OPENTHREAD_CONFIG_STACK_VERSION_REV);
422 SuccessOrExit(error = vendorStackVersionTlv.AppendTo(*mFinalizeMessage));
423
424 if (aVendorData != nullptr)
425 {
426 SuccessOrExit(error = Tlv::Append<VendorDataTlv>(*mFinalizeMessage, aVendorData));
427 }
428
429 if (aProvisioningUrl != nullptr)
430 {
431 SuccessOrExit(error = Tlv::Append<ProvisioningUrlTlv>(*mFinalizeMessage, aProvisioningUrl));
432 }
433
434 exit:
435 if (error != kErrorNone)
436 {
437 FreeJoinerFinalizeMessage();
438 }
439
440 return error;
441 }
442
FreeJoinerFinalizeMessage(void)443 void Joiner::FreeJoinerFinalizeMessage(void)
444 {
445 VerifyOrExit(mState == kStateIdle && mFinalizeMessage != nullptr);
446
447 mFinalizeMessage->Free();
448 mFinalizeMessage = nullptr;
449
450 exit:
451 return;
452 }
453
SendJoinerFinalize(void)454 void Joiner::SendJoinerFinalize(void)
455 {
456 OT_ASSERT(mFinalizeMessage != nullptr);
457
458 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
459 LogCertMessage("[THCI] direction=send | type=JOIN_FIN.req |", *mFinalizeMessage);
460 #endif
461
462 SuccessOrExit(Get<Tmf::SecureAgent>().SendMessage(*mFinalizeMessage, Joiner::HandleJoinerFinalizeResponse, this));
463 mFinalizeMessage = nullptr;
464
465 LogInfo("Sent %s", UriToString<kUriJoinerFinalize>());
466
467 exit:
468 return;
469 }
470
HandleJoinerFinalizeResponse(void * aContext,otMessage * aMessage,const otMessageInfo * aMessageInfo,otError aResult)471 void Joiner::HandleJoinerFinalizeResponse(void *aContext,
472 otMessage *aMessage,
473 const otMessageInfo *aMessageInfo,
474 otError aResult)
475 {
476 static_cast<Joiner *>(aContext)->HandleJoinerFinalizeResponse(AsCoapMessagePtr(aMessage), &AsCoreType(aMessageInfo),
477 aResult);
478 }
479
HandleJoinerFinalizeResponse(Coap::Message * aMessage,const Ip6::MessageInfo * aMessageInfo,Error aResult)480 void Joiner::HandleJoinerFinalizeResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult)
481 {
482 OT_UNUSED_VARIABLE(aMessageInfo);
483
484 uint8_t state;
485
486 VerifyOrExit(mState == kStateConnected && aResult == kErrorNone);
487 OT_ASSERT(aMessage != nullptr);
488
489 VerifyOrExit(aMessage->IsAck() && aMessage->GetCode() == Coap::kCodeChanged);
490
491 SuccessOrExit(Tlv::Find<StateTlv>(*aMessage, state));
492
493 SetState(kStateEntrust);
494 mTimer.Start(kResponseTimeout);
495
496 LogInfo("Received %s %d", UriToString<kUriJoinerFinalize>(), state);
497
498 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
499 LogCertMessage("[THCI] direction=recv | type=JOIN_FIN.rsp |", *aMessage);
500 #endif
501
502 exit:
503 Get<Tmf::SecureAgent>().Disconnect();
504 IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
505 }
506
HandleTmf(Coap::Message & aMessage,const Ip6::MessageInfo & aMessageInfo)507 template <> void Joiner::HandleTmf<kUriJoinerEntrust>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
508 {
509 Error error;
510 Dataset::Info datasetInfo;
511
512 VerifyOrExit(mState == kStateEntrust && aMessage.IsConfirmablePostRequest(), error = kErrorDrop);
513
514 LogInfo("Received %s", UriToString<kUriJoinerEntrust>());
515 LogCert("[THCI] direction=recv | type=JOIN_ENT.ntf");
516
517 datasetInfo.Clear();
518
519 SuccessOrExit(error = Tlv::Find<NetworkKeyTlv>(aMessage, datasetInfo.Update<Dataset::kNetworkKey>()));
520
521 datasetInfo.Set<Dataset::kChannel>(Get<Mac::Mac>().GetPanChannel());
522 datasetInfo.Set<Dataset::kPanId>(Get<Mac::Mac>().GetPanId());
523
524 Get<ActiveDatasetManager>().SaveLocal(datasetInfo);
525
526 LogInfo("Joiner successful!");
527
528 SendJoinerEntrustResponse(aMessage, aMessageInfo);
529
530 // Delay extended address configuration to allow DTLS wrap up.
531 mTimer.Start(kConfigExtAddressDelay);
532
533 exit:
534 LogWarnOnError(error, "process joiner entrust");
535 }
536
SendJoinerEntrustResponse(const Coap::Message & aRequest,const Ip6::MessageInfo & aRequestInfo)537 void Joiner::SendJoinerEntrustResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aRequestInfo)
538 {
539 Error error = kErrorNone;
540 Coap::Message *message;
541 Ip6::MessageInfo responseInfo(aRequestInfo);
542
543 message = Get<Tmf::Agent>().NewPriorityResponseMessage(aRequest);
544 VerifyOrExit(message != nullptr, error = kErrorNoBufs);
545
546 message->SetSubType(Message::kSubTypeJoinerEntrust);
547
548 responseInfo.GetSockAddr().Clear();
549 SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, responseInfo));
550
551 SetState(kStateJoined);
552
553 LogInfo("Sent %s response", UriToString<kUriJoinerEntrust>());
554 LogCert("[THCI] direction=send | type=JOIN_ENT.rsp");
555
556 exit:
557 FreeMessageOnError(message, error);
558 }
559
HandleTimer(void)560 void Joiner::HandleTimer(void)
561 {
562 Error error = kErrorNone;
563
564 switch (mState)
565 {
566 case kStateConnected:
567 case kStateEntrust:
568 error = kErrorResponseTimeout;
569 break;
570
571 case kStateJoined:
572 Mac::ExtAddress extAddress;
573
574 extAddress.GenerateRandom();
575 Get<Mac::Mac>().SetExtAddress(extAddress);
576 Get<Mle::MleRouter>().UpdateLinkLocalAddress();
577
578 error = kErrorNone;
579 break;
580
581 case kStateIdle:
582 case kStateDiscover:
583 case kStateConnect:
584 OT_ASSERT(false);
585 }
586
587 Finish(error);
588 }
589
590 // LCOV_EXCL_START
591
StateToString(State aState)592 const char *Joiner::StateToString(State aState)
593 {
594 static const char *const kStateStrings[] = {
595 "Idle", // (0) kStateIdle
596 "Discover", // (1) kStateDiscover
597 "Connecting", // (2) kStateConnect
598 "Connected", // (3) kStateConnected
599 "Entrust", // (4) kStateEntrust
600 "Joined", // (5) kStateJoined
601 };
602
603 struct EnumCheck
604 {
605 InitEnumValidatorCounter();
606 ValidateNextEnum(kStateIdle);
607 ValidateNextEnum(kStateDiscover);
608 ValidateNextEnum(kStateConnect);
609 ValidateNextEnum(kStateConnected);
610 ValidateNextEnum(kStateEntrust);
611 ValidateNextEnum(kStateJoined);
612 };
613
614 return kStateStrings[aState];
615 }
616
617 // LCOV_EXCL_STOP
618
619 } // namespace MeshCoP
620 } // namespace ot
621
622 #endif // OPENTHREAD_CONFIG_JOINER_ENABLE
623