DroneCAN.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. /*
  2. DroneCAN class for handling OpenDroneID messages
  3. with thanks to David Buzz for ArduPilot ESP32 HAL
  4. */
  5. #include <Arduino.h>
  6. #include "board_config.h"
  7. #include "version.h"
  8. #include <time.h>
  9. #include "DroneCAN.h"
  10. #include <canard.h>
  11. #include <uavcan.protocol.NodeStatus.h>
  12. #include <uavcan.protocol.GetNodeInfo.h>
  13. #include <uavcan.protocol.RestartNode.h>
  14. #include <uavcan.protocol.dynamic_node_id.Allocation.h>
  15. #include <dronecan.remoteid.BasicID.h>
  16. #include <dronecan.remoteid.Location.h>
  17. #include <dronecan.remoteid.SelfID.h>
  18. #include <dronecan.remoteid.System.h>
  19. #include <dronecan.remoteid.OperatorID.h>
  20. #include <dronecan.remoteid.ArmStatus.h>
  21. #define BOARD_ID 10001
  22. #ifndef CAN_APP_NODE_NAME
  23. #define CAN_APP_NODE_NAME "ArduPilot RemoteIDModule"
  24. #endif
  25. #define CAN_DEFAULT_NODE_ID 0 // use DNA
  26. #define UNUSED(x) (void)(x)
  27. static void onTransferReceived_trampoline(CanardInstance* ins, CanardRxTransfer* transfer);
  28. static bool shouldAcceptTransfer_trampoline(const CanardInstance* ins, uint64_t* out_data_type_signature, uint16_t data_type_id,
  29. CanardTransferType transfer_type,
  30. uint8_t source_node_id);
  31. // decoded messages
  32. void DroneCAN::init(void)
  33. {
  34. can_driver.init(1000000);
  35. canardInit(&canard, (uint8_t *)canard_memory_pool, sizeof(canard_memory_pool),
  36. onTransferReceived_trampoline, shouldAcceptTransfer_trampoline, NULL);
  37. #if CAN_DEFAULT_NODE_ID
  38. canardSetLocalNodeID(&canard, CAN_DEFAULT_NODE_ID);
  39. #endif
  40. canard.user_reference = (void*)this;
  41. }
  42. void DroneCAN::update(void)
  43. {
  44. if (do_DNA()) {
  45. const uint32_t now_ms = millis();
  46. if (now_ms - last_node_status_ms >= 1000) {
  47. last_node_status_ms = now_ms;
  48. node_status_send();
  49. arm_status_send();
  50. }
  51. }
  52. processTx();
  53. processRx();
  54. }
  55. void DroneCAN::node_status_send(void)
  56. {
  57. uint8_t buffer[UAVCAN_PROTOCOL_NODESTATUS_MAX_SIZE];
  58. node_status.uptime_sec = millis() / 1000U;
  59. node_status.vendor_specific_status_code = 0;
  60. const uint16_t len = uavcan_protocol_NodeStatus_encode(&node_status, buffer);
  61. static uint8_t tx_id;
  62. canardBroadcast(&canard,
  63. UAVCAN_PROTOCOL_NODESTATUS_SIGNATURE,
  64. UAVCAN_PROTOCOL_NODESTATUS_ID,
  65. &tx_id,
  66. CANARD_TRANSFER_PRIORITY_LOW,
  67. (void*)buffer,
  68. len);
  69. }
  70. void DroneCAN::arm_status_send(void)
  71. {
  72. uint8_t buffer[DRONECAN_REMOTEID_ARMSTATUS_MAX_SIZE];
  73. dronecan_remoteid_ArmStatus arm_status {};
  74. const uint8_t status = parse_fail==nullptr?MAV_ODID_GOOD_TO_ARM:MAV_ODID_PRE_ARM_FAIL_GENERIC;
  75. const char *reason = parse_fail==nullptr?"":parse_fail;
  76. arm_status.status = status;
  77. arm_status.error.len = strlen(reason);
  78. strncpy((char*)arm_status.error.data, reason, sizeof(arm_status.error.data));
  79. const uint16_t len = dronecan_remoteid_ArmStatus_encode(&arm_status, buffer);
  80. static uint8_t tx_id;
  81. canardBroadcast(&canard,
  82. DRONECAN_REMOTEID_ARMSTATUS_SIGNATURE,
  83. DRONECAN_REMOTEID_ARMSTATUS_ID,
  84. &tx_id,
  85. CANARD_TRANSFER_PRIORITY_LOW,
  86. (void*)buffer,
  87. len);
  88. }
  89. void DroneCAN::onTransferReceived(CanardInstance* ins,
  90. CanardRxTransfer* transfer)
  91. {
  92. if (canardGetLocalNodeID(ins) == CANARD_BROADCAST_NODE_ID) {
  93. if (transfer->transfer_type == CanardTransferTypeBroadcast &&
  94. transfer->data_type_id == UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_ID) {
  95. handle_allocation_response(ins, transfer);
  96. }
  97. return;
  98. }
  99. const uint32_t now_ms = millis();
  100. switch (transfer->data_type_id) {
  101. case UAVCAN_PROTOCOL_GETNODEINFO_ID:
  102. handle_get_node_info(ins, transfer);
  103. break;
  104. case UAVCAN_PROTOCOL_RESTARTNODE_ID:
  105. Serial.printf("RestartNode\n");
  106. delay(20);
  107. esp_restart();
  108. break;
  109. case DRONECAN_REMOTEID_BASICID_ID:
  110. Serial.printf("Got BasicID\n");
  111. handle_BasicID(transfer);
  112. break;
  113. case DRONECAN_REMOTEID_LOCATION_ID:
  114. Serial.printf("Got Location\n");
  115. handle_Location(transfer);
  116. break;
  117. case DRONECAN_REMOTEID_SELFID_ID:
  118. Serial.printf("Got SelfID\n");
  119. handle_SelfID(transfer);
  120. break;
  121. case DRONECAN_REMOTEID_SYSTEM_ID:
  122. Serial.printf("Got System\n");
  123. handle_System(transfer);
  124. break;
  125. case DRONECAN_REMOTEID_OPERATORID_ID:
  126. Serial.printf("Got OperatorID\n");
  127. handle_OperatorID(transfer);
  128. break;
  129. default:
  130. //Serial.printf("reject %u\n", transfer->data_type_id);
  131. break;
  132. }
  133. }
  134. bool DroneCAN::shouldAcceptTransfer(const CanardInstance* ins,
  135. uint64_t* out_data_type_signature,
  136. uint16_t data_type_id,
  137. CanardTransferType transfer_type,
  138. uint8_t source_node_id)
  139. {
  140. if (canardGetLocalNodeID(ins) == CANARD_BROADCAST_NODE_ID &&
  141. data_type_id == UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_ID) {
  142. *out_data_type_signature = UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_SIGNATURE;
  143. return true;
  144. }
  145. #define ACCEPT_ID(name) case name ## _ID: *out_data_type_signature = name ## _SIGNATURE; return true
  146. switch (data_type_id) {
  147. ACCEPT_ID(UAVCAN_PROTOCOL_GETNODEINFO);
  148. ACCEPT_ID(UAVCAN_PROTOCOL_RESTARTNODE);
  149. ACCEPT_ID(DRONECAN_REMOTEID_BASICID);
  150. ACCEPT_ID(DRONECAN_REMOTEID_LOCATION);
  151. ACCEPT_ID(DRONECAN_REMOTEID_SELFID);
  152. ACCEPT_ID(DRONECAN_REMOTEID_OPERATORID);
  153. ACCEPT_ID(DRONECAN_REMOTEID_SYSTEM);
  154. return true;
  155. }
  156. //Serial.printf("%u: reject ID 0x%x\n", millis(), data_type_id);
  157. return false;
  158. }
  159. static void onTransferReceived_trampoline(CanardInstance* ins,
  160. CanardRxTransfer* transfer)
  161. {
  162. DroneCAN *dc = (DroneCAN *)ins->user_reference;
  163. dc->onTransferReceived(ins, transfer);
  164. }
  165. /*
  166. see if we want to process this packet
  167. */
  168. static bool shouldAcceptTransfer_trampoline(const CanardInstance* ins,
  169. uint64_t* out_data_type_signature,
  170. uint16_t data_type_id,
  171. CanardTransferType transfer_type,
  172. uint8_t source_node_id)
  173. {
  174. DroneCAN *dc = (DroneCAN *)ins->user_reference;
  175. return dc->shouldAcceptTransfer(ins, out_data_type_signature,
  176. data_type_id,
  177. transfer_type,
  178. source_node_id);
  179. }
  180. void DroneCAN::processTx(void)
  181. {
  182. for (const CanardCANFrame* txf = NULL; (txf = canardPeekTxQueue(&canard)) != NULL;) {
  183. CANFrame txmsg {};
  184. txmsg.dlc = CANFrame::dataLengthToDlc(txf->data_len);
  185. memcpy(txmsg.data, txf->data, txf->data_len);
  186. txmsg.id = (txf->id | CANFrame::FlagEFF);
  187. // push message with 1s timeout
  188. if (can_driver.send(txmsg)) {
  189. canardPopTxQueue(&canard);
  190. tx_fail_count = 0;
  191. } else {
  192. if (tx_fail_count < 8) {
  193. tx_fail_count++;
  194. } else {
  195. canardPopTxQueue(&canard);
  196. }
  197. break;
  198. }
  199. }
  200. }
  201. void DroneCAN::processRx(void)
  202. {
  203. CANFrame rxmsg;
  204. while (can_driver.receive(rxmsg)) {
  205. CanardCANFrame rx_frame {};
  206. uint64_t timestamp = micros64();
  207. rx_frame.data_len = CANFrame::dlcToDataLength(rxmsg.dlc);
  208. memcpy(rx_frame.data, rxmsg.data, rx_frame.data_len);
  209. rx_frame.id = rxmsg.id;
  210. int err = canardHandleRxFrame(&canard, &rx_frame, timestamp);
  211. #if 0
  212. Serial.printf("%u: FX %08x %02x %02x %02x %02x %02x %02x %02x %02x (%u) -> %d\n",
  213. millis(),
  214. rx_frame.id,
  215. rxmsg.data[0], rxmsg.data[1], rxmsg.data[2], rxmsg.data[3],
  216. rxmsg.data[4], rxmsg.data[5], rxmsg.data[6], rxmsg.data[7],
  217. rx_frame.data_len,
  218. err);
  219. #else
  220. UNUSED(err);
  221. #endif
  222. }
  223. }
  224. CANFrame::CANFrame(uint32_t can_id, const uint8_t* can_data, uint8_t data_len, bool canfd_frame) :
  225. id(can_id)
  226. {
  227. if ((can_data == nullptr) || (data_len == 0) || (data_len > MaxDataLen)) {
  228. return;
  229. }
  230. memcpy(this->data, can_data, data_len);
  231. if (data_len <= 8) {
  232. dlc = data_len;
  233. } else {
  234. dlc = 8;
  235. }
  236. }
  237. uint8_t CANFrame::dataLengthToDlc(uint8_t data_length)
  238. {
  239. if (data_length <= 8) {
  240. return data_length;
  241. } else if (data_length <= 12) {
  242. return 9;
  243. } else if (data_length <= 16) {
  244. return 10;
  245. } else if (data_length <= 20) {
  246. return 11;
  247. } else if (data_length <= 24) {
  248. return 12;
  249. } else if (data_length <= 32) {
  250. return 13;
  251. } else if (data_length <= 48) {
  252. return 14;
  253. }
  254. return 15;
  255. }
  256. uint8_t CANFrame::dlcToDataLength(uint8_t dlc)
  257. {
  258. /*
  259. Data Length Code 9 10 11 12 13 14 15
  260. Number of data bytes 12 16 20 24 32 48 64
  261. */
  262. if (dlc <= 8) {
  263. return dlc;
  264. } else if (dlc == 9) {
  265. return 12;
  266. } else if (dlc == 10) {
  267. return 16;
  268. } else if (dlc == 11) {
  269. return 20;
  270. } else if (dlc == 12) {
  271. return 24;
  272. } else if (dlc == 13) {
  273. return 32;
  274. } else if (dlc == 14) {
  275. return 48;
  276. }
  277. return 64;
  278. }
  279. uint64_t DroneCAN::micros64(void)
  280. {
  281. uint32_t us = micros();
  282. if (us < last_micros32) {
  283. base_micros64 += 0x100000000ULL;
  284. }
  285. last_micros32 = us;
  286. return us + base_micros64;
  287. }
  288. /*
  289. handle a GET_NODE_INFO request
  290. */
  291. void DroneCAN::handle_get_node_info(CanardInstance* ins, CanardRxTransfer* transfer)
  292. {
  293. uint8_t buffer[UAVCAN_PROTOCOL_GETNODEINFO_RESPONSE_MAX_SIZE] {};
  294. uavcan_protocol_GetNodeInfoResponse pkt {};
  295. node_status.uptime_sec = millis() / 1000U;
  296. pkt.status = node_status;
  297. pkt.software_version.major = FW_VERSION_MAJOR;
  298. pkt.software_version.minor = FW_VERSION_MINOR;
  299. pkt.software_version.optional_field_flags = UAVCAN_PROTOCOL_SOFTWAREVERSION_OPTIONAL_FIELD_FLAG_VCS_COMMIT | UAVCAN_PROTOCOL_SOFTWAREVERSION_OPTIONAL_FIELD_FLAG_IMAGE_CRC;
  300. pkt.software_version.vcs_commit = GIT_VERSION;
  301. readUniqueID(pkt.hardware_version.unique_id);
  302. pkt.hardware_version.major = BOARD_ID >> 8;
  303. pkt.hardware_version.minor = BOARD_ID & 0xFF;
  304. snprintf((char*)pkt.name.data, sizeof(pkt.name.data), "%s", CAN_APP_NODE_NAME);
  305. pkt.name.len = strnlen((char*)pkt.name.data, sizeof(pkt.name.data));
  306. uint16_t total_size = uavcan_protocol_GetNodeInfoResponse_encode(&pkt, buffer);
  307. canardRequestOrRespond(ins,
  308. transfer->source_node_id,
  309. UAVCAN_PROTOCOL_GETNODEINFO_SIGNATURE,
  310. UAVCAN_PROTOCOL_GETNODEINFO_ID,
  311. &transfer->transfer_id,
  312. transfer->priority,
  313. CanardResponse,
  314. &buffer[0],
  315. total_size);
  316. }
  317. void DroneCAN::handle_allocation_response(CanardInstance* ins, CanardRxTransfer* transfer)
  318. {
  319. // Rule C - updating the randomized time interval
  320. send_next_node_id_allocation_request_at_ms =
  321. millis() + UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_MIN_REQUEST_PERIOD_MS +
  322. random(1, UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_MAX_FOLLOWUP_DELAY_MS);
  323. if (transfer->source_node_id == CANARD_BROADCAST_NODE_ID) {
  324. node_id_allocation_unique_id_offset = 0;
  325. return;
  326. }
  327. // Copying the unique ID from the message
  328. uavcan_protocol_dynamic_node_id_Allocation msg;
  329. uavcan_protocol_dynamic_node_id_Allocation_decode(transfer, &msg);
  330. // Obtaining the local unique ID
  331. uint8_t my_unique_id[sizeof(msg.unique_id.data)] {};
  332. readUniqueID(my_unique_id);
  333. // Matching the received UID against the local one
  334. if (memcmp(msg.unique_id.data, my_unique_id, msg.unique_id.len) != 0) {
  335. node_id_allocation_unique_id_offset = 0;
  336. return;
  337. }
  338. if (msg.unique_id.len < sizeof(msg.unique_id.data)) {
  339. // The allocator has confirmed part of unique ID, switching to the next stage and updating the timeout.
  340. node_id_allocation_unique_id_offset = msg.unique_id.len;
  341. send_next_node_id_allocation_request_at_ms -= UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_MIN_REQUEST_PERIOD_MS;
  342. } else {
  343. // Allocation complete - copying the allocated node ID from the message
  344. canardSetLocalNodeID(ins, msg.node_id);
  345. Serial.printf("Node ID allocated: %u\n", unsigned(msg.node_id));
  346. }
  347. }
  348. bool DroneCAN::do_DNA(void)
  349. {
  350. if (canardGetLocalNodeID(&canard) != CANARD_BROADCAST_NODE_ID) {
  351. return true;
  352. }
  353. const uint32_t now = millis();
  354. if (now - last_DNA_start_ms < 1000 && node_id_allocation_unique_id_offset == 0) {
  355. return false;
  356. }
  357. last_DNA_start_ms = now;
  358. uint8_t node_id_allocation_transfer_id = 0;
  359. UNUSED(node_id_allocation_transfer_id);
  360. send_next_node_id_allocation_request_at_ms =
  361. now + UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_MIN_REQUEST_PERIOD_MS +
  362. random(1, UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_MAX_FOLLOWUP_DELAY_MS);
  363. uint8_t allocation_request[CANARD_CAN_FRAME_MAX_DATA_LEN - 1] {};
  364. allocation_request[0] = 0;
  365. if (node_id_allocation_unique_id_offset == 0) {
  366. allocation_request[0] |= 1;
  367. }
  368. uint8_t my_unique_id[sizeof(uavcan_protocol_dynamic_node_id_Allocation::unique_id.data)] {};
  369. readUniqueID(my_unique_id);
  370. static const uint8_t MaxLenOfUniqueIDInRequest = 6;
  371. uint8_t uid_size = (uint8_t)(sizeof(uavcan_protocol_dynamic_node_id_Allocation::unique_id.data) - node_id_allocation_unique_id_offset);
  372. if (uid_size > MaxLenOfUniqueIDInRequest) {
  373. uid_size = MaxLenOfUniqueIDInRequest;
  374. }
  375. memmove(&allocation_request[1], &my_unique_id[node_id_allocation_unique_id_offset], uid_size);
  376. // Broadcasting the request
  377. static uint8_t tx_id;
  378. canardBroadcast(&canard,
  379. UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_SIGNATURE,
  380. UAVCAN_PROTOCOL_DYNAMIC_NODE_ID_ALLOCATION_ID,
  381. &tx_id,
  382. CANARD_TRANSFER_PRIORITY_LOW,
  383. &allocation_request[0],
  384. (uint16_t) (uid_size + 1));
  385. node_id_allocation_unique_id_offset = 0;
  386. return false;
  387. }
  388. void DroneCAN::readUniqueID(uint8_t id[6])
  389. {
  390. esp_efuse_mac_get_default(id);
  391. }
  392. #define IMIN(a,b) ((a)<(b)?(a):(b))
  393. #define COPY_FIELD(fname) mpkt.fname = pkt.fname
  394. #define COPY_STR(fname) memcpy(mpkt.fname, pkt.fname.data, IMIN(pkt.fname.len, sizeof(mpkt.fname)))
  395. void DroneCAN::handle_BasicID(CanardRxTransfer* transfer)
  396. {
  397. dronecan_remoteid_BasicID pkt {};
  398. auto &mpkt = basic_id;
  399. dronecan_remoteid_BasicID_decode(transfer, &pkt);
  400. last_basic_id_ms = millis();
  401. memset(&mpkt, 0, sizeof(mpkt));
  402. COPY_STR(id_or_mac);
  403. COPY_FIELD(id_type);
  404. COPY_FIELD(ua_type);
  405. COPY_STR(uas_id);
  406. }
  407. void DroneCAN::handle_SelfID(CanardRxTransfer* transfer)
  408. {
  409. dronecan_remoteid_SelfID pkt {};
  410. auto &mpkt = self_id;
  411. dronecan_remoteid_SelfID_decode(transfer, &pkt);
  412. last_self_id_ms = millis();
  413. memset(&mpkt, 0, sizeof(mpkt));
  414. COPY_STR(id_or_mac);
  415. COPY_FIELD(description_type);
  416. COPY_STR(description);
  417. }
  418. void DroneCAN::handle_System(CanardRxTransfer* transfer)
  419. {
  420. dronecan_remoteid_System pkt {};
  421. auto &mpkt = system;
  422. dronecan_remoteid_System_decode(transfer, &pkt);
  423. last_system_ms = millis();
  424. memset(&mpkt, 0, sizeof(mpkt));
  425. COPY_STR(id_or_mac);
  426. COPY_FIELD(operator_location_type);
  427. COPY_FIELD(classification_type);
  428. COPY_FIELD(operator_latitude);
  429. COPY_FIELD(operator_longitude);
  430. COPY_FIELD(area_count);
  431. COPY_FIELD(area_radius);
  432. COPY_FIELD(area_ceiling);
  433. COPY_FIELD(area_floor);
  434. COPY_FIELD(category_eu);
  435. COPY_FIELD(class_eu);
  436. COPY_FIELD(operator_altitude_geo);
  437. COPY_FIELD(timestamp);
  438. }
  439. void DroneCAN::handle_OperatorID(CanardRxTransfer* transfer)
  440. {
  441. dronecan_remoteid_OperatorID pkt {};
  442. auto &mpkt = operator_id;
  443. dronecan_remoteid_OperatorID_decode(transfer, &pkt);
  444. last_operator_id_ms = millis();
  445. memset(&mpkt, 0, sizeof(mpkt));
  446. COPY_STR(id_or_mac);
  447. COPY_FIELD(operator_id_type);
  448. COPY_STR(operator_id);
  449. }
  450. void DroneCAN::handle_Location(CanardRxTransfer* transfer)
  451. {
  452. dronecan_remoteid_Location pkt {};
  453. auto &mpkt = location;
  454. dronecan_remoteid_Location_decode(transfer, &pkt);
  455. last_location_ms = millis();
  456. memset(&mpkt, 0, sizeof(mpkt));
  457. COPY_STR(id_or_mac);
  458. COPY_FIELD(status);
  459. COPY_FIELD(direction);
  460. COPY_FIELD(speed_horizontal);
  461. COPY_FIELD(speed_vertical);
  462. COPY_FIELD(latitude);
  463. COPY_FIELD(longitude);
  464. COPY_FIELD(altitude_barometric);
  465. COPY_FIELD(altitude_geodetic);
  466. COPY_FIELD(height_reference);
  467. COPY_FIELD(height);
  468. COPY_FIELD(horizontal_accuracy);
  469. COPY_FIELD(vertical_accuracy);
  470. COPY_FIELD(barometer_accuracy);
  471. COPY_FIELD(speed_accuracy);
  472. COPY_FIELD(timestamp);
  473. COPY_FIELD(timestamp_accuracy);
  474. }
  475. #if 0
  476. // xprintf is useful when debugging in C code such as libcanard
  477. extern "C" {
  478. void xprintf(const char *fmt, ...);
  479. }
  480. void xprintf(const char *fmt, ...)
  481. {
  482. char buffer[200] {};
  483. va_list ap;
  484. va_start(ap, fmt);
  485. uint32_t n = vsnprintf(buffer, sizeof(buffer), fmt, ap);
  486. va_end(ap);
  487. Serial.printf("%s", buffer);
  488. }
  489. #endif