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.
On GitHub I found iwlistparse.py. https://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.
Parts
- Raspberry Pi Zero WH (Wireless with Soldered Headers)
- Adafruit 128x64 OLED Bonnet for Raspberry Pi
- 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.
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.
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 ################################################################ ################################################################ ################################################################