main index Note: My APRS-related pages begin here!

Problem: collecting and decoding APRS data on a Linux machine, and accessing decoded data with a local webserver.

Solution:

Well, I have a 24h/24 running Linux fanless box (currently I do not plan to store heard APRS packets on public a database, like findu or aprs.fi). I just want to have it ready for my computers.

That Linux box has 128Mb RAM only and already works as printer server and internet connection sharing device. Let's add another feature!

Click here for latest APRS stations heard here!


APRS parsing: a Finnish solution

The guys at aprs.fi did a great work: a full APRS decoding library written in Perl language: the "Ham-APRS-FAP" library, freely downloadable on the CPAN web site.

Well, I do not like too much the cryptic Perl: Ruby is 100 times better. So I will use a Perl script only to only work on APRS data stream: incoming APRS packet are decoded by FAP library and then sent out with a timestamp and a few fields separated by the character ">".

Here is my echo.pl script that uses the FAP library (using "isax25" because I want to filter out "internet-style" packets):


use Ham::APRS::FAP qw(parseaprs distance direction);
use strict;

# my position (needed to calc distance/direction):
my $plat=40.123456;
my $plon=14.456789;
my $id='KJ4HDR';

# variables
my $aprspacket; my $r; my $d; my $c;
my $sec; my $min; my $hr; my $md; my $mon; my $yr; my $wd; my $yd; my $isd;
local $| = 1;

while(<>)    # main loop
{
  my %p;
  $aprspacket = $_;
  $r = parseaprs($aprspacket, \%p, 'isax25' => 1);
  if($r == 1)
  {
    $d = distance($plon,$plat,$p{longitude},$p{latitude});
    $c = direction($plon,$plat,$p{longitude},$p{latitude});

    ($sec,$min,$hr,$md,$mon,$yr,$wd,$yd,$isd)=localtime(time);

    printf "%4d-%02d-%02d %02d:%02d:%02d>%s>%s>%s>%s>%s>%s>%s>%s>%s>%s>%s\n",
      $yr+1900,$mon+1,$md,$hr,$min,$sec,
      $p{srccallsign}, $p{type}, $p{latitude}, $p{longitude},
      $p{course}, $p{speed}, $p{altitude}, $p{posambiguity},
      $d, $c, $p{comment};

#   printf(STDERR "%02d/%02d %02d:%02d:%02d %10s %6d km  %3d*  %d km/h  %s.\n",
#     $mon+1,$md,$hr,$min,$sec,
#     $p{srccallsign}, $d, $c, $p{speed}, $p{comment});
#
#   if($p{srccallsign} eq $id)
#   {
#     system("echo my position: $id $p{latitude} $p{longitude}");
#   }
  }
}

This just reads APRS data from standard input (which will be a redirection by serial output of my TH-D7), and outputs strings like: 2009-02-09 10:55:13>KJ4HDR>location>1.234>5.678>>>>> 3.456>7.89> (I commented out the stderr logging and the "reaction" block when my position incoming).

These strings are "splittable" on the ">" character. The first fields are a timestamp, the sender of the APRS message, the type of message (status, location, dx...) and the other APRS parameters. The last two numbers define distance (in meters) and course. I don't care about all those decimals, because I wanted to keep the Perl script as simple as I could.

Now, let's set up a web server to wrap the data into HTML pages. You don't need to set up Apache (or even a Lighttpd or a Thttpd) and a bunch of modules to do the dirty work.

Here is the dispatcher.rb script, remarkably simple. Start two Ruby threads, one for the web server (that will just reply an OK for every incoming request, adding the accumulated APRS data in the "page" variable, zeroing it after serving it), and the other for getting outputs from the above echo.pl script.


#!/usr/bin/env ruby

require 'socket'

page = "\n"
answ = "HTTP/1.0 200 OK\r\n\r\n"

serv = Thread.new {
                    s = TCPServer.new 80
                    loop do
                      c = s.accept
                      c << answ + page
                      page = "\n"
                      c.close
                    end
                  }

scan = Thread.new {
                    while a = gets
                      page << a
                    end
                  }

serv.join
scan.join

The "scan" thread just adds the unformatted data got on its stdin; it could well build up a "<table cellspacing... <tr... <td..." (I'll leave this as an exercise; my current release status is here).

Currently I prefer to serve raw data without destroying it immediately after serving it: save in "oldp" the last served page.

When the page "/" is requested, then serve fresh data. When any other page is requested (for example, "/olddata") then serve the last output page. Also, add a timestamp comment line (prepended by "#") at the beginning and at the end of every page. This way, if I cannot download an entire page (or abort before getting it), I can try to read the "old page" data to retrieve it (well, it's just a queue of two elements).

Yet, this dispatcher.rb Ruby script is remarkably simple:


#!/usr/bin/env ruby

require 'socket'

def now a
  "# #{a}: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n"
end

Thread.abort_on_exception = true

page = now "start"
oldp = now "no ''old'' data since"
answ = "HTTP/1.0 200 OK\r\n\r\n"

serv = Thread.new {
                    s = TCPServer.new 80
                    loop do
                      c = s.accept

                      if c.gets.split[1]=="/"
                        page += now("end")
                        c << answ + page
                        oldp = page
                        page = now("start")
                      else
                        c << answ + oldp
                      end

                      c.close
                    end
                  }

scan = Thread.new {
                    while a = gets
                      page << a
                    end
                  }

serv.join
scan.join

Now I just have to create a shell script invoking the Perl one and the Ruby one: here is the aprs.sh script. The first lines contain some paranoid initialization: after speed setting (and disabling any TTY terminal echo), send a carriage return, then a "reset" command, and then an empty command.


#!/bin/bash

stty -F /dev/ttyS0 9600 raw -echo -echoctl -echoe -echok -echoke -echonl -echoprt
echo -n -e "\rreset\r" > /dev/ttyS0
sleep 3
echo -n -e "\r"        > /dev/ttyS0

echo --- start: `date`

perl echo.pl < /dev/ttyS0 | ruby dispatcher.rb

The echo.pl reads APRS raw data from /dev/ttyS0 (speed: 9600, because the TNC of the TH-D7 uses 9600 even when doing APRS at 1200) and serves its output to the dispatcher.rb script which serves data on an HTTP server.

This is currently working on my Linux fanless machine (no discs, no fans, no moving parts: booting a text-only Slackware 12 off a 1Gb compactflash, running in 128Mb RAM including ram-disc).