#!/usr/bin/perl 
# 
# dartsfilter.pl
# 
# The DARTS filtering and prediction algorithm testbed. 
#
# Copyright (c) 2003 Jeffrey M. Laughlin, n1ywb@arrl.org
# 
# This program is free software; you can redistribute it and/or modify     
# it under the terms of the GNU General Public License as published by     
# the Free Software Foundation; either version 2 of the License, or        
# (at your option) any later version.                                      
#                                                                          
# This program is distributed in the hope that it will be useful, 
# but WITHOUT ANY WARRANTY; without even the implied warranty of           
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            
# GNU General Public License for more details.                             
# 
# You should have received a copy of the GNU General Public License        
# along with this program; if not, write to the Free Software              
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use Curses;
use Time::HiRes qw( ualarm gettimeofday );

$SIG{INT}  = \&sighndl;
$SIG{TERM} = \&sighndl;

# curses initialization
initscr;
cbreak;
noecho;
clear;
refresh;

refresh();

sysopen(DARTS, "/dev/usb/darts-usb0", 0) || (
    cleanup() && 
    die "couln't open darts device");

while (1)
{
    my @strength = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
    my @antinoise = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
    my $buff;
    my @vals;
    my $total;
    my $last_bearing;
    my $steady;
    my @confidence;

    # This strangeness allows us to read data constantly from the device, but
    # once every second stop for a moment to process the data from the previous
    # second.
    eval {
        my $siglast;
        # Local anonymous signal handler functions
        local $SIG{ALRM} = sub { 
            local($sig) = @_;
            $siglast = $sig;
        };
        local $SIG{INT}  = sub { die "int\n" };
        local $SIG{TERM} = sub { die "term\n" };
        ualarm (1000000,0); # Set an alarm 
        $last_bearing = -1;
        $steady = 0;
        while (1)
        {
            # Read only the exact amount of data
            if ( sysread(DARTS, $buff, 1 + 4 + 4) < 1 + 4 + 4 )
            {
                cleanup();
                die ("read too little data");
            }
            # Decode the data structure
            # DARTS returns a data structure consisting of a char and two longs
            @vals = unpack("Cll", $buff);

            ++ $strength[$vals[0]];
            ++ $total;

            # See if this bearing was also the last bearing
            # This determines the noise level
            if ( $vals[0] == $last_bearing )
            {
                ++ $steady;
            }

            # If the alarm went off, get ready to exit the loop
            if ($siglast) { 
                last; 
                $vals[0] = -1;
            }

            # If the bearing changed, calculate the antinoise
            if ( $vals[0] != $last_bearing )
            {
                $antinoise[$last_bearing] += $steady**2;
                $steady = 0;
            }

            $last_bearing = $vals[0];
        }
    };

    # See if the loop exit was caused by a signal
    if ($@)
    {
        if ( $@ eq "alarm\n") {
          # Alarm signal, do nothing
        }
        elsif ( $@ eq "int\n" || $@ eq "term\n" ) {
            cleanup();
            die;
        }
        else {
            cleanup();
            die "Unexpected error '$@'";
        }
    }

    # Process data from the previous second 
    clear();
    move(0,0);

    # Normalize input values to 1
    for ($i = 0; $i < @strength; ++ $i)
    {
        if ($strength[$i] == 0) {
            $antinoise[$i] = 1; }
        else {
            $antinoise[$i] /= $strength[$i] ** 2; }

        $strength[$i] /= $total;
    }
    
    @confidence = fuzzyfilter($total, \@strength, \@antinoise);
    
    # The bearing with the highest confidence wins. This is dumb, it should use a
    # smarter algorithm. The hard part is that the function is cylindrical, not
    # linear.
    $greatest = 0;
    for ($i = 0; $i < @confidence; ++ $i)
    {
        printw("%2d : %.4f : %.4f : %1.3f\n", $i, $strength[$i], $antinoise[$i], $confidence[$i]); 
        if ($confidence[$i] > $confidence[$greatest])
        {
            $greatest = $i;
        }
    }

    printw("total - %4d\n", $total);

    draw_compass($greatest);
    
    refresh();

#    sleep(5);
    if ($sigtermed) {
        last;
    }
}

cleanup();

# Release curses and close the device file.
sub cleanup {
    standend;
    clear;
    refresh;
    endwin;
    close (DARTS);
}

# Signal handler for main loop
sub sighndl {
    local($sig) = @_;
    $sigtermed = $sig;
}

# Draws the compass on the screen, with the argument as the highlighted
# point.
sub draw_compass {
    my $led;
    my @x;
    my @y;
    my $i;
    my $offset = 35;

    $led = shift();
    @x = (15, 20, 24, 28, 30, 28, 24, 20, 15, 10,  6,  2,  0,  2,  6, 10, 15);
    @y = ( 0,  0,  1,  3,  6,  9, 11, 12, 12, 12, 11,  9,  6,  3,  1,  0,  6);
    
    for( $i = 0; $i <= 16; ++ $i )
    {
        addch($y[$i], $offset + $x[$i] + 1, '*');
        if ($i == $led) {
            addch($y[$i], $offset + $x[$i] - 1 + 1, '(');
            addch($y[$i], $offset + $x[$i] + 1 + 1, ')');
        }
    }
}

# Fuzzy logic filtering function.
sub fuzzyfilter {
    my $total = shift();
    my $strength = shift();
    my $antinoise = shift();
    my $maxval = 0;
    my $maxbearing = -1;
    my $i;
    my @confidence;

    for ( $i = 0; $i < @$strength; ++ $i ) {
        my $s1; my $s3; my $s5; my $s7; my $s9;
        my $n1; my $n3; my $n5; my $n7; my $n9;
        my $c1; my $c3; my $c5; my $c7; my $c9;

        # Variable names are like an "s-meter"
        # Compute input memberships. 
        $s1 = membership($$strength[$i], -1.0, 0.0, 0.3333);
        $s3 = membership($$strength[$i], 0.0, 0.3333, 0.6666);
        $s5 = membership($$strength[$i], 0.3333, 0.5, 0.75);
        $s7 = membership($$strength[$i], 0.5, 0.75, 1.0);
        $s9 = membership($$strength[$i], 0.75, 1.0, 2.0);

        $n1 = membership($$antinoise[$i], -1.0, 0.0, 0.2);
        $n3 = membership($$antinoise[$i], 0.1, 0.2, 0.3);
        $n5 = membership($$antinoise[$i], 0.2, 0.3, 0.5);
        $n7 = membership($$antinoise[$i], 0.4, 0.6, 0.8);
        $n9 = membership($$antinoise[$i], 0.5, 0.9, 1.0, 2.0);

        # Compute output memberships.
        $c1 = fuzzy_or($s1, $n1); 
        $c3 = fuzzy_or($s3, $n3);
        $c5 = fuzzy_or($s5, $n5);
        $c7 = fuzzy_or($s7, $n7);
        $c9 = fuzzy_and($s9, $n9); 

        # Compute overall level of confidence
        # This is sort of a lazy way to do this, but it works. Doing something
        # like finding the center of mass would be better.
        $confidence[$i] = which_is_biggest($c1, $c3, $c5, $c7, $c9) / 4.0; 
    }

    return @confidence;
}

# Takes a list, returns the index of the largest item in the list.
sub which_is_biggest {
    my $biggest = 0;
    my $bigval = 0;
    my $i;

    for ($i = 0; $i < @_; ++ $i) {
        if ( $_[$i] > $bigval )
        {
            $bigval = $_[$i];
            $biggest = $i;
        }
    }

    return $biggest;
}

# Computes membership in a fuzzy function. Takes an X value, and either two or
# three points. In the case of three points, the function is triangular, with
# points y(0) and y(2) = 0, and point y(1) = 1. In the case of four points, 
# the function is a trapazoid, with points y(0) and y(3) = 0, and points y(1)
# and y(2) = 1.
sub membership {
    my $xval = shift();
    my $yval;
    my @p = @_;

    if ($xval >= $p[0] && $xval <= $p[1]) {
        $yval = ( $xval - $p[0] ) * ( 1 / ( $p[1] - $p[0] ) ); }
    
    if ( @p == 4 ) {
        if ($xval >= $p[1] && $xval <= $p[2]) {
            $yval = 1;
        }
        shift(@p);
    }

    if ($xval >= $p[1] && $xval <= $p[2]) {
        $yval = ( $p[2] - ( $xval - $p[1] ) ) * ( 1 / ( $p[2] - $p[1] ) ); }
   
    return $yval;
} 

# Returns the fuzzy-and of a list of inputs
sub fuzzy_and {
    my $min = shift();
    my $v;
    foreach $v (@_) {
        $min = $v if $min > $v;
    }
    return $min;
}

# Returns the fuzzy-or of a list of inputs
sub fuzzy_or {
    my $max = shift();
    my $v;
    foreach $v (@_) {
        $max = $v if $max < $v;
    }
    return $max;
 }

