venerdì 4 settembre 2009

Beagleboard: setting date via GPS

My Beagleboard now sets its date/time reading them from a GPS unit. W00T!

Below, a few notes about this hack.

Please note: Beagleboard, up to revision C2, does have a real-time clock but you cannot attach any backup battery (the clock "stops" when Beagleboard is not powered). Recent Beagleboards (revision C3 and later) appear to have a place to attach one (read the Beagleboard System Reference Manual - BBSRM - for more details).

My GPS unit is an old Garmin GPS II Plus, its firmware dates back to 1997. It sports an RS232 port, talking standard NMEA at 4800 baud.

I made the cable to connect to the RS232 header of the Beagleboard - the one which we were using as /dev/ttyS2 for serial console access. I will then be able to log in the Beagleboard only using network access (ouch!) via ssh or with an external USB keyboard/mouse/hub solution.

First, I recommend compiling and installing the latest gpsd (gpsd is available in the Narcissus packaging, but if you ask for opkg install gpsd you will have a lot of possibly annoying freesmartphone.org stuff installed with it).

While you could easily parse GMT date/time from raw NMEA strings, the gpsd makes all GPS-related stuff as easy as accessing a socket, without having to deal with RS232/USB/bluetooth/etc issues and binary/text/NMEA/etc protocols and coordinate/approximation/etc calculations; also, many programs at the same time can access GPS data.

How to start gpsd at boot time, instead of the getty on the /dev/ttyS2 RS232 port?

First, you have to be sure that you can access a Linux shell without using the RS232 console (for example, using a keyboard on a USB hub connected to the Beagleboard).

Now edit /etc/inittab replacing this "S" line:
S:234:respawn:/sbin/getty 115200 ttyS2

with these two "S" and "G" lines:
S:234:respawn:/sbin/getty 115200 ttyS2
G:5:respawn:/usr/sbin/gpsd -n -N /dev/ttyS2

Note: "-n" means "don't wait for a client connection before polling GPS data (this option appears to be deprecated on latest gpsd versions); "-N" means "run in foreground" (if you forget it, it will respawn forever!); "/dev/ttyS2" is the RS232 port.

Before rebooting to apply changes, make sure you can use the Beagleboard without the serial port.

After rebooting, enter a terminal shell via ssh or using an external keyboard. Check that the gpsd is running:
ps guxa | grep gpsd

If the gpsd is running, you will see the /usr/sbin/gpsd process (and -maybe- the "grep" as well).

Now you can telnet to the gpsd socket and issue a command to watch the real time stream of gpsd "cooked" data:
telnet localhost 2947
W=1

You will see lots of output lines:
GPSD,X=1252018312.141204,I=Generic NMEA
GPSD,W=1
GPSD,O=?
GPSD,Y=GSV 1252012212.000 10:3 30 168 0...

To stop, type W=0 and then ctrl-D (or just ctrl-D).

If you want to read "raw" (non-"cooked") NMEA data, just enter the R command. Entering the R command again will stop raw data output.

Now you can execute the cgps (console gps utility); press q to exit. On the left window there are position/speed/etc data (in case of poor GPS coverage, the horiz/vert/course errors will float even to 300 feet, but I think this is a problem of my GPS unit); the right window shows satellite list and information. Sadly, cgps only knows about mph and feet...

Now start gpsmon (another console gps utility); press ^C to exit. This contains far more information - everything gpsd can parse or predict: geoid, DOP/HDOP, magnetic variation, etc.

Now I can write a Ruby program (yes, I happen to love Ruby language; I don't like Python and Perl) to fetch the GPS date (which is always GMT) from gpsd, update it to GMT+1, and then issue the system date command to update it.

My GPS unit always reports date/time on its screen, but does not emit it if unable to read it via GPS. So gpsd won't give a date/time when the GPS receiver has poor GPS coverage. Also, it appears that soon after opening the socket, the date is not available (gpsd answers "D=?"); just wait a little and retry. I will loop waiting for either 4.0 seconds total pause, or a correct timestamp is received; any other error (timeouts on socket port, or invalid timestamp) will stop the program.

require 'socket'

GMT = +1.0   # local time here is GMT + 1 hour

gpsd = TCPSocket.open("localhost", 2947)
gdat = ""
trys = 4.0   # try for 4 seconds...

while trys >= 0.0
  gpsd.puts "d"            # request date from gpsd daemon
  gdat = gpsd.gets.chomp   # read response

  break  if gdat != "GPSD,D=?"  &&  gdat != "GPSD"

  sleep 0.2                # unknown date, sleep some time...
  trys = trys -0.2         # ...and retry until timeout
end

# split "GPSD,D=2009-09-03T20:57:35.0Z" in date/time fields,
# then add GMT difference and finally issue a system set date
# using its weird syntax month/day/hour/min/year.seconds
system (Time.gm(*gdat.split(/[=Z]/)[1].split(/[-:T\.]/).
                collect { |i| eval(i.gsub(/^0/,"")) }) + GMT*3600).
       strftime("date --utc %m%d%H%M%Y.%S")

Once a GMT time is fetched via GPS, erase the header and footer part (delimited by "=" and "Z"), split the fields (separated by "-", ":", "T" or "."), collect their numeric values (erasing leading zeros), add the GMT difference (yes, you could set GMT=-9.5 to have GMT -09:30) and then build a date --utc command with reordered and striped parameters (don't complain about it: the date weird syntax dates back to 1969 or something like that...)

Yes, I happen to love Perl-style Chaotic Programming ;)

I placed it in /etc/gpsdate.rb and edited /etc/timezone to match my area (Europe/Rome). Then, if a date is available, ruby /etc/gpsdate.rb will set up the system date/time, else will fail with some error message; it won't loop forever.

Now, how to add it at the end of boot scripts?

To my surprise, there isn't any rc.local or boot.local - I had to add in mine.

I see that /etc/inittab at sysinit stage executes /etc/init.d/rcS - so I modified that rcS script to add a call to some /etc/rc.local created by me.

I see there a bug: it issues an exec (instead of a "call") to /etc/init.d/rc S (yes, "rc" script with "S" parameter - different from the above "rcS" script...) so the subsequent lines of the rcS won't be executed; I wiped out that "exec" and added soon after a call to /etc/rc.local - thus having this modified version:

#       Call all parts in order.
/etc/init.d/rc S
#
#       execute my local script
/etc/rc.local
#
#       Finish setup if needed. The comment above about
#       /sbin/unconfigured.sh applies here as well!
if [ -x /sbin/setup.sh ]
then
  /sbin/setup.sh
fi

Then I created my /etc/rc.local script containing:
# to me: better having this static IP setup here:
ifconfig usb0 192.168.5.3

ruby /etc/gpsdate.rb &> /tmp/bootdate.txt
date >> /tmp/bootdate.txt
sync

Yes, I know some of you will complain, requiring me to use the standard init.d method, build start scripts, end scripts and so on... Sorry! When in a hurry, you sometimes forget the Elegant Solution Method, and prefer the Happy Tinkering Method...! :)

Now, after the reboot, I see that the /tmp/bootdate.txt contains the output of my gpsdate.rb (either a GMT date string or some error text, and then the "current" system date).


Let's write a Ruby script to make some GPS logging; instead of logging those weird NMEA strings, just log the gpsd parsed data:

require 'socket'
gpsd = TCPSocket.open("localhost", 2947)
gpsd.puts "W=1"
while a = gpsd.gets.chomp
  puts a
end

It appears that it outputs some 140-150 lines per minute (up to 20 kilobytes per minute, or up to 1.2 megabytes per hours) can be written to disk. Mumble, mumble... I should mount Beagleboard + GPS on my bike and write a program that in real-time gives me statistics and GPS stuff and after a month gives me some large graphic diagram... = :) !!!

6 commenti:

  1. I forgot to say that you should turn on the GPS after the Beagleboard started booting.

    If you don't do it, the first NMEA strings sent by the GPS on the ttyS2 will be catched by the uBoot bootloader as... unknown boot commands! :)

    This will be solved by compiling again the uBoot without RS232 support (or with RS232 support excluding the standard RS232).

    RispondiElimina
  2. I did all the gpsd tinkering because I want to have my beagleboard always connected to a GPS unit (I am planning a portable or wearable system).

    RispondiElimina
  3. Not only on the Beagleboard|

    If you have gpsd installed on your Linux box, the Ruby script that sets the date will work as well.

    RispondiElimina
  4. Note:

    the modification of /etc/inittab (the "S" and "G" lines) assumes that you use an "initdefault" of 5 (look on the first lines of the inittab file).

    The inittab is parsed during boot.

    You may use some /etc/init.d script as well.

    RispondiElimina
  5. If you use the Angstrom Linux distribution, you can install Ruby interpreter by using opkg install ruby after connecting the Beagleboard to internet (via Ethernet, for example).

    RispondiElimina
  6. If you disable serial (ttyS2) usage in U-Boot and Linux kernel command-line parameters, you don't need anymore to turn on the GPS "after a few seconds the Beagleboard started booting".

    No U-boot recompiling is actually needed (just a few setenv/saveenv; I will detail later how to do).

    RispondiElimina