Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions wireless_monitor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 2.8.3)
project(wireless_monitor)

#############
## Build ##
#############

find_package(catkin REQUIRED)

catkin_python_setup()

catkin_package()

#############
## Test ##
#############

if (CATKIN_ENABLE_TESTING)
catkin_add_nosetests(test)
endif()

#############
## Install ##
#############

install(PROGRAMS
scripts/wireless_monitor
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
13 changes: 13 additions & 0 deletions wireless_monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Wireless monitor

## Nodes

### wireless_monitor

Monitors a given wireless interface using `psutil` and `iwconfig`.

#### Parameters

- `~interface`: Wireless interface to monitor (default=`'wlo1'`)
- `~link_quality_warning_percentage`: On what link quality a warning level will be given (default=`0.5`)
- `~rate`: Publish rate (default=`1`)
13 changes: 13 additions & 0 deletions wireless_monitor/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<package format="2">
<name>wireless_monitor</name>
<version>0.0.0</version>
<description>Monitor a wireless network interface</description>
<maintainer email="rein@eurotec.com">Rein Appeldoorn</maintainer>

<license>BSD</license>

<buildtool_depend>catkin</buildtool_depend>

<exec_depend>python-psutil</exec_depend>
<exec_depend>wireless-tools</exec_depend>
</package>
76 changes: 76 additions & 0 deletions wireless_monitor/scripts/wireless_monitor
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

import rospy
from diagnostic_msgs.msg import DiagnosticStatus
from diagnostic_updater import DiagnosticTask, Updater

from wireless_monitor.wireless import get_iwconfig_dict_and_link_quality, get_wifi_ifname, NetCount


class WirelessTask(DiagnosticTask):
def __init__(self, interface, link_quality_warning_percentage):
DiagnosticTask.__init__(self, "Wireless Information")
self._interface = interface
self._link_quality_warning_percentage = float(link_quality_warning_percentage)
self._net_count = NetCount(interface)

def _update_with_iwconfig_status(self, stat):
iwconfig_dict, link_quality = get_iwconfig_dict_and_link_quality(self._interface)

if link_quality is None:
stat.summary(DiagnosticStatus.OK, "Link quality unknown")
elif link_quality > self._link_quality_warning_percentage:
stat.summary(DiagnosticStatus.OK, "Link quality OK")
else:
stat.summary(DiagnosticStatus.WARN, "Link quality low")

for k, v in iwconfig_dict.iteritems():
stat.add(k, v)

def _update_with_psutil_net_io_counter(self, stat):
for k, v in self._net_count.get_net_count_dict().items():
stat.add(k, v)

def run(self, stat):
if self._interface is None:
stat.summary(DiagnosticStatus.OK, "No wireless device")
return stat

try:
self._update_with_iwconfig_status(stat)
self._update_with_psutil_net_io_counter(stat)
except RuntimeError as e:
stat.summary(DiagnosticStatus.ERROR, "Failed to update wireless information: {}".format(e))

return stat


def main():
rospy.init_node('wireless_monitor')

interface = None
try:
interface = get_wifi_ifname(rospy.get_param('~interface', None))
except Exception as e:
rospy.logerr(e)

updater = Updater()
updater.setHardwareID(str(interface))
updater.add(WirelessTask(
interface,
rospy.get_param("~link_quality_warning_percentage", 0.0)
))

rate = rospy.Rate(rospy.get_param("~rate", 1))
while not rospy.is_shutdown():
updater.force_update() # We want do determine the rate ourselves (not the internal rate of the updater)
rate.sleep()


if __name__ == '__main__':
main()
16 changes: 16 additions & 0 deletions wireless_monitor/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup

d = generate_distutils_setup(
packages=['wireless_monitor'],
package_dir={'': 'src'}
)

setup(**d)
28 changes: 28 additions & 0 deletions wireless_monitor/src/wireless_monitor/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

import subprocess


def execute_command(cmd, shell=False, cwd=None):
"""
Execute a command and return the stripped output
:param cmd: The command (list) to run
:param shell: Whether to use the shell executor
:param cwd: Current working directory
:return: Stripped output
:raises RuntimeError
"""
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, cwd=cwd)
except OSError as e:
raise RuntimeError("Could not execute command {}: {}".format(cmd, e))

stdout, stderr = p.communicate()

if p.returncode != 0:
raise RuntimeError("Could not execute command {}".format(cmd))
return stdout.strip()
108 changes: 108 additions & 0 deletions wireless_monitor/src/wireless_monitor/wireless.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

import os
import psutil
import re
import time

from .util import execute_command


def get_wifi_ifnames():
"""
Get a list of wifi interface names
:return: List of interface names
"""
return [ifname for ifname in os.listdir('/sys/class/net/') if ifname[:2] == "wl"]


def get_wifi_ifname(wifi_ifname_preference=None):
"""
Get wifi interface name with an optional preference

If the preference is specified, the preference will be returned if it exists
If the preference is NOT specified, the first wireless adapter will be returned

:param wifi_ifname_preference: Optiona wifi ifname prefence e.g. wlan0
:return: The wifi interface name
:raises RuntimeError if there are no wireless devices available
:raises ValueError if there are wireless devices available but the preference does not exist
"""
wifi_ifnames = get_wifi_ifnames()
if not wifi_ifnames:
raise RuntimeError("No wifi ifnames found")
if wifi_ifname_preference:
if wifi_ifname_preference in wifi_ifnames:
return wifi_ifname_preference
raise ValueError(
"Wifi ifname preference {} not in wifi ifnames {}".format(wifi_ifname_preference, wifi_ifnames))
return wifi_ifnames[0]


def _parse_iwconfig_output(output):
result = {}
items = [e.strip() for e in output.replace('\n', ' ').split(' ') if ":" in e or "=" in e]
for item in items:
key, value = re.search('(.+?)[:=](.+)', item).groups()
result[key] = value.strip().strip("\"")
return result


def get_iwconfig_dict_and_link_quality(ifname):
"""
Get the iwconfig status dictionary + link quality of an interface
:param ifname: Wireless interface name
:return: iwconfig status dictionary + link quality
:raises: RuntimeError if the iwconfig information cannot be obtained
"""
output = execute_command('iwconfig {}'.format(ifname), shell=True)
iwconfig_dict = _parse_iwconfig_output(output)

link_quality = None
if 'Link Quality' in iwconfig_dict:
numerator, denominator = iwconfig_dict['Link Quality'].split("/")
link_quality = float(numerator) / float(denominator)

return iwconfig_dict, link_quality


class NetCount(object):
def __init__(self, ifname):
"""
Net counter that monitors traffic
:param ifname: Interface name
"""
self._ifname = ifname
self._last_count = None

def get_net_count_dict(self):
"""
Get the net count + rate for the specified interface
:return: Net count dictionary
:raises RuntimeError if getting counters fails
"""
def _rate(current, last, dt):
return ((current - last) / 1e3) / dt

now = time.time()
counts = psutil.net_io_counters(pernic=True)
if self._ifname not in counts:
raise RuntimeError("Could not get net IO counter for interface {}".format(self._ifname))

count = counts[self._ifname]
result = dict(count.__dict__)

if self._last_count:
last_stamp, last_count = self._last_count
dt = now - last_stamp

if dt > 0:
result["Download rate [KB/s]"] = "{:.3f}".format(_rate(count.bytes_recv, last_count.bytes_recv, dt))
result["Upload rate [KB/s]"] = "{:.3f}".format(_rate(count.bytes_sent, last_count.bytes_sent, dt))

self._last_count = now, count
return result
18 changes: 18 additions & 0 deletions wireless_monitor/test/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

from nose.tools import raises

from wireless_monitor.util import execute_command


def test_execute_command():
assert "hi" == execute_command(["echo", "hi"])


@raises(RuntimeError)
def test_execute_invalid_command():
execute_command(["--&*(", "hi"])
55 changes: 55 additions & 0 deletions wireless_monitor/test/test_wireless.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#
# Copyright (c) 2019, Eurotec, Netherlands
# All rights reserved.
#
# \author Rein Appeldoorn

import random
from nose.tools import raises

from wireless_monitor.wireless import get_wifi_ifname, get_iwconfig_dict_and_link_quality, get_wifi_ifnames, NetCount

wifi_ifnames = get_wifi_ifnames()


def test_wifi_ifnames():
if wifi_ifnames:
assert get_wifi_ifname() in wifi_ifnames

wifi_ifname = random.choice(wifi_ifnames)
assert get_wifi_ifname(wifi_ifname) == wifi_ifname

try:
get_wifi_ifname("lan0")
except Exception as e:
assert isinstance(e, ValueError)
else:
try:
get_wifi_ifname()
except Exception as e:
assert isinstance(e, RuntimeError)


def test_iwconfig():
if wifi_ifnames:
iwconfig_dict, quality = get_iwconfig_dict_and_link_quality(get_wifi_ifname())
assert isinstance(iwconfig_dict, dict)
assert quality is None or isinstance(quality, float)


@raises(RuntimeError)
def test_invalid_iwconfig():
get_iwconfig_dict_and_link_quality("lan0")


def test_net_count():
if wifi_ifnames:
nc = NetCount(get_wifi_ifname())
assert "Download rate [KB/s]" not in nc.get_net_count_dict()
assert "Download rate [KB/s]" in nc.get_net_count_dict()


@raises(RuntimeError)
def test_invalid_net_count():
nc = NetCount("invalid_interface")
nc.get_net_count_dict()