// -*- tab-width: 8; Mode: C++; c-basic-offset: 8; indent-tabs-mode: nil -*- // // Copyright (c) 2010 Michael Smith. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. // // // This is a serial port proxy shim for use with MAVLink-capable MAV // controllers. It allows the controller to interact with FlightGear // and control a MAV inside the simulation. // // In addition to bridging between MAVLink and FlightGear, MAVLink data // is forwarded to QGroundControl if it is running on the local system. // // The controller must be willing to send the RC_CHANNELS_SCALED // stream in response to a request, and it should be able to operate // using the data supplied by the GPS_RAW and RAW_IMU messages. // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma pack(1) #include "mavlink_types.h" mavlink_system_t mavlink_system; void comm_send_ch(mavlink_channel_t chan, uint8_t ch); #define MAVLINK_USE_CONVENIENCE_FUNCTIONS #include "ardupilotmega/mavlink.h" #define TARGET_SYSTEM 7 /* XXX what should these really be? */ #define TARGET_COMPONENT 1 /* * Binary packet as exchanged with FG. * * Described in MAVLink.xml */ struct fgIMUData { // GPS double latitude; double longitude; double altitude; double heading; double velocityN; double velocityE; // IMU double accelX; double accelY; double accelZ; double rateRoll; double ratePitch; double rateYaw; // trailer #define MSG_MAGIC 0x4c56414d uint32_t magic; } __attribute__((packed)); struct fgControlData { double aileron; double elevator; double rudder; double throttle; } __attribute__((packed)); /* utility functions for transforming FG blobs */ void swap32(void *p); void swap64(void *p); /* diagnostic tool */ void hexdump(void *p, size_t s); /* local port addresses for communication with FlightGear - these are the FG defaults */ #define IMU_LISTEN_PORT 5501 #define CTRL_SEND_PORT 5500 /* local port addresses for communication with QGroundControl */ #define QGCS_LISTEN_PORT 14551 #define QGCS_SEND_PORT 14550 /* * debug logs */ #define FAC_MAIN 0 #define FAC_CTRL 1 #define FAC_IMU 2 #define FAC_QGCS 3 char *fac_names[] = {"", "CTRL: ", "IMU: ", "QGCS: "}; #define log(_fac, _fmt, _args...) \ do { \ fprintf(stderr, "%s" _fmt "\n", fac_names[FAC_##_fac], ##_args); \ } while(0) #define debug(_fac, _lvl, _fmt, _args...) \ do { \ if (gDebug >= _lvl) fprintf(stderr, "%s" _fmt "\n", fac_names[FAC_##_fac], ##_args); \ } while(0) int gShouldQuit; /* set to 1 if threads should exit */ int gDebug; /* set to 1 if -d is passed */ void usage(void); /* print usage message */ void shouldQuit(int sig); /* signal that the program should quit */ /* * Thread handling aircraft control instructions from the MAV controller to FlightGear. * Also forwards MAVLink packets to QGroundControl. */ pthread_t gCTRLThread; void *ctrlThread(void *arg); void ctrlHandleMessage(mavlink_message_t *msg); /* thread handling pseudo-IMU data from FlightGear to the MAV controller */ pthread_t gIMUThread; void *imuThread(void *arg); int imuGetMessage(int sock, struct fgIMUData *msg); void imuSendPacket(struct fgIMUData *msg); /* thread handling MAVLink traffic from QGroundControl to the MAV controller */ pthread_t gQGCSThread; void *qgsThread(void *arg); int fgSock; /* socket used for flightgear comms */ struct sockaddr_in fgAddr = {sizeof(fgAddr), AF_INET}; int qgcsSock; /* socket used for qgroundcontrol comms */ struct sockaddr_in qgcsAddr = {sizeof(qgcsAddr), AF_INET}; int port; /* serial port connected to MAV controller */ pthread_mutex_t portMutex; /* lock that must be held while writing to the serial port */ int main(int argc, char *argv[]) { int ch; struct sockaddr_in sin = {sizeof(sin), AF_INET}; int opt; struct termios t; /* handle arguments */ while ((ch = getopt(argc, argv, "ds:")) != -1) { switch(ch) { case 'd': gDebug++; break; case '?': default: usage(); } } argc -= optind; argv += optind; if (pthread_mutex_init(&portMutex, NULL)) errx(1, "port mutex"); /* * set up the flightgear socket */ fgSock = socket(AF_INET, SOCK_DGRAM, 0); if (fgSock < 0) err(1, "IMU socket"); sin.sin_port = htons(IMU_LISTEN_PORT); sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); fgAddr.sin_port = htons(CTRL_SEND_PORT); fgAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* bind the listening side of the socket */ for (;;) { if (bind(fgSock, (struct sockaddr *)&sin, sizeof(sin)) < 0) { if (errno != EADDRINUSE) err(1, "IMU socket bind"); log(IMU, "socket in use, waiting..."); sleep(5); } else { break; } } /* set socket options */ opt = 1; if (setsockopt(fgSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) err(1, "IMU setsockopt SO_REUSEADDR"); /* * set up the qgroundcontrol socket */ qgcsSock = socket(AF_INET, SOCK_DGRAM, 0); if (qgcsSock < 0) err(1, "QGroundControl socket"); qgcsAddr.sin_port = htons(QGCS_SEND_PORT); qgcsAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sin.sin_port = htons(QGCS_LISTEN_PORT); sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* bind the listening side of the socket */ for (;;) { if (bind(qgcsSock, (struct sockaddr *)&sin, sizeof(sin)) < 0) { if (errno != EADDRINUSE) err(1, "QGroundControl socket bind"); log(QGCS, "socket in use, waiting..."); sleep(5); } else { break; } } /* set socket options */ opt = 1; if (setsockopt(qgcsSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) err(1, "QGroundControl setsockopt SO_REUSEADDR"); /* * set up the serial port at 57600 */ if (argc < 1) errx(1, "missing serial port name"); if (0 >= (port = open(argv[0], O_RDWR | O_NONBLOCK))) err(1, "could not open port %s", argv[0]); if (tcgetattr(port, &t)) err(1, "tcgetattr"); cfmakeraw(&t); t.c_cflag |= CLOCAL; cfsetspeed(&t, 57600); if (tcsetattr(port, TCSANOW, &t)) err(1, "tcsetattr"); /* start worker threads */ if (pthread_create(&gIMUThread, NULL, imuThread, NULL)) err(1, "IMU thread"); if (pthread_create(&gCTRLThread, NULL, ctrlThread, NULL)) err(1, "CTRL thread"); if (pthread_create(&gQGCSThread, NULL, qgsThread, NULL)) err(1, "QGS thread"); /* don't install new handlers until the threads are ready */ signal(SIGHUP, shouldQuit); siginterrupt(SIGHUP, 1); signal(SIGINT, shouldQuit); siginterrupt(SIGINT, 1); signal(SIGTERM, shouldQuit); siginterrupt(SIGTERM, 1); signal(SIGQUIT, shouldQuit); siginterrupt(SIGQUIT, 1); pthread_join(gIMUThread, NULL); log(IMU, "service terminated"); pthread_join(gCTRLThread, NULL); log(CTRL, "service terminated"); pthread_join(gQGCSThread, NULL); log(QGCS, "service terminated"); close(port); close(fgSock); close(qgcsSock); return(0); } #if __linux__ #define getprogname program_invocation_short_name #endif void usage() { fprintf(stderr, "usage: %s [-d[-d]]\n", getprogname()); exit(1); } void shouldQuit(int sig) { if (!gShouldQuit) { fputs("\b\b \b\b", stderr); /* overwrite tty's ^C */ if (sig) debug(MAIN, 1, "caught signal %d, cleaning up", sig); pthread_kill(gIMUThread, SIGINT); pthread_kill(gCTRLThread, SIGINT); pthread_kill(gQGCSThread, SIGINT); gShouldQuit = 1; } } /* * IMU thread listens for datagrams from FlightGear and forwards them out * the serial port. */ void * imuThread(void *arg) { struct fgIMUData msg; /* do not take signals on this thread */ pthread_sigmask(SIG_BLOCK, NULL, NULL); log(IMU, "Listening for FlightGear binary data on port %d", IMU_LISTEN_PORT); /* handle IMU messages */ while (!gShouldQuit) { if (imuGetMessage(fgSock, &msg)) imuSendPacket(&msg); } return(NULL); } int imuGetMessage(int fgSock, struct fgIMUData *msg) { ssize_t received; /* get a message */ received = recvfrom(fgSock, msg, sizeof(*msg), MSG_WAITALL, NULL, NULL); if (received != sizeof(*msg)) { if (received < 0) { log(IMU, "receive error: %s", strerror(errno)); shouldQuit(0); return(0); } else { if (0 == received) { log(IMU, "Received zero-length data packet, check that MAVLink.xml is correctly installed."); shouldQuit(0); } else { log(IMU, "received %ld instead of %lu", received, sizeof(*msg)); } return(0); } } swap32(&msg->magic); if (msg->magic != MSG_MAGIC) { log(IMU, "bad magic 0x%08x not 0x%08x", msg->magic, MSG_MAGIC); return(0); } /* endian-swap message fields */ swap64(&msg->latitude); swap64(&msg->longitude); swap64(&msg->altitude); swap64(&msg->heading); swap64(&msg->velocityN); swap64(&msg->velocityE); swap64(&msg->accelX); swap64(&msg->accelY); swap64(&msg->accelZ); swap64(&msg->rateRoll); swap64(&msg->ratePitch); swap64(&msg->rateYaw); return(1); } void imuSendPacket(struct fgIMUData *msg) { struct timeval tv; uint64_t usec; int i; gettimeofday(&tv, NULL); usec = (uint64_t)tv.tv_sec + tv.tv_usec; pthread_mutex_lock(&portMutex); debug(IMU, 2, "***"); debug(IMU, 2, "lat %f lon %f alt %f", msg->latitude, msg->longitude, msg->altitude); debug(IMU, 2, "head %f velocityN %f velocityE %f", msg->heading, msg->velocityN, msg->velocityE); debug(IMU, 2, "accX %f accY %f accZ %f", msg->accelX, msg->accelY, msg->accelZ); debug(IMU, 2, "roll %f pitch %f yaw %f", msg->rateRoll, msg->ratePitch, msg->rateYaw); #define ft2m(_x) ((_x) * 0.3408) /* feet to metres */ #define dps2mrps(_x) ((_x) * 17.453293) /* degrees per second to milliradians per second */ #define fpss2mg(_x) ((_x) * 1000/ 32.2) /* feet per second per second to milligees */ #define rad2deg(_x) fmod((((_x) * 57.29578) + 360), 360) /* radians to degrees */ mavlink_msg_gps_raw_send(0, usec, 3, /* 3D fix */ msg->latitude, msg->longitude, ft2m(msg->altitude), 0, /* no uncertainty */ 0, /* no uncertainty */ ft2m(sqrt((msg->velocityN * msg->velocityN) + (msg->velocityE * msg->velocityE))), rad2deg(atan2(msg->velocityE, msg->velocityN))); mavlink_msg_raw_imu_send(0, usec, fpss2mg(msg->accelX), fpss2mg(msg->accelY), fpss2mg(msg->accelZ), dps2mrps(msg->rateRoll), dps2mrps(msg->ratePitch), dps2mrps(msg->rateYaw), 0, /* xmag */ 0, /* ymag */ 0); /* zmag */ pthread_mutex_unlock(&portMutex); } void * ctrlThread(void *arg) { fd_set readset, errorset; mavlink_message_t msg; mavlink_status_t status; int result; /* do not take signals on this thread */ pthread_sigmask(SIG_BLOCK, NULL, NULL); log(CTRL, "Listening for MAVLink packets and sending to %d", CTRL_SEND_PORT); /* loop handling data arriving on the serial port */ while (!gShouldQuit) { /* wait for data or an exceptional condition */ FD_ZERO(&readset); FD_ZERO(&errorset); FD_SET(port, &readset); FD_SET(port, &errorset); if (select(port + 1, &readset, NULL, &errorset, NULL) < 0) { if (errno != EINTR) log(CTRL, "select error: %s", strerror(errno)); goto abort; } /* exception? */ if (FD_ISSET(port, &errorset)) { log(CTRL, "serial port error"); goto abort; } /* no data available for reading? */ if (!FD_ISSET(port, &readset)) continue; /* read data in chunks and process */ for (;;) { uint8_t buf[128]; /* 11ms @ 115200 */ ssize_t cnt; uint8_t *ch; /* port is nonblocking, cnt may be as small as 1 byte */ cnt = read(port, buf, sizeof(buf)); if (cnt < 0) { /* have we run out of data? */ if (errno == EWOULDBLOCK) break; /* some other error */ log(CTRL, "serial port error"); goto abort; } /* port closed for some reason */ if (cnt == 0) { log(CTRL, "serial port closed"); goto abort; } /* process characters, possibly handle messages */ for (ch = buf; cnt; ch++, cnt--) if (mavlink_parse_char(0, *ch, &msg, &status)) ctrlHandleMessage(&msg); } } abort: shouldQuit(0); return(NULL); } void ctrlHandleMessage(mavlink_message_t *msg) { struct fgControlData cdata; time_t now; static time_t lastRCMessage; uint8_t buf[1024]; int len; time(&now); switch(msg->msgid) { case MAVLINK_MSG_ID_HEARTBEAT: /* * If we have received a heartbeat, we are connected * to something. If we haven't seen an RC_CHANNELS_SCALED * message in the last couple of seconds, ask for it to * be added to the stream. */ if ((now - lastRCMessage) > 2) { debug(CTRL, 1, "got heartbeat, requesting RC channel datastream"); pthread_mutex_lock(&portMutex); mavlink_msg_request_data_stream_send(0, TARGET_SYSTEM, TARGET_COMPONENT, MAV_DATA_STREAM_RAW_CONTROLLER, 10, /* 10Hz enough? */ 1); /* start */ pthread_mutex_unlock(&portMutex); /* suppress re-sending for 2 seconds */ lastRCMessage = now; } break; case MAVLINK_MSG_ID_RC_CHANNELS_SCALED: /* build the control message for FG */ /* XXX ArduPilotMega channel ordering */ cdata.aileron = (double)mavlink_msg_rc_channels_scaled_get_chan1_scaled(msg) / 10000; cdata.elevator = (double)mavlink_msg_rc_channels_scaled_get_chan2_scaled(msg) / 10000; cdata.throttle = (double)mavlink_msg_rc_channels_scaled_get_chan3_scaled(msg) / 10000; cdata.rudder = (double)mavlink_msg_rc_channels_scaled_get_chan4_scaled(msg) / 10000; debug(CTRL, 2, "%+6.4f %+6.4f %+6.4f %+6.4f", cdata.aileron, cdata.elevator, cdata.throttle, cdata.rudder); /* swap and send it to FG */ swap64(&cdata.aileron); swap64(&cdata.elevator); swap64(&cdata.throttle); swap64(&cdata.rudder); sendto(fgSock, &cdata, sizeof(cdata), 0, (struct sockaddr *)&fgAddr, sizeof(fgAddr)); /* update our timestamp so that we don't re-request the stream */ lastRCMessage = now; } /* pass the message on to QGCS */ len = mavlink_msg_to_send_buffer(buf, msg); sendto(qgcsSock, buf, len, 0, (struct sockaddr *)&qgcsAddr, sizeof(qgcsAddr)); } /* * QGS thread listens for datagrams from QGroundControl and forwards them out * the serial port. */ void * qgsThread(void *arg) { struct fgIMUData msg; ssize_t received; uint8_t buf[1024]; int sent, result; /* do not take signals on this thread */ pthread_sigmask(SIG_BLOCK, NULL, NULL); log(QGCS, "Listening for QGroundControl data on port %d", QGCS_LISTEN_PORT); while (!gShouldQuit) { /* get a message */ received = recvfrom(qgcsSock, buf, sizeof(buf), MSG_WAITALL, NULL, NULL); /* XXX might want to intercept messages here that would turn off data that we need */ /* and forward it */ pthread_mutex_lock(&portMutex); sent = 0; while (sent < received) { if (gShouldQuit) break; result = write(port, buf + sent, received - sent); if (result < 0) { if (result == EAGAIN) continue; warn("serial passthrough write failed"); goto abort; } if (result == 0) { warnx("serial port closed"); goto abort; } sent += result; } pthread_mutex_unlock(&portMutex); } abort: shouldQuit(0); return(NULL); } void comm_send_ch(mavlink_channel_t chan, uint8_t ch) { if (write(port, &ch, 1) != 1) warn("serial packet write failed"); } void swap32(void *p) { *(int32_t *)p = htonl(*(int32_t *)p); } union temp64 { int64_t ll; int32_t l[2]; }; void swap64(void *p) { union temp64 *f, t; f = (union temp64 *)p; t.l[0] = htonl(f->l[1]); t.l[1] = htonl(f->l[0]); f->ll = t.ll; } void hexdump(void *p, size_t s) { int i, j, lim; uint8_t *c = (uint8_t *)p; for (i = 0; i < s; i += 32) { printf("%04x:", i); lim = s - i; if (lim > 32) lim = 32; for (j = 0; j < lim; j++) printf(" %02x", *c++); printf("\n"); } }