Sometimes you want your Linux (in this case, raspbian-based) device to be able to receive live GPS positioning, time updates, etc. This post will be a quick one to show you how to get gpsd working on your system, and pinning your GPS device to a symlinked udev device.
Setup
My development setup for this post consists of a Raspberry Pi zero connected to a USB hub + ethernet device, and is being powered by a power over ethernet (PoE) splitter. You can also see a camera connected to the Pi, but I won’t be using this in this post. The GPS device I’m using is a BU-353 S4 USB GPS receiver, but the general flow should work for most USB GPS receivers.
Determining Your GPS Device
In this post we’ll be setting up a custom /dev/gps device. This can be particularly useful if you’re connecting multiple “USB-to-serial” devices on your single board computer. (GPS data is typically provided over a serial device, and most USB GPS receivers have the conversion hardware inside of them already.)
We’ll use a number of attributes from our specific GPS device to determine which one it is when it is plugged in. To get all of the useful information I find it easiest to watch the output of udevadm monitor
as you plug in the device:
- Make sure your USB GPS is unplugged
- Run the command: udevadm monitor
- Plug in your device
- Ctrl+c ‘udevadm monitor’ to get your terminal back.
Your output should look something like this:
KERNEL[1654.154728] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2 (usb)
KERNEL[1654.180457] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0 (usb)
KERNEL[1654.188490] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0 (usb-serial)
KERNEL[1654.192137] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty/ttyUSB0 (tty)
UDEV [1654.272291] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2 (usb)
UDEV [1654.281351] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0 (usb)
UDEV [1654.287403] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0 (usb-serial)
UDEV [1654.322383] add /devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty/ttyUSB0 (tty)
You can see that our device till be ‘ttyUSB0’. We’ll find this at /dev/ttyUSB0. Now we need to find out more information about the device so we can create a new udev rule. To do this we’ll run the following command: (NOTE: ‘/dev/ttyUSB0’ may be different for you; use the destination you got from the last command.)
udevadm info -a -p $(udevadm info -q path -n /dev/ttyUSB0)
And after we do, we get a LOT of information back:
Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.
looking at device '/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0/tty/ttyUSB0':
KERNEL=="ttyUSB0"
SUBSYSTEM=="tty"
DRIVER==""
looking at parent device '/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB0':
KERNELS=="ttyUSB0"
SUBSYSTEMS=="usb-serial"
DRIVERS=="pl2303"
ATTRS{port_number}=="0"
looking at parent device '/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2:1.0':
KERNELS=="1-1.2:1.0"
SUBSYSTEMS=="usb"
DRIVERS=="pl2303"
ATTRS{authorized}=="1"
ATTRS{bAlternateSetting}==" 0"
ATTRS{bInterfaceClass}=="ff"
ATTRS{bInterfaceNumber}=="00"
ATTRS{bInterfaceProtocol}=="00"
ATTRS{bInterfaceSubClass}=="00"
ATTRS{bNumEndpoints}=="03"
ATTRS{supports_autosuspend}=="1"
looking at parent device '/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2':
KERNELS=="1-1.2"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{authorized}=="1"
ATTRS{avoid_reset_quirk}=="0"
ATTRS{bConfigurationValue}=="1"
ATTRS{bDeviceClass}=="00"
ATTRS{bDeviceProtocol}=="00"
ATTRS{bDeviceSubClass}=="00"
ATTRS{bMaxPacketSize0}=="64"
ATTRS{bMaxPower}=="100mA"
ATTRS{bNumConfigurations}=="1"
ATTRS{bNumInterfaces}==" 1"
ATTRS{bcdDevice}=="0400"
ATTRS{bmAttributes}=="80"
ATTRS{busnum}=="1"
ATTRS{configuration}==""
ATTRS{devnum}=="6"
ATTRS{devpath}=="1.2"
ATTRS{devspec}==" (null)"
ATTRS{idProduct}=="2303"
ATTRS{idVendor}=="067b"
ATTRS{ltm_capable}=="no"
ATTRS{manufacturer}=="Prolific Technology Inc. "
ATTRS{maxchild}=="0"
ATTRS{product}=="USB-Serial Controller D"
ATTRS{quirks}=="0x0"
ATTRS{removable}=="unknown"
ATTRS{speed}=="12"
ATTRS{urbnum}=="21"
ATTRS{version}==" 1.10"
looking at parent device '/devices/platform/soc/20980000.usb/usb1/1-1':
KERNELS=="1-1"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{authorized}=="1"
ATTRS{avoid_reset_quirk}=="0"
ATTRS{bConfigurationValue}=="1"
ATTRS{bDeviceClass}=="09"
ATTRS{bDeviceProtocol}=="01"
ATTRS{bDeviceSubClass}=="00"
ATTRS{bMaxPacketSize0}=="64"
ATTRS{bMaxPower}=="100mA"
ATTRS{bNumConfigurations}=="1"
ATTRS{bNumInterfaces}==" 1"
ATTRS{bcdDevice}=="0111"
ATTRS{bmAttributes}=="e0"
ATTRS{busnum}=="1"
ATTRS{configuration}==""
ATTRS{devnum}=="2"
ATTRS{devpath}=="1"
ATTRS{idProduct}=="0101"
ATTRS{idVendor}=="1a40"
ATTRS{ltm_capable}=="no"
ATTRS{maxchild}=="4"
ATTRS{product}=="USB 2.0 Hub"
ATTRS{quirks}=="0x0"
ATTRS{removable}=="unknown"
ATTRS{speed}=="480"
ATTRS{urbnum}=="86"
ATTRS{version}==" 2.00"
looking at parent device '/devices/platform/soc/20980000.usb/usb1':
KERNELS=="usb1"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{authorized}=="1"
ATTRS{authorized_default}=="1"
ATTRS{avoid_reset_quirk}=="0"
ATTRS{bConfigurationValue}=="1"
ATTRS{bDeviceClass}=="09"
ATTRS{bDeviceProtocol}=="01"
ATTRS{bDeviceSubClass}=="00"
ATTRS{bMaxPacketSize0}=="64"
ATTRS{bMaxPower}=="0mA"
ATTRS{bNumConfigurations}=="1"
ATTRS{bNumInterfaces}==" 1"
ATTRS{bcdDevice}=="0414"
ATTRS{bmAttributes}=="e0"
ATTRS{busnum}=="1"
ATTRS{configuration}==""
ATTRS{devnum}=="1"
ATTRS{devpath}=="0"
ATTRS{idProduct}=="0002"
ATTRS{idVendor}=="1d6b"
ATTRS{interface_authorized_default}=="1"
ATTRS{ltm_capable}=="no"
ATTRS{manufacturer}=="Linux 4.14.70+ dwc_otg_hcd"
ATTRS{maxchild}=="1"
ATTRS{product}=="DWC OTG Controller"
ATTRS{quirks}=="0x0"
ATTRS{removable}=="unknown"
ATTRS{serial}=="20980000.usb"
ATTRS{speed}=="480"
ATTRS{urbnum}=="26"
ATTRS{version}==" 2.00"
looking at parent device '/devices/platform/soc/20980000.usb':
KERNELS=="20980000.usb"
SUBSYSTEMS=="platform"
DRIVERS=="dwc_otg"
ATTRS{busconnected}=="Bus Connected = 0x1"
ATTRS{buspower}=="Bus Power = 0x1"
ATTRS{bussuspend}=="Bus Suspend = 0x0"
ATTRS{devspeed}=="Device Speed = 0x0"
ATTRS{driver_override}=="(null)"
ATTRS{enumspeed}=="Device Enumeration Speed = 0x1"
ATTRS{fr_interval}=="Frame Interval = 0x1d4b"
ATTRS{ggpio}=="GGPIO = 0x00000000"
ATTRS{gnptxfsiz}=="GNPTXFSIZ = 0x01000306"
ATTRS{gotgctl}=="GOTGCTL = 0x001c0001"
ATTRS{gpvndctl}=="GPVNDCTL = 0x00000000"
ATTRS{grxfsiz}=="GRXFSIZ = 0x00000306"
ATTRS{gsnpsid}=="GSNPSID = 0x4f54280a"
ATTRS{guid}=="GUID = 0x2708a000"
ATTRS{gusbcfg}=="GUSBCFG = 0x20001700"
ATTRS{hcd_frrem}=="HCD Dump Frame Remaining"
ATTRS{hcddump}=="HCD Dump"
ATTRS{hnp}=="HstNegScs = 0x0"
ATTRS{hnpcapable}=="HNPCapable = 0x1"
ATTRS{hprt0}=="HPRT0 = 0x00001005"
ATTRS{hptxfsiz}=="HPTXFSIZ = 0x02000406"
ATTRS{hsic_connect}=="HSIC Connect = 0x1"
ATTRS{inv_sel_hsic}=="Invert Select HSIC = 0x0"
ATTRS{mode}=="Mode = 0x1"
ATTRS{mode_ch_tim_en}=="Mode Change Ready Timer Enable = 0x0"
ATTRS{rd_reg_test}=="Time to read GNPTXFSIZ reg 10000000 times: 980 msecs (98 jiffies)"
ATTRS{regdump}=="Register Dump"
ATTRS{regoffset}=="0xffffffff"
ATTRS{regvalue}=="invalid offset"
ATTRS{rem_wakeup_pwrdn}==""
ATTRS{remote_wakeup}=="Remote Wakeup Sig = 0 Enabled = 0 LPM Remote Wakeup = 0"
ATTRS{spramdump}=="SPRAM Dump"
ATTRS{srp}=="SesReqScs = 0x1"
ATTRS{srpcapable}=="SRPCapable = 0x1"
ATTRS{wr_reg_test}=="Time to write GNPTXFSIZ reg 10000000 times: 460 msecs (46 jiffies)"
looking at parent device '/devices/platform/soc':
KERNELS=="soc"
SUBSYSTEMS=="platform"
DRIVERS==""
ATTRS{driver_override}=="(null)"
looking at parent device '/devices/platform':
KERNELS=="platform"
SUBSYSTEMS==""
DRIVERS==""
The items of interest will be available in the first few sections. In particular:
- Line 8 - KERNEL==”ttyUSB0”
- Line 9 - SUBSYSTEM==”tty”
- Line 14 - SUBSYSTEMS=”usb”
- Line 52 - ATTRS{idProduct}==”2303”
- Line 53 - ATTRS{idVendor}==”067b”
For this exercise this will be enough for us. However, Prolific Technology Inc. USB/serial adapters are not uncommon. If you start to see conflicts you may need to for other differences to key off of as well. The section of lines 31-62 in my example will probably be your best bet to start with.
Building Your udev Rule
Using the information in the lines above we’ll construct a new file, as root, at ‘/etc/udev/rules.d/85-gps-usb.rules’ which will have the following contents:
ACTION=="add", KERNEL=="ttyUSB[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idProduct}=="2303", ATTRS{idVendor}=="067b", SYMLINK+="gps"
Note that it ALMOST exactly matched the lines above. The additions I’ve made are:
- ACTION==”add” - This ensures the rule is run when this type of device is added to the system
- KERNEL==”ttyUSB[0-9]* - We don’t know what number ttyUSB device we’ll receive, so accept anything that starts with ttyUSB and then has some number(s) after it
- SYMLINK+=”gps”- This creates the actual /dev/gps symlink to the /dev/ttyUSB#
udev SHOULD reload rules files automatically, but if you’re worried about it feel free to run this command to force a reload: sudo udevadm control –reload
Now, to test your work, unplug your USB GPS device (if plugged in), wait a second, and then plug it back in. After you plug it back in you should find that the /dev/gps symlink exists and points to a /dev/ttyUSB# device.
Setting Up gpsd
Most Linux applications retrieve GPS data from a program called gpsd. Installation on Ubuntu / Debian based systems is quite easy:
sudo apt install -y gpsd gpsd-clients
Now, all we need to do it point gpsd at our device. I’ve read various articles which state that it will automatically discover GPS devices, but I’ve had issues with this in the past so I like to point it at our newly create /dev/gps symlink. Open /etc/default/gpsd, and change the DEVICES line to read like the following:
DEVICES="/dev/gps"
Now we get to test our setup. Restart gpsd with the following command:
sudo service gpsd restart
To check that we’re receiving GPS data we can watch gpsd though the console using the ‘cgps’ command. It might take a while for your device to sync its clock and start getting data (TWELVE minutes or more), or it might take less. Either way, if your receiver is outside (or at least near enough a window) you should start seeing satellite signals and then a location and precision values.
Extra Credit / See Also
The following sites were useful in creating this article:
- Arch Linux’s page on udev