• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 includes definitions for managing MeshCoP Datasets.
32  *
33  */
34 
35 #ifndef MESHCOP_DATASET_MANAGER_HPP_
36 #define MESHCOP_DATASET_MANAGER_HPP_
37 
38 #include "openthread-core-config.h"
39 
40 #include "coap/coap.hpp"
41 #include "common/locator.hpp"
42 #include "common/non_copyable.hpp"
43 #include "common/timer.hpp"
44 #include "mac/channel_mask.hpp"
45 #include "meshcop/dataset.hpp"
46 #include "meshcop/dataset_local.hpp"
47 #include "net/udp6.hpp"
48 
49 namespace ot {
50 
51 namespace MeshCoP {
52 
53 class DatasetManager : public InstanceLocator
54 {
55 public:
56     /**
57      * This method returns a pointer to the Timestamp.
58      *
59      * @returns A pointer to the Timestamp.
60      *
61      */
62     const Timestamp *GetTimestamp(void) const;
63 
64     /**
65      * This method restores the Operational Dataset from non-volatile memory.
66      *
67      * @retval kErrorNone      Successfully restore the dataset.
68      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
69      *
70      */
71     Error Restore(void);
72 
73     /**
74      * This method retrieves the dataset from non-volatile memory.
75      *
76      * @param[out]  aDataset  Where to place the dataset.
77      *
78      * @retval kErrorNone      Successfully retrieved the dataset.
79      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
80      *
81      */
Read(Dataset & aDataset) const82     Error Read(Dataset &aDataset) const { return mLocal.Read(aDataset); }
83 
84     /**
85      * This method retrieves the dataset from non-volatile memory.
86      *
87      * @param[out]  aDatasetInfo  Where to place the dataset (as `Dataset::Info`).
88      *
89      * @retval kErrorNone      Successfully retrieved the dataset.
90      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
91      *
92      */
Read(Dataset::Info & aDatasetInfo) const93     Error Read(Dataset::Info &aDatasetInfo) const { return mLocal.Read(aDatasetInfo); }
94 
95     /**
96      * This method retrieves the dataset from non-volatile memory.
97      *
98      * @param[out]  aDataset  Where to place the dataset.
99      *
100      * @retval kErrorNone      Successfully retrieved the dataset.
101      * @retval kErrorNotFound  There is no corresponding dataset stored in non-volatile memory.
102      *
103      */
Read(otOperationalDatasetTlvs & aDataset) const104     Error Read(otOperationalDatasetTlvs &aDataset) const { return mLocal.Read(aDataset); }
105 
106     /**
107      * This method retrieves the channel mask from local dataset.
108      *
109      * @param[out]  aChannelMask  A reference to the channel mask.
110      *
111      * @retval kErrorNone      Successfully retrieved the channel mask.
112      * @retval kErrorNotFound  There is no valid channel mask stored in local dataset.
113      *
114      */
115     Error GetChannelMask(Mac::ChannelMask &aChannelMask) const;
116 
117     /**
118      * This method applies the Active or Pending Dataset to the Thread interface.
119      *
120      * @retval kErrorNone   Successfully applied configuration.
121      * @retval kErrorParse  The dataset has at least one TLV with invalid format.
122      *
123      */
124     Error ApplyConfiguration(void) const;
125 
126     /**
127      * This method updates the Operational Dataset when detaching from the network.
128      *
129      * On detach, the Operational Dataset is restored from non-volatile memory.
130      *
131      */
132     void HandleDetach(void);
133 
134     /**
135      * This method sends a MGMT_SET request to the Leader.
136      *
137      * @param[in]  aDatasetInfo  The Operational Dataset.
138      * @param[in]  aTlvs         Any additional raw TLVs to include.
139      * @param[in]  aLength       Number of bytes in @p aTlvs.
140      * @param[in]  aCallback     A pointer to a function that is called on response reception or timeout.
141      * @param[in]  aContext      A pointer to application-specific context for @p aCallback.
142      *
143      * @retval kErrorNone    Successfully send the meshcop dataset command.
144      * @retval kErrorNoBufs  Insufficient buffer space to send.
145      * @retval kErrorBusy    A previous request is ongoing.
146      *
147      */
148     Error SendSetRequest(const Dataset::Info &    aDatasetInfo,
149                          const uint8_t *          aTlvs,
150                          uint8_t                  aLength,
151                          otDatasetMgmtSetCallback aCallback,
152                          void *                   aContext);
153 
154     /**
155      * This method sends a MGMT_GET request.
156      *
157      * @param[in]  aDatasetComponents  An Operational Dataset components structure specifying components to request.
158      * @param[in]  aTlvTypes           A pointer to array containing additional raw TLV types to be requested.
159      * @param[in]  aLength             Number of bytes in @p aTlvTypes.
160      * @param[in]  aAddress            The IPv6 destination address for the MGMT_GET request.
161      *
162      * @retval kErrorNone     Successfully send the meshcop dataset command.
163      * @retval kErrorNoBufs   Insufficient buffer space to send.
164      *
165      */
166     Error SendGetRequest(const Dataset::Components &aDatasetComponents,
167                          const uint8_t *            aTlvTypes,
168                          uint8_t                    aLength,
169                          const otIp6Address *       aAddress) const;
170 #if OPENTHREAD_FTD
171     /**
172      * This method appends the MLE Dataset TLV but excluding MeshCoP Sub Timestamp TLV.
173      *
174      * @param[in] aMessage       The message to append the TLV to.
175      *
176      * @retval kErrorNone    Successfully append MLE Dataset TLV without MeshCoP Sub Timestamp TLV.
177      * @retval kErrorNoBufs  Insufficient available buffers to append the message with MLE Dataset TLV.
178      *
179      */
180     Error AppendMleDatasetTlv(Message &aMessage) const;
181 #endif
182 
183 protected:
184     /**
185      * This class defines a generic Dataset TLV to read from a message.
186      *
187      */
188     OT_TOOL_PACKED_BEGIN
189     class DatasetTlv : public Tlv
190     {
191     public:
192         /**
193          * This method reads the Dataset TLV from a given message at a given offset.
194          *
195          * @param[in]  aMessage  A message to read the TLV from.
196          * @param[in]  aOffset   An offset into the message to read from.
197          *
198          * @retval kErrorNone    The TLV was read successfully.
199          * @retval kErrorParse   The TLV was not well-formed and could not be parsed.
200          *
201          */
202         Error ReadFromMessage(const Message &aMessage, uint16_t aOffset);
203 
204     private:
205         uint8_t mValue[Dataset::kMaxValueSize];
206     } OT_TOOL_PACKED_END;
207 
208     /**
209      * This constructor initializes the object.
210      *
211      * @param[in]  aInstance      A reference to the OpenThread instance.
212      * @param[in]  aType          Dataset type, Active or Pending.
213      * @param[in]  aTimerHandler  The registration timer handler.
214      *
215      */
216     DatasetManager(Instance &aInstance, Dataset::Type aType, TimerMilli::Handler aTimerHandler);
217 
218     /**
219      * This method gets the Operational Dataset type (Active or Pending).
220      *
221      * @returns The Operational Dataset type.
222      *
223      */
GetType(void) const224     Dataset::Type GetType(void) const { return mLocal.GetType(); }
225 
226     /**
227      * This method clears the Operational Dataset.
228      *
229      */
230     void Clear(void);
231 
232     /**
233      * This method saves the Operational Dataset in non-volatile memory.
234      *
235      * @param[in]  aDataset  The Operational Dataset.
236      *
237      * @retval kErrorNone   Successfully applied configuration.
238      * @retval kErrorParse  The dataset has at least one TLV with invalid format.
239      *
240      */
241     Error Save(const Dataset &aDataset);
242 
243     /**
244      * This method saves the Operational Dataset in non-volatile memory.
245      *
246      * @param[in]  aDatasetInfo  The Operational Dataset as `Dataset::Info`.
247      *
248      * @retval kErrorNone             Successfully saved the dataset.
249      * @retval kErrorNotImplemented   The platform does not implement settings functionality.
250      *
251      */
252     Error Save(const Dataset::Info &aDatasetInfo);
253 
254     /**
255      * This method saves the Operational Dataset in non-volatile memory.
256      *
257      * @param[in]  aDataset  The Operational Dataset.
258      *
259      * @retval kErrorNone             Successfully saved the dataset.
260      * @retval kErrorNotImplemented   The platform does not implement settings functionality.
261      *
262      */
263     Error Save(const otOperationalDatasetTlvs &aDataset);
264 
265     /**
266      * This method sets the Operational Dataset for the partition.
267      *
268      * This method also updates the non-volatile version if the partition's Operational Dataset is newer.
269      *
270      * @param[in]  aTimestamp  The timestamp for the Operational Dataset.
271      * @param[in]  aMessage    The message buffer.
272      * @param[in]  aOffset     The offset where the Operational Dataset begins.
273      * @param[in]  aLength     The length of the Operational Dataset.
274      *
275      * @retval kErrorNone     Successfully parsed the Dataset from the @p aMessage and saved it.
276      * @retval kErrorParse    Could not parse the Dataset from @p aMessage.
277      *
278      */
279     Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint8_t aLength);
280 
281     /**
282      * This method saves the Operational Dataset in non-volatile memory.
283      *
284      * @param[in]  aDataset  The Operational Dataset.
285      *
286      * @retval kErrorNone   Successfully applied configuration.
287      * @retval kErrorParse  The dataset has at least one TLV with invalid format.
288      *
289      */
290     Error SaveLocal(const Dataset &aDataset);
291 
292     /**
293      * This method handles a MGMT_GET request message.
294      *
295      * @param[in]  aMessage      The CoAP message buffer.
296      * @param[in]  aMessageInfo  The message info.
297      *
298      */
299     void HandleGet(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const;
300 
301     /**
302      * This method compares the partition's Operational Dataset with that stored in non-volatile memory.
303      *
304      * If the partition's Operational Dataset is newer, the non-volatile storage is updated.
305      * If the partition's Operational Dataset is older, the registration process is started.
306      *
307      */
308     void HandleNetworkUpdate(void);
309 
310     /**
311      * This method initiates a network data registration message with the Leader.
312      *
313      */
314     void HandleTimer(void);
315 
316 #if OPENTHREAD_FTD
317     /**
318      * This method handles the MGMT_SET request message.
319      *
320      * @param[in]  aMessage      The CoAP message buffer.
321      * @param[in]  aMessageInfo  The message info.
322      *
323      * @retval kErrorNone  The MGMT_SET request message was handled successfully.
324      * @retval kErrorDrop  The MGMT_SET request message was dropped.
325      *
326      */
327     Error HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
328 #endif
329 
330     DatasetLocal mLocal;
331     Timestamp    mTimestamp;
332     bool         mTimestampValid : 1;
333 
334 private:
335     static void HandleMgmtSetResponse(void *               aContext,
336                                       otMessage *          aMessage,
337                                       const otMessageInfo *aMessageInfo,
338                                       Error                aError);
339     void        HandleMgmtSetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aError);
340 
IsActiveDataset(void) const341     bool  IsActiveDataset(void) const { return GetType() == Dataset::kActive; }
IsPendingDataset(void) const342     bool  IsPendingDataset(void) const { return GetType() == Dataset::kPending; }
343     void  SignalDatasetChange(void) const;
344     void  HandleDatasetUpdated(void);
345     Error AppendDatasetToMessage(const Dataset::Info &aDatasetInfo, Message &aMessage) const;
346     void  SendSet(void);
347     void  SendGetResponse(const Coap::Message &   aRequest,
348                           const Ip6::MessageInfo &aMessageInfo,
349                           uint8_t *               aTlvs,
350                           uint8_t                 aLength) const;
351 
352 #if OPENTHREAD_FTD
353     void SendSetResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aMessageInfo, StateTlv::State aState);
354 #endif
355 
356     static constexpr uint8_t  kMaxDatasetTlvs = 16;   // Maximum number of TLVs in a Dataset.
357     static constexpr uint32_t kSendSetDelay   = 5000; // Milliseconds
358 
359     bool       mMgmtPending : 1;
360     TimerMilli mTimer;
361 
362     otDatasetMgmtSetCallback mMgmtSetCallback;
363     void *                   mMgmtSetCallbackContext;
364 };
365 
366 class ActiveDatasetManager : public DatasetManager, private NonCopyable
367 {
368 public:
369     /**
370      * This constructor initializes the ActiveDatasetManager object.
371      *
372      * @param[in]  aInstance  A reference to the OpenThread instance.
373      *
374      */
375     explicit ActiveDatasetManager(Instance &aInstance);
376 
377     /**
378      * This method indicates whether the Active Dataset is partially complete.
379      *
380      * This method is primarily used to determine whether a user has supplied a partial Active Dataset for use
381      * with joining a network.
382      *
383      * @retval TRUE   if an Active Dataset is saved but does not include an Active Timestamp.
384      * @retval FALSE  if an Active Dataset is not saved or does include an Active Timestamp.
385      *
386      */
387     bool IsPartiallyComplete(void) const;
388 
389     /**
390      * This method indicates whether or not a valid network is present in the Active Operational Dataset.
391      *
392      * @retval TRUE if a valid network is present in the Active Dataset.
393      * @retval FALSE if a valid network is not present in the Active Dataset.
394      *
395      */
396     bool IsCommissioned(void) const;
397 
398     /**
399      * This method clears the Active Operational Dataset.
400      *
401      */
Clear(void)402     void Clear(void) { DatasetManager::Clear(); }
403 
404     /**
405      * This method saves the Operational Dataset in non-volatile memory.
406      *
407      * This method also reconfigures the Thread interface.
408      *
409      * @param[in]  aDataset  The Operational Dataset.
410      *
411      */
Save(const Dataset & aDataset)412     void Save(const Dataset &aDataset) { IgnoreError(DatasetManager::Save(aDataset)); }
413 
414     /**
415      * This method sets the Operational Dataset for the partition.
416      *
417      * This method also reconfigures the Thread interface.
418      * This method also updates the non-volatile version if the partition's Operational Dataset is newer.
419      *
420      * @param[in]  aTimestamp  The timestamp for the Operational Dataset.
421      * @param[in]  aMessage    The message buffer.
422      * @param[in]  aOffset     The offset where the Operational Dataset begins.
423      * @param[in]  aLength     The length of the Operational Dataset.
424      *
425      * @retval kErrorNone     Successfully parsed the Dataset from the @p aMessage and saved it.
426      * @retval kErrorParse    Could not parse the Dataset from @p aMessage.
427      *
428      */
429     Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint8_t aLength);
430 
431     /**
432      * This method sets the Operational Dataset in non-volatile memory.
433      *
434      * @param[in]  aDatasetInfo  The Operational Dataset as `Dataset::Info`.
435      *
436      * @retval kErrorNone            Successfully saved the dataset.
437      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
438      *
439      */
Save(const Dataset::Info & aDatasetInfo)440     Error Save(const Dataset::Info &aDatasetInfo) { return DatasetManager::Save(aDatasetInfo); }
441 
442     /**
443      * This method sets the Operational Dataset in non-volatile memory.
444      *
445      * @param[in]  aDataset  The Operational Dataset.
446      *
447      * @retval kErrorNone            Successfully saved the dataset.
448      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
449      *
450      */
Save(const otOperationalDatasetTlvs & aDataset)451     Error Save(const otOperationalDatasetTlvs &aDataset) { return DatasetManager::Save(aDataset); }
452 
453 #if OPENTHREAD_FTD
454 
455     /**
456      * This method creates a new Operational Dataset to use when forming a new network.
457      *
458      * @param[out]  aDatasetInfo  The Operational Dataset as `Dataset::Info`.
459      *
460      * @retval kErrorNone    Successfully created a new Operational Dataset.
461      * @retval kErrorFailed  Failed to generate random values for new parameters.
462      *
463      */
CreateNewNetwork(Dataset::Info & aDatasetInfo)464     Error CreateNewNetwork(Dataset::Info &aDatasetInfo) { return aDatasetInfo.GenerateRandom(GetInstance()); }
465 
466     /**
467      * This method starts the Leader functions for maintaining the Active Operational Dataset.
468      *
469      */
470     void StartLeader(void);
471 
472     /**
473      * This method stops the Leader functions for maintaining the Active Operational Dataset.
474      *
475      */
476     void StopLeader(void);
477 
478     /**
479      * This method generate a default Active Operational Dataset.
480      *
481      * @retval kErrorNone          Successfully generated an Active Operational Dataset.
482      * @retval kErrorAlready       A valid Active Operational Dataset already exists.
483      * @retval kErrorInvalidState  Device is not currently attached to a network.
484      *
485      */
486     Error GenerateLocal(void);
487 #endif
488 
489 private:
490     static void HandleTimer(Timer &aTimer);
HandleTimer(void)491     void        HandleTimer(void) { DatasetManager::HandleTimer(); }
492 
493     static void HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
494     void        HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const;
495 
496 #if OPENTHREAD_FTD
497     static void HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
498     void        HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
499 #endif
500 
501     Coap::Resource mResourceGet;
502 
503 #if OPENTHREAD_FTD
504     Coap::Resource mResourceSet;
505 #endif
506 };
507 
508 class PendingDatasetManager : public DatasetManager, private NonCopyable
509 {
510 public:
511     /**
512      * This constructor initializes the PendingDatasetManager object.
513      *
514      * @param[in]  aInstance     A reference to the OpenThread instance.
515      *
516      */
517     explicit PendingDatasetManager(Instance &aInstance);
518 
519     /**
520      * This method clears the Pending Operational Dataset.
521      *
522      * This method also stops the Delay Timer if it was active.
523      *
524      */
525     void Clear(void);
526 
527     /**
528      * This method clears the network Pending Operational Dataset.
529      *
530      * This method also stops the Delay Timer if it was active.
531      *
532      */
533     void ClearNetwork(void);
534 
535     /**
536      * This method saves the Operational Dataset in non-volatile memory.
537      *
538      * This method also starts the Delay Timer.
539      *
540      * @param[in]  aDatasetInfo  The Operational Dataset as `Dataset::Info`.
541      *
542      * @retval kErrorNone            Successfully saved the dataset.
543      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
544      *
545      */
546     Error Save(const Dataset::Info &aDatasetInfo);
547 
548     /**
549      * This method saves the Operational Dataset in non-volatile memory.
550      *
551      * This method also starts the Delay Timer.
552      *
553      * @param[in]  aDataset  The Operational Dataset.
554      *
555      * @retval kErrorNone            Successfully saved the dataset.
556      * @retval kErrorNotImplemented  The platform does not implement settings functionality.
557      *
558      */
559     Error Save(const otOperationalDatasetTlvs &aDataset);
560 
561     /**
562      * This method sets the Operational Dataset for the partition.
563      *
564      * This method also updates the non-volatile version if the partition's Operational Dataset is newer.
565      *
566      * This method also starts the Delay Timer.
567      *
568      * @param[in]  aTimestamp  The timestamp for the Operational Dataset.
569      * @param[in]  aMessage    The message buffer.
570      * @param[in]  aOffset     The offset where the Operational Dataset begins.
571      * @param[in]  aLength     The length of the Operational Dataset.
572      *
573      */
574     Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint8_t aLength);
575 
576     /**
577      * This method saves the Operational Dataset in non-volatile memory.
578      *
579      * @param[in]  aDataset  The Operational Dataset.
580      *
581      * @retval kErrorNone   Successfully applied configuration.
582      * @retval kErrorParse  The dataset has at least one TLV with invalid format.
583      *
584      */
585     Error Save(const Dataset &aDataset);
586 
587 #if OPENTHREAD_FTD
588     /**
589      * This method starts the Leader functions for maintaining the Active Operational Dataset.
590      *
591      */
592     void StartLeader(void);
593 
594     /**
595      * This method stops the Leader functions for maintaining the Active Operational Dataset.
596      *
597      */
598     void StopLeader(void);
599 
600     /**
601      * This method generates a Pending Dataset from an Active Dataset.
602      *
603      * @param[in]  aTimestamp  The Active Dataset Timestamp.
604      * @param[in]  aMessage    The MGMT_SET message that contains an Active Dataset.
605      *
606      */
607     void ApplyActiveDataset(const Timestamp &aTimestamp, Coap::Message &aMessage);
608 #endif
609 
610 private:
611     void StartDelayTimer(void);
612 
613     static void HandleTimer(Timer &aTimer);
HandleTimer(void)614     void        HandleTimer(void) { DatasetManager::HandleTimer(); }
615 
616     static void HandleDelayTimer(Timer &aTimer);
617     void        HandleDelayTimer(void);
618 
619     static void HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
620     void        HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const;
621 
622 #if OPENTHREAD_FTD
623     static void HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
624     void        HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
625 #endif
626 
627     TimerMilli mDelayTimer;
628 
629     Coap::Resource mResourceGet;
630 
631 #if OPENTHREAD_FTD
632     Coap::Resource mResourceSet;
633 #endif
634 };
635 
636 } // namespace MeshCoP
637 } // namespace ot
638 
639 #endif // MESHCOP_DATASET_MANAGER_HPP_
640