A messenger application for Raspberry Pi Zerofor A.U.TH (Real time Embedded systems).
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

700 lines
20 KiB

  1. /*!
  2. * \file core.c
  3. * Core messenger functionality
  4. * \author Christos Choutouridis AEM:8997 <cchoutou@ece.auth.gr>
  5. */
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <stdarg.h>
  10. #include <pthread.h>
  11. #include "core.h"
  12. //! Global data
  13. //! @{
  14. msgList_t msgList; //!< The message list for our application.
  15. //! @}
  16. /*
  17. * Local data types
  18. */
  19. static pthread_mutex_t lock_msgList; //!< mutex for msgList locking
  20. static pthread_mutex_t lock_devList; //!< mutex for devList locking
  21. static pthread_mutex_t lock_stderr; //!< mutex for stderr locking
  22. static pthread_mutex_t lock_stdout; //!< mutex for stderr locking
  23. static pthread_mutex_t lock_stats; //!< mutex for stats locking
  24. //! Helper API
  25. //! @{
  26. //! Macro to create a in_addr_t
  27. #define _ADHOC_SUBNET(A, B, C, D) (((A)<<24) | ((B)<<16) | ((C)<<8) | (D))
  28. /*!
  29. * in_addr_t to devAEM_t conversion utility function
  30. * @param in_addr Internet address (host byte order)
  31. * @return devAEM_t representation of the address
  32. * @note
  33. * for example:
  34. * @code
  35. * devAEM_t dev = addr2devAEM (ntohl(clntAddr.sin_addr.s_addr));
  36. * @endcode
  37. */
  38. devAEM_t addr2devAEM (in_addr_t in_addr) {
  39. return (in_addr & 0x000000FF) + ((in_addr >> 8) & 0x000000FF) * 100;
  40. }
  41. /*!
  42. * devAEM_t to in_addr_t conversion utility function
  43. * @param dev devAEM_t address
  44. * @return Internet address (host byte order)
  45. * @note
  46. * for example:
  47. * @code
  48. * sockaddr_in_t srvAddr;
  49. * srvAddr.sin_addr.s_addr = htonl (devAEM2addr (dev));
  50. * @endcode
  51. */
  52. in_addr_t devAEM2addr (devAEM_t dev) {
  53. uint32_t add = _ADHOC_SUBNET (ADHOC_NET_A, ADHOC_NET_B, ADHOC_NET_C, ADHOC_NET_D);
  54. add |= (dev % 100) & 0x000000FF;
  55. add |= ((dev / 100) & 0x000000FF) << 8;
  56. return add;
  57. }
  58. /*!
  59. * DevIP_t to devAEM_t conversion utility function
  60. * @param ip pointer to devIP_t type address
  61. * @return devAEM_t representation of the address
  62. * @note
  63. * We discard the first 2 fields
  64. */
  65. devAEM_t ip2AEM (devIP_t* ip) {
  66. return ip->C*100 + ip->D;
  67. }
  68. /*!
  69. * devAEM_t to DevIP_t conversion utility function
  70. * @param dev devAEM_t representation of the address
  71. * @return devIP_t type address
  72. */
  73. devIP_t AEM2ip (devAEM_t dev) {
  74. devIP_t ip = {
  75. .A =ADHOC_NET_A, .B=ADHOC_NET_B, .C=dev/100, .D=dev%100
  76. };
  77. return ip;
  78. }
  79. //! @}
  80. /*
  81. * Log related local data
  82. */
  83. static char_t* _frm_msg_in = "In from dev=%d, message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  84. static char_t* _frm_msg_out = "Out to dev=%d, message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  85. static char_t* _frm_msg_new = "New message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  86. //! log API
  87. //! @{
  88. #define _HEAD_SIZE 25
  89. /*!
  90. * Initialize log functionality
  91. * @return The status of the operation
  92. */
  93. status_t log_init (void) {
  94. // Try to initialize pthreads for logging
  95. if (pthread_mutex_init(&lock_stderr, NULL) != 0) {
  96. fprintf (stderr, "Error %s: mutex init has failed\n", __FUNCTION__ );
  97. return MSG_ERROR;
  98. }
  99. if (pthread_mutex_init(&lock_stdout, NULL) != 0) {
  100. fprintf (stderr, "Error %s: mutex init has failed\n", __FUNCTION__ );
  101. return MSG_ERROR;
  102. }
  103. return MSG_OK;
  104. }
  105. /*!
  106. * Logs incoming messages
  107. * @param msg Pointer to msg to log
  108. * @note
  109. * This function depends on settings.outLevel
  110. */
  111. void log_msg_in (msg_t* msg) {
  112. if (settings.outLevel >= OUTLEVEL_1) {
  113. pthread_mutex_lock(&lock_stdout); // lock stdout
  114. fprintf (stdout, _frm_msg_in, // print and flush
  115. msg->sender,
  116. msg->cMsg.from,
  117. msg->cMsg.to,
  118. msg->cMsg.ts,
  119. msg->cMsg.text
  120. );
  121. fflush(stdout);
  122. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  123. }
  124. }
  125. /*!
  126. * Logs outgoing messages
  127. * @param msg Pointer to msg to log
  128. * @param dev device that accepts the message
  129. * @note
  130. * This function depends on settings.outLevel
  131. */
  132. void log_msg_out (msg_t* msg, devAEM_t dev) {
  133. if (settings.outLevel >= OUTLEVEL_1) {
  134. pthread_mutex_lock(&lock_stdout); // lock stdout
  135. fprintf (stdout, _frm_msg_out, // print and flush
  136. dev,
  137. msg->cMsg.from,
  138. msg->cMsg.to,
  139. msg->cMsg.ts,
  140. msg->cMsg.text
  141. );
  142. fflush(stdout);
  143. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  144. }
  145. }
  146. /*!
  147. * Logs new messages
  148. * @param msg Pointer to msg to log
  149. * @note
  150. * This function depends on settings.outLevel
  151. */
  152. void log_msg_new (msg_t* msg) {
  153. if (settings.outLevel >= OUTLEVEL_1) {
  154. pthread_mutex_lock(&lock_stdout); // lock stdout
  155. fprintf (stdout, _frm_msg_new, // print and flush
  156. msg->cMsg.from,
  157. msg->cMsg.to,
  158. msg->cMsg.ts,
  159. msg->cMsg.text
  160. );
  161. fflush(stdout);
  162. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  163. }
  164. }
  165. /*!
  166. * Debug message log to stdout
  167. * Variadic formating print
  168. * @param fmt Pointer to printf like format string
  169. */
  170. void log_debug (const char *fmt, ...) {
  171. if (settings.outLevel >= OUTLEVEL_2) {
  172. va_list ap;
  173. va_start(ap, fmt);
  174. pthread_mutex_lock(&lock_stdout);
  175. vfprintf (stdout, fmt, ap);
  176. fflush(stdout);
  177. pthread_mutex_unlock(&lock_stdout);
  178. va_end(ap);
  179. }
  180. }
  181. /*!
  182. * Debug error message log to stderr
  183. * Variadic formating print
  184. * @param fmt Pointer to printf like format string
  185. */
  186. void log_error (const char *fmt, ...) {
  187. va_list ap;
  188. va_start(ap, fmt);
  189. pthread_mutex_lock(&lock_stderr);
  190. vfprintf (stderr, fmt, ap);
  191. fflush(stderr);
  192. pthread_mutex_unlock(&lock_stderr);
  193. va_end(ap);
  194. }
  195. //! @}
  196. //! cMsg_t API
  197. //! @{
  198. /*!
  199. * Make a new message
  200. * @param msg Pointer to message to create
  201. */
  202. void cMsg_make (cMsg_t* msg) {
  203. static int msgID =0; // unique msg ID
  204. msg->from = settings.me; // from me
  205. do {
  206. // randomly select recipient device
  207. msg->to = devList[rand() % AEMLIST_SIZE].dev;
  208. } while (msg->to == settings.me);
  209. msg->ts = time(NULL);
  210. // stream the first fields and take the quote text iterator
  211. sprintf (msg->text, "%s #%d", MESSAGE_BODY, msgID++);
  212. }
  213. /*!
  214. * Message concatenation
  215. * This function synthesize the ascii message for transmission
  216. * @param msg Pointer to message to serialize
  217. * @param buffer Pointer to output buffer
  218. * @return The size of the resulting message
  219. */
  220. size_t cMsg_serialize (cMsg_t* msg, char_t* buffer) {
  221. return sprintf (buffer, "%d_%d_%lld_%s",
  222. msg->from,
  223. msg->to,
  224. msg->ts,
  225. msg->text
  226. );
  227. }
  228. /*!
  229. * Custom strtok functionality
  230. * @param str Pointer to string to parse
  231. * @param max The size of string
  232. * @param c Single delimiter character (non compatible with strtok())
  233. * @return Pointer to token (non compatible with strtok())
  234. * @note
  235. * This function alters the given string by adding null termination in the places
  236. * of delimiters
  237. */
  238. char_t* _strtok (char_t* str, size_t max, char_t c) {
  239. static char_t* last = NULL;
  240. char_t* ret = str;
  241. // init last
  242. if (str != NULL) last = str;
  243. // loop
  244. for (size_t i=0 ; i<max ; ++i) {
  245. if (*last == c) {
  246. *last++ = 0;
  247. return ret;
  248. }
  249. ++last;
  250. }
  251. return NULL;
  252. }
  253. /*!
  254. * Parse an incoming message
  255. *
  256. * @param cMsg Pointer to cMsg object to store the parsed data
  257. * @param rawMsg Pointer to raw message
  258. * @param size The size f raw message buffer
  259. * @return The status of the operation
  260. * @arg MSG_OK Success
  261. * @arg MSG_ERROR Parse failure, incoming message format error
  262. */
  263. status_t cMsg_parse (cMsg_t* cMsg, char_t* rawMsg, size_t size) {
  264. // Check message integrity
  265. if (size > MSG_TEXT_SIZE)
  266. return MSG_ERROR;
  267. // Parse message
  268. char_t* rest = rawMsg;
  269. char_t* tok[4];
  270. bool_t done = true;
  271. for (size_t i =0; i < 3; ++i) {
  272. tok[i] = _strtok (rest, size, MSG_DELIMITER);
  273. if (tok[i] == NULL) {
  274. done = false;
  275. break;
  276. }
  277. else {
  278. int l = strlen(rest);
  279. rest += l + 1;
  280. size -= l + 1;
  281. }
  282. }
  283. tok[3] = rest;
  284. if (done) {
  285. cMsg->from = atoi (tok[0]);
  286. cMsg->to = atoi (tok[1]);
  287. cMsg->ts = atoi (tok[2]);
  288. strcpy (cMsg->text, tok[3]);
  289. return MSG_OK;
  290. }
  291. return MSG_ERROR;
  292. }
  293. /*! getter for cMsg_t member fromAEM */
  294. uint32_t cMsg_getFrom(cMsg_t* cMsg) { return cMsg->from; }
  295. /*! getter for cMsg_t member toAEM */
  296. uint32_t cMsg_getTo(cMsg_t* cMsg) { return cMsg->to; }
  297. /*! getter for cMsg_t member fromAEM */
  298. uint64_t cMsg_getTs(cMsg_t* cMsg) { return cMsg->ts; }
  299. /*! getter for payload text member */
  300. char_t* cMsg_getText(cMsg_t* cMsg) { return cMsg->text; }
  301. /*!
  302. * Predicate to check core message equality
  303. * @param m1 Pointer to message 1
  304. * @param m2 Pointer to message 2
  305. * @return Equality result (true, false)
  306. */
  307. bool_t cMsg_equal (cMsg_t* m1, cMsg_t* m2) {
  308. if (m1->from != m2->from) return false;
  309. if (m1->to != m2->to) return false;
  310. if (m1->ts != m2->ts) return false;
  311. if (strncmp (m1->text, m2->text, strlen(m1->text)))
  312. return false;
  313. return true;
  314. }
  315. //! @}
  316. /*!
  317. * mgs_t API
  318. */
  319. //! @{
  320. /*!
  321. * message initialization
  322. * @param msg
  323. */
  324. void msg_init (msg_t* msg) {
  325. memset ((void*)msg, 0, sizeof(msg_t)); // init to 0
  326. }
  327. //! @}
  328. //! devList API
  329. //! @{
  330. /*!
  331. * Initialize the devList
  332. * @param msgList Pointer to mesList t initialize
  333. * @return The status of the operation
  334. */
  335. status_t devList_init (devList_t* devList) {
  336. devAEM_t l[] = AEMLIST;
  337. if (pthread_mutex_init(&lock_devList, NULL) != 0) {
  338. log_error ("Error: mutex init has failed\n");
  339. return MSG_ERROR;
  340. }
  341. memset ((void*)devList, 0, sizeof(devList_t));
  342. for (size_t i =0 ; i<AEMLIST_SIZE ; ++i)
  343. devList[i].dev = l[i];
  344. return MSG_OK;
  345. }
  346. /*!
  347. * Returns an iterator for devList AND msg_t.recipients
  348. * @param dev The device to search
  349. * @return The iterator, namely the index to devList array in which is the \p dev
  350. */
  351. dIter_t devList_getIter (devAEM_t dev) {
  352. for (dIter_t i =0 ; i<AEMLIST_SIZE ; ++i) {
  353. if (devList[i].dev == dev)
  354. return i;
  355. }
  356. return -1; // return end()
  357. }
  358. //! Acquires devList resources
  359. void devList_acquire (void) { pthread_mutex_lock(&lock_devList); }
  360. //! Releases devList resources
  361. void devList_release (void) { pthread_mutex_unlock(&lock_devList); }
  362. //! @}
  363. //! msgList API
  364. //! @{
  365. /*! Macro helper to saturate increased values */
  366. #define _top_saturate(test, apply, value) do { \
  367. if (test >= value) apply = value; \
  368. } while (0)
  369. /*! Macro helper to saturate decreased values */
  370. #define _btm_saturate(test, apply, value) do { \
  371. if (test < value) apply = value; \
  372. while (0)
  373. /*!
  374. * Initialize the msgList
  375. * @param msgList Pointer to mesList t initialize
  376. * @return The status of the operation
  377. */
  378. status_t msgList_init (msgList_t* msgList) {
  379. if (pthread_mutex_init(&lock_msgList, NULL) != 0) {
  380. log_error ("Error: mutex init has failed\n");
  381. return MSG_ERROR;
  382. }
  383. memset((void*)msgList, 0, sizeof(msgList_t));
  384. msgList->first =-1;
  385. msgList->last =-1;
  386. srand (time(NULL));
  387. return MSG_OK;
  388. }
  389. /*!
  390. * @brief msgList iterator pre-increment in the msg_t direction
  391. *
  392. * This iterator force a ring buffer behavior. This function takes pointer
  393. * to iterator to alter but return the altered value so it can be directly
  394. * used in expressions
  395. *
  396. * @param it Pointer to iterator to increase
  397. * @return The iterator values
  398. */
  399. mIter_t msgList_preInc (mIter_t* it) {
  400. if (++*it >= MSG_LIST_SIZE) *it = 0;
  401. return *it;
  402. }
  403. /*!
  404. * @brief msgList iterator pre-decrement in the msg_t direction
  405. *
  406. * This iterator force a ring buffer behavior. This function takes pointer
  407. * to iterator to alter but return the altered value so it can be directly
  408. * used in expressions
  409. *
  410. * @param it Pointer to iterator to decrease
  411. * @return The iterator values
  412. */
  413. mIter_t msgList_preDec (mIter_t* it) {
  414. if (--*it < 0) *it = MSG_LIST_SIZE-1;
  415. return *it;
  416. }
  417. /*!
  418. * @param this Pointer to msgList to use
  419. * @return An iterator to the first message of MSG_LIST_SIZE
  420. * of msgList.m[]
  421. * @note
  422. * As the msgList is a ring buffer we can not have a sensible end() iterator.
  423. * end() will eventually merge with begin() when the size reaches MSG_LIST_SIZE
  424. * For that reason in all of our loops through msgList we shall take a begin()
  425. * iterator and loop using size as sentinel for example:
  426. * @code
  427. * mIter_t it = msgList_begin (&msgList); // get a message iterator
  428. * size_t size= msgList_size(&msgList); // get current msgList size
  429. * for (size_t i=0 ; i<size ; ++i, msgList_preInc (&it)) { // don't forget to increase iterator
  430. * // use msgList[it]
  431. * }
  432. * @endcode
  433. */
  434. mIter_t msgList_begin (msgList_t* this) {
  435. return this->first;
  436. }
  437. /*!
  438. * @param this Pointer to msgList to use
  439. * @return An iterator to the last inserted message of MSG_LIST_SIZE
  440. * of msgList.m[]
  441. */
  442. mIter_t msgList_last (msgList_t* this) {
  443. return this->last;
  444. }
  445. /*!
  446. * @param this Pointer to msgList to use
  447. * @return The current used slots of msgList
  448. * @note
  449. * As the msgList is a ring buffer we can not have a sensible end() iterator.
  450. * end() will eventually merge with begin() when the size reaches MSG_LIST_SIZE
  451. * For that reason in all of our loops through msgList we shall take a begin()
  452. * iterator and loop using size as sentinel
  453. * @code
  454. * mIter_t it = msgList_begin (&msgList); // get a message iterator
  455. * size_t size= msgList_size(&msgList); // get current msgList size
  456. * for (size_t i=0 ; i<size ; ++i, msgList_preInc (&it)) { // don't forget to increase iterator
  457. * // use msgList[it]
  458. * }
  459. * @endcode
  460. */
  461. size_t msgList_size (msgList_t* this) {
  462. return this->size;
  463. }
  464. /*!
  465. * Searches for a message in the message list.
  466. *
  467. * @param this The msgList object to work with
  468. * @param msg Pointer to message to search
  469. * @return Iterator to message if found, or -1 if not
  470. */
  471. mIter_t msgList_find (msgList_t* this, msg_t* msg) {
  472. mIter_t it =this->last; // get iterator
  473. // search from end to start to find msg, return on success
  474. for (size_t i=0 ; i < this->size ; ++i) {
  475. if (cMsg_equal (&this->m[it].cMsg, &msg->cMsg))
  476. return it;
  477. msgList_preDec(&it);
  478. // We start from the end as we think, its more possible
  479. // to find msg in the recent messages.
  480. }
  481. return (mIter_t)-1; // fail to find
  482. }
  483. /*!
  484. * Add a new message in the message list
  485. *
  486. * @param this The msgList object to work with
  487. * @param msg Pointer to message
  488. * @return Iterator to the added item (last)
  489. */
  490. mIter_t msgList_add (msgList_t* this, msg_t* msg) {
  491. if (this->first == -1) // if its first time init "first" iterator
  492. this->first = 0;
  493. this->m[msgList_preInc(&this->last)] = *msg; // store data *++it = *msg;
  494. _top_saturate(++this->size, this->size, MSG_LIST_SIZE); // count the items with saturation
  495. // if we reacher full capacity, move along first also
  496. if ((this->first == this->last) && (this->size > 1))
  497. msgList_preInc(&this->first);
  498. return this->last; // return the iterator to newly created slot
  499. }
  500. //! Acquires msgList resources
  501. void msgList_acquire () { pthread_mutex_lock (&lock_msgList); }
  502. //! releases msgList resources
  503. void msgList_release () { pthread_mutex_unlock (&lock_msgList); }
  504. //! @}
  505. //! Statistics API
  506. //! @{
  507. /*!
  508. * Initialize statistics
  509. * @param s Pointer to stat_t struct to initialize
  510. * @return The status of the operation
  511. */
  512. status_t stats_init (stats_t* s) {
  513. memset ((void*)s, 0, sizeof (stats_t));
  514. if (pthread_mutex_init(&lock_stats, NULL) != 0) {
  515. log_error ("Error: mutex init has failed\n");
  516. return MSG_ERROR;
  517. }
  518. return MSG_OK;
  519. }
  520. /*!
  521. * Update statistics for newly created messages
  522. * @param msg Pointer to msg
  523. */
  524. void statsUpdateCreate (msg_t* msg) {
  525. pthread_mutex_lock (&lock_stats);
  526. ++stats.totalMsg;
  527. ++stats.myMsg;
  528. // average message size
  529. int32_t saved = stats.totalMsg - stats.duplicateMsg;
  530. if ((saved-1) > 0) {
  531. // Append to average
  532. int32_t l = strlen(msg->cMsg.text);
  533. stats.avMsgSize += l / (fpdata_t)(saved -1);
  534. stats.avMsgSize *= (fpdata_t)(saved-1)/saved;
  535. }
  536. pthread_mutex_unlock (&lock_stats);
  537. }
  538. /*!
  539. * Update statistics for incoming message
  540. * @param msg Pointer to incoming message
  541. * @param dup Flag to indicate if the message was duplicate
  542. */
  543. void statsUpdateIn (msg_t* msg, bool_t dup) {
  544. pthread_mutex_lock (&lock_stats);
  545. bool_t forMe = msg->cMsg.to == settings.me;
  546. stats.totalMsg++;
  547. stats.duplicateMsg += (dup) ? 1:0;
  548. stats.forMeMsg += (forMe) ? 1:0;
  549. stats.inDirectMsg += (forMe && (msg->cMsg.from == msg->sender)) ? 1:0;
  550. // averages
  551. int32_t saved = stats.totalMsg - stats.duplicateMsg;
  552. if ((saved-1) > 0) {
  553. // Append to message size average
  554. int32_t l = strlen(msg->cMsg.text);
  555. stats.avMsgSize += l / (fpdata_t)(saved -1);
  556. stats.avMsgSize *= (fpdata_t)(saved-1)/saved;
  557. if (settings.trackTime) {
  558. // append to time to me average
  559. tstamp_t dt = (tstamp_t)time(NULL) - msg->cMsg.ts;
  560. if (dt < 0)
  561. dt = 0;
  562. stats.avTimeToMe += dt / (fpdata_t)(saved -1);
  563. stats.avTimeToMe *= (fpdata_t)(saved-1)/saved;
  564. }
  565. }
  566. pthread_mutex_unlock (&lock_stats);
  567. }
  568. /*!
  569. * Update statistics for outgoing message
  570. * @param msg Pointer to message
  571. * @param dev The recipient device
  572. */
  573. void statsUpdateOut (msg_t* msg, devAEM_t dev) {
  574. pthread_mutex_lock (&lock_stats);
  575. stats.outDirectMsg += (msg->cMsg.to == dev) ? 1:0;
  576. pthread_mutex_unlock (&lock_stats);
  577. }
  578. /*!
  579. * Statistics print functionality
  580. * @param stats Pointer to stats to print
  581. * @return The status of the operation
  582. */
  583. status_t statsPrint (stats_t* stats) {
  584. FILE* fp = fopen ("statistics.txt", "w");
  585. if (fp == NULL) {
  586. fclose (fp);
  587. return MSG_ERROR;
  588. }
  589. fprintf (fp, "\n Statistics\n============\n");
  590. fprintf (fp, "Total messages: %d\n", stats->totalMsg);
  591. fprintf (fp, "Duplicate messages: %d\n", stats->duplicateMsg);
  592. fprintf (fp, "Messages for me: %d\n", stats->forMeMsg);
  593. fprintf (fp, "Messages by me: %d\n",stats->myMsg);
  594. fprintf (fp, "In messages direct for me: %d\n", stats->inDirectMsg);
  595. fprintf (fp, "Out direct messages: %d\n", stats->outDirectMsg);
  596. fprintf (fp, "Average message size: %g\n", stats->avMsgSize);
  597. fprintf (fp, "Average time to me: %g\n", stats->avTimeToMe);
  598. fclose (fp);
  599. return MSG_OK;
  600. }
  601. /*!
  602. * Device online timing print functionality
  603. * @param devList Pointer to devList to print
  604. * @return The status of the operation
  605. */
  606. status_t statsTimesPrint (devList_t *devList) {
  607. FILE* fp = fopen ("devices.txt", "w");
  608. if (fp == NULL) {
  609. fclose (fp);
  610. return MSG_ERROR;
  611. }
  612. fprintf (fp, "\n Device timings\n================\n");
  613. for (size_t i =0 ; i<AEMLIST_SIZE ; ++i) {
  614. fprintf (fp, "Device %u found on %lld, last: %lld\n",
  615. devList[i].dev, devList[i].begin, devList[i].end);
  616. }
  617. fclose (fp);
  618. return MSG_OK;
  619. }
  620. //! @}