A messenger application for Raspberry Pi Zerofor A.U.TH (Real time Embedded systems).
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

700 lignes
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. * @example
  33. * @code{.c}
  34. * devAEM_t dev = addr2devAEM (ntohl(clntAddr.sin_addr.s_addr));
  35. * @endcode
  36. */
  37. devAEM_t addr2devAEM (in_addr_t in_addr) {
  38. return (in_addr & 0x000000FF) + ((in_addr >> 8) & 0x000000FF) * 100;
  39. }
  40. /*!
  41. * devAEM_t to in_addr_t conversion utility function
  42. * @param dev devAEM_t address
  43. * @return Internet address (host byte order)
  44. * @example
  45. * @code{.c}
  46. * sockaddr_in_t srvAddr;
  47. * srvAddr.sin_addr.s_addr = htonl (devAEM2addr (dev));
  48. * @endcode
  49. */
  50. in_addr_t devAEM2addr (devAEM_t dev) {
  51. uint32_t add = _ADHOC_SUBNET (ADHOC_NET_A, ADHOC_NET_B, ADHOC_NET_C, ADHOC_NET_D);
  52. add |= (dev % 100) & 0x000000FF;
  53. add |= ((dev / 100) & 0x000000FF) << 8;
  54. return add;
  55. }
  56. /*!
  57. * DevIP_t to devAEM_t conversion utility function
  58. * @param ip pointer to devIP_t type address
  59. * @return devAEM_t representation of the address
  60. * @note
  61. * We discard the first 2 fields
  62. */
  63. devAEM_t ip2AEM (devIP_t* ip) {
  64. return ip->C*100 + ip->D;
  65. }
  66. /*!
  67. * devAEM_t to DevIP_t conversion utility function
  68. * @param dev devAEM_t representation of the address
  69. * @return devIP_t type address
  70. */
  71. devIP_t AEM2ip (devAEM_t dev) {
  72. devIP_t ip = {
  73. .A =ADHOC_NET_A, .B=ADHOC_NET_B, .C=dev/100, .D=dev%100
  74. };
  75. return ip;
  76. }
  77. //! @}
  78. /*
  79. * Log related local data
  80. */
  81. static char_t* _frm_msg_in = "In from dev=%d, message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  82. static char_t* _frm_msg_out = "Out to dev=%d, message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  83. static char_t* _frm_msg_new = "New message: from=%d, to=%d, timestamp=%lld, text=%s\n";
  84. //! log API
  85. //! @{
  86. #define _HEAD_SIZE 25
  87. /*!
  88. * Initialize log functionality
  89. * @return The status of the operation
  90. */
  91. status_t log_init (void) {
  92. // Try to initialize pthreads for logging
  93. if (pthread_mutex_init(&lock_stderr, NULL) != 0) {
  94. fprintf (stderr, "Error %s: mutex init has failed\n", __FUNCTION__ );
  95. return MSG_ERROR;
  96. }
  97. if (pthread_mutex_init(&lock_stdout, NULL) != 0) {
  98. fprintf (stderr, "Error %s: mutex init has failed\n", __FUNCTION__ );
  99. return MSG_ERROR;
  100. }
  101. return MSG_OK;
  102. }
  103. /*!
  104. * Logs incoming messages
  105. * @param msg Pointer to msg to log
  106. * @note
  107. * This function depends on \sa settings.outLevel
  108. */
  109. void log_msg_in (msg_t* msg) {
  110. if (settings.outLevel >= OUTLEVEL_1) {
  111. pthread_mutex_lock(&lock_stdout); // lock stdout
  112. fprintf (stdout, _frm_msg_in, // print and flush
  113. msg->sender,
  114. msg->cMsg.from,
  115. msg->cMsg.to,
  116. msg->cMsg.ts,
  117. msg->cMsg.text
  118. );
  119. fflush(stdout);
  120. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  121. }
  122. }
  123. /*!
  124. * Logs outgoing messages
  125. * @param msg Pointer to msg to log
  126. * @param dev device that accepts the message
  127. * @note
  128. * This function depends on \sa settings.outLevel
  129. */
  130. void log_msg_out (msg_t* msg, devAEM_t dev) {
  131. if (settings.outLevel >= OUTLEVEL_1) {
  132. pthread_mutex_lock(&lock_stdout); // lock stdout
  133. fprintf (stdout, _frm_msg_out, // print and flush
  134. dev,
  135. msg->cMsg.from,
  136. msg->cMsg.to,
  137. msg->cMsg.ts,
  138. msg->cMsg.text
  139. );
  140. fflush(stdout);
  141. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  142. }
  143. }
  144. /*!
  145. * Logs new messages
  146. * @param msg Pointer to msg to log
  147. * @note
  148. * This function depends on \sa settings.outLevel
  149. */
  150. void log_msg_new (msg_t* msg) {
  151. if (settings.outLevel >= OUTLEVEL_1) {
  152. pthread_mutex_lock(&lock_stdout); // lock stdout
  153. fprintf (stdout, _frm_msg_new, // print and flush
  154. msg->cMsg.from,
  155. msg->cMsg.to,
  156. msg->cMsg.ts,
  157. msg->cMsg.text
  158. );
  159. fflush(stdout);
  160. pthread_mutex_unlock(&lock_stdout); // unlock stdout
  161. }
  162. }
  163. /*!
  164. * Debug message log to stdout
  165. * Variadic formating print
  166. * @param fmt Pointer to printf like format string
  167. */
  168. void log_debug (const char *fmt, ...) {
  169. if (settings.outLevel >= OUTLEVEL_2) {
  170. va_list ap;
  171. va_start(ap, fmt);
  172. pthread_mutex_lock(&lock_stdout);
  173. vfprintf (stdout, fmt, ap);
  174. fflush(stdout);
  175. pthread_mutex_unlock(&lock_stdout);
  176. va_end(ap);
  177. }
  178. }
  179. /*!
  180. * Debug error message log to stderr
  181. * Variadic formating print
  182. * @param fmt Pointer to printf like format string
  183. */
  184. void log_error (const char *fmt, ...) {
  185. va_list ap;
  186. va_start(ap, fmt);
  187. pthread_mutex_lock(&lock_stderr);
  188. vfprintf (stderr, fmt, ap);
  189. fflush(stderr);
  190. pthread_mutex_unlock(&lock_stderr);
  191. va_end(ap);
  192. }
  193. //! @}
  194. //! cMsg_t API
  195. //! @{
  196. /*!
  197. * Make a new message
  198. * @param msg Pointer to message to create
  199. */
  200. void cMsg_make (cMsg_t* msg) {
  201. static int msgID =0; // unique msg ID
  202. msg->from = settings.me; // from me
  203. do {
  204. // randomly select recipient device
  205. msg->to = devList[rand() % AEMLIST_SIZE].dev;
  206. } while (msg->to == settings.me);
  207. msg->ts = time(NULL);
  208. // stream the first fields and take the quote text iterator
  209. sprintf (msg->text, "%s #%d", MESSAGE_BODY, msgID++);
  210. }
  211. /*!
  212. * Message concatenation
  213. * This function synthesize the ascii message for transmission
  214. * @param msg Pointer to message to serialize
  215. * @param buffer Pointer to output buffer
  216. * @return The size of the resulting message
  217. */
  218. size_t cMsg_serialize (cMsg_t* msg, char_t* buffer) {
  219. return sprintf (buffer, "%d_%d_%lld_%s",
  220. msg->from,
  221. msg->to,
  222. msg->ts,
  223. msg->text
  224. );
  225. }
  226. /*!
  227. * Custom strtok functionality
  228. * @param str Pointer to string to parse
  229. * @param max The size of string
  230. * @param c Single delimiter character (non compatible with strtok())
  231. * @return Pointer to token (non compatible with strtok())
  232. * @note
  233. * This function alters the given string by adding null termination in the places
  234. * of delimiters
  235. */
  236. char_t* _strtok (char_t* str, size_t max, char_t c) {
  237. static char_t* last = NULL;
  238. char_t* ret = str;
  239. // init last
  240. if (str != NULL) last = str;
  241. // loop
  242. for (size_t i=0 ; i<max ; ++i) {
  243. if (*last == c) {
  244. *last++ = 0;
  245. return ret;
  246. }
  247. ++last;
  248. }
  249. return NULL;
  250. }
  251. /*!
  252. * Parse an incoming message
  253. *
  254. * @param cMsg Pointer to cMsg object to store the parsed data
  255. * @param rawMsg Pointer to raw message
  256. * @param size The size f raw message buffer
  257. * @return The status of the operation
  258. * @arg MSG_OK Success
  259. * @arg MSG_ERROR Parse failure, incoming message format error
  260. */
  261. status_t cMsg_parse (cMsg_t* cMsg, char_t* rawMsg, size_t size) {
  262. // Check message integrity
  263. if (size > MSG_TEXT_SIZE)
  264. return MSG_ERROR;
  265. // Parse message
  266. char_t* rest = rawMsg;
  267. char_t* tok[4];
  268. bool_t done = true;
  269. for (size_t i =0; i < 3; ++i) {
  270. tok[i] = _strtok (rest, size, MSG_DELIMITER);
  271. if (tok[i] == NULL) {
  272. done = false;
  273. break;
  274. }
  275. else {
  276. int l = strlen(rest);
  277. rest += l + 1;
  278. size -= l + 1;
  279. }
  280. }
  281. tok[3] = rest;
  282. if (done) {
  283. cMsg->from = atoi (tok[0]);
  284. cMsg->to = atoi (tok[1]);
  285. cMsg->ts = atoi (tok[2]);
  286. strcpy (cMsg->text, tok[3]);
  287. return MSG_OK;
  288. }
  289. return MSG_ERROR;
  290. }
  291. /*! getter for cMsg_t member fromAEM */
  292. uint32_t cMsg_getFrom(cMsg_t* cMsg) { return cMsg->from; }
  293. /*! getter for cMsg_t member toAEM */
  294. uint32_t cMsg_getTo(cMsg_t* cMsg) { return cMsg->to; }
  295. /*! getter for cMsg_t member fromAEM */
  296. uint64_t cMsg_getTs(cMsg_t* cMsg) { return cMsg->ts; }
  297. /*! getter for payload text member */
  298. char_t* cMsg_getText(cMsg_t* cMsg) { return cMsg->text; }
  299. /*!
  300. * Predicate to check core message equality
  301. * @param m1 Pointer to message 1
  302. * @param m2 Pointer to message 2
  303. * @return Equality result (true, false)
  304. */
  305. bool_t cMsg_equal (cMsg_t* m1, cMsg_t* m2) {
  306. if (m1->from != m2->from) return false;
  307. if (m1->to != m2->to) return false;
  308. if (m1->ts != m2->ts) return false;
  309. if (strncmp (m1->text, m2->text, strlen(m1->text)))
  310. return false;
  311. return true;
  312. }
  313. //! @}
  314. /*!
  315. * mgs_t API
  316. */
  317. //! @{
  318. /*!
  319. * message initialization
  320. * @param msg
  321. */
  322. void msg_init (msg_t* msg) {
  323. memset ((void*)msg, 0, sizeof(msg_t)); // init to 0
  324. }
  325. //! @}
  326. //! devList API
  327. //! @{
  328. /*!
  329. * Initialize the devList
  330. * @param msgList Pointer to mesList t initialize
  331. * @return The status of the operation
  332. */
  333. status_t devList_init (devList_t* devList) {
  334. devAEM_t l[] = AEMLIST;
  335. if (pthread_mutex_init(&lock_devList, NULL) != 0) {
  336. log_error ("Error: mutex init has failed\n");
  337. return MSG_ERROR;
  338. }
  339. memset ((void*)devList, 0, sizeof(devList_t));
  340. for (size_t i =0 ; i<AEMLIST_SIZE ; ++i)
  341. devList[i].dev = l[i];
  342. return MSG_OK;
  343. }
  344. /*!
  345. * Returns an iterator for \sa devList AND \sa msg_t.recipients
  346. * @param dev The device to search
  347. * @return The iterator, namely the index to devList array in which is the \p dev
  348. */
  349. dIter_t devList_getIter (devAEM_t dev) {
  350. for (dIter_t i =0 ; i<AEMLIST_SIZE ; ++i) {
  351. if (devList[i].dev == dev)
  352. return i;
  353. }
  354. return -1; // return end()
  355. }
  356. //! Acquires devList resources
  357. void devList_acquire (void) { pthread_mutex_lock(&lock_devList); }
  358. //! Releases devList resources
  359. void devList_release (void) { pthread_mutex_unlock(&lock_devList); }
  360. //! @}
  361. //! msgList API
  362. //! @{
  363. /*! Macro helper to saturate increased values */
  364. #define _top_saturate(test, apply, value) do { \
  365. if (test >= value) apply = value; \
  366. } while (0)
  367. /*! Macro helper to saturate decreased values */
  368. #define _btm_saturate(test, apply, value) do { \
  369. if (test < value) apply = value; \
  370. while (0)
  371. /*!
  372. * Initialize the msgList
  373. * @param msgList Pointer to mesList t initialize
  374. * @return The status of the operation
  375. */
  376. status_t msgList_init (msgList_t* msgList) {
  377. if (pthread_mutex_init(&lock_msgList, NULL) != 0) {
  378. log_error ("Error: mutex init has failed\n");
  379. return MSG_ERROR;
  380. }
  381. memset((void*)msgList, 0, sizeof(msgList_t));
  382. msgList->first =-1;
  383. msgList->last =-1;
  384. srand (time(NULL));
  385. return MSG_OK;
  386. }
  387. /*!
  388. * @brief msgList iterator pre-increment in the msg_t direction
  389. *
  390. * This iterator force a ring buffer behavior. This function takes pointer
  391. * to iterator to alter but return the altered value so it can be directly
  392. * used in expressions
  393. *
  394. * @param it Pointer to iterator to increase
  395. * @return The iterator values
  396. */
  397. mIter_t msgList_preInc (mIter_t* it) {
  398. if (++*it >= MSG_LIST_SIZE) *it = 0;
  399. return *it;
  400. }
  401. /*!
  402. * @brief msgList iterator pre-decrement in the msg_t direction
  403. *
  404. * This iterator force a ring buffer behavior. This function takes pointer
  405. * to iterator to alter but return the altered value so it can be directly
  406. * used in expressions
  407. *
  408. * @param it Pointer to iterator to decrease
  409. * @return The iterator values
  410. */
  411. mIter_t msgList_preDec (mIter_t* it) {
  412. if (--*it < 0) *it = MSG_LIST_SIZE;
  413. return *it;
  414. }
  415. /*!
  416. * @param this Pointer to msgList to use
  417. * @return An iterator to the first message of \sa MSG_LIST_SIZE
  418. * of msgList.m[]
  419. * @note
  420. * As the msgList is a ring buffer we can not have a sensible end() iterator.
  421. * end() will eventually merge with begin() when the size reaches \sa MSG_LIST_SIZE
  422. * For that reason in all of our loops through msgList we shall take a begin()
  423. * iterator and loop using size as sentinel
  424. * @example
  425. * @code{.c}
  426. * mIter_t it = msgList_begin (&msgList); // get a message iterator
  427. * size_t size= msgList_size(&msgList); // get current msgList size
  428. * for (size_t i=0 ; i<size ; ++i, msgList_preInc (&it)) { // don't forget to increase iterator
  429. * // use msgList[it]
  430. * }
  431. * @endcode
  432. */
  433. mIter_t msgList_begin (msgList_t* this) {
  434. return this->first;
  435. }
  436. /*!
  437. * @param this Pointer to msgList to use
  438. * @return An iterator to the last inserted message of \sa MSG_LIST_SIZE
  439. * of msgList.m[]
  440. */
  441. mIter_t msgList_last (msgList_t* this) {
  442. return this->last;
  443. }
  444. /*!
  445. * @param this Pointer to msgList to use
  446. * @return The current used slots of msgList
  447. * @note
  448. * As the msgList is a ring buffer we can not have a sensible end() iterator.
  449. * end() will eventually merge with begin() when the size reaches \sa MSG_LIST_SIZE
  450. * For that reason in all of our loops through msgList we shall take a begin()
  451. * iterator and loop using size as sentinel
  452. * @example
  453. * @code{.c}
  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 \sa 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. //! @}