Wifi Sniffer

Updated 04 June 2019

This project started because I wanted to know the signal strength of the Wifi in various places around my house. I had set up a Pi Zero as a simple watering system controller. It would switch a solenoid on and off based on a 7 day schedule; which I could setup via a web page from my desktop PC. The place I chose for the Pi Zero was not the best as it was on the border of stable Wifi (-79dBm).  Having a portable Pi Zero that would display the signal strength of wherever it was placed, seem like a good idea.

wifi strength gauge

On GitHub I found iwlistparse.pyhttps://gist.github.com/dubkov/79738a7a15fcb47809bc

Python code that parses the text output of the command iwlist wlan0 scan into columns and displays them on the HDMI screen.  Using this code I was able to see all the networks around me and their signal information in a neat column display.

 

I then used this concept to parse the text output of command iwconfig wlan0. Which shows the signal strength of the locally connected Wifi.  Another command, hostname -I, allowed the display of the IP address assigned to the Pi Zero by the router.

 

Now I had all the information; I just needed a way to display it on the Pi Zero. The Adafruit 128x64 OLED Bonnet proved a good solution. Although pricey, it has a reasonable size display, a number of push buttons and fits neatly on top of a Pimoroni Pibow Zero W Case. 

Wifi sniffer parts - dissasembled

Parts

  1. Raspberry Pi Zero WH (Wireless with Soldered Headers)
  2. Adafruit 128x64 OLED Bonnet for Raspberry Pi
  3. Pimoroni Pibow Zero W Case or you could use the Slim Case by Core Electronics

Software

The Adafruit Python library makes using the OLED display very easy. I changed from the default font and used PressStart2P.ttf; loaded from http://www.dafont.com/bitmap.php; it seemed to be the best. In all, I tried about 20 different fonts from this site.  (this font also worked good for the Adafruit 128x32 display, the screen on this one is very small) 

The screen area displays the font in an 8x8 format, so we have 16 characters across and 8 lines down. I experimented with smaller fonts but it became too hard to read. When showing all Wifi only the first 8 characters of the SSID are displayed, then the signal strength, then channel number. I thought channel display would be good, as most routers are set to channel 11; and it is best to switch to a less busy channel if you can find one.  Also, only the top 8 Wifi signals are shown.

The Pi Zero is set up to connect to the local house Wifi or to the Wifi hotspot of my phone.

When the Python code was complete the following line was added to :-

/etc/rc.local    sudo python wifi64-01.py &

Not the most elegant way to start a turnkey system, but it works.

The ampersand symbol on the end spawns it as a separate process, allowing rc.local to complete.

Note it uses Python, not Python3. The original iwlistparse.py was written in 2010.

The final product uses buttons A,B & C of the OLED Bonnet to switch between; showing all Wifi in the area, only the connected Wifi or blanking the screen. Showing all Wifi, is similar to what a phone does, but the Pi gives actual figures of signal strength rather than just 3 small bars.

wifi sniffer menu

Conclusion


The cost of this project is not cheap. The setup will be used to better position the Wifi router so signal strength is more even across the house. And to map the Wifi distribution around the house. Then the parts will most likely be used for another project.
The experience gained developing the Python code and using the OLED Bonnet will stand in good stead for future projects.

wifi sniffer completed and in action

Wifi sniffer visible wifi networks and signal strength

The Code

Python code

#####################################################################
#!/usr/bin/env python
#
# wifi64-01.py
# James Grigg - 16 Mar 2019
# Display network cells on 128x64 OLED
# Press buttons to change or clear display
# 16x8 display, font = PressStart2P.ttf (8 px font)
#
# Adapted from iwlistparse.py Python code from GitHub
#
#####################################################################

import sys
import time
import subprocess

import RPi.GPIO as GPIO

import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

#####################################################################
RST = None 	# no reset on OLED 128x64
# Note the following are only used with SPI:
DC = 23
SPI_PORT = 0
SPI_DEVICE = 0

# GPIO pin definitions
L_pin = 27
R_pin = 23
C_pin = 4
U_pin = 17
D_pin = 22

A_pin = 5
B_pin = 6

GPIO.setmode(GPIO.BCM)
GPIO.setup(A_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(B_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(L_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(U_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(R_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(D_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(C_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)


# Initialize library.
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST)
disp.begin()
disp.clear()
disp.display()

# Create blank image for drawing.
width = disp.width 	# 128 pixels
height = disp.height 	#  64 pixels
image = Image.new('1', (width,height))
draw = ImageDraw.Draw(image)

font = ImageFont.truetype('PressStart2P.ttf', 8)

top = 0
left = 0
right = 127
bottom = 31

interface = "wlan0"

#####################################################################
# menu display
#####################################################################
def draw_menu():
    draw.rectangle((0,0,width-1,height-1), outline=1, fill=0)
    draw.text((left, top+0),  "                ",  font=font, fill=255)
    draw.text((left, top+8),  "  -- Ready --   ",  font=font, fill=255)
    draw.text((left, top+16), "                ",  font=font, fill=255)
    draw.text((left, top+24), " Press:         ",  font=font, fill=255)
    draw.text((left, top+32), "  A = List local",  font=font, fill=255)
    draw.text((left, top+40), "  B = Connected ",  font=font, fill=255)
    draw.text((left, top+48), "  C = Clear     ",  font=font, fill=255)
    draw.text((left, top+56), "                ",  font=font, fill=255)
    disp.image(image)
    disp.display()
    return

#####################################################################
#get the information related to the fields
#####################################################################
def get_name(cell):
    nam = matching_line(cell,"ESSID:")[1:-1]
    return nam[0:8]

def get_quality(cell):
    quality = matching_line(cell,"Quality=").split()[0].split('/')
    return str(int(round(float(quality[0]) / float(quality[1]) * 100))).rjust(3) + " %"

def get_channel(cell):
    return matching_line(cell,"Channel:")

def get_signal_level(cell):
    sig = matching_line(cell,"Quality=").split("Signal level=")[1]
    return sig[0:3]

    return enc

#####################################################################
#look up dictionary rules
#####################################################################
rules={"Name":get_name,
       "Quality":get_quality,
       "Channel":get_channel,
       "Signal":get_signal_level
       }
#####################################################################
#Sort cell information, removes duplicate entries
#####################################################################
def sort_cells(cells):
    sortby = "Quality"
    reverse = True
    cells.sort(None, lambda el:el[sortby], reverse)

#####################################################################
#table output
#####################################################################
columns=["Name","Signal","Channel"]

#####################################################################
#Returns the first matching line in a list of lines. If the first part of line
#matches keyword, returns the end of that line. Otherwise returns None
#####################################################################
def matching_line(lines, keyword):
    for line in lines:
        matching=match(line,keyword)
        if matching!=None:
            return matching
    return None

def match(line,keyword):
    line=line.lstrip()
    length=len(keyword)
    if line[:length] == keyword:
        return line[length:]
    else:
        return None

#####################################################################
#Applies the dictionary rules to the bunch of text describing a cell and
#returns the corresponding dictionary entry
#####################################################################
def parse_cell(cell):
    parsed_cell={}
    for key in rules:
        rule=rules[key]
        parsed_cell.update({key:rule(cell)})
    return parsed_cell

#####################################################################
#get cell information in text line format for OLED display
#####################################################################
def cells_Text_Line(cells):

    linenum = 0
    temp = ""
    table=[]
    justified_table=[]		#[] = ordered and changable list
    list={}			#{}  = unordered unindexed list

    for cell in cells:
        cell_properties=[]
        for column in columns:
            cell_properties.append(cell[column])
        table.append(cell_properties)

    widths=map(max,map(lambda l:map(len,l),zip(*table))) 

    for line in table:
        justified_line=[]
        for i,el in enumerate(line):
            justified_line.append(el.ljust(widths[i]))
        justified_table.append(justified_line)

    for line in justified_table:
        for el in line:
            temp = temp + el + " "	#build text line string for list elements
        list[linenum] = temp		#save text line
        linenum = linenum + 1		#point to next line
        temp=""				#clear temporary buffer
    return list

################################################################
# check for keypressed
################################################################
def checkKey():
    if not GPIO.input(A_pin):		#0 = key press
       return ("A")
    elif not GPIO.input(B_pin):
       return ("B")
    elif not GPIO.input(C_pin):
       return ("C")
    else:
        return("None")

################################################################
# show connected information
################################################################
def listconnected():

  key = False
  while not key:
    i = 0
    lines = {}
    IP = ""
    SSID = ""
    Signal = ""
    draw.rectangle((0,0,width-1,height-1), outline=0, fill=0)

    cmd = "hostname -I | cut -d\' \' -f1"
    IP = subprocess.check_output(cmd, shell = True )
    if IP == "\n":
        draw.text((left, top), "not connected", font=font, fill=255)
    else:
        proc = subprocess.Popen(["iwconfig", interface],stdout=subprocess.PIPE, universal_newlines=True)
        out, err = proc.communicate()
        for line in out.split("\n"):
            lines[i] = str(line)
            i = i + 1
        SSID = lines[0][30:].rstrip()[:-1]          #line 0 has SSID
        Signal = lines[6][43:].rstrip()             #line 6 has signal strength
        draw.text((left, top), str(IP), font=font, fill=255)
        draw.text((left, top+12), str(SSID), font=font, fill=255)
        draw.text((left+73, top+24), str(Signal), font=font, fill=255)

    disp.image(image)
    disp.display()
    if not checkKey() == "None":
      key = True

  return

##################################################################
# list up to 8 detected wifi networks SSID,Signal Strength,channel
##################################################################
def listall():

  key = False
  while not key:
    numCells = 0
    cells = [[]]
    parsed_cells = []
    textlines = {}

#get the network cell list in raw format
    proc = subprocess.Popen(["iwlist", interface, "scan"],stdout=subprocess.PIPE, universal_newlines=True)
    out, err = proc.communicate()

#seperate the raw list into individual cell lists
    for line in out.split("\n"):
        cell_line = match(line,"Cell ")
        if cell_line != None:
            cells.append([])
            line = cell_line[-27:]
            numCells = numCells + 1
        cells[-1].append(line.rstrip())
    cells=cells[1:]
#parse the cells into the desired columns
    for cell in cells:
        parsed_cells.append(parse_cell(cell))

#sort and format the cell information
    sort_cells(parsed_cells)
    textlines = cells_Text_Line(parsed_cells)   #format cell information as text line for OLED

# Write lines of text to data image OLED display, first 8 lines only
    draw.rectangle((0,0,width-1,height-1), outline=0, fill=0)   #clear image for new output, otherwise it overwrites
    numLine = 0
    while numLine < 7:
        if numLine < numCells:
            draw.text((left, top+numLine*8), str(textlines[numLine]),  font=font, fill=255)
        numLine = numLine + 1

    disp.image(image)
    disp.display()
    if not checkKey() == "None":
      key = True
  return

################################################################
#  Main loop - repeat until stdin interrupt then exit
################################################################
try:
    draw_menu()
    while True:
        if checkKey() == "A":
            listall()
        elif checkKey() == "B":
            listconnected()
        elif checkKey() == "C":
            disp.clear()
            disp.display()

except KeyboardInterrupt:	#crtl-C pressed on stdin
    disp.clear()		#leave display blank
    disp.display()
    GPIO.cleanup()		#GPIO pin clear

################################################################
################################################################
################################################################

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.