Scanmeternm: New WiFi Antenna Pointing Tool

The old Scanmeter high gain WiFi antenna pointer written in Bash still works very well.

However, it depends on the output of iwlist command to read wireless APs. For some reason iwlist is not reliable on my new  Ubuntu machine.

I wrote a new antenna pointing tool in Python called Scanmeternm. Instead of iwlist, I am reading the wireless router power using ncmli dev wifi command. It’s certainly easier to customize for casual coders than the old Bash Scanmeter. For example, you could probably use the script to control an Arduino stepper motor and automatically rotate antenna on top of a vehicle.

Usage

The usage is very straightforward, and it is more user friendly than the old Scanmeter.

If you download to your Downloads directory, you’d add executable privilege:

chmod +x ~/Downloads/scanmeternm.py

Run as:

./~/Downloads/scanmeternm.py

Caveats

  • Requires Network-manager package
  • Does not work in Windows, maybe not even in OS X. I don’t know if Network-manager is available for Mac.
  • I don’t know what units are used for nmcli dev wifi output. They may be linear or logarithmic. The meter readout green/red threshold is entirely arbitrary.
Scanmeternm.py Wifi Antenna Pointer
 
  1. #! /usr/bin/env python
  2. # Measure wifi signal based on nmcli. Works only if Network-manager is installed.
  3. # Scanmeternm V1.0, skifactz.com/wifi
  4. from subprocess import Popen, PIPE, STDOUT
  5. import sys
  6. import os
  7. import time
  8. # User customizable variables
  9. # Customize meter readout here
  10. threshold = 52   # Red/green threshold
  11. char = u'\u2588' # Graphics character, default is a box
  12. # Menu scanned list variables
  13. ssidMaxLength = 17 # Truncate length to satisfy tab based layout fit within 80 columns.
  14. tab = '\t' # Tab character. Can use ' ' or '\t' * 2 to modify layout
  15. # Lookup tables for 2.4 GHz and 5 GHz frequency to channel conversion.
  16. lut24 = {
  17. 2412:'01 (2.4 GHz)',
  18. 2417:'02 (2.4 GHz)',
  19. 2422:'03 (2.4 GHz)',
  20. 2427:'04 (2.4 GHz)',
  21. 2432:'05 (2.4 GHz)',
  22. 2437:'06 (2.4 GHz)',
  23. 2442:'07 (2.4 GHz)',
  24. 2447:'08 (2.4 GHz)',
  25. 2452:'09 (2.4 GHz)',
  26. 2457:'10 (2.4 GHz)',
  27. 2462:'11 (2.4 GHz)',
  28. 2467:'12 (2.4 GHz)',
  29. 2472:'13 (2.4 GHz)',
  30. 2484:'14 (2.4 GHz)'
  31. };
  32. lut5 = {
  33. 5035:'07  (5 GHz)',
  34. 5040:'08  (5 GHz)',
  35. 5045:'09  (5 GHz)',
  36. 5055:'11  (5 GHz)',
  37. 5060:'12  (5 GHz)',
  38. 5080:'16  (5 GHz)',
  39. 5170:'34  (5 GHz)',
  40. 5180:'36  (5 GHz)',
  41. 5190:'38  (5 GHz)',
  42. 5200:'40  (5 GHz)',
  43. 5210:'42  (5 GHz)',
  44. 5220:'44  (5 GHz)',
  45. 5230:'46  (5 GHz)',
  46. 5240:'48  (5 GHz)',
  47. 5260:'52  (5 GHz)',
  48. 5280:'56  (5 GHz)',
  49. 5300:'60  (5 GHz)',
  50. 5320:'64  (5 GHz)',
  51. 5500:'100 (5 GHz)',
  52. 5520:'104 (5 GHz)',
  53. 5540:'108 (5 GHz)',
  54. 5560:'112 (5 GHz)',
  55. 5580:'116 (5 GHz)',
  56. 5600:'120 (5 GHz)',
  57. 5620:'124 (5 GHz)',
  58. 5640:'128 (5 GHz)',
  59. 5660:'132 (5 GHz)',
  60. 5680:'136 (5 GHz)',
  61. 5700:'140 (5 GHz)',
  62. 5720:'144 (5 GHz)',
  63. 5745:'149 (5 GHz)',
  64. 5765:'153 (5 GHz)',
  65. 5785:'157 (5 GHz)',
  66. 5805:'161 (5 GHz)',
  67. 5825:'165 (5 GHz)'
  68. };
  69. # Convert 2.4 GHz and 5 GHz to channel numbers.
  70. def getchannel(freq):
  71.       if freq[0] == '2':
  72.             return lut24[int(freq)]
  73.       if freq[0] == '5':
  74.             return lut5[int(freq)]
  75.       return 'N/A'
  76. # Prints list of scanned APs for main menu and returns the list
  77. def aplist():
  78.       # Clear Linux console
  79.       Popen(['clear'])
  80.       # Scan available APs and save in scannedMenu list.
  81.       scan = Popen(['nmcli', 'dev', 'wifi'], stdout=PIPE, stderr=STDOUT)
  82.       scannedMenu=[]
  83.       for line in scan.stdout:
  84.             scannedMenu.append(line)
  85.       # Print menu header.
  86.       print 'No. SSID',
  87.       print ' ' * (ssidMaxLength - 4), # White spaces minus SSID length
  88.       print 'BSSID                    CHANNEL         POWER   ENCRYPT.'
  89.       print u'\u00af' * 80
  90.       # SSID and SECURITY fields may contain spaces. We use the ':' marks as the parsing ref.      
  91.       for line in scannedMenu[1:]:
  92.             ssid      = line.split("'")[1]
  93.             cells      = line.split(':')
  94.             bssid = ':'.join([cells[0][-2:],
  95.                               cells[1], cells[2],
  96.                               cells[3],
  97.                               cells[4],
  98.                               cells[5][:2]])
  99.             cells      = cells[5].split() # Split cells using space for the rest of the fields.
  100.             chan      = getchannel(cells[2])
  101.             power      = cells[6]
  102.             encrypt      = cells[7]
  103.       
  104.             lineNo = str(scannedMenu.index(line)) # Number APs
  105.             print lineNo.zfill(2) + ' ',
  106.             print ssid[:ssidMaxLength],
  107.             print ' ' * (ssidMaxLength - len(ssid)), # Padding based on maximum SSID length
  108.             print tab.join([bssid, chan, power, encrypt])
  109.       print
  110.       return scannedMenu
  111. # Menu user input returns selected bssid and facilitates user commands like 'S' and 'Q'.
  112. def menuinput(scannedMenu):
  113.       ap = False
  114.       def enterap():
  115.             ap = raw_input('Enter access point number. Enter S for new scan or Q to quit. \n')
  116.             if ap == 's' or ap == 'S':
  117.                   aplist()
  118.             if ap == 'q' or ap == 'Q':
  119.                   sys.exit()
  120.             if ap.isdigit() == False:
  121.                   ap = False
  122.             if int(ap) > len(scannedMenu) - 1: # -1 because of the nmcli header line
  123.                   ap = False
  124.             if int(ap) == 0:
  125.                   print 'No such AP.'
  126.                   ap = False
  127.             return ap
  128.       # User input, loop until ap no longer False
  129.       while ap == False:
  130.             ap = enterap()
  131.       # User input selection assignent
  132.       for line in scannedMenu[1:]:
  133.             lineNo = scannedMenu.index(line) # index number      
  134.             if int(ap) == lineNo:
  135.                   ssidSel            = line.split("'")[1]
  136.                   cells            = line.split(':')
  137.                   bssidSel      = ':'.join([cells[0][-2:],
  138.                                           cells[1],
  139.                                           cells[2],
  140.                                           cells[3],
  141.                                           cells[4],
  142.                                           cells[5][:2]])
  143.                   return ssidSel, bssidSel
  144. # Main menu control calls AP scanning, prints the list and calls user AP selection.
  145. def mainControl():
  146.       # print ap list, get the list
  147.       scannedMenu = aplist()
  148.       # user menu input. get selected ssid and bssid
  149.       global scanselection # Declaring global list so it is available to AP scan below
  150.       scanselection = menuinput(scannedMenu)
  151.       print 'You selected:', scanselection[0], scanselection[1] + '\n'
  152.       print 'CTRL + C exits scan mode.' + '\n'
  153.       time.sleep(.33)
  154. mainControl()
  155. def red(text):
  156.       return ('\033[91m' + text + '\033[0m')
  157. def green(text):
  158.       return ('\033[92m' + text + '\033[0m')
  159. # Here we print power reading to the scaled console in form of an dynamic histogram.
  160. def readout(power):
  161.       if powerScaled == 0:
  162.             print '%s is out of range or not transmitting.' %scanselection[0]
  163.       if powerScaled < thresholdScaled:
  164.             lPower = powerScaled
  165.       else:
  166.             lPower = thresholdScaled
  167.       hPower = powerScaled - thresholdScaled
  168.       sys.stdout.write(red(char) * lPower)
  169.       sys.stdout.write(green(char) * hPower)
  170.       print power
  171.       #print '\r'
  172. while True:
  173.       try:
  174.             # We declare these two variables for the scenario when the radio signal drops
  175.             powerScaled = 0
  176.             thresholdScaled = 0
  177.             scan = Popen(['nmcli', 'dev', 'wifi'], stdout=PIPE, stderr=STDOUT)
  178.             # Is the selected bssid continuosly present in the scan? If so, proceed.
  179.             for line in scan.stdout:
  180.                   if scanselection[1] in line:
  181.                         cells      = line.split(':')
  182.                         cells      = cells[5].split() # Split cells using white space for the rest.
  183.                         power      = int(cells[6])
  184.                   
  185.                         # Dynamically scale max nmcli signal to maximum console width, non-dB units
  186.                         rows, columns = os.popen('stty size', 'r').read().split()
  187.                         scaleFactor = float(columns) / 100 # Max terminal width / max nmcli signal
  188.                         powerScaled = int(round(power * scaleFactor))
  189.                         thresholdScaled = int(round(threshold * scaleFactor))
  190.             
  191.             # Print readout      
  192.             readout(powerScaled)
  193. # On CTRL + C go to main menu
  194.       except KeyboardInterrupt:
  195.             mainControl()

Leave a Reply

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