Fawkes API Fawkes Development Version
imu_cruizcore_xg1010.cpp
1
2/***************************************************************************
3 * imu_cruizcore_xg1010.cpp - Retrieve IMU data from CruizCore XG1010
4 *
5 * Created: Sun Jun 22 21:44:17 2014
6 * Copyright 2008-2014 Tim Niemueller [www.niemueller.de]
7 ****************************************************************************/
8
9/* This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Library General Public License for more details.
18 *
19 * Read the full text in the LICENSE.GPL file in the doc directory.
20 */
21
22#include "imu_cruizcore_xg1010.h"
23
24#include <core/threading/mutex.h>
25#include <core/threading/mutex_locker.h>
26#include <tf/types.h>
27#include <utils/math/angle.h>
28#ifdef USE_TIMETRACKER
29# include <utils/time/tracker.h>
30#endif
31#include <utils/time/tracker_macros.h>
32
33#include <boost/bind/bind.hpp>
34#include <cstdio>
35#include <cstdlib>
36#include <cstring>
37#include <unistd.h>
38
39using namespace fawkes;
40
41#define RECONNECT_INTERVAL 2000
42
43/** @class CruizCoreXG1010AcquisitionThread "imu_cruizcore_xg1010.h"
44 * IMU acquisition thread for CruizCore XG1010 gyros.
45 * @author Tim Niemueller
46 */
47
48/// @cond OLD_BOOST_COMPAT
49
50#if BOOST_VERSION < 104800
51
52// The default maximum number of bytes to transfer in a single operation.
53enum { default_max_transfer_size = 65536 };
54
55class transfer_exactly_t
56{
57public:
58 typedef std::size_t result_type;
59
60 explicit transfer_exactly_t(std::size_t size) : size_(size)
61 {
62 }
63
64 template <typename Error>
65 std::size_t
66 operator()(const Error &err, std::size_t bytes_transferred)
67 {
68 return (!!err || bytes_transferred >= size_)
69 ? 0
70 : (size_ - bytes_transferred < default_max_transfer_size
71 ? size_ - bytes_transferred
72 : std::size_t(default_max_transfer_size));
73 }
74
75private:
76 std::size_t size_;
77};
78
79inline transfer_exactly_t
80transfer_exactly(std::size_t size)
81{
82 return transfer_exactly_t(size);
83}
84#endif
85
86/// @endcond
87
88/** Constructor.
89 * @param cfg_name short name of configuration group
90 * @param cfg_prefix configuration path prefix
91 * @param continuous true to run continuous, false otherwise
92 */
94 std::string &cfg_prefix,
95 bool continuous)
96: IMUAcquisitionThread(cfg_name.c_str(), continuous, cfg_name, cfg_prefix),
97 serial_(io_service_),
98 io_service_work_(io_service_),
99 deadline_(io_service_)
100{
101 set_name("CruizCoreXG1010(%s)", cfg_name.c_str());
102}
103
104void
106{
107 deadline_.expires_at(boost::posix_time::pos_infin);
108 check_deadline();
109
110 cfg_serial_ = config->get_string((cfg_prefix_ + "device").c_str());
111 cfg_baud_rate_ = config->get_uint((cfg_prefix_ + "baud_rate").c_str());
112 cfg_freq_ = config->get_uint((cfg_prefix_ + "data_frequency").c_str());
113
114 if (cfg_freq_ != 25 && cfg_freq_ != 50 && cfg_freq_ != 100) {
115 throw Exception("Invalid data frequency, must be 25, 50, or 100");
116 }
117 if (cfg_baud_rate_ != 115200 && cfg_baud_rate_ != 57600 && cfg_baud_rate_ != 38400
118 && cfg_baud_rate_ != 28800 && cfg_baud_rate_ != 19200 && cfg_baud_rate_ != 9600
119 && cfg_baud_rate_ != 4800) {
120 throw Exception("Invalid baud rate");
121 }
122
123 if ((cfg_freq_ > 25 && cfg_baud_rate_ < 9600) || (cfg_freq_ > 50 && cfg_baud_rate_ < 19200)) {
124 throw Exception("Baud rate too low for frequency");
125 }
126
127 // wait up to two expected packets
128 receive_timeout_ = (1000 / cfg_freq_) * 2;
129 //logger->log_debug(name(), "Receive timeout: %u ms", receive_timeout_);
130
131 // No acceleration data available, set to -1
132 linear_acceleration_[0] = -1.;
133
134 // from XG1010 data sheet
136
137 open_device();
138
139 if (cfg_continuous_)
141
142#ifdef USE_TIMETRACKER
143 tt_ = new TimeTracker();
144 tt_loopcount_ = 0;
145 ttc_full_loop_ = tt_->add_class("Full Loop");
146 ttc_read_ = tt_->add_class("Read");
147 ttc_catch_up_ = tt_->add_class("Catch up");
148 ttc_parse_ = tt_->add_class("Parse");
149#endif
150}
151
152void
154{
155 close_device();
156 if (cfg_continuous_)
158#ifdef USE_TIMETRACKER
159 delete tt_;
160#endif
161}
162
163void
165{
166 TIMETRACK_START(ttc_full_loop_);
167
168 if (serial_.is_open()) {
169 // reset data for the case that we timeout or fail
170 angular_velocity_[2] = 0;
171 orientation_[0] = orientation_[1] = orientation_[2] = orientation_[3] = 0.;
172
173 try {
174 TIMETRACK_START(ttc_read_);
175 deadline_.expires_from_now(boost::posix_time::milliseconds(receive_timeout_));
176
177 ec_ = boost::asio::error::would_block;
178 bytes_read_ = 0;
179
180 size_t to_read = CRUIZCORE_XG1010_PACKET_SIZE;
181 // if there is partial data in the buffer we are running behind
182 // read off old packet, catch up later will trigger if we had more data
183 if (input_buffer_.size() > 0) {
184 const size_t bsize = input_buffer_.size();
185 size_t full_frames = bsize / CRUIZCORE_XG1010_PACKET_SIZE;
186 size_t remaining =
187 CRUIZCORE_XG1010_PACKET_SIZE - (bsize - full_frames * CRUIZCORE_XG1010_PACKET_SIZE);
188 to_read = remaining;
189 }
190
191 //logger->log_debug(name(), "Read %zu bytes", to_read);
192 boost::asio::async_read(serial_,
193 input_buffer_,
194#if BOOST_VERSION >= 104800
195 boost::asio::transfer_exactly(to_read),
196 (boost::lambda::var(ec_) = boost::lambda::_1,
197 boost::lambda::var(bytes_read_) = boost::lambda::_2));
198#else
199 transfer_exactly(to_read),
200 boost::bind(&CruizCoreXG1010AcquisitionThread::handle_read,
201 this,
202 boost::asio::placeholders::error,
203 boost::asio::placeholders::bytes_transferred));
204#endif
205
206 do
207 io_service_.run_one();
208 while (ec_ == boost::asio::error::would_block);
209
210 //logger->log_debug(name(), "Done");
211
212 TIMETRACK_END(ttc_read_);
213
214 data_mutex_->lock();
215 timestamp_->stamp();
217
218 if (ec_) {
219 if (ec_.value() == boost::system::errc::operation_canceled) {
220 logger->log_error(name(), "Data timeout, will try to reconnect");
221 } else {
222 logger->log_warn(name(), "Data read error: %s\n", ec_.message().c_str());
223 }
224 data_mutex_->lock();
225 new_data_ = true;
227 close_device();
228 } else {
229 TIMETRACK_START(ttc_catch_up_);
230 bytes_read_ = 0;
231 bool catch_up = false;
232 size_t read_size = 0;
233 do {
234 ec_ = boost::asio::error::would_block;
235
236 size_t to_read = CRUIZCORE_XG1010_PACKET_SIZE;
237 if (catch_up) {
238 deadline_.expires_from_now(boost::posix_time::milliseconds(receive_timeout_));
239 size_t full_frames = read_size / CRUIZCORE_XG1010_PACKET_SIZE;
240 size_t remaining = CRUIZCORE_XG1010_PACKET_SIZE
241 - (read_size - full_frames * CRUIZCORE_XG1010_PACKET_SIZE);
242 to_read = remaining;
243 } else {
244 deadline_.expires_from_now(boost::posix_time::microseconds(10));
245 }
246 catch_up = false;
247
248 bytes_read_ = 0;
249
250 //logger->log_debug(name(), "Catch up read %zu bytes", to_read);
251 boost::asio::async_read(serial_,
252 input_buffer_,
253#if BOOST_VERSION >= 104800
254 boost::asio::transfer_exactly(to_read),
255 (boost::lambda::var(ec_) = boost::lambda::_1,
256 boost::lambda::var(bytes_read_) = boost::lambda::_2));
257#else
258 transfer_exactly(to_read),
259 boost::bind(&CruizCoreXG1010AcquisitionThread::handle_read,
260 this,
261 boost::asio::placeholders::error,
262 boost::asio::placeholders::bytes_transferred));
263#endif
264
265 do
266 io_service_.run_one();
267 while (ec_ == boost::asio::error::would_block);
268
269 if (bytes_read_ > 0) {
270 read_size += bytes_read_;
271 catch_up = (read_size % CRUIZCORE_XG1010_PACKET_SIZE != 0);
272 ec_ = boost::system::error_code();
273 }
274 } while (bytes_read_ > 0);
275 }
276
277 TIMETRACK_END(ttc_catch_up_);
278
279 if (ec_ && ec_.value() != boost::system::errc::operation_canceled) {
280 logger->log_warn(name(), "Data read error: %s\n", ec_.message().c_str());
281
282 data_mutex_->lock();
283 new_data_ = true;
285 close_device();
286 } else {
287 if (input_buffer_.size() >= CRUIZCORE_XG1010_PACKET_SIZE) {
288 TIMETRACK_START(ttc_parse_);
289 if (input_buffer_.size() > CRUIZCORE_XG1010_PACKET_SIZE) {
290 input_buffer_.consume(input_buffer_.size() - CRUIZCORE_XG1010_PACKET_SIZE);
291 }
292 std::istream in_stream(&input_buffer_);
293 in_stream.read((char *)in_packet_, CRUIZCORE_XG1010_PACKET_SIZE);
294
295 /*
296 printf("Packet (%zu): ", bytes_read_);
297 for (size_t i = 0; i < bytes_read_; ++i) {
298 printf("%x ", in_packet_[i] & 0xff);
299 }
300 printf("\n");
301 */
302
303 try {
304 parse_packet();
305 } catch (Exception &e) {
306 logger->log_warn(name(), e);
307 try {
308 resync();
309 logger->log_info(name(), "Successfully resynced");
310 } catch (Exception &e) {
311 logger->log_warn(name(), "Resync failed, trying to re-open");
312 close_device();
313 }
314 }
315 TIMETRACK_END(ttc_parse_);
316 } else {
318 "*** INVALID number of bytes in buffer: %zu\n",
319 input_buffer_.size());
320 }
321 }
322 } catch (boost::system::system_error &e) {
323 if (e.code() == boost::asio::error::eof) {
324 close_device();
325 logger->log_warn(name(), "CruizCoreXG1010 connection lost, trying to re-open");
326 } else {
327 logger->log_warn(name(), "CruizCore failed read: %s", e.what());
328 }
329 }
330 } else {
331 try {
332 open_device();
333 logger->log_warn(name(), "Reconnected to device");
334 } catch (Exception &e) {
335 // ignore, keep trying
336 usleep(RECONNECT_INTERVAL * 1000);
337 }
338 }
339
340 if (cfg_continuous_)
342
343 yield();
344 TIMETRACK_END(ttc_full_loop_);
345#ifdef USE_TIMETRACKER
346 if (++tt_loopcount_ >= 50) {
347 tt_loopcount_ = 0;
348 tt_->print_to_stdout();
349 }
350#endif
351}
352
353void
354CruizCoreXG1010AcquisitionThread::open_device()
355{
356 try {
357 input_buffer_.consume(input_buffer_.size());
358
359 serial_.open(cfg_serial_);
360 // according to CruizCore R1050K (sensor in XG1010) technical manual
361 serial_.set_option(
362 boost::asio::serial_port::stop_bits(boost::asio::serial_port::stop_bits::one));
363 serial_.set_option(boost::asio::serial_port::parity(boost::asio::serial_port::parity::none));
364 serial_.set_option(boost::asio::serial_port::baud_rate(cfg_baud_rate_));
365
366 send_init_packet(/* enable transfer */ true);
367
368 resync();
369 } catch (boost::system::system_error &e) {
370 throw Exception("CruizCore-XG1010 failed I/O: %s", e.what());
371 }
372}
373
374void
375CruizCoreXG1010AcquisitionThread::send_init_packet(bool enable_transfer)
376{
377 /* Format according to Technical Manual of R1050K for XG1010
378 Field Command Separator Example
379 INIT $MIA COMMA (,) $MIA,
380 FORMAT F, I or A COMMA (,) I,
381 BAUD RATE B,BAUDRATE COMMA (,) B,115200,
382 OUTPUT RATE R COMMA (,) R,100,
383 TYPE D or R COMMA (,) D,
384 OUTPUT Y or N COMMA (,) Y,
385 FLASH Y or N COMMA (,) Y,
386 CHECKSUM SUM of COMMAND ASTERISK(*) *C4
387 */
388 char *cmd_packet;
389 if (asprintf(&cmd_packet,
390 "$MIA,I,B,%u,R,%u,D,%s,N* ",
391 cfg_baud_rate_,
392 cfg_freq_,
393 enable_transfer ? "Y" : "N")
394 == -1) {
395 throw Exception("Failed to create command packet");
396 }
397
398 size_t cmd_packet_len = strlen(cmd_packet);
399
400 // calculate checksum
401 unsigned int checksum = 0;
402 for (size_t i = 1; i < cmd_packet_len - 3; ++i)
403 checksum += cmd_packet[i];
404 checksum &= 0xFF;
405
406 char checksum_str[3];
407 snprintf(checksum_str, 3, "%X", checksum);
408 cmd_packet[cmd_packet_len - 2] = checksum_str[0];
409 cmd_packet[cmd_packet_len - 1] = checksum_str[1];
410
411 std::string cmd_packet_s(cmd_packet, cmd_packet_len);
412 free(cmd_packet);
413
414 logger->log_debug(name(), "Sending init packet: %s", cmd_packet_s.c_str());
415
416 boost::asio::write(serial_, boost::asio::buffer(cmd_packet_s.c_str(), cmd_packet_len));
417}
418
419void
420CruizCoreXG1010AcquisitionThread::resync()
421{
422 /*
423 // the idea here would be to:
424 // 1. stop data transfer
425 // 2. read all remaining data, i.e. flush serial line
426 // 3. start data transfer again
427 // 4. synchronize with packet stream
428 // the only problem being that once the transfer has been stopped it
429 // cannot be enabled again, this seems to be a bug in the CruizCore XG1010.
430
431 // stop transfer and sniff off any data still arriving
432 send_init_packet(/ enable transfer / false);
433 try {
434 do {
435 ec_ = boost::asio::error::would_block;
436 deadline_.expires_from_now(boost::posix_time::milliseconds(receive_timeout_));
437 bytes_read_ = 0;
438
439 boost::asio::async_read(serial_, input_buffer_,
440#if BOOST_VERSION >= 104800
441 (boost::lambda::var(ec_) = boost::lambda::_1,
442 boost::lambda::var(bytes_read_) = boost::lambda::_2));
443#else
444 boost::bind(
445 &CruizCoreXG1010AcquisitionThread::handle_read,
446 this,
447 boost::asio::placeholders::error,
448 boost::asio::placeholders::bytes_transferred
449 ));
450#endif
451
452 do io_service_.run_one(); while (ec_ == boost::asio::error::would_block);
453
454 input_buffer_.consume(input_buffer_.size());
455
456 logger->log_warn(name(), "EC: %s\n", ec_.message().c_str());
457 } while (!ec_ && bytes_read_ > 0);
458 } catch (boost::system::system_error &e) {
459 // ignore, just assume done, if there really is an error we'll
460 // catch it later on
461 }
462
463 send_init_packet(/ enable transfer / true);
464 */
465
466#if BOOST_VERSION >= 104700
467 tcflush(serial_.lowest_layer().native_handle(), TCIOFLUSH);
468#else
469 tcflush(serial_.lowest_layer().native(), TCIOFLUSH);
470#endif
471
472 // Try 10 times
473 const int NUM_TRIES = 10;
474 for (int t = 1; t <= NUM_TRIES; ++t) {
475 try {
476 ec_ = boost::asio::error::would_block;
477 bytes_read_ = 0;
478
479 deadline_.expires_from_now(boost::posix_time::milliseconds(receive_timeout_ * 10));
480 boost::asio::async_read_until(serial_,
481 input_buffer_,
482 std::string("\xff\xff"),
483#if BOOST_VERSION >= 104800
484 (boost::lambda::var(ec_) = boost::lambda::_1,
485 boost::lambda::var(bytes_read_) = boost::lambda::_2));
486#else
487 boost::bind(&CruizCoreXG1010AcquisitionThread::handle_read,
488 this,
489 boost::asio::placeholders::error,
490 boost::asio::placeholders::bytes_transferred));
491#endif
492
493 do
494 io_service_.run_one();
495 while (ec_ == boost::asio::error::would_block);
496
497 if (ec_) {
498 if (ec_.value() == boost::system::errc::operation_canceled) {
499 throw Exception("Timeout (1) on initial synchronization");
500 } else {
501 throw Exception("Error (1) on initial synchronization: %s", ec_.message().c_str());
502 }
503 }
504
505 input_buffer_.consume(bytes_read_ - 2);
506
507 deadline_.expires_from_now(boost::posix_time::milliseconds(receive_timeout_));
508 ec_ = boost::asio::error::would_block;
509 bytes_read_ = 0;
510 boost::asio::async_read(serial_,
511 input_buffer_,
512#if BOOST_VERSION >= 104800
513 boost::asio::transfer_exactly(CRUIZCORE_XG1010_PACKET_SIZE - 2),
514 (boost::lambda::var(ec_) = boost::lambda::_1,
515 boost::lambda::var(bytes_read_) = boost::lambda::_2));
516#else
517 transfer_exactly(CRUIZCORE_XG1010_PACKET_SIZE - 2),
518 boost::bind(&CruizCoreXG1010AcquisitionThread::handle_read,
519 this,
520 boost::asio::placeholders::error,
521 boost::asio::placeholders::bytes_transferred));
522#endif
523
524 do
525 io_service_.run_one();
526 while (ec_ == boost::asio::error::would_block);
527
528 if (ec_) {
529 if (ec_.value() == boost::system::errc::operation_canceled) {
530 throw Exception("Timeout (2) on initial synchronization");
531 } else {
532 throw Exception("Error (2) on initial synchronization: %s", ec_.message().c_str());
533 }
534 }
535
536 std::istream in_stream(&input_buffer_);
537 in_stream.read((char *)in_packet_, CRUIZCORE_XG1010_PACKET_SIZE);
538
539 parse_packet();
540 } catch (Exception &e) {
541 if (t == NUM_TRIES) {
542 e.append("Resync failed after %d tries", NUM_TRIES);
543 throw;
544 }
545 //else {
546 //logger->log_warn(name(), "Resync retry %i failed, retrying %i more time%s",
547 // t, NUM_TRIES - t, (NUM_TRIES - t == 1) ? "" : "s");
548 //}
549 }
550 }
551 deadline_.expires_at(boost::posix_time::pos_infin);
552}
553
554void
555CruizCoreXG1010AcquisitionThread::close_device()
556{
557 serial_.close();
558}
559
560void
561CruizCoreXG1010AcquisitionThread::parse_packet()
562{
563 /*
564 printf("Packet: ");
565 for (size_t i = 0; i < CRUIZCORE_XG1010_PACKET_SIZE; ++i) {
566 printf("%x ", in_packet_[i] & 0xff);
567 }
568 printf("\n");
569 */
570
571 if (in_packet_[0] != 0xFF || in_packet_[1] != 0xFF) {
572 throw Exception("Failed to parse packet, invalid header");
573 }
574
575 short int rate = (in_packet_[2] & 0xFF) | ((in_packet_[3] << 8) & 0xFF00);
576 short int angle = (in_packet_[4] & 0xFF) | ((in_packet_[5] << 8) & 0XFF00);
577
578 int checksum = 0xffff + rate + angle;
579 if (((unsigned char)(checksum & 0xFF) != in_packet_[6])
580 || ((unsigned char)((checksum >> 8) & 0xFF) != in_packet_[7])) {
581 /*
582 printf("Packet: ");
583 for (size_t i = 0; i < CRUIZCORE_XG1010_PACKET_SIZE; ++i) {
584 printf("%x ", in_packet_[i] & 0xff);
585 }
586 printf("\n");
587 */
588 throw Exception("Failed to parse packet, checksum mismatch");
589 }
590
591 data_mutex_->lock();
592 new_data_ = true;
593
594 angular_velocity_[2] = -deg2rad(rate / 100.f);
595
596 tf::Quaternion q = tf::create_quaternion_from_yaw(-deg2rad(angle / 100.f));
597 orientation_[0] = q.x();
598 orientation_[1] = q.y();
599 orientation_[2] = q.z();
600 orientation_[3] = q.w();
601
603}
604
605/** Check whether the deadline has passed.
606 * We compare the deadline against
607 * the current time since a new asynchronous operation may have moved the
608 * deadline before this actor had a chance to run.
609 */
610void
611CruizCoreXG1010AcquisitionThread::check_deadline()
612{
613 if (deadline_.expires_at() <= boost::asio::deadline_timer::traits_type::now()) {
614 serial_.cancel();
615 deadline_.expires_at(boost::posix_time::pos_infin);
616 }
617
618#if BOOST_VERSION >= 104800
619 deadline_.async_wait(
620 boost::lambda::bind(&CruizCoreXG1010AcquisitionThread::check_deadline, this));
621#else
622 deadline_.async_wait(boost::bind(&CruizCoreXG1010AcquisitionThread::check_deadline, this));
623#endif
624}
virtual void finalize()
Finalize the thread.
CruizCoreXG1010AcquisitionThread(std::string &cfg_name, std::string &cfg_prefix, bool continuous)
Constructor.
virtual void init()
Initialize the thread.
virtual void loop()
Code to execute in the thread.
IMU acqusition thread.
float linear_acceleration_[3]
Pre-allocated linear acceleration as array, 3 entries ordered (x,y,z).
fawkes::Mutex * data_mutex_
Lock while writing to distances or echoes array or marking new data.
double angular_velocity_covariance_[9]
Pre-allocated angular velocity covariance, row major matrix ordered x, y, z.
virtual void loop()
Code to execute in the thread.
float orientation_[4]
Pre-allocated orientation quaternion as array, 4 entries ordered (x,y,z,w).
virtual void finalize()
Finalize the thread.
std::string cfg_prefix_
Configuration path prefix.
bool cfg_continuous_
True if running continuous.
float angular_velocity_[3]
Pre-allocated angular velocities as array, 3 entries ordered (x,y,z).
bool new_data_
Set to true in your loop if new data is available.
virtual void init()
Initialize the thread.
fawkes::Time * timestamp_
Time when the most recent data was received.
Configuration * config
This is the Configuration member used to access the configuration.
Definition: configurable.h:41
virtual unsigned int get_uint(const char *path)=0
Get value from configuration which is of type unsigned int.
virtual std::string get_string(const char *path)=0
Get value from configuration which is of type string.
Base class for exceptions in Fawkes.
Definition: exception.h:36
void append(const char *format,...) noexcept
Append messages to the message list.
Definition: exception.cpp:333
virtual void log_debug(const char *component, const char *format,...)=0
Log debug message.
virtual void log_warn(const char *component, const char *format,...)=0
Log warning message.
virtual void log_error(const char *component, const char *format,...)=0
Log error message.
virtual void log_info(const char *component, const char *format,...)=0
Log informational message.
Logger * logger
This is the Logger member used to access the logger.
Definition: logging.h:41
void lock()
Lock this mutex.
Definition: mutex.cpp:87
void unlock()
Unlock the mutex.
Definition: mutex.cpp:131
const char * name() const
Get name of thread.
Definition: thread.h:100
void yield()
Yield the processor to another thread or process.
Definition: thread.cpp:883
void set_name(const char *format,...)
Set name of thread.
Definition: thread.cpp:748
Time tracking utility.
Definition: tracker.h:37
Time & stamp()
Set this time to the current time.
Definition: time.cpp:704
Fawkes library namespace.
float deg2rad(float deg)
Convert an angle given in degrees to radians.
Definition: angle.h:36