gps – c

Programming, Uncategorized

gps – c

Update: Missing memset(&new_config, 0, sizeof(new_config) in set_input_mode function would sometimes cause garbled incoming data from gps. Seems to have fixed the issue.

The software implementation we discussed last time (written in awk) is fairly hamstrung. Running topreveals that the cpu usage idles at ~20% on my machine (my machine is no slouch either…). Aside from the drain on system resources, it’s also not easy to take the software further. We need a more powerful language

This post will begin a gps software implementation in the c programming language. Below is a list of features to implement. This post will address the first

  • Communicate with serial gps devices
  • Start a simple UDP server
  • Write a simple UDP client, to demonstrate how to fetch data
  • Daemonize the program

Once again, this is not the complete, tested, functional and maintained gps solution you’re looking for. Why not check out gpsd

Lets get started by setting up the project directory and makefile

$ mkdir ~/Documents/whereami
$ cd ~/Documents/whereami
$ mkdir include lib obj src
$ touch makefile readme.md
$ vim makefile
#name of the final executable
EXE = whereami 

#directory for source and files
SRC_DIR = src
OBJ_DIR = obj

SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(SRC:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

#preprocessor, cflags, linker flags, and linked libs
CPPFLAGS += -Iinclude
CFLAGS += -Wall
LDFLAGS += -Llib
LDLIBS += -lm -lpthread
#we'll be using pthreads

.PHONY: all clean

all: $(EXE)

$(EXE): $(OBJ)
	$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

clean:
	$(RM) $(OBJ)

The project layout and makefile are fairly generic. We won’t be using most of it right away, but it’s nice to have a solid structure before setting out.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//for serial connection
#include <termios.h>
#include <fcntl.h>

#define BAUDRATE B9600
#define DEVICE_NAME "/dev/ttyUSB0"
#define DEVICE_LINE_BUFLEN 100	//size of buffer when reading lines from device

static int fd; //gps device file descriptor
static struct termios old_config; //old terminal config, to reset later

/*
 * Called at process exit to reset the terminal state to how we found it.
 */
void reset_input_mode()
{
...
}

/*
 * Set the terminal mode so that we can read the gps device.
 */
int set_input_mode()
{
...
}

/*
 * Fill line[] buffer with incoming line from device
 * return number of characters filled into buffer.
 */
int serial_getline(char line[]){
...
}

int main(void)
{
	fd = open(DEVICE_NAME, O_RDONLY | O_NOCTTY); //open the gps device read only, don't control the tty
	...
	set_input_mode(); //set the terminal input mode

	char buf[DEVICE_LINE_BUFLEN];
	while(1){
		serial_getline(buf);
		printf("%s\n", buf);
	}
	return EXIT_SUCCESS; //never reached
}

We include <termios.h> and <fcntl.h> so that we can control the serial device settings. We define the BAUDRATE and DEVICE_NAME constants. These may need to change for your specific device. (Note, I’m using a different device location than previous posts, I invested in a USB-TTL converter, as pictured above). The main() function is straightforward. We open the DEVICE_NAME for reading. Then set the terminal input mode, before looping forever printing line-by-line data from the gps device.

/*
 * Set the terminal mode so that we can read the gps device.
 */
int set_input_mode()
{
	struct termios new_config;
	memset(&new_config, 0, sizeof(new_config);

	//get the current terminal settings
	if(tcgetattr(fd, &old_config) == -1){
		perror("tcgetattr termios function error\n");
		exit(EXIT_FAILURE);
	}
	
	atexit(reset_input_mode); //reset terminal settings once we're done
	
	new_config.c_cflag |= CLOCAL | CREAD;
	new_config.c_cflag &= ~CSIZE;
	new_config.c_cflag |= CS8; //set 8-bit char
	new_config.c_cflag &= ~PARENB; //unset parity bit
	new_config.c_cflag &= ~CSTOPB; //unset double stop bit
	new_config.c_cflag &= ~CRTSCTS; //unset hardware control

	new_config.c_lflag |= ICANON | ISIG; //set canonical input
	new_config.c_lflag &= ~(ECHO | ECHOE | ECHONL | IEXTEN); //unset echo

	new_config.c_iflag &= ~INPCK;
	new_config.c_iflag |= IGNCR; //set ignore carriage return
	new_config.c_iflag &= ~(INLCR | ICRNL | IUCLC | IMAXBEL);
	new_config.c_iflag &= ~(IXON | IXOFF | IXANY); //unset control char's

	new_config.c_oflag &= ~OPOST; //unset post processing

	cfsetospeed(&new_config, BAUDRATE); //set outgoing baudrate
	cfsetispeed(&new_config, BAUDRATE); //set incoming baudrate

	tcflush(fd, TCIOFLUSH); //flush

	//set terminal settings with above.
	if(tcsetattr(fd, TCSAFLUSH, &new_config) < 0){	//TCSAFLUSH wait for incoming data to
		perror("tcsetattr function error.\n");	//finish before writing changes
		exit(EXIT_FAILURE);			//TCSANOW sometimes causes incomplete
	}						//lines to be written, avoid here.
	
	return 0;
}

set_input_mode()sets up DEVICE_NAME as a serial device for reading.
Some important bits: new_config.c_lflag |= ICANON | ISIG; sets the device into canonical mode, the incoming data is stored in a buffer and fed to us line at a time.
cfsetospeed(&new_config, BAUDRATE);sets the outgoing data speed to BAUDRATE (defined at the tope of the file). Its counterpart, cfsetispeedsets the incoming speed to the same.
atexit(reset_input_mode); is used to call the reset_input_mode() function when the program terminates. This ensures the terminal settings are left as we found them.
Check out the man pages for a full rundown on the bits set: man tcsetattr

/*
 * Called at process exit to reset the terminal state to how we found it.
 */
void reset_input_mode()
{
	tcsetattr(fd, TCSANOW, &old_config);
	close(fd);
}

Nothing complicated in reset_input_mode(). We reset the terminal settings we messed with earlier, then close the gps device file descriptor.

/*
 * Fill line[] buffer with incoming line from device
 * return number of characters filled into buffer.
 */
int serial_getline(char line[]){
	if(fd < 0){
		printf("device not opened correctly.\n");
		exit(EXIT_FAILURE);
	}
	//read incoming data from device into line buffer.
	int rrv = read(fd, line, DEVICE_LINE_BUFLEN - 1);
	if(rrv < 0){
		perror("read error\n");
		exit(EXIT_FAILURE);
	}
	line[rrv - 1] = '\0'; //intentionally overwrite the newline with null byte
	return(rrv - 2);
}

serial_getline(char line[]) is used to read a full line from the gps. I made a design choice to strip the newline here, as it is sometimes ugly to print. This will turn out to be an annoyance later…

The final code is below. Running the code doesn’t do anything new, recall we achieved the exact same thing earlier using stty and cat. Next blog we’ll extend the code to include a (basic) UDP server which clients can query for the latest GPGGA data.

$ make
cc -Iinclude -Wall -c src/whereami.c -o obj/whereami.o
cc -Llib obj/whereami.o -lm -lpthread -o whereami
$ ./whereami
$GPRMC,041817.00,V,,,,,,,031118,,,N*7C
$GPVTG,,,,,,,,,N*30
$GPGGA,041817.00,,,,,0,00,99.99,,,,,,*6D
$GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30
$GPGSV,3,1,11,02,29,036,22,06,30,090,,12,52,228,,13,16,006,13*76
$GPGSV,3,2,11,15,34,329,22,17,13,140,,19,41,136,,24,79,194,*7F
$GPGSV,3,3,11,25,23,258,23,29,07,323,21,32,06,217,22*4F
$GPGLL,,,,,041817.00,V,N*41
$GPRMC,041818.00,V,,,,,,,031118,,,N*73
...
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

//for serial connection
#include <termios.h>
#include <fcntl.h>

#define BAUDRATE B9600
#define DEVICE_NAME "/dev/ttyUSB0"
#define DEVICE_LINE_BUFLEN 100	//size of buffer when reading lines from device

static int fd; //gps device file descriptor
static struct termios old_config; //old terminal config, to reset later

/*
 * Called at process exit to reset the terminal state to how we found it.
 */
void reset_input_mode()
{
	tcsetattr(fd, TCSANOW, &old_config);
	close(fd);
}

/*
 * Set the terminal mode so that we can read the gps device.
 */
int set_input_mode()
{
	struct termios new_config;
	memset(&new_config, 0, sizeof(new_config);
	
	//get the current terminal settings
	if(tcgetattr(fd, &old_config) == -1){
		perror("tcgetattr termios function error\n");
		exit(EXIT_FAILURE);
	}
	
	atexit(reset_input_mode); //reset terminal settings once we're done
	
	new_config.c_cflag |= CLOCAL | CREAD;
	new_config.c_cflag &= ~CSIZE;
	new_config.c_cflag |= CS8; //set 8-bit char
	new_config.c_cflag &= ~PARENB; //unset parity bit
	new_config.c_cflag &= ~CSTOPB; //unset double stop bit
	new_config.c_cflag &= ~CRTSCTS; //unset hardware control

	new_config.c_lflag |= ICANON | ISIG; //set canonical input
	new_config.c_lflag &= ~(ECHO | ECHOE | ECHONL | IEXTEN); //unset echo

	new_config.c_iflag &= ~INPCK;
	new_config.c_iflag |= IGNCR; //set ignore carriage return
	new_config.c_iflag &= ~(INLCR | ICRNL | IUCLC | IMAXBEL);
	new_config.c_iflag &= ~(IXON | IXOFF | IXANY); //unset control char's

	new_config.c_oflag &= ~OPOST; //unset post processing

	cfsetospeed(&new_config, BAUDRATE); //set outgoing baudrate
	cfsetispeed(&new_config, BAUDRATE); //set incoming baudrate

	tcflush(fd, TCIOFLUSH); //flush

	//set terminal settings with above.
	if(tcsetattr(fd, TCSAFLUSH, &new_config) < 0){	//TCSAFLUSH wait for incoming data to
		perror("tcsetattr function error.\n");	//finish before writing changes
		exit(EXIT_FAILURE);			//TCSANOW sometimes causes incomplete
	}						//lines to be written, avoid here.
	
	return 0;
}

/*
 * Fill line[] buffer with incoming line from device
 * return number of characters filled into buffer.
 */
int serial_getline(char line[]){
	if(fd < 0){
		printf("device not opened correctly.\n");
		exit(EXIT_FAILURE);
	}
	//read incoming data from device into line buffer.
	int rrv = read(fd, line, DEVICE_LINE_BUFLEN - 1);
	if(rrv < 0){
		perror("read error\n");
		exit(EXIT_FAILURE);
	}
	line[rrv - 1] = '\0'; //intentionally overwrite the newline with null byte
	return(rrv - 2);
}

int main(void)
{
	fd = open(DEVICE_NAME, O_RDONLY | O_NOCTTY); //open the gps device
	if(fd < 0){
		perror("open device error\n");
		exit(EXIT_FAILURE);
	}
	
	set_input_mode(); //set the terminal input mode

	char buf[DEVICE_LINE_BUFLEN];
	while(1){
		serial_getline(buf);
		printf("%s\n", buf);
	}
	return EXIT_SUCCESS; //never reached
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.