source: core/trunk/client/flarmclient.pl @ 314

Last change on this file since 314 was 314, checked in by smoser, 11 years ago

Release 1.2.0

  • Property svn:mime-type set to text/plain
File size: 12.2 KB
RevLine 
[314]1#-------------------------------------------------------------------------------
2# This file is part of the FLARM®-Radar Project.
3#   
4#   Copyright by the Authors
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#   http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18#   Project Website: www.flarmradar.ch
19#   Email: info@flarmradar.ch
20#   Authors:
21#     2012-2014 Simon Moser
22#     2013-2014 Dominic Spreitz
23#     2014-2014 Giorgio Tresoldi
24#-------------------------------------------------------------------------------
[310]25#!/usr/bin/perl
[296]26#-------------------------------------------------------------------------------
27# This file is part of the FLARM®-Radar Project.
28#   
29#   Copyright 2012-2014 Simon Moser
30#   Copyright 2013-2014 Dominic Spreitz
31#
32# Licensed under the Apache License, Version 2.0 (the "License");
33# you may not use this file except in compliance with the License.
34# You may obtain a copy of the License at
35#
36#   http://www.apache.org/licenses/LICENSE-2.0
37#
38# Unless required by applicable law or agreed to in writing, software
39# distributed under the License is distributed on an "AS IS" BASIS,
40# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
41# See the License for the specific language governing permissions and
42# limitations under the License.
43#
44#   Project Website: www.flarmradar.ch
45#   Email: info@flarmradar.ch
46#-------------------------------------------------------------------------------
[130]47
48use strict;
49use warnings;
50use Getopt::Std;
51use File::Basename;
[224]52use Time::HiRes qw(gettimeofday);
[133]53use POSIX qw(strftime);
54use POSIX qw(setsid);
[130]55use LWP::UserAgent;
[278]56use Device::SerialPort;
[293]57
58# constants
[278]59use constant {
60        OPS => 0b001,
61        DEBUG => 0b010,
62        TRACE => 0b100
63};
[293]64# FIXME: increas that value
65my $MAX_READ_OP = 10000;
[310]66my $MAX_CMD_WAIT = 3;
[293]67
68# variables
[130]69my %config;
70my %options;
[278]71my $log;
72my $log_initialized = 0;
[310]73my $flarm_initialized = 0;
[293]74my $read_cnt = 0;
[288]75my $flarm;
[278]76
[293]77# defaults
[278]78my $log_ops = 1;
[282]79my $log_debug = 0;
[278]80my $log_trace = 0;
[251]81my $interval = 3;
82my $skip = 1;
[130]83
[278]84# signal handlers
85# toggle $log_trace with 'kill -USR1 <pid>'
86$SIG{USR1} = sub {
87        if ($log_trace) {
88                $log_trace = 0;
89        } else {
90                $log_trace = 1;
91        }
92};
93$SIG{INT} = sub {
94        die("terminate program");
95};
[130]96
[278]97# display a help page
[130]98sub usage {
99  print <<EOF;
100NAME
[278]101  $0 -- stream flarm records to server
[130]102
103SYNOPSIS
[278]104  $0 [-c config_file] [-d] [-f data_file] [-i n] [-j m] [-r] [-t] [-h]
[130]105
106DESCRIPTION
107  The following options are available:
108 
109  -c    Use the specified configuration file. Use the default configuration
110                file as a starting point for customization.
111               
[284]112  -d    Write out debug information
113                WARNING: If your configuration is to write the out put to a file (see
114                option 'log') then this might fill up the file system.                 
[224]115               
[284]116  -f    Read the data from the specified data file instead of the serial
117                device. This is mainly used for testing and development.
[253]118
[251]119  -i    Bundle records, send a request to the server every i-th GPGGA record.
120                Used for bandwidth optimization. Defaults to 3.
[212]121               
[251]122  -j    Send every j-th record to the server. Used for bandwidth
123                optimization. Defaults to 1.
124               
[284]125  -t    Trace client operations, write out all data read from the flarm device
126                WARNING: If your configuration is to write the out put to a file (see
127                option 'log') then this might fill up the file system.
[224]128               
[130]129  -h    Print this help.
130
131EOF
132  exit 0;
133}
134
[278]135# a very basic logger
[204]136sub logit {
[278]137        my ($aspect, $msg) = @_;
138       
[293]139        unless ($log_initialized) {
[279]140                if (exists($config{'log'})) {
[284]141                        open($log, ">> $config{'log'}") || die("Failed to open log file $config{'log'}: $!");
[293]142                        select($log);
143                        $| = 1;
[278]144                } else {
145                        $log = *STDOUT;
146                }
147                $log_initialized = 1;
[204]148        }
[278]149        print $log "$msg\n" if (($aspect & OPS) && $log_ops);
150        print $log "$msg\n" if (($aspect & TRACE) && $log_trace);
151        print $log "$msg\n" if (($aspect & DEBUG) && $log_debug);
[204]152}
153
[278]154# read config file
[130]155sub readconfig {
[278]156        my ($config_file) = @_;
157        open(CONF, "< $config_file") || die("failed to open config file for reading: $!");
[130]158        while(my $line = <CONF>) {
159                chomp($line);
160                next if $line =~ /^\s*#/;
161                next if $line =~ /^\s*$/;
162                if ($line =~ /^\s*(\S*)\s*=\s*(\S*)\s*$/) {
163                        $config{$1} = $2;
164                }
165        }
166        close(CONF);
167}
168
[293]169# send a command to the flarm, optionally wait for the expected answer
170sub send_flarm_cmd {
171        my ($cmd, $expected_answer) = @_;
[310]172        if (defined($flarm)) {
[311]173                logit(OPS, exact_time() . " write to flarm cmd=" . $cmd);
[310]174                my $count_out = $flarm->write($cmd . "\r\n");
175            warn "write failed\n" unless ($count_out);
176           
177                my $read_start = exact_time("%s");
178                my $record;
179               
180                if (defined $expected_answer) {
181                        while (1) {
182                                until($record = $flarm->lookfor()) {
183                                        logit(DEBUG, exact_time() . " wait for data from flarm device");
184                                        sleep(1);
185                                }
186                               
187                                logit(DEBUG, "read from flarm=" . $record);
188                                if ($record =~ /$expected_answer/) {
[311]189                                        logit(OPS, exact_time() . " command accepted: " . $expected_answer);
[310]190                                        last;
191                                }
192       
193                                my $elapsed = exact_time("%s") - $read_start;
194                                if ($elapsed > $MAX_CMD_WAIT) {
[311]195                                        logit(OPS, exact_time() . " WARN: did not receive expected answer in time (" . $MAX_CMD_WAIT . " s)");
[310]196                                        last;
197                                }
198                        }
199                }
200        }
[293]201}
202
[310]203# intialize the flarm device
204sub initialize_flarm_device {
[311]205        logit(OPS, exact_time() . " initialize flarm device");
[310]206       
207        # ACFT: type of aircraft, default 'unknown'
208        if (defined($config{'acft_type'})) {
209                send_flarm_cmd("\$PFLAC,S,ACFT," . $config{'acft_type'}, "PFLAC,A,ACFT");
210        }
211       
212        # NMEAOUT: which sentences are sent to the data port, default '1', all incl. propriatery
213        if (defined($config{'nmeaout'})) {
214                send_flarm_cmd("\$PFLAC,S,NMEAOUT," . $config{'nmeaout'}, "PFLAC,A,NMEAOUT");
215        }
216
217        # RANGE_ detection range of aircraft, default 25500, max value 25500
218        if (defined($config{'range'})) {
219                send_flarm_cmd("\$PFLAC,S,RANGE," . $config{'range'}, "PFLAC,A,RANGE");
220        }
221
222        # THRE: threshold for aircraft on ground, default '2'
223        if (defined($config{'threshold'})) {
224                send_flarm_cmd("\$PFLAC,S,THRE," . $config{'threshold'}, "PFLAC,A,THRE");
225        }
226       
227        # UI: visual and output, default '3' (disable buzzer, enable led warnings)
228        if (defined($config{'visual'})) {
229                send_flarm_cmd("\$PFLAC,S,UI," . $config{'visual'}, "PFLAC,A,UI");
230        }
231       
232        # no idea how it would behave if it was set to stealth, better switch off
233        send_flarm_cmd("\$PFLAC,S,PRIV,0", "PFLAC,A,PRIV");
234       
235        # not documented but this removes the vertical restriction
[312]236        send_flarm_cmd("\$PFLAC,S,DEBUG,9", "PFLAC,A,DEBUG,9");
[310]237       
238        $flarm_initialized = 1;
[311]239        logit(OPS, exact_time() . " device initialization completed");
[293]240}
241
[288]242# abstraction for reading data (from flarm device or from file)
243sub readdata {
244        unless (defined($flarm)) {
245                # initialize our flarm object
246                if (defined($options{'f'})) {
[311]247                        logit(OPS, exact_time() . " read flarm data from file " . $options{'f'});
[288]248                        open($flarm, "< $options{'f'}") || die("failed to open flarm data file for reading: $!");
249                } else {
250                        # check that we have the required configuration options
251                        die("missing option 'serial_device' in configuration file") unless (defined($config{'serial_device'}));
252                        die("device does not exist: " . $config{'serial_device'}) unless (-c $config{'serial_device'});
253                        die("missing option 'baud' in configuration file") unless (defined($config{'baud'}));
254                       
255                        # create serial device object
[311]256                        logit(OPS, exact_time() . " initialize serial port reader on device=" . $config{'serial_device'} . " with baud=" . $config{'baud'});
[289]257                        $flarm = new Device::SerialPort ($config{'serial_device'}) || die "Can't open $config{'serial_device'}!\n";
[288]258                        $flarm->baudrate($config{'baud'});
259                        $flarm->parity("none");
260                        $flarm->databits(8);
261                        $flarm->stopbits(1);
262                        $flarm->are_match("\r\n");
[289]263                       
[310]264                        if ($config{'do_device_config'}) {
265                                initialize_flarm_device() unless ($flarm_initialized);
266                        }
[288]267                }
268        }
269       
[293]270        # read data
[288]271        my $record = undef;
272        if (defined($options{'f'})) {
273                $record = <$flarm>;
274        } else {
[293]275                my $read_start = exact_time("%s");
[288]276                until($record = $flarm->lookfor()) {
[293]277                        logit(DEBUG, exact_time() . " wait for data from flarm device");
[288]278                        sleep(1);
[293]279                        $read_start = exact_time("%s");
[288]280                }
[293]281                my $read_duration = exact_time("%s") - $read_start;
[310]282                if ($read_duration > 0.8) {
[311]283                        logit(OPS, exact_time() . " trigger re-initialization of serial port, long read duration=" . $read_duration);
[310]284                        $flarm = undef;
285                }
[293]286               
287                # pro-active workaround to prevent failures during continuous operation
288                $read_cnt++;
289                if ($read_cnt >= $MAX_READ_OP) {
290                        logit(DEBUG, exact_time() . " reset serial port reader");
291                        $flarm = undef;
292                        $read_cnt = 0;
293                }
[288]294        }
295        chomp($record);
296        return $record;
297}
298
[278]299# as the name says
[224]300sub exact_time {
[293]301        my ($format) = @_;
302        $format = "%H:%M:%S" unless defined($format);
303        return strftime($format, localtime()) . "." . (gettimeofday())[1];
[224]304}
305
[130]306# send the records to the server. We don't make a request for each record for
307# performance reasons.
308sub flush {
[278]309        my ($records, $url, $ua) = @_;
310        logit(DEBUG, exact_time() . " start flushing data to server");
[224]311       
[293]312        my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
313    my $date= sprintf("%04d", ($year + 1900))."/".sprintf("%02d", ($mon + 1))."/".sprintf("%02d", $mday);
[130]314        my $resturl = $url . "/" . $date;
[278]315        logit(DEBUG, exact_time() . " request resource: " . $resturl);
[224]316       
317        # compose the request
[130]318        my $request = HTTP::Request->new('PUT');
319        $request->url($resturl);
320        $request->header('stationKey'=>$config{'key'});
[224]321        my $content = compress($records);
[278]322        logit(DEBUG, exact_time() . " put on wire: " . $content);
[224]323        $request->content($content);
324       
[130]325        # run the request
[278]326        logit(DEBUG, exact_time() . " start server push");
[130]327        my $response = $ua->request($request);
[278]328        logit(DEBUG, exact_time() . " end server push");
[224]329       
[130]330        # analyze the response
331        my $code = $response->code;
[278]332        $response->code == 200 || logit(DEBUG, "error processing records (" . $response->code . ") records=[" . $records . "]");
333        logit(DEBUG, exact_time() . " end flushing data");
[130]334}
335
[223]336# remove all unused records, debug information, etc.
337sub compress {
338        my ($records) = @_;
339        my $on_wire;
340        foreach my $record (split(';', $records)) {
341                if ($record =~ /^\$GPGGA,/ || $record =~ /^\$PFLAA,/) {
342                        $on_wire = (defined($on_wire)) ? $on_wire . ";" . $record : $record;
343                }               
344        }
345        return $on_wire;
346}
347
[278]348#
349# here starts our main program
350#
[261]351
[130]352# parse options
[251]353getopts('c:di:j:f:th', \%options);
[130]354
[278]355my $cfile;
[130]356if (defined($options{'c'})) {
357        $cfile = $options{'c'};
[278]358} elsif (-f "/etc/flarmclient.conf") {
359        $cfile = "/etc/flarmclient.conf";
360} else {
361        # last resort, search for a config file in the local directory
362        $cfile = "./flarmclient.conf";
[130]363}
[278]364
365# read config file
[283]366die ("no config file found") unless (-f "$cfile");
[278]367readconfig($cfile);
368
[224]369if (defined($options{'d'})) {
[278]370        $log_debug = 1;
[224]371}
[212]372if (defined($options{'i'})) {
373        $interval = $options{'i'};
374}
[251]375if (defined($options{'j'})) {
376        $skip = $options{'j'}
377}
[130]378if (defined($options{'h'})) {
379        usage();
380}
[224]381if (defined($options{'t'})) {
[278]382        $log_trace = 1;
[224]383}
[204]384
[130]385# validation: key must be present in config file
[251]386die("no key found in config file " . $cfile . " (option: key)") unless defined($config{'key'});
[130]387
388# force a flush right away and after every write or print
389local $| = 1;
390
[278]391# ok, everything is setup
[311]392logit(OPS, exact_time() . " start client, connect to " . $config{'url'});
[257]393
[278]394# create UserAgent object
395my $ua = new LWP::UserAgent;
[130]396
397my $buf;
[212]398my $i = 0;
[289]399
[288]400while(my $record = readdata()) {
401       
402        # send only n-th sequence to the server (option -s). A sequence is terminated with a GPGGA-record
403        if ($i % $skip == 0) {
[278]404                       
[288]405                logit(TRACE, $record);
406                $buf = (defined($buf)) ? "$buf;$record" : $record;
407        }
[251]408               
[288]409        # a GPGGA record terminates the sequence
410        if ($record =~ /^\$GPGGA,/) {
411                if ($i % ($interval * $skip) == 0) {
412                        flush($buf, $config{'url'}, $ua) ;
413                        $buf = undef;
[293]414                        if (defined($options{'f'})) {
415                                sleep($interval * $skip)
416                        } elsif ($i > $MAX_READ_OP) {
417                                # force a re-initialization
418                                $i = 0;
419                                $flarm = undef;
420                        }
421                } else {
422                        $i++;
423                }               
[130]424        }
425}
426exit 0;
427
[278]428END {
[311]429        logit(OPS, exact_time() . " client is shutting down");
[278]430        if (exists($config{"log"})) {
431                close($log);
432        }
433        undef $ua;
434        undef $flarm;
435}
436
Note: See TracBrowser for help on using the repository browser.