#!/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

"""
print "running ssm.py"

import gtk, 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.4'

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

dirty = []

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

# 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 = 294
    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(pygame.draw.aaline(screen, c_link, (width/2, height/3), (tr.centerx, tr.centery)))
    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(pygame.draw.aaline(screen, c_signal, (width/3, height*2/3), (tr.centerx, tr.centery)))
    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(pygame.draw.aaline(screen, c_noise, (width*2/3, height*2/3), (tr.centerx, tr.centery)))
    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)
    y = tr.top
    dirty.append(screen.blit(ts, tr))
    ts = fn.render('press erase to clear samples', 1, c_black, bg)
    tr = ts.get_rect(centerx=width/2, bottom=y)
    y = tr.top
    dirty.append(screen.blit(ts, tr))

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

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

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

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

    tx = '  last %d samples' % len(samples)
    ts = fn.render(tx, 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)

license_show_count = (1000 / pause) * license_show_time

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

fg = black
bg = white

class Ssm:
    def run(self):
        global fc, screen, width, height, license_show_count

        self.clock = pygame.time.Clock()
        self.running = True
        fc = FontCache()

        screen = pygame.display.get_surface()
        width, height = screen.get_size()
        print "width=%d height=%d" % (width, height)
        if width < 640:
            print "bah, stupid situation, bailing out"
            sys.exit()
        pygame.mouse.set_visible(False)

        screen.fill(bg)
        pygame.display.flip()
        pygame.time.set_timer(pygame.USEREVENT, pause)

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

        while self.running:
            self.clock.tick(10)
            while gtk.events_pending():
                gtk.main_iteration()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                    return
                elif event.type == pygame.VIDEORESIZE:
                    print "resize"
                    pygame.display.set_mode(event.size,  pygame.RESIZABLE)
                    screen.fill(bg)
                    pygame.display.flip()

                if 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)
                pygame.display.flip()

def main():
    pygame.init()
    pygame.display.set_mode((0, 0), pygame.RESIZABLE)
    game = Ssm()
    game.run()
    sys.exit(0)

if __name__ == '__main__':
    main()
