Skip to content

Commit

Permalink
implement thresholds
Browse files Browse the repository at this point in the history
  • Loading branch information
jantman committed Aug 12, 2020
1 parent f9a8107 commit 3ca1dc7
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 28 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ The end result of this process for a given survey (Title) should be 8 ``.png`` i
* **tcp_upload_Mbps_TITLE.png** - Heatmap of iperf3 transfer rate, TCP, uploading from client to server.
* **udp_Mbps_TITLE.png** - Heatmap of iperf3 transfer rate, UDP, uploading from client to server.

If you'd like to synchronize the colors/thresholds across multiple heatmaps, such as when comparing different AP placements, you can run ``wifi-heatmap-thresholds`` passing it each of the titles / output JSON filenames. This will generate a ``thresholds.json`` file in the current directory, suitable for passing to the ``wifi-heatmap`` ``-t`` / ``--thresholds`` option.

Running In Docker
-----------------

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
'console_scripts': [
'wifi-scan = wifi_survey_heatmap.scancli:main',
'wifi-survey = wifi_survey_heatmap.ui:main',
'wifi-heatmap = wifi_survey_heatmap.heatmap:main'
'wifi-heatmap = wifi_survey_heatmap.heatmap:main',
'wifi-heatmap-thresholds = wifi_survey_heatmap.thresholds:main'
]
},
cffi_modules=[
Expand Down
90 changes: 63 additions & 27 deletions wifi_survey_heatmap/heatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,29 @@

class HeatMapGenerator(object):

def __init__(self, image_path, title, ignore_ssids=[], aps=None):
graphs = {
'rssi': 'RSSI (level)',
'quality': 'iwstats Quality',
'tcp_upload_Mbps': 'TCP Upload Mbps',
'tcp_download_Mbps': 'TCP Download Mbps',
'udp_Mbps': 'UDP Upload Mbps',
'jitter': 'UDP Jitter (ms)'
}

def __init__(
self, image_path, title, ignore_ssids=[], aps=None, thresholds=None
):
self._ap_names = {}
if aps is not None:
with open(aps, 'r') as fh:
self._ap_names = {
x.upper(): y for x, y in json.loads(fh.read()).items()
}
self._image_path = image_path
self._layout = None
self._image_width = 0
self._image_height = 0
self._corners = [(0, 0), (0, 0), (0, 0), (0, 0)]
self._title = title
if not self._title.endswith('.json'):
self._title += '.json'
Expand All @@ -143,22 +158,17 @@ def __init__(self, image_path, title, ignore_ssids=[], aps=None):
'Initialized HeatMapGenerator; image_path=%s title=%s',
self._image_path, self._title
)
self._layout = imread(self._image_path)
self._image_width = len(self._layout[0])
self._image_height = len(self._layout) - 1
self._corners = [
(0, 0), (0, self._image_height),
(self._image_width, 0), (self._image_width, self._image_height)
]
logger.debug(
'Loaded image with width=%d height=%d',
self._image_width, self._image_height
)
with open(self._title, 'r') as fh:
self._data = json.loads(fh.read())
logger.info('Loaded %d measurement points', len(self._data))

def generate(self):
self.thresholds = {}
if thresholds is not None:
logger.info('Loading thresholds from: %s', thresholds)
with open(thresholds, 'r') as fh:
self.thresholds = json.loads(fh.read())
logger.debug('Thresholds: %s', self.thresholds)

def load_data(self):
a = defaultdict(list)
for row in self._data:
a['x'].append(row['x'])
Expand All @@ -179,6 +189,24 @@ def generate(self):
a['ap'].append(ap + '_2.4')
else:
a['ap'].append(ap + '_5G')
return a

def _load_image(self):
self._layout = imread(self._image_path)
self._image_width = len(self._layout[0])
self._image_height = len(self._layout) - 1
self._corners = [
(0, 0), (0, self._image_height),
(self._image_width, 0), (self._image_width, self._image_height)
]
logger.debug(
'Loaded image with width=%d height=%d',
self._image_width, self._image_height
)

def generate(self):
self._load_image()
a = self.load_data()
for x, y in self._corners:
a['x'].append(x)
a['y'].append(y)
Expand All @@ -195,14 +223,7 @@ def generate(self):
y = np.linspace(0, self._image_height, num_y)
gx, gy = np.meshgrid(x, y)
gx, gy = gx.flatten(), gy.flatten()
for k, ptitle in {
'rssi': 'RSSI (level)',
'quality': 'iwstats Quality',
'tcp_upload_Mbps': 'TCP Upload Mbps',
'tcp_download_Mbps': 'TCP Download Mbps',
'udp_Mbps': 'UDP Upload Mbps',
'jitter': 'UDP Jitter (ms)'
}.items():
for k, ptitle in self.graphs.items():
self._plot(
a, k, '%s - %s' % (self._title, ptitle), gx, gy, num_x, num_y
)
Expand Down Expand Up @@ -300,6 +321,7 @@ def _add_inner_title(self, ax, title, loc, size=None, **kwargs):
return at

def _plot(self, a, key, title, gx, gy, num_x, num_y):
logger.debug('Plotting: %s', key)
pp.rcParams['figure.figsize'] = (
self._image_width / 300, self._image_height / 300
)
Expand All @@ -313,15 +335,26 @@ def _plot(self, a, key, title, gx, gy, num_x, num_y):
# Render the interpolated data to the plot
pp.axis('off')
# begin color mapping
norm = matplotlib.colors.Normalize(
vmin=min(a[key]), vmax=max(a[key]), clip=True
)
if 'min' in self.thresholds.get(key, {}):
vmin = self.thresholds[key]['min']
logger.debug('Using min threshold from thresholds: %s', vmin)
else:
vmin = min(a[key])
logger.debug('Using calculated min threshold: %s', vmin)
if 'max' in self.thresholds.get(key, {}):
vmax = self.thresholds[key]['max']
logger.debug('Using max threshold from thresholds: %s', vmax)
else:
vmax = max(a[key])
logger.debug('Using calculated max threshold: %s', vmax)
norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax, clip=True)
mapper = cm.ScalarMappable(norm=norm, cmap='RdYlBu_r')
# end color mapping
image = pp.imshow(
z,
extent=(0, self._image_width, self._image_height, 0),
cmap='RdYlBu_r', alpha=0.5, zorder=100
cmap='RdYlBu_r', alpha=0.5, zorder=100,
vmin=vmin, vmax=vmax
)
pp.colorbar(image)
pp.imshow(self._layout, interpolation='bicubic', zorder=1, alpha=1)
Expand Down Expand Up @@ -359,6 +392,8 @@ def parse_args(argv):
help='verbose output. specify twice for debug-level output.')
p.add_argument('-i', '--ignore', dest='ignore', action='append',
default=[], help='SSIDs to ignore from channel graph')
p.add_argument('-t', '--thresholds', dest='thresholds', action='store',
type=str, help='thresholds JSON file path')
p.add_argument('-a', '--ap-names', type=str, dest='aps', action='store',
default=None,
help='If specified, a JSON file mapping AP MAC/BSSID to '
Expand Down Expand Up @@ -412,7 +447,8 @@ def main():
set_log_info()

HeatMapGenerator(
args.IMAGE, args.TITLE, ignore_ssids=args.ignore, aps=args.aps
args.IMAGE, args.TITLE, ignore_ssids=args.ignore, aps=args.aps,
thresholds=args.thresholds
).generate()


Expand Down
130 changes: 130 additions & 0 deletions wifi_survey_heatmap/thresholds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
The latest version of this package is available at:
<http://github.com/jantman/wifi-survey-heatmap>
##################################################################################
Copyright 2017 Jason Antman <[email protected]> <http://www.jasonantman.com>
This file is part of wifi-survey-heatmap, also known as wifi-survey-heatmap.
wifi-survey-heatmap is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
wifi-survey-heatmap is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with wifi-survey-heatmap. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
##################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/wifi-survey-heatmap> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
##################################################################################
AUTHORS:
Jason Antman <[email protected]> <http://www.jasonantman.com>
##################################################################################
"""

import sys
import argparse
import logging
import json
from collections import defaultdict

from wifi_survey_heatmap.heatmap import HeatMapGenerator

FORMAT = "[%(asctime)s %(levelname)s] %(message)s"
logging.basicConfig(level=logging.WARNING, format=FORMAT)
logger = logging.getLogger()


class ThresholdGenerator(object):

def generate(self, titles):
res = defaultdict(dict)
items = [HeatMapGenerator(None, t).load_data() for t in titles]
for key in HeatMapGenerator.graphs.keys():
res[key]['min'] = min([
min(x[key]) for x in items
])
res[key]['max'] = max([
max(x[key]) for x in items
])
with open('thresholds.json', 'w') as fh:
fh.write(json.dumps(res))
logger.info('Wrote: thresholds.json')


def parse_args(argv):
"""
parse arguments/options
this uses the new argparse module instead of optparse
see: <https://docs.python.org/2/library/argparse.html>
"""
p = argparse.ArgumentParser(
description='wifi survey heatmap threshold generator'
)
p.add_argument('-v', '--verbose', dest='verbose', action='count', default=0,
help='verbose output. specify twice for debug-level output.')
p.add_argument(
'TITLE', type=str, help='Title for survey (and data filename)',
nargs='+'
)
args = p.parse_args(argv)
return args


def set_log_info():
"""set logger level to INFO"""
set_log_level_format(logging.INFO,
'%(asctime)s %(levelname)s:%(name)s:%(message)s')


def set_log_debug():
"""set logger level to DEBUG, and debug-level output format"""
set_log_level_format(
logging.DEBUG,
"%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - "
"%(name)s.%(funcName)s() ] %(message)s"
)


def set_log_level_format(level, format):
"""
Set logger level and format.
:param level: logging level; see the :py:mod:`logging` constants.
:type level: int
:param format: logging formatter format string
:type format: str
"""
formatter = logging.Formatter(fmt=format)
logger.handlers[0].setFormatter(formatter)
logger.setLevel(level)


def main():
args = parse_args(sys.argv[1:])

# set logging level
if args.verbose > 1:
set_log_debug()
elif args.verbose == 1:
set_log_info()

ThresholdGenerator().generate(args.TITLE)


if __name__ == '__main__':
main()

0 comments on commit 3ca1dc7

Please sign in to comment.