serial_ntrip_nmea.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. #include <fcntl.h>
  2. #include <termios.h>
  3. #include <unistd.h>
  4. #include <sys/ioctl.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <cstring>
  9. #include <cerrno>
  10. #include <vector>
  11. #include <deque>
  12. #include <thread>
  13. #include <atomic>
  14. #include <sstream>
  15. #include <iomanip>
  16. #include <signal.h>
  17. #include "rclcpp/rclcpp.hpp"
  18. #include "nmea_msgs/msg/sentence.hpp"
  19. #include "geometry_msgs/msg/twist_stamped.hpp"
  20. #include <cmath>
  21. class SerialNtripNmea : public rclcpp::Node
  22. {
  23. public:
  24. SerialNtripNmea() : Node("serial_ntrip_nmea")
  25. {
  26. // 声明参数
  27. this->declare_parameter("serial_port", "/dev/ttyUSB0");
  28. this->declare_parameter("baud_rate", 115200);
  29. this->declare_parameter("ntrip_host", "103.143.19.54");
  30. this->declare_parameter("ntrip_port", 8002);
  31. this->declare_parameter("mountpoint", "RTCM32GRCpro");
  32. this->declare_parameter("username", "7ripg398");
  33. this->declare_parameter("password", "97344");
  34. this->declare_parameter("nmea_topic", "navsat/nmea_sentence");
  35. this->declare_parameter("frame_id", "sentence");
  36. // 获取参数
  37. serial_port_ = this->get_parameter("serial_port").as_string();
  38. baud_rate_ = this->get_parameter("baud_rate").as_int();
  39. ntrip_host_ = this->get_parameter("ntrip_host").as_string();
  40. ntrip_port_ = this->get_parameter("ntrip_port").as_int();
  41. mountpoint_ = this->get_parameter("mountpoint").as_string();
  42. username_ = this->get_parameter("username").as_string();
  43. password_ = this->get_parameter("password").as_string();
  44. nmea_topic_ = this->get_parameter("nmea_topic").as_string();
  45. frame_id_ = this->get_parameter("frame_id").as_string();
  46. // 创建NMEA消息发布器
  47. nmea_pub_ = this->create_publisher<nmea_msgs::msg::Sentence>(nmea_topic_, 10);
  48. // 额外创建一个带 RMC 航向修正的 NMEA 话题,保持原有话题行为不变
  49. nmea_fix_pub_ = this->create_publisher<nmea_msgs::msg::Sentence>("navsat/nmea_sentence_fix", 10);
  50. // 订阅 /cruise/wheel/twist 用于静止判定
  51. twist_sub_ = this->create_subscription<geometry_msgs::msg::TwistStamped>(
  52. "/cruise/wheel/twist", 10,
  53. std::bind(&SerialNtripNmea::twist_callback, this, std::placeholders::_1));
  54. RCLCPP_INFO(this->get_logger(), "SerialNtripNmea初始化:");
  55. RCLCPP_INFO(this->get_logger(), " 串口: %s @ %d baud", serial_port_.c_str(), baud_rate_);
  56. RCLCPP_INFO(this->get_logger(), " NTRIP服务器: %s:%d", ntrip_host_.c_str(), ntrip_port_);
  57. RCLCPP_INFO(this->get_logger(), " 挂载点: %s", mountpoint_.c_str());
  58. RCLCPP_INFO(this->get_logger(), " NMEA话题: %s", nmea_topic_.c_str());
  59. // 启动服务
  60. if (setup_serial() && connect_ntrip()) {
  61. running_ = true;
  62. start_server();
  63. }
  64. }
  65. ~SerialNtripNmea()
  66. {
  67. running_ = false;
  68. if (serial_fd_ >= 0) close(serial_fd_);
  69. if (ntrip_socket_ >= 0) close(ntrip_socket_);
  70. }
  71. private:
  72. std::string serial_port_;
  73. int baud_rate_;
  74. std::string ntrip_host_;
  75. int ntrip_port_;
  76. std::string mountpoint_;
  77. std::string username_;
  78. std::string password_;
  79. std::string nmea_topic_;
  80. std::string frame_id_;
  81. int serial_fd_{-1};
  82. int ntrip_socket_{-1};
  83. rclcpp::Publisher<nmea_msgs::msg::Sentence>::SharedPtr nmea_pub_;
  84. rclcpp::Publisher<nmea_msgs::msg::Sentence>::SharedPtr nmea_fix_pub_;
  85. rclcpp::Subscription<geometry_msgs::msg::TwistStamped>::SharedPtr twist_sub_;
  86. rclcpp::TimerBase::SharedPtr status_timer_;
  87. std::atomic<bool> running_{false};
  88. std::atomic<bool> ntrip_connected_{false};
  89. // 静止判定:从 /cruise/wheel/twist 获取,linear.x==0 且 angular.z==0 为静止
  90. std::atomic<bool> is_stationary_{false};
  91. // RMC 静止航向修正相关状态(仅用于 navsat/nmea_sentence_fix)
  92. std::deque<double> rmc_heading_history_;
  93. bool in_stationary_mode_{false};
  94. bool stationary_heading_locked_{false};
  95. double stationary_locked_heading_{0.0};
  96. bool setup_serial()
  97. {
  98. serial_fd_ = open(serial_port_.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
  99. if (serial_fd_ < 0) {
  100. RCLCPP_ERROR(this->get_logger(), "串口打开失败: %s - %s",
  101. serial_port_.c_str(), strerror(errno));
  102. return false;
  103. }
  104. struct termios tty;
  105. if (tcgetattr(serial_fd_, &tty) != 0) {
  106. RCLCPP_ERROR(this->get_logger(), "获取串口属性失败: %s", strerror(errno));
  107. return false;
  108. }
  109. // 设置波特率
  110. speed_t speed;
  111. switch (baud_rate_) {
  112. case 9600: speed = B9600; break;
  113. case 19200: speed = B19200; break;
  114. case 38400: speed = B38400; break;
  115. case 57600: speed = B57600; break;
  116. case 115200: speed = B115200; break;
  117. case 230400: speed = B230400; break;
  118. default:
  119. RCLCPP_ERROR(this->get_logger(), "不支持的波特率: %d", baud_rate_);
  120. return false;
  121. }
  122. cfsetospeed(&tty, speed);
  123. cfsetispeed(&tty, speed);
  124. // 设置串口参数
  125. tty.c_cflag &= ~PARENB;
  126. tty.c_cflag &= ~CSTOPB;
  127. tty.c_cflag &= ~CSIZE;
  128. tty.c_cflag |= CS8;
  129. tty.c_cflag &= ~CRTSCTS;
  130. tty.c_cflag |= (CREAD | CLOCAL);
  131. tty.c_lflag = 0;
  132. tty.c_iflag = 0;
  133. tty.c_oflag = 0;
  134. tty.c_cc[VTIME] = 0;
  135. tty.c_cc[VMIN] = 0;
  136. if (tcsetattr(serial_fd_, TCSANOW, &tty) != 0) {
  137. RCLCPP_ERROR(this->get_logger(), "设置串口属性失败: %s", strerror(errno));
  138. return false;
  139. }
  140. tcflush(serial_fd_, TCIOFLUSH);
  141. RCLCPP_INFO(this->get_logger(), "串口连接成功: %s", serial_port_.c_str());
  142. return true;
  143. }
  144. bool connect_ntrip()
  145. {
  146. ntrip_socket_ = socket(AF_INET, SOCK_STREAM, 0);
  147. if (ntrip_socket_ < 0) {
  148. RCLCPP_ERROR(this->get_logger(), "创建NTRIP socket失败: %s", strerror(errno));
  149. return false;
  150. }
  151. struct sockaddr_in ntrip_addr;
  152. ntrip_addr.sin_family = AF_INET;
  153. ntrip_addr.sin_port = htons(ntrip_port_);
  154. ntrip_addr.sin_addr.s_addr = inet_addr(ntrip_host_.c_str());
  155. if (connect(ntrip_socket_, (struct sockaddr*)&ntrip_addr, sizeof(ntrip_addr)) < 0) {
  156. RCLCPP_ERROR(this->get_logger(), "连接NTRIP服务器失败: %s", strerror(errno));
  157. return false;
  158. }
  159. // 构建NTRIP请求
  160. std::string auth_string = username_ + ":" + password_;
  161. std::string auth_b64 = base64_encode(auth_string);
  162. std::stringstream request;
  163. request << "GET /" << mountpoint_ << " HTTP/1.1\r\n";
  164. request << "User-Agent: NTRIP SerialNtripNmea/1.0\r\n";
  165. request << "Authorization: Basic " << auth_b64 << "\r\n";
  166. request << "Accept: gnss/data\r\n";
  167. request << "Connection: close\r\n";
  168. request << "\r\n";
  169. std::string request_str = request.str();
  170. RCLCPP_INFO(this->get_logger(), "发送NTRIP请求到服务器: %s:%d", ntrip_host_.c_str(), ntrip_port_);
  171. ssize_t sent = send(ntrip_socket_, request_str.c_str(), request_str.length(), 0);
  172. if (sent < 0) {
  173. RCLCPP_ERROR(this->get_logger(), "发送NTRIP请求失败: %s", strerror(errno));
  174. return false;
  175. }
  176. // 读取响应
  177. char buffer[1024];
  178. ssize_t received = recv(ntrip_socket_, buffer, sizeof(buffer) - 1, 0);
  179. if (received > 0) {
  180. buffer[received] = '\0';
  181. std::string response(buffer);
  182. if (response.find("200") != std::string::npos) {
  183. ntrip_connected_ = true;
  184. RCLCPP_INFO(this->get_logger(), "NTRIP连接成功: %s", mountpoint_.c_str());
  185. return true;
  186. } else {
  187. RCLCPP_ERROR(this->get_logger(), "NTRIP连接失败: %s", response.c_str());
  188. }
  189. } else {
  190. RCLCPP_ERROR(this->get_logger(), "未收到NTRIP服务器响应");
  191. }
  192. return false;
  193. }
  194. void twist_callback(const geometry_msgs::msg::TwistStamped::ConstSharedPtr msg)
  195. {
  196. constexpr double eps = 1e-6;
  197. bool stationary = (std::fabs(msg->twist.linear.x) < eps) &&
  198. (std::fabs(msg->twist.angular.z) < eps);
  199. is_stationary_.store(stationary);
  200. }
  201. void start_server()
  202. {
  203. // 启动串口数据读取和NTRIP发送线程
  204. std::thread serial_thread(&SerialNtripNmea::read_serial_and_process, this);
  205. serial_thread.detach();
  206. // 启动NTRIP数据接收和写入串口线程
  207. std::thread ntrip_thread(&SerialNtripNmea::read_ntrip_and_write_serial, this);
  208. ntrip_thread.detach();
  209. // 使用定时器显示状态,使 spin() 能处理 twist 订阅回调
  210. status_timer_ = this->create_wall_timer(
  211. std::chrono::seconds(1),
  212. [this]() {
  213. if (rclcpp::ok() && running_) {
  214. RCLCPP_INFO(this->get_logger(), "运行状态 - NTRIP连接: %s", ntrip_connected_ ? "是" : "否");
  215. }
  216. });
  217. }
  218. void read_serial_and_process()
  219. {
  220. std::vector<char> buffer(1024);
  221. std::string nmea_buffer;
  222. RCLCPP_INFO(this->get_logger(), "开始读取串口数据并处理");
  223. while (rclcpp::ok() && running_) {
  224. ssize_t bytes_read = read(serial_fd_, buffer.data(), buffer.size());
  225. if (bytes_read > 0) {
  226. // 处理接收到的数据
  227. for (ssize_t i = 0; i < bytes_read; ++i) {
  228. char c = buffer[i];
  229. nmea_buffer += c;
  230. // 检测NMEA语句结束符(换行符)
  231. if (c == '\n') {
  232. // 移除换行符和回车符
  233. if (!nmea_buffer.empty()) {
  234. // 先移除可能的换行符 '\n'
  235. if (nmea_buffer.back() == '\n') {
  236. nmea_buffer.pop_back();
  237. // 再移除可能的回车符 '\r'
  238. if (!nmea_buffer.empty() && nmea_buffer.back() == '\r') {
  239. nmea_buffer.pop_back();
  240. }
  241. } else if (nmea_buffer.back() == '\r') {
  242. // 如果只有回车符,直接移除
  243. nmea_buffer.pop_back();
  244. }
  245. }
  246. // 检查是否为有效的NMEA语句(以$开头)
  247. if (!nmea_buffer.empty() && nmea_buffer[0] == '$') {
  248. // 发布NMEA消息到ROS话题
  249. auto nmea_msg = nmea_msgs::msg::Sentence();
  250. nmea_msg.header.frame_id = frame_id_;
  251. nmea_msg.header.stamp = this->now();
  252. nmea_msg.sentence = nmea_buffer;
  253. // 原始 NMEA 话题,行为保持不变
  254. nmea_pub_->publish(nmea_msg);
  255. // 带 RMC 航向修正的 NMEA 话题
  256. auto nmea_msg_fix = nmea_msg;
  257. nmea_msg_fix.sentence = make_fixed_nmea_sentence(nmea_buffer);
  258. nmea_fix_pub_->publish(nmea_msg_fix);
  259. RCLCPP_DEBUG(this->get_logger(), "发布NMEA消息: %s", nmea_buffer.c_str());
  260. // 检查是否为GGA语句,发送到NTRIP服务器
  261. bool is_gga = false;
  262. if (nmea_buffer.length() >= 6) {
  263. std::string nmea_type = nmea_buffer.substr(1, 5);
  264. if (nmea_type == "GPGGA" || nmea_type == "GNGGA") {
  265. is_gga = true;
  266. }
  267. }
  268. if (is_gga && ntrip_connected_) {
  269. // 发送GGA数据到NTRIP服务器
  270. ssize_t sent = send(ntrip_socket_, nmea_buffer.c_str(), nmea_buffer.length(),
  271. MSG_DONTWAIT | MSG_NOSIGNAL);
  272. if (sent > 0) {
  273. RCLCPP_DEBUG(this->get_logger(), "发送GGA数据到NTRIP: %s", nmea_buffer.c_str());
  274. } else if (sent < 0) {
  275. if (errno == EPIPE || errno == ECONNRESET) {
  276. RCLCPP_ERROR(this->get_logger(), "NTRIP连接断开");
  277. ntrip_connected_ = false;
  278. }
  279. }
  280. }
  281. }
  282. nmea_buffer.clear();
  283. }
  284. }
  285. RCLCPP_DEBUG(this->get_logger(), "从串口接收到 %zd bytes 数据", bytes_read);
  286. } else if (bytes_read < 0) {
  287. if (errno == EAGAIN || errno == EWOULDBLOCK) {
  288. usleep(10000); // 10ms轮询间隔
  289. } else {
  290. RCLCPP_ERROR(this->get_logger(), "串口读取错误: %s", strerror(errno));
  291. break;
  292. }
  293. } else {
  294. usleep(10000); // 10ms轮询间隔
  295. }
  296. }
  297. }
  298. void read_ntrip_and_write_serial()
  299. {
  300. std::vector<char> buffer(4096);
  301. while (rclcpp::ok() && running_ && ntrip_connected_) {
  302. ssize_t bytes_read = recv(ntrip_socket_, buffer.data(), buffer.size(), MSG_DONTWAIT);
  303. if (bytes_read > 0) {
  304. // 将NTRIP返回的数据写入串口
  305. ssize_t bytes_written = write(serial_fd_, buffer.data(), bytes_read);
  306. if (bytes_written > 0) {
  307. RCLCPP_DEBUG(this->get_logger(), "写入串口NTRIP数据: %zd bytes", bytes_written);
  308. } else if (bytes_written < 0) {
  309. if (errno == EAGAIN || errno == EWOULDBLOCK) {
  310. RCLCPP_WARN(this->get_logger(), "串口写入缓冲区满");
  311. } else {
  312. RCLCPP_ERROR(this->get_logger(), "串口写入错误: %s", strerror(errno));
  313. }
  314. }
  315. } else if (bytes_read == 0) {
  316. RCLCPP_ERROR(this->get_logger(), "NTRIP服务器断开连接");
  317. ntrip_connected_ = false;
  318. break;
  319. } else {
  320. if (errno == EAGAIN || errno == EWOULDBLOCK) {
  321. usleep(10000); // 10ms轮询间隔
  322. } else {
  323. RCLCPP_ERROR(this->get_logger(), "NTRIP读取错误: %s", strerror(errno));
  324. ntrip_connected_ = false;
  325. break;
  326. }
  327. }
  328. }
  329. // 尝试重新连接
  330. if (running_ && !ntrip_connected_) {
  331. RCLCPP_INFO(this->get_logger(), "尝试重新连接NTRIP服务器...");
  332. usleep(5000000); // 5秒后重试
  333. if (connect_ntrip()) {
  334. std::thread ntrip_thread(&SerialNtripNmea::read_ntrip_and_write_serial, this);
  335. ntrip_thread.detach();
  336. }
  337. }
  338. }
  339. std::string base64_encode(const std::string& input)
  340. {
  341. static const std::string base64_chars =
  342. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  343. "abcdefghijklmnopqrstuvwxyz"
  344. "0123456789+/";
  345. std::string encoded;
  346. int i = 0;
  347. int j = 0;
  348. unsigned char char_array_3[3];
  349. unsigned char char_array_4[4];
  350. for (char c : input) {
  351. char_array_3[i++] = c;
  352. if (i == 3) {
  353. char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
  354. char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
  355. char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
  356. char_array_4[3] = char_array_3[2] & 0x3f;
  357. for(i = 0; i < 4; i++)
  358. encoded += base64_chars[char_array_4[i]];
  359. i = 0;
  360. }
  361. }
  362. if (i) {
  363. for(j = i; j < 3; j++)
  364. char_array_3[j] = '\0';
  365. char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
  366. char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
  367. char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
  368. char_array_4[3] = char_array_3[2] & 0x3f;
  369. for (j = 0; j < i + 1; j++)
  370. encoded += base64_chars[char_array_4[j]];
  371. while(i++ < 3)
  372. encoded += '=';
  373. }
  374. return encoded;
  375. }
  376. // 生成带 RMC 航向修正的 NMEA 语句,仅影响 navsat/nmea_sentence_fix 话题
  377. std::string make_fixed_nmea_sentence(const std::string& sentence)
  378. {
  379. // 只处理以 $ 开头的 RMC 语句,其余直接原样返回
  380. if (sentence.size() < 6 || sentence[0] != '$') {
  381. return sentence;
  382. }
  383. std::string type = sentence.substr(1, 5);
  384. if (type != "GPRMC" && type != "GNRMC") {
  385. return sentence;
  386. }
  387. // 分离出不含校验和的部分($ 与 * 之间)
  388. std::size_t asterisk_pos = sentence.find('*');
  389. std::string body_no_checksum;
  390. if (asterisk_pos == std::string::npos) {
  391. // 没有显式校验和时,直接去掉起始的 $
  392. body_no_checksum = sentence.substr(1);
  393. } else {
  394. body_no_checksum = sentence.substr(1, asterisk_pos - 1);
  395. }
  396. // 使用逗号分隔字段
  397. std::vector<std::string> fields;
  398. std::size_t start = 0;
  399. while (true) {
  400. std::size_t comma = body_no_checksum.find(',', start);
  401. if (comma == std::string::npos) {
  402. fields.emplace_back(body_no_checksum.substr(start));
  403. break;
  404. } else {
  405. fields.emplace_back(body_no_checksum.substr(start, comma - start));
  406. start = comma + 1;
  407. }
  408. }
  409. // RMC 标准字段中,索引 7 为地速(节),索引 8 为真航向
  410. if (fields.size() <= 8) {
  411. return sentence;
  412. }
  413. bool heading_ok = false;
  414. double heading_deg = 0.0;
  415. if (!fields[8].empty()) {
  416. try {
  417. heading_deg = std::stod(fields[8]);
  418. heading_ok = true;
  419. } catch (...) {
  420. heading_ok = false;
  421. }
  422. }
  423. if (!heading_ok) {
  424. // 航向字段无法解析时,不做任何改动
  425. return sentence;
  426. }
  427. // 静止判定:从 /cruise/wheel/twist 获取,linear.x==0 且 angular.z==0 为静止
  428. bool stationary = is_stationary_.load();
  429. // 进入/退出静止模式(在写入历史前判定,使用前10条那条数据的航向)
  430. if (stationary) {
  431. std::cout << "进入静止模式" << std::endl;
  432. bool just_entered = !in_stationary_mode_;
  433. in_stationary_mode_ = true;
  434. if (just_entered && rmc_heading_history_.size() >= 10) {
  435. stationary_locked_heading_ = rmc_heading_history_[rmc_heading_history_.size() - 10];
  436. stationary_heading_locked_ = true;
  437. }
  438. } else {
  439. std::cout << "退出静止模式" << std::endl;
  440. in_stationary_mode_ = false;
  441. stationary_heading_locked_ = false;
  442. }
  443. // 记录当前 RMC 航向到历史(用于下次静止时取前10条)
  444. rmc_heading_history_.push_back(heading_deg);
  445. if (rmc_heading_history_.size() > 50) {
  446. rmc_heading_history_.pop_front();
  447. }
  448. double output_heading = heading_deg;
  449. if (in_stationary_mode_ && stationary_heading_locked_) {
  450. output_heading = stationary_locked_heading_;
  451. }
  452. // 更新航向字段
  453. {
  454. std::ostringstream oss;
  455. oss << std::fixed << std::setprecision(2) << output_heading;
  456. fields[8] = oss.str();
  457. }
  458. // 重新拼接不含校验和的部分
  459. std::ostringstream body_oss;
  460. for (std::size_t i = 0; i < fields.size(); ++i) {
  461. if (i > 0) {
  462. body_oss << ',';
  463. }
  464. body_oss << fields[i];
  465. }
  466. std::string new_body_no_checksum = body_oss.str();
  467. // 重新计算校验和(对 $ 与 * 之间的字符做 XOR)
  468. unsigned char checksum = 0;
  469. for (char ch : new_body_no_checksum) {
  470. checksum ^= static_cast<unsigned char>(ch);
  471. }
  472. std::ostringstream result;
  473. result << '$' << new_body_no_checksum << '*'
  474. << std::uppercase << std::hex << std::setw(2) << std::setfill('0')
  475. << static_cast<int>(checksum);
  476. return result.str();
  477. }
  478. };
  479. int main(int argc, char** argv)
  480. {
  481. // 忽略SIGPIPE信号
  482. signal(SIGPIPE, SIG_IGN);
  483. rclcpp::init(argc, argv);
  484. auto node = std::make_shared<SerialNtripNmea>();
  485. rclcpp::spin(node);
  486. rclcpp::shutdown();
  487. return 0;
  488. }