Raspberry Pi + 7 segment display = wireless device counter

This blog post will look at using the GPIO (General Purpose Input/Output) ports of the Raspberry Pi to drive a 7 segment display, which will display the number of wireless devices on a wireless network. I think this is a project where the Raspberry Pi can really shine – it would take more work and hardware to get a “normal” PC to do anything with a single electronics component, especially in days where parallel and serial ports are becoming rare.

This is what the final project looks like:

Raspberry Pi as wireless session counter

The 7 segment display is updated as wireless clients join and leave the network, displaying the concurrent number of active sessions/devices on your wireless network.

Anyway, here’s a summary of how it all works:

First, we’ll connect a 7 segment display to the Raspberry Pi and control it from a Python script. Then, we’ll see what methods we can use to find out what number we want to display (i.e. determine how many wireless sessions are active on your network), and lastly we’ll make sure that the number is updated instantly, or at least in a timely manner.

I’ll first go over how to set this up for a more typical home wireless router, and then show how this integrates neatly with FreeRADIUS as part of a WPA2 Enterprise wireless network (I discussed how to set that up on a Raspberry Pi in an earlier blogpost). Here’s the final FreeRADIUS version in action:

Disclaimer: If you implement any parts of this article, you’re doing that at your own risk. Please make sure you know what you’re doing – I cannot be held responsible if you damage your Raspberry Pi by (for example) short-circuiting pins or if you run insecure programs in a production environment. I will discuss security aspects to the best of my knowledge to help you, but the ultimate responsibility lies with you.

Part 1: controlling the 7 segment display

This is actually surprisingly straightforward. I wrote a very simple Python library which could be used to display an integer on the 7 segment display by toggling the appropriate pins using the RPi.GPIO library. Here’s a link to my sevensegment Python library on Github, in case you want to take a look (the readme file also explains how I connected the Raspberry Pi to my 7 segment display). All I needed to do was figure out which segments need to be turned on to display a particular number, and then store that mapping. This reminded me of my first year electronics lab exercises at university. 🙂

Here’s a snippet to show you the idea. The segments on a 7 segment display are usually labelled a-g, and my datasheet told me that I needed to turn the pins attached to each segment off to turn the segment on:

# map segments of the display (a-g) to pins on the Raspberry Pi
pins = {'a': 11, 'b': 12, } # etc..
numbers = ["abcdef", # 0 - all segments apart from the middle one (g)
           "bc", ] # 1  # etc...

def off():
    for p in pins.values():
        GPIO.output(self.segments[s], True) # active low

def set(number):
    off()  # simple but crude
    for segment in numbers[number]:
        GPIO.output(self.segments[s], False)

Again, the full library is here. Remember that you need to run anything that wants to mess with the GPIO pins on the Raspberry Pi under root or using sudo.

Part 2a: How many wireless sessions are currently active on my network? (“normal” router edition)

I will use the Technicolor Gateway TG582n (BeBox v2, O2 wireless box V) as an example here, but this should also work on other Technicolor wireless routers (e.g. the O2 wireless box IV or TG587nv2, various BeBox models). These devices have the advantage that they come with a Telnet interface, which provides the ‘wireless stations list’ command. Here’s some example output:

{Administrator}=>wireless stations list
flags: station is [A]associated (on 802.11 level)
       station is auth[O]rized (WPA handshake is ok)
       station is in [P]ower save mode
       station is [W]ME (QOS) capable
Station Name          Hardware Address   bss                             Flags      Time (assoc/idle)   
unknown               ab:cd:ef:01:23:45  mySSID                                AOPW  40832/18

Total number of associated stations : 1

As you can see, its last line tells us exactly what we want to know. Now you can use the pexpect Python module (homepage, pexpect@pypi) to write a simple Python script that will get this data for you programmatically. The idea of doing this type of telnet scripting is not a new idea, but quite useful in this context. If your router really only has a web GUI, you’ll need to look into using a scraper or perhaps even a browser automation tool like Selenium. .

Here’s a sketch, just to give you the idea (don’t copy/paste this code yet 🙂 ):

#!/usr/bin/env python
import re, pexpect
from sevensegment import sevensegment

conn = pexpect.spawn('telnet 192.168.1.254')
conn.expect('Username :')
conn.sendline("Administrator\r") # or SuperUser for O2 wireless boxes
conn.expect('Password :')
conn.sendline("UltraSecretPassword\r") # try your serial number
conn.expect("{Administrator}=>") # change this if you changed the username above
conn.sendline("wireless stations list\r")
conn.expect('{Administrator}=>') # and this
output = conn.before
# extract nr of associated stations : X
m=re.search(r"Total number of associated stations : ([0-9]+)", output)

num_devices = int(m.groups()[0])

conn.sendline("exit\r") # log off
conn.close()

# now you just need to display it
s=sevensegment.SevenSegmentDisplay()
s.set(num_devices)

Essentially what the script above does is:

  1. launch the telnet process
  2. wait for username/password prompt and send username/password as reply back
  3. wait for “normal” ‘{Administrator}=>’ prompt and respond with the ‘wireless station list’ command.
  4. Wait for it to complete (i.e. the prompt to return), and get the output from just before the prompt.
  5. Parse the output and display the resulting number on the 7 segment display

I wrapped up the telnet communication stuff in a little library (tg_lib), so that it’s easier to use. Here’s how:

from tg_lib import Connection
c = Connection('192.168.1.254', 'Administrator', 'Password')
output = c.run('wireless stations list')
m=re.search(r"Total number of associated stations : ([0-9]+)", output)
print(m.groups()[0])  # the number

Of course you can use this to get DSL or other stats from the router as well, or even apply configurations on-demand.

Security notes: It is probably advisable to create a less privileged user account on your router for this purpose, as well as making sure you store your passwords responsibly. It is also worth bearing in mind that telnet connections are unencrypted, so it is possible to sniff the password as it is sent over the wire/air to your router. If you can, you should use SSH instead.

Updating the counter using cron

The simplest way to update your counter is to use a cron job. Cron jobs are executed regularly at given defined times of the day, so for example to run your script every 5 minutes, you can do the following (as root, since your script needs to run as root for the GPIOs to work):

crontab -e

This will open your editor. Add a line like this:

*/5 * * * *  python /path/to/script.py

The first column contains the minutes of each hour you want the script to run (in this case, “every 5 minutes”), the other columns are for hours, day of the month etc. Save and close the file, and you’re good to go! (See this link for details on the format if you’re confused or want more information)

Updating the counter: on-demand methods

Unfortunately this part is a bit tricky with a standard home router, since you really need some mechanism to call your script whenever a wireless client joins or leaves your network.With a WPA2 enterprise network you can hook into the FreeRADIUS server, since that is asked to authenticate and authorise users, and it has neat module support (see below for details on how to do this). For the rest of this section I’ll assume you don’t have that.

If you have a router running aftermarket firmware (OpenWRT, tomato, dd-wrt), and hence have total control over your device, there may be a way, though I haven’t looked into that in detail. Please leave a comment if you find one though!

If you’re running your stock firmware, this will be tricky, since it really is outside the scope of what most hardware manufacturers envision. One way to do it would be to turn off the DHCP server on your router, and run one on your Raspberry Pi instead and hook into that, but I’ll leave that as an “exercise for the reader” ;). If you have any other good suggestions please do leave a comment!

Part 2b: How many wireless sessions are currently active on my network? (FreeRADIUS edition)

(In case you’re not familiar with WPA2 Enterprise, (Free)RADIUS etc, please take a look at my other blogpost for some background information)

I used FreeRADIUS’ accounting data for this, as I figured that would be a more reliable source of information than counting associations and disassociations myself (you may need to do this though, if your router does not support RADIUS accounting). The accounting data is stored in MySQL, and looks something like this, though the underlying table has many more columns:

A sample of FreeRADIUS accounting data displayed in daloRADIUS.

So, to count the number of sessions that are currently active, we just need to execute the following MySQL query:

SELECT COUNT(DISTINCT `acctuniqueid`) FROM `radacct` WHERE `acctstarttime` \
IS NOT NULL && `acctstoptime` IS NULL && `nasporttype` = "Wireless-802.11";

`radacct` is the database where the accounting data is stored, and currently active sessions will have a start time and no end time. The `acctuniqueid` key seems to be some sort of hash on various properties of the session in an attempt to create a more unique key than the accounting session id (See the <freeradius install directory>/modules/acct_unique file for more details). Of course, if you want to display the number of users rather than the number of sessions, feel free to modify the SQL query accordingly.

Now all you need is a little Python script that will execute the above MySQL query and pass the returned number to the sevensegment library:

# lcd.py
from sevensegment import sevensegment
import time
import _mysql  # dependency: http://mysql-python.sourceforge.net/

s=sevensegment.SevenSegmentDisplay()
con=None

try:
    con = _mysql.connect('localhost', 'user', 'pw', 'radius')
    con.query("""SELECT COUNT(DISTINCT `acctuniqueid`) FROM `radacct` WHERE `acctstarttime` \
IS NOT NULL && `acctstoptime` IS NULL && `nasporttype` = "Wireless-802.11"; """)
    #extract result
    result = con.use_result()
    num_online = int(result.fetch_row()[0][0])
    #display
    print("%d users are online!" % num_online)
    s.set(num_online)

except sevensegment.NumberOutOfRange:
    print("That number is out of range")

except _mysql.Error, e:
    print "Error %d: %s" % (e.args[0], e.args[1])
    sys.exit(1)

finally:
    if con:
        con.close()

Keeping the counter up to date using a FreeRADIUS module

Now you could of course run the Python script every minute or so using a cron job, but surely it would be more efficient to update the counter every time someone joins or leaves the wireless network, wouldn’t it?

The elegant solution consists of writing a very simple FreeRADIUS module, which we can call at the end of the ‘accounting { }’ block of your site (in my case, I added the statement ‘lcd’ to the accounting block in <freeradius install directory>/sites-available/default). The call to your custom module does not need to be at the end of the accounting block, just after the ‘sql’ statement, so that your script can find the latest information in the database.

If you don’t have RADIUS accounting data available because your router does not support that, you can also call your script from another section, say after authentication – but then you need to worry about checking whether the authentication was successful or not, additionally to tracking who’s on your network.

I based my module on the ‘echo’ example, which can just execute a program (you can check the ‘<freeradius install directory>/modules/echo’ file if you’re interested in the details). Here’s the gist:

# <radius>/modules/lcd
exec lcd {
        wait = yes
        program = "python /path/to/lcd.py"
        # shell escape environment variables - probably not really needed
        shell_escape = yes
}

Unfortunately, this will only work if you run FreeRADIUS as root (or using sudo), otherwise writing to the GPIO pins will fail. The usual way to “fix” this is to change the ownership of the 7 segment display controller program to ‘root’ and then set the setuid flag. The setuid flag causes the program to run under it’s owner no matter who is running it, meaning that the pins will be toggled by root, even if some unprivileged user is running the program. However, setting the setuid flag does not work in the perhaps expected way on Python files, since the program actually executing the Python code is the Python interpreter. To get around this, I just wrote a tiny C wrapper program that executes ‘python /path/to/lcd.py’, changed the ownership of the wrapper and set the suid flag.

This is what my wrapper looks like:

//lcd-wrapper.c - to compile, run: gcc -o lcd-wrapper lcd-wrapper.c
#include <stdlib.h>
#include <unistd.h>

int main() {
    setuid(0);
    setgid(0);
    system("whoami");
    system("python /path/to/lcd.py");
    exit(0);
}

More security notes: There are good reasons why you shouldn’t use this wrapper in a production environment – for example, bad things can happen if someone can change the contents of lcd.py and run the lcd-wrapper.. A better solution would be to perhaps embed the Python script and Python interpreter in a C/C++ program and run the embedded interpreter on the internally stored script, though then you may as well implement the whole program in C++… I didn’t go there though, since this was mainly a prototyping exercise for me. 🙂

Congratulations, you now have a device telling everybody how popular your wireless network is!

=> If you just want the code, it’s all in this github repository. <=

 

Full disclosure

 

I am currently a Cisco employee, and the views expressed on this blog are mine and do not necessarily reflect the views of Cisco.

 

2 Replies to “Raspberry Pi + 7 segment display = wireless device counter”

  1. Hi Michiel,

    I’m a French System Engineer, I also play with the RASPERRY PI, and I’m happy to find friends who are doing the same.

    I use it to develop tiny net admin applications and I play with GPIOs as well.

    I would be happy to here from you. Don’t hesitate ping me. I will be happy to be linked with you and exchange about Raspberry PI within network environnements.

    Patrick

  2. I wrote this little script that scrapes the association list on Openwrt, counts it and then generates a number… It doesn’t seem to cause any real load with a 1 second delay, but you could make it run more or less often, or even run it from cron… Then you just have to decide how to transfer the data to the pi…

    Either a simple TCP server connection or you could write the value to the web server dir and have the pi just fetch it every x seconds…

    import os, time

    lastval = -1

    while 1:
    resp = os.popen(‘iwinfo wlan1 assoclist’).read()
    resp = resp.split(‘RX: ‘)
    if len(resp) – 1 != lastval:
    print len(resp) – 1 # first entry is the preamble about the mac address
    lastval = len(resp) -1
    time.sleep(1)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.