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