#!/usr/bin/python
"""
    Signal Strength Meter
    Copyright (C) 2010  James Cameron (quozl@laptop.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

"""
import sys, pygame

license = [
"Signal Strength Meter",
"Copyright (C) 2010 James Cameron <quozl@laptop.org>",
" ",
"This program comes with ABSOLUTELY NO WARRANTY;",
"for details see source.",
" ",
"This is free software, and you are welcome to ",
"redistribute it under certain conditions; see ",
"source for details.",
" "
]
version = '0.3'

pause = 100             # milliseconds between screen updates
license_show_time = 10  # number of seconds to show license for
thickness = 4           # thickness of circles in pixels
freeze = False

fonts = ('DejaVuSansMono.ttf', 'DejaVuLGCSansMono.ttf')
fontpaths = ('/usr/share/fonts/dejavu/', '/usr/share/fonts/truetype/ttf-dejavu/')

# colours
c_black = (0, 0, 0)
c_blue  = (0, 0, 192)
c_green = (0, 192, 0)
c_red   = (192, 0, 0)

c_license = c_black
c_link    = c_blue
c_signal  = c_green
c_noise   = c_red

def get_rf_raw():
    """ obtain wireless status from first network interface """
    fp = open('/proc/net/wireless', 'r')
    head = fp.readline()
    head = fp.readline()
    line = fp.readline()
    fp.close()
    (x, x, link, signal, noise, x, x, x, x, x, x) = line.split()
    return (int(link.replace('.', ' ')),
            int(signal.replace('.', ' ')),
            int(noise.replace('.', ' ')))

samples = []

def get_rf():
    global samples, min_l, min_s, min_n, max_l, max_s, max_n, avg_l, avg_s, avg_n

    raw = get_rf_raw()
    samples.append(raw)
    q = len(samples)
    if q > 100:
        samples = samples[1:]
        q -= 1
    (l, s, n) = raw
    (min_l, min_s, min_n) = (l, s, n)
    (max_l, max_s, max_n) = (l, s, n)
    (tot_l, tot_s, tot_n) = (0, 0, 0)
    for sample in samples:
        (l, s, n) = sample
        (min_l, min_s, min_n) = (min(min_l, l), max(min_s, s), max(min_n, n))
        (max_l, max_s, max_n) = (max(max_l, l), min(max_s, s), min(max_n, n))
        tot_l += l
        tot_s += s
        tot_n += n
    avg_l = tot_l / q
    avg_s = tot_s / q
    avg_n = tot_n / q
    return raw
#    return (avg_l, avg_s, avg_n)

# what to do when user wants to leave
def op_quit():
    pygame.display.quit()
    pygame.quit()
    sys.exit()

# produce a screenshot for documentation purposes
def op_copy():
    pygame.image.save(screen, "/tmp/ssm-snapshot.png")
    print "snapshot taken"

# toggle pause of screen updates
def op_freeze():
    global freeze

    freeze = not freeze

def op_clear():
    global samples

    samples = []

# control keyboard table, relates keys to functions
kb_table_control = {
    pygame.K_d: op_quit, # control/d to quit
    pygame.K_c: op_copy, # control/c to make screen snapshot
    }

# normal keyboard table, relates keys to functions
kb_table_normal = {
    pygame.K_q: op_quit, # q to quit
    pygame.K_f: op_freeze, # f to freeze
    pygame.K_SPACE: op_freeze, # space to freeze
    pygame.K_BACKSPACE: op_clear, # backspace to clear samples
    }

def kb(event):
    """ handle keyboard events from user """

    # ignore the shift and control keys
    if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: return
    if event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL: return

    # check for control key sequences pressed
    if (event.mod & pygame.KMOD_CTRL):
        if kb_table_control.has_key(event.key):
            handler = kb_table_control[event.key]
            handler()
        return

    # check for normal keys pressed
    if kb_table_normal.has_key(event.key):
        handler = kb_table_normal[event.key]
        handler()
        return

class FontCache:
    def __init__(self):
        self.cache = {}

    def read(self, names, size):
        if names == None:
            return pygame.font.Font(None, size)

        for name in names:
            for path in fontpaths:
                try:
                    return pygame.font.Font(path + name, size)
                except:
                    continue
        return pygame.font.Font(None, size)

    def get(self, names, size):
        key = (names, size)
        if key not in self.cache:
            self.cache[key] = self.read(names, size)
        return self.cache[key]

def draw_license():
    c = min(255 * (100 - license_show_count) / 100, 255)
    fn = fc.get(fonts, 34)
    x = 50
    y = 394
    for line in license:
        ts = fn.render(line, 1, (c, c, c), bg)
        tr = ts.get_rect(left=x, top=y)
        y = tr.bottom
        dirty.append(screen.blit(ts, tr))

def draw_labels():
    fr = 40 # framing pad for data labels
    fn = fc.get(fonts, 35)
    ts = fn.render('ssm.py ' + version, 1, c_black, bg)
    tr = ts.get_rect(left=0, top=0)
    dirty.append(screen.blit(ts, tr))
    ts = fn.render('LINK QUALITY', 1, c_link, bg)
    tr = ts.get_rect(centerx=width/2, top=fr)
    dirty.append(screen.blit(ts, tr))
    ts = fn.render('SIGNAL LEVEL', 1, c_signal, bg)
    tr = ts.get_rect(left=fr, bottom=height-fr)
    dirty.append(screen.blit(ts, tr))
    ts = fn.render('NOISE LEVEL', 1, c_noise, bg)
    tr = ts.get_rect(right=width-fr, bottom=height-fr)
    dirty.append(screen.blit(ts, tr))
    fn = fc.get(fonts, 22)
    ts = fn.render('press q to quit', 1, c_black, bg)
    tr = ts.get_rect(centerx=width/2, bottom=height)
    y = tr.top
    dirty.append(screen.blit(ts, tr))
    ts = fn.render('press space to pause', 1, c_black, bg)
    tr = ts.get_rect(centerx=width/2, bottom=y)
    dirty.append(screen.blit(ts, tr))

def draw_results(show):
    colour = c_black
    if not show:
        colour = bg
    fn = fc.get(fonts, 20)

    ts = fn.render('min  link %3.0f signal %3.0f noise %3.0f' % (min_l, min_s, min_n),
                   1, colour, bg)
    tr = ts.get_rect(right=width-10, top=10)
    x = tr.left
    y = tr.bottom
    dirty.append(screen.blit(ts, tr))

    ts = fn.render('avg  link %3.0f signal %3.0f noise %3.0f' % (avg_l, avg_s, avg_n),
                   1, colour, bg)
    tr = ts.get_rect(left=x, top=y)
    x = tr.left
    y = tr.bottom
    dirty.append(screen.blit(ts, tr))

    ts = fn.render('max  link %3.0f signal %3.0f noise %3.0f' % (max_l, max_s, max_n),
                   1, colour, bg)
    tr = ts.get_rect(left=x, top=y)
    x = tr.left
    y = tr.bottom
    dirty.append(screen.blit(ts, tr))

    ts = fn.render('  last %d samples' % len(samples), 1, colour, bg)
    tr = ts.get_rect(right=width-10, top=y+10)
    x = tr.left
    y = tr.bottom
    dirty.append(screen.blit(ts, tr))

def draw_item(show, colour, x, y, radius, v):
    if not show:
        colour = bg
    if radius < 0:
        radius = 0
    p = pygame.draw.circle(screen, colour, (x, y), radius+thickness, thickness)
    fn = fc.get(fonts, 50)
    ts = fn.render(v, 1, colour, bg)
    tr = ts.get_rect(centerx=x, centery=y)
    q = screen.blit(ts, tr)
    dirty.append(pygame.Rect.union(p, q))

def draw_link(link, show):
    draw_item(show, c_link, width/2, height/3, link*15/10, "%d %%" % link)

def draw_signal(signal, show):
    if signal != -256: # scanning?
        draw_item(show, c_signal, width/3, height*2/3, 156+signal, "%d dBm" % signal)

def draw_noise(noise, show):
    draw_item(show, c_noise, width*2/3, height*2/3, 156+noise, "%d dBm" % noise)


from optparse import OptionParser
parser = OptionParser(usage="usage: %prog [options] message",
                     version="%prog " + version)
parser.add_option("--verbose",
                  action="store_true", dest="verbose", default=False,
                  help="generate verbose output")
parser.add_option("--no-license",
                  action="store_true", dest="no_license", default=False,
                  help="do not display license")
(opt, args) = parser.parse_args()

if not opt.no_license:
    for line in license:
        print line

pygame.init()
fc = FontCache()

screen = pygame.display.set_mode()
width, height = screen.get_size()
pygame.mouse.set_visible(False)

white = (255, 255, 255)
black = (0, 0, 0)

pygame.time.set_timer(pygame.USEREVENT, pause)

if opt.no_license: license_show_time = 0
license_show_count = (1000 / pause) * license_show_time

fg = black
bg = white

# prepare background
screen.fill(bg)
pygame.display.flip()

# initial drawing pass
(link, signal, noise) = get_rf()
dirty = []
draw_labels()
draw_link(link, True)
draw_signal(signal, True)
draw_noise(noise, True)
pygame.display.update(dirty)

# main event loop
while True:
    event = pygame.event.wait()
    if event.type == pygame.QUIT:
        op_quit()
    elif event.type == pygame.KEYDOWN:
        kb(event)
    elif event.type != pygame.USEREVENT:
        continue

    if freeze:
        continue

    dirty = []
    if license_show_count > 0:
        # slow screen redraw with license present
        # everything is redrawn
        license_show_count = license_show_count - 1
        draw_link(link, False)
        draw_signal(signal, False)
        draw_noise(noise, False)
        draw_results(False)
        if license_show_count == 0:
            dirty.append(screen.fill(bg))
        else:
            draw_license()
        (link, signal, noise) = get_rf()
        draw_labels()
        draw_link(link, True)
        draw_signal(signal, True)
        draw_noise(noise, True)
        draw_results(True)
    else:
        # fast screen redraw with license absent
        # only changes are redrawn
        (old_link, old_signal, old_noise) = (link, signal, noise)
        (link, signal, noise) = get_rf()

        if signal == -256: # scanning?
            signal = old_signal

        draw_results(False)
        if link != old_link:
            draw_link(old_link, False)
            draw_link(link, True)
        if signal != old_signal:
            draw_signal(old_signal, False)
            draw_signal(signal, True)
        if noise != old_noise:
            draw_noise(old_noise, False)
            draw_noise(noise, True)
        draw_results(True)

    # update only the portions of screen that were changed
    pygame.display.update(dirty)
