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.
 #! /usr/bin/env python

# Measure wifi signal based on nmcli. Works only if Network-manager is installed.
# Scanmeternm V1.0, skifactz.com/wifi

from subprocess import Popen, PIPE, STDOUT
import sys
import os
import time

# User customizable variables
# Customize meter readout here
threshold = 52   # Red/green threshold
char = u'\u2588' # Graphics character, default is a box

# Menu scanned list variables
ssidMaxLength = 17 # Truncate length to satisfy tab based layout fit within 80 columns.
tab = '\t' # Tab character. Can use ' ' or '\t' * 2 to modify layout


# Lookup tables for 2.4 GHz and 5 GHz frequency to channel conversion.

lut24 = {
2412:'01 (2.4 GHz)',
2417:'02 (2.4 GHz)',
2422:'03 (2.4 GHz)',
2427:'04 (2.4 GHz)',
2432:'05 (2.4 GHz)',
2437:'06 (2.4 GHz)',
2442:'07 (2.4 GHz)',
2447:'08 (2.4 GHz)',
2452:'09 (2.4 GHz)',
2457:'10 (2.4 GHz)',
2462:'11 (2.4 GHz)',
2467:'12 (2.4 GHz)',
2472:'13 (2.4 GHz)',
2484:'14 (2.4 GHz)'
};

lut5 = {
5035:'07  (5 GHz)',
5040:'08  (5 GHz)',
5045:'09  (5 GHz)',
5055:'11  (5 GHz)',
5060:'12  (5 GHz)',
5080:'16  (5 GHz)',
5170:'34  (5 GHz)',
5180:'36  (5 GHz)',
5190:'38  (5 GHz)',
5200:'40  (5 GHz)',
5210:'42  (5 GHz)',
5220:'44  (5 GHz)',
5230:'46  (5 GHz)',
5240:'48  (5 GHz)',
5260:'52  (5 GHz)',
5280:'56  (5 GHz)',
5300:'60  (5 GHz)',
5320:'64  (5 GHz)',
5500:'100 (5 GHz)',
5520:'104 (5 GHz)',
5540:'108 (5 GHz)',
5560:'112 (5 GHz)',
5580:'116 (5 GHz)',
5600:'120 (5 GHz)',
5620:'124 (5 GHz)',
5640:'128 (5 GHz)',
5660:'132 (5 GHz)',
5680:'136 (5 GHz)',
5700:'140 (5 GHz)',
5720:'144 (5 GHz)',
5745:'149 (5 GHz)',
5765:'153 (5 GHz)',
5785:'157 (5 GHz)',
5805:'161 (5 GHz)',
5825:'165 (5 GHz)'
};

# Convert 2.4 GHz and 5 GHz to channel numbers.
def getchannel(freq):
      if freq[0] == '2':
            return lut24[int(freq)]
      if freq[0] == '5':
            return lut5[int(freq)]
      return 'N/A'

# Prints list of scanned APs for main menu and returns the list
def aplist():
      # Clear Linux console
      Popen(['clear'])

      # Scan available APs and save in scannedMenu list.
      scan = Popen(['nmcli', 'dev', 'wifi'], stdout=PIPE, stderr=STDOUT)
      scannedMenu=[]
      for line in scan.stdout:
            scannedMenu.append(line)

      # Print menu header.
      print 'No. SSID',
      print ' ' * (ssidMaxLength - 4), # White spaces minus SSID length
      print 'BSSID                    CHANNEL         POWER   ENCRYPT.'
      print u'\u00af' * 80

      # SSID and SECURITY fields may contain spaces. We use the ':' marks as the parsing ref.      
      for line in scannedMenu[1:]:
            ssid      = line.split("'")[1]
            cells      = line.split(':')
            bssid = ':'.join([cells[0][-2:],
                              cells[1], cells[2],
                              cells[3],
                              cells[4],
                              cells[5][:2]])
            cells      = cells[5].split() # Split cells using space for the rest of the fields.
            chan      = getchannel(cells[2])
            power      = cells[6]
            encrypt      = cells[7]
      
            lineNo = str(scannedMenu.index(line)) # Number APs
            print lineNo.zfill(2) + ' ',
            print ssid[:ssidMaxLength],
            print ' ' * (ssidMaxLength - len(ssid)), # Padding based on maximum SSID length
            print tab.join([bssid, chan, power, encrypt])
      print

      return scannedMenu

# Menu user input returns selected bssid and facilitates user commands like 'S' and 'Q'.
def menuinput(scannedMenu):
      ap = False

      def enterap():
            ap = raw_input('Enter access point number. Enter S for new scan or Q to quit. \n')
            if ap == 's' or ap == 'S':
                  aplist()
            if ap == 'q' or ap == 'Q':
                  sys.exit()
            if ap.isdigit() == False:
                  ap = False
            if int(ap) > len(scannedMenu) - 1: # -1 because of the nmcli header line
                  ap = False
            if int(ap) == 0:
                  print 'No such AP.'
                  ap = False
            return ap

      # User input, loop until ap no longer False
      while ap == False:
            ap = enterap()

      # User input selection assignent
      for line in scannedMenu[1:]:
            lineNo = scannedMenu.index(line) # index number      
            if int(ap) == lineNo:
                  ssidSel            = line.split("'")[1]
                  cells            = line.split(':')
                  bssidSel      = ':'.join([cells[0][-2:],
                                          cells[1],
                                          cells[2],
                                          cells[3],
                                          cells[4],
                                          cells[5][:2]])
                  return ssidSel, bssidSel


# Main menu control calls AP scanning, prints the list and calls user AP selection.
def mainControl():
      # print ap list, get the list
      scannedMenu = aplist()

      # user menu input. get selected ssid and bssid
      global scanselection # Declaring global list so it is available to AP scan below
      scanselection = menuinput(scannedMenu)

      print 'You selected:', scanselection[0], scanselection[1] + '\n'
      print 'CTRL + C exits scan mode.' + '\n'
      time.sleep(.33)

mainControl()


def red(text):
      return ('\033[91m' + text + '\033[0m')

def green(text):
      return ('\033[92m' + text + '\033[0m')

# Here we print power reading to the scaled console in form of an dynamic histogram.
def readout(power):
      if powerScaled == 0:
            print '%s is out of range or not transmitting.' %scanselection[0]
      if powerScaled < thresholdScaled:
            lPower = powerScaled
      else:
            lPower = thresholdScaled
      hPower = powerScaled - thresholdScaled
      sys.stdout.write(red(char) * lPower)
      sys.stdout.write(green(char) * hPower)
      print power
      #print '\r'

while True:
      try:
            # We declare these two variables for the scenario when the radio signal drops
            powerScaled = 0
            thresholdScaled = 0

            scan = Popen(['nmcli', 'dev', 'wifi'], stdout=PIPE, stderr=STDOUT)
            # Is the selected bssid continuosly present in the scan? If so, proceed.
            for line in scan.stdout:
                  if scanselection[1] in line:
                        cells      = line.split(':')
                        cells      = cells[5].split() # Split cells using white space for the rest.
                        power      = int(cells[6])
                  
                        # Dynamically scale max nmcli signal to maximum console width, non-dB units
                        rows, columns = os.popen('stty size', 'r').read().split()
                        scaleFactor = float(columns) / 100 # Max terminal width / max nmcli signal
                        powerScaled = int(round(power * scaleFactor))
                        thresholdScaled = int(round(threshold * scaleFactor))
            
            # Print readout      
            readout(powerScaled)

# On CTRL + C go to main menu
      except KeyboardInterrupt:
            mainControl()

Leave a Reply

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