/* * BlueGPS -- a tool to control the RBT 3000 GPS Datalogger * Copyright (C) 2006 Till Harbaum and Simon Budig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include #include #include #ifdef __FreeBSD__ #include #include #include #else #include #endif #ifdef __FreeBSD__ #include #else #include #include #endif #include "rbt3000.h" #define RFCOMM_CHANNEL 1 #define NAUTIC_MILE (1.85185) #define KPH2KNOTS(a) ((a)/NAUTIC_MILE) typedef enum { STATE_AA, STATE_55, STATE_LEN_LO, STATE_LEN_HI, STATE_PAYLOAD, STATE_SUM, STATE_33, STATE_CC } RbtParserState; typedef enum { SET_LOG_ENABLE = 1 << 0, SET_LOG_OVERWRITE = 1 << 1, SET_LOG_HEIGHT = 1 << 2, SET_CONST_TIME = 1 << 3, SET_CONST_DIST = 1 << 4, SET_MAX_SPEED = 1 << 5, SET_NAME = 1 << 6, SET_PASSWORD = 1 << 7, ERASE_DATALOG = 1 << 8, } BlueGPSCommand; typedef enum { MSG_QUIET, MSG_NORMAL, MSG_VERBOSE, } BlueMsgLevel; typedef struct { bdaddr_t device; int device_fd; int req_quit; int need_password; int expected_entries; BlueMsgLevel messages; char password[4]; char *filename; FILE *outfile; int have_old_config; datalog_config_t old_config; int commands; char new_name[15]; datalog_config_t new_config; char new_password[4]; } BlueGPSContext; BlueGPSContext *context = NULL; void rbt3000_handle_reply (int device_fd, unsigned char *buffer, int len); void print_message (BlueMsgLevel level, char *format, ...) __attribute__((__format__ (__printf__, 2, 3))); int full_write (int fd, void *buf, int count) { int pos = 0, ret = 0; while (ret >= 0 && pos < count) { ret = write (fd, buf + pos, count - pos); if (ret >= 0) pos += ret; } if (ret < 0) return ret; else return count; } void sigproc (int sig) { signal (SIGINT, sigproc); if (context->req_quit) exit (1); context->req_quit = 1; } void print_message (BlueMsgLevel level, char *format, ...) { va_list ap; if (level <= context->messages) { va_start (ap, format); vfprintf (stderr, format, ap); va_end (ap); } } void print_datalog_config (BlueMsgLevel level, datalog_config_t *datalog) { print_message (level, "Datalog: %s\n" " - when full: %s\n" " - height info: %s\n", datalog->enabled ? "on" : "off", datalog->overwrite ? "overwrite" : "stop", datalog->include_altitude ? "on" : "off"); print_message (level, " - constant time: "); if (datalog->interval) print_message (level, "%lds\n", datalog->interval); else print_message (level, "off\n"); print_message (level, " - constant dist: "); if (datalog->constant_distance) print_message (level, "%ldm\n", datalog->constant_distance); else print_message (level, "off\n"); print_message (level, " - maximum speed: "); if (datalog->speeding_limit > 0.0001) print_message (level, "%.2f km/h\n", datalog->speeding_limit); else print_message (level, "off\n"); } unsigned char nmea_checksum (unsigned char *str) { unsigned char check = 0; while (*str) { if (*str != '$') check ^= *str; str ++; } return check; } void fprint_datalog_entry_nmea (FILE *stream, datalog_entry_t *entry) { unsigned char nmea[128]; int latdegrees, londegrees; float latminutes, lonminutes; char latdir, londir; nmea[127] = 0; latdegrees = floor (fabs (entry->latitude)); latminutes = 60 * fmod (fabs (entry->latitude), 1.0); latdir = entry->latitude < 0 ? 'S' : 'N'; londegrees = floor (fabs (entry->longitude)); lonminutes = 60 * fmod (fabs (entry->longitude), 1.0); londir = entry->longitude < 0 ? 'W' : 'E'; /* GPRMC message */ snprintf ((char *) nmea, 127, "$GPRMC,%02u%02u%06.3f,A,%02d%07.4f,%c,%03d%07.4f,%c," "%.3f,%.3f,%02u%02u%02u,,", entry->hour, entry->minute, entry->second, latdegrees, latminutes, latdir, londegrees, lonminutes, londir, KPH2KNOTS (entry->speed), entry->direction, entry->day, entry->month, entry->year); fprintf (stream, "%s*%02X\r\n", nmea, nmea_checksum (nmea)); /* GPGGA message */ snprintf ((char *) nmea, 127, "$GPGGA,%02u%02u%06.3f,%02d%07.4f,%c,%03d%07.4f,%c," "1,%02d,%.1f,%.2f,M,,M,,", entry->hour, entry->minute, entry->second, latdegrees, latminutes, latdir, londegrees, lonminutes, londir, entry->satellites, entry->HDOP, entry->altitude); fprintf (stream, "%s*%02X\r\n", nmea, nmea_checksum (nmea)); /* GPGLL message */ snprintf ((char *) nmea, 127, "$GPGLL,%02d%07.4f,%c,%03d%07.4f,%c," "%02u%02u%06.3f,A", latdegrees, latminutes, latdir, londegrees, lonminutes, londir, entry->hour, entry->minute, entry->second); fprintf (stream, "%s*%02X\r\n", nmea, nmea_checksum (nmea)); /* GPVTG message */ snprintf ((char *) nmea, 127, "$GPVTG,%.3f,T,,M,%.3f,N,%.3f,K", entry->direction, KPH2KNOTS(entry->speed), entry->speed); fprintf (stream, "%s*%02X\r\n", nmea, nmea_checksum (nmea)); /* GPGSA message */ snprintf ((char *) nmea, 127, "$GPGSA,A,,,,,,,,,,,,,,%.2f,%.2f,,", entry->PDOP, entry->HDOP); fprintf (stream, "%s*%02X\r\n", nmea, nmea_checksum (nmea)); } void print_progress_bar (BlueMsgLevel level, int sent, int total) { #define BAR_WIDTH 50 int i; print_message (level, "\rDownloading: ["); for (i = 0; i < BAR_WIDTH; i++) print_message (level, "%c", i < (BAR_WIDTH * sent / total) ? '#' : '-'); print_message (level, "] %d/%d \r", sent, total); fflush (stderr); #undef BAR_WIDTH } int rbt3000_connect (char *device_addr) { #ifdef __FreeBSD__ struct sockaddr_rfcomm rem_addr; struct hostent * he; memset(&rem_addr, 0, sizeof(rem_addr)); rem_addr.rfcomm_len = sizeof(rem_addr); rem_addr.rfcomm_family = AF_BLUETOOTH; rem_addr.rfcomm_channel = RFCOMM_CHANNEL; if ((he = bt_gethostbyname(device_addr))) { context->device = *(bdaddr_t *) he->h_addr_list[0]; if (context->messages == MSG_VERBOSE) printf("Actual BT address for '%s': %s\n", device_addr, bt_ntoa(&(context->device),NULL)); } else if (!bt_aton(device_addr, &context->device)) { perror("No such device address"); return 0; } rem_addr.rfcomm_bdaddr = context->device; #else struct sockaddr_rc rem_addr; rem_addr.rc_family = AF_BLUETOOTH; rem_addr.rc_channel = RFCOMM_CHANNEL; baswap (&context->device, strtoba (device_addr)); rem_addr.rc_bdaddr = context->device; #endif /* bluez connects to BlueClient */ if ((context->device_fd = socket (PF_BLUETOOTH, SOCK_STREAM, #ifdef __FreeBSD__ BLUETOOTH_PROTO_RFCOMM #else BTPROTO_RFCOMM #endif )) < 0 ) { perror ("Can't create socket"); return 0; } /* connect on rfcomm */ if (connect (context->device_fd, (struct sockaddr *) &rem_addr, sizeof (rem_addr)) < 0 ) { perror ("Can't connect"); close (context->device_fd); return 0; } return 1; } void rbt3000_init (int fd) { /* sending $RCMD402*2E */ uint8_t cmd1[] = { 0xa0, 0xa2, 0x00, 0x31, 0xa5, 0x00, 0x04, 0x04, 0x00, 0x00, 0xe1, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0xff, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb2, 0xb0, 0xb3 }; print_message (MSG_NORMAL, "Initializing RBT-3000...\n"); full_write (fd, "\r\n", 2); full_write (fd, "$RCMD402*2E\r\n", 13); full_write (fd, cmd1, sizeof (cmd1)); } void rbt3000_parse_byte (int device_fd, unsigned char byte) { static RbtParserState state = STATE_AA; static int len = 0; static int count = 0; static int xsum = 0; static unsigned char *buffer = NULL; switch (state) { case STATE_AA: if (byte == 0xAA) state = STATE_55; break; case STATE_55: if (byte == 0x55) state = STATE_LEN_LO; else state = (byte != 0xAA) ? STATE_AA : STATE_55; break; case STATE_LEN_LO: len = byte; state = STATE_LEN_HI; break; case STATE_LEN_HI: len += 256 * byte; if (buffer) free (buffer); buffer = malloc (len); xsum = count = 0; state = STATE_PAYLOAD; break; case STATE_PAYLOAD: if (count < len) { buffer[count] = byte; xsum ^= byte; count ++; } if (count == len) { state = STATE_SUM; } break; case STATE_SUM: if (xsum == byte) { state = STATE_33; } else { print_message (MSG_QUIET, "Checksum error\n"); state = (byte != 0xAA) ? STATE_AA : STATE_55; } break; case STATE_33: if (byte == 0x33) { state = STATE_CC; } else { print_message (MSG_QUIET, "Postamble 0x33 error\n"); state = (byte != 0xAA) ? STATE_AA : STATE_55; } break; case STATE_CC: if (byte == 0xCC) { rbt3000_handle_reply (device_fd, buffer, len); state = STATE_AA; } else { print_message (MSG_QUIET, "Postamble 0xCC error\n"); state = (byte != 0xAA) ? STATE_AA : STATE_55; } break; } } int rbt3000_send_cmd_seq (int fd, char command, char *data, int len) { unsigned char buffer[5] = { 0xAA, 0x55, 0x00, 0x00, 0x00 }; int ret; /* Preamble */ buffer[2] = (len + 1) & 0xff; buffer[3] = (len + 1) >> 8; buffer[4] = command; ret = full_write (fd, buffer, 5); if (ret < 0) return ret; if (len > 0) { ret = full_write (fd, data, len); if (ret < 0) return ret; } /* Postamble */ buffer[0] = command; while (len) { buffer[0] ^= *data; data++; len--; } buffer[1] = 0x33; buffer[2] = 0xCC; ret = full_write (fd, buffer, 3); if (ret < 0) return ret; return 0; } int rbt3000_send_cmd (int fd, char cmd) { return rbt3000_send_cmd_seq (fd, cmd, NULL, 0); } void rbt3000_abort (char *message) { print_message (MSG_QUIET, "%s Aborting.\n", message); context->req_quit = 1; } void rbt3000_handle_reply_action (int device_fd, unsigned char *buffer, int len) { static int entrycount; switch (buffer[0]) { case CMD_ECHO: break; case CMD_STATUS: if (len - 1 == sizeof (status_t)) { status_t *status = (status_t *) (buffer + 1); print_message (MSG_NORMAL, "Device name: %s\n", status->name); print_message (MSG_VERBOSE, "Firmware version: %s\n" "Battery: %s\n" "Password: %s\n", status->version, status->batt ? (status->batt == 2 ? "low" : "mid") : "high", status->password ? "enabled" : "disabled"); print_datalog_config (MSG_VERBOSE, &status->datalog); context->old_config = status->datalog; context->have_old_config = 1; context->need_password = status->password; } else { rbt3000_abort ("Wrong status reply size."); } break; case CMD_PASSWORD: if (buffer[1]) context->need_password = 0; break; case CMD_LOGSTATE: if (len - 1 == sizeof (datalog_state_t)) { datalog_state_t *state = (datalog_state_t *) (buffer + 1); print_message (MSG_VERBOSE, "Entries in Datalog: %ld (%.2f%% free)\n", state->entries, 100 * state->space); if (context->filename) { context->expected_entries = state->entries; if (strcmp (context->filename, "-") == 0) context->outfile = stdout; else context->outfile = fopen (context->filename, "w"); if (!context->outfile) { perror ("Unable to open output file"); context->req_quit = 1; } } } else { rbt3000_abort ("Wrong logstate reply size."); } break; case CMD_GETLOG: entrycount = 0; break; case CMD_DATALGENTRY: if (len - 1 == sizeof (datalog_entry_t)) { datalog_entry_t *entry = (datalog_entry_t *) (buffer + 1); if (entrycount == entry->block && entrycount < context->expected_entries) { entrycount++; print_progress_bar (MSG_NORMAL, entrycount, context->expected_entries); fprint_datalog_entry_nmea (context->outfile, entry); } else { rbt3000_abort ("\nWrong sequence number in Logentry."); } } else if (len - 1 == sizeof (datalog_end_entry_t)) { if (context->outfile != stdout) fclose (context->outfile); print_message (MSG_NORMAL, "\n"); if (entrycount != context->expected_entries) rbt3000_abort ("\nUnexpected end of Logentries."); context->expected_entries = 0; } else { rbt3000_abort ("\nWrong datalgentry reply size."); } break; case CMD_SETLOGCONF: if (len - 1 == 24) { datalog_config_t *status = (datalog_config_t *) (buffer + 1); print_message (MSG_NORMAL, "New Datalog configuration:\n"); print_datalog_config (MSG_NORMAL, status); print_message (MSG_NORMAL, "\n"); } else { rbt3000_abort ("Wrong setlogconf reply size."); } break; case CMD_PASSWORD_SET: if (len - 1 == 1) print_message (MSG_NORMAL, "Password set to new value\n"); else rbt3000_abort ("Wrong password_set reply size."); break; case CMD_PASSWORD_ACT: if (len - 1 == 1) print_message (MSG_NORMAL, "Password protection %s.\n", buffer[1] ? "enabled" : "disabled"); else rbt3000_abort ("Wrong password_act reply size."); break; case CMD_SETNAME: if (len - 1 == 15) { buffer[15] = 0x0; print_message (MSG_NORMAL, "Name set to \"%s\"\n\n", buffer + 1); } else { rbt3000_abort ("Wrong setname reply size."); } break; case CMD_LOGERASE: if (buffer[1]) print_message (MSG_NORMAL, "Erasing Log successful\n"); else print_message (MSG_QUIET, "Erasing Log failed\n"); break; default: print_message (MSG_QUIET, "Unexpected reply in conf: %02x (len %d)\n", buffer[0] ,len); } } void rbt3000_handle_reply (int device_fd, unsigned char *buffer, int len) { static time_t alive_timer = 0; static int connected = 0; time_t now; if (context->req_quit) return; else rbt3000_handle_reply_action (device_fd, buffer, len); switch (buffer[0]) { case CMD_ECHO: if (!connected) { connected = 1; rbt3000_send_cmd (device_fd, CMD_STATUS); } break; case CMD_STATUS: if (context->need_password) { if (context->password[0]) rbt3000_send_cmd_seq (device_fd, CMD_PASSWORD, context->password, 4); else rbt3000_abort ("Password required but not specified."); } else { rbt3000_send_cmd (device_fd, CMD_LOGSTATE); } break; case CMD_PASSWORD: if (!context->need_password) rbt3000_send_cmd (device_fd, CMD_LOGSTATE); else rbt3000_abort ("Wrong password given."); break; case CMD_PASSWORD_SET: rbt3000_send_cmd_seq (device_fd, CMD_PASSWORD_ACT, "\001", 1); break; case CMD_LOGSTATE: if (context->expected_entries) { rbt3000_send_cmd_seq (device_fd, CMD_GETLOG, "\x00\x00\x00\x00\x01\x00\x00\x00" "\x00\x00\x00\x00\xDC\x05\x00\x00", 16); return; } /* intentionally no break here */ case CMD_GETLOG: case CMD_DATALGENTRY: if (context->expected_entries) { if (!alive_timer) alive_timer = time (NULL); now = time (NULL); if (now - alive_timer > 2) { rbt3000_send_cmd (device_fd, CMD_ALIVE); alive_timer = now; } return; } if (context->have_old_config && context->commands & (SET_LOG_ENABLE | SET_LOG_OVERWRITE | SET_LOG_HEIGHT | SET_CONST_TIME | SET_CONST_DIST | SET_MAX_SPEED)) { if (context->commands & SET_LOG_ENABLE) context->old_config.enabled = context->new_config.enabled; if (context->commands & SET_LOG_OVERWRITE) context->old_config.overwrite = context->new_config.overwrite; if (context->commands & SET_LOG_HEIGHT) context->old_config.include_altitude = context->new_config.include_altitude; if (context->commands & SET_CONST_TIME) context->old_config.interval = context->new_config.interval; if (context->commands & SET_CONST_DIST) context->old_config.constant_distance = context->new_config.constant_distance; if (context->commands & SET_MAX_SPEED) context->old_config.speeding_limit = context->new_config.speeding_limit; rbt3000_send_cmd_seq (device_fd, CMD_SETLOGCONF, (char *) &context->old_config, sizeof (context->old_config)); return; } /* intentionally no break here */ case CMD_SETLOGCONF: if (context->commands & SET_NAME) { rbt3000_send_cmd_seq (device_fd, CMD_SETNAME, context->new_name, 15); return; } /* intentionally no break here */ case CMD_SETNAME: if (context->commands & SET_PASSWORD) { if (context->new_password[0]) rbt3000_send_cmd_seq (device_fd, CMD_PASSWORD_SET, context->new_password, 4); else rbt3000_send_cmd_seq (device_fd, CMD_PASSWORD_ACT, "\0", 1); return; } /* intentionally no break here */ case CMD_PASSWORD_ACT: if (context->commands & ERASE_DATALOG) { rbt3000_send_cmd (device_fd, CMD_LOGERASE); return; } /* intentionally no break here */ case CMD_LOGERASE: context->req_quit = 1; break; default: print_message (MSG_QUIET, "Unknown reply: 0x%02x (len %d)\n", buffer[0] ,len); } } void mainloop (int device_fd) { fd_set rfds, wfds; struct timeval tv = { 1, 0 }; unsigned char buffer[1024]; int i, ret; context->req_quit = 0; while (!context->req_quit) { FD_ZERO (&rfds); FD_ZERO (&wfds); FD_SET (device_fd, &rfds); tv.tv_sec = 1; tv.tv_usec = 0; ret = select (device_fd + 1, &rfds, &wfds, NULL, &tv); if (ret < 0) { perror ("select"); break; } if (ret == 0) rbt3000_send_cmd (device_fd, CMD_ECHO); if (FD_ISSET (device_fd, &rfds)) { ret = read (device_fd, &buffer, 1024); if (ret < 0) perror ("read"); for (i = 0; i < ret; i++) rbt3000_parse_byte (device_fd, buffer[i]); } } } void usage (char *message) { print_message (MSG_QUIET, "BlueGPS V"VERSION"\n" " (c) 2006 by Till Harbaum \n" " and Simon Budig \n" "\n" "Usage: bluegps [options] bdaddr\n" "\n" "Options:\n" " -d download nmea datalog to file\n" " -d - download nmea datalog to stdout\n" " -e erase datalog\n" " -p provide password (4 digits)\n" "\n" " -h show this help\n" " -q operate quietly\n" " -v operate verbously\n" "\n" "Device configuration:\n" " -n set name of RBT-3000\n" " -w set and enable new password (4 digits)\n" " -W disable password\n" "\n" "Logging configuration:\n" " -l enable logging\n" " -L disable logging\n" " -i log point every seconds\n" " -I no time-based logging\n" " -c log point every meters\n" " -C no distance-based logging\n" " -s log points when faster than km/h\n" " -S disable speed based logging\n" " -a include altitude information in log\n" " -A exclude altitude information in log\n" " -o overwrite when memory is full\n" " -O stop logging when memory is full\n" "\n"); if (message) print_message (MSG_QUIET, "%s\n", message); exit (1); } int main (int argc, char *argv[]) { int i; context = calloc (sizeof (BlueGPSContext), 1); context->messages = MSG_NORMAL; while ((i = getopt (argc, argv, "aAc:Cd:ehi:IlLn:oOp:qs:Svw:W")) != -1) { switch (i) { case 'a': context->new_config.include_altitude = 1; context->commands |= SET_LOG_HEIGHT; break; case 'A': context->new_config.include_altitude = 0; context->commands |= SET_LOG_HEIGHT; break; case 'c': context->new_config.constant_distance = atol (optarg); context->commands |= SET_CONST_DIST; break; case 'C': context->new_config.constant_distance = 0; context->commands |= SET_CONST_DIST; break; case 'd': context->filename = strdup (optarg); break; case 'e': context->commands |= ERASE_DATALOG; break; case 'h': usage (NULL); break; case 'i': context->new_config.interval = atol (optarg); context->commands |= SET_CONST_TIME; break; case 'I': context->new_config.interval = 0; context->commands |= SET_CONST_TIME; break; case 'l': context->new_config.enabled = 1; context->commands |= SET_LOG_ENABLE; break; case 'L': context->new_config.enabled = 0; context->commands |= SET_LOG_ENABLE; break; case 'n': strncpy (context->new_name, optarg, 14); context->new_name[14] = 0; context->commands |= SET_NAME; break; case 'o': context->new_config.overwrite = 1; context->commands |= SET_LOG_OVERWRITE; break; case 'O': context->new_config.overwrite = 0; context->commands |= SET_LOG_OVERWRITE; break; case 'p': if (strlen (optarg) == 4 && isdigit (optarg[0]) && isdigit (optarg[1]) && isdigit (optarg[2]) && isdigit (optarg[3])) { strncpy (context->password, optarg, 4); } else { usage ("Password must consist exactly 4 digits."); } break; case 'q': context->messages = MSG_QUIET; break; case 's': context->new_config.speeding_limit = atof (optarg); context->commands |= SET_MAX_SPEED; break; case 'S': context->new_config.speeding_limit = 0; context->commands |= SET_MAX_SPEED; break; case 'v': context->messages = MSG_VERBOSE; break; case 'w': if (strlen (optarg) == 4 && isdigit (optarg[0]) && isdigit (optarg[1]) && isdigit (optarg[2]) && isdigit (optarg[3])) { strncpy (context->new_password, optarg, 4); context->commands |= SET_PASSWORD; } else { usage ("Password must consist exactly 4 digits."); } break; case 'W': context->new_password[0] = 0; context->commands |= SET_PASSWORD; break; } } if (context->filename == NULL && context->commands == 0 && context->messages == MSG_NORMAL) context->messages = MSG_VERBOSE; if (optind == argc) { usage ("Specify Device-Address"); } if (optind > argc + 1) { usage ("too many Arguments given"); } if (!rbt3000_connect (argv[optind])) exit (1); signal (SIGINT, sigproc); rbt3000_init (context->device_fd); mainloop (context->device_fd); if (context->device_fd) close (context->device_fd); free (context); return 0; }