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()