diff --git a/examples/sniffer/Makefile b/examples/sniffer/Makefile new file mode 100644 index 0000000000..c5b26f562b --- /dev/null +++ b/examples/sniffer/Makefile @@ -0,0 +1,23 @@ +# Set the name of your application: +APPLICATION = sniffer + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# Define modules that are used +USEMODULE += fmt +USEMODULE += gnrc +USEMODULE += netdev_default +USEMODULE += auto_init_gnrc_netif +USEMODULE += shell +USEMODULE += shell_commands +USEMODULE += ps +USEMODULE += ztimer64_usec + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/sniffer/README.md b/examples/sniffer/README.md new file mode 100644 index 0000000000..14dbe0c9df --- /dev/null +++ b/examples/sniffer/README.md @@ -0,0 +1,22 @@ +About +===== + +This application is built to run together with the script `./tools/sniffer.py` +as a sniffer for (wireless) data traffic. This application works with any board +with any network device that supports the gnrc network stack (or precisely the +gnrc parts up to the link-layer). Further the network device (and its driver) +needs to support promiscuous and raw mode for usable output. Finally the board +needs to include auto-initialization code for the targeted network device. + + +Usage +===== + +Compile and flash this application to the board of your choice. You can check +if everything on the RIOT side works by connecting to the board via UART and by +checking with `ifconfig` if a network device is available. Also note the +interface number for the following commands. Then you can check with +`ifconfig promisc` if promiscuous mode is supported and with +`ifconfig raw` if raw mode is supported by the driver/network device. + +For further information on setting up the host part, see `./tools/README.md`. diff --git a/examples/sniffer/main.c b/examples/sniffer/main.c new file mode 100644 index 0000000000..48314c406c --- /dev/null +++ b/examples/sniffer/main.c @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015-18 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup app_sniffer + * @brief Sniffer application based on the new network stack + * @{ + * + * @file + * @brief Sniffer application for RIOT + * + * @author Hauke Petersen + * @author Martine Lenders + * + * @} + */ + +#include + +#include "fmt.h" +#include "thread.h" +#include "shell.h" +#include "net/gnrc.h" +#include "ztimer64.h" + +/** + * @brief Buffer size used by the shell + */ +#define SHELL_BUFSIZE (64U) + +/** + * @brief Priority of the RAW dump thread + */ +#define RAWDUMP_PRIO (THREAD_PRIORITY_MAIN - 1) + +/** + * @brief Message queue size of the RAW dump thread + */ +#define RAWDUMP_MSG_Q_SIZE (32U) + +/** + * @brief Stack for the raw dump thread + */ +static char rawdmp_stack[THREAD_STACKSIZE_SMALL]; + +/** + * @brief Make a raw dump of the given packet contents + */ +void dump_pkt(gnrc_pktsnip_t *pkt) +{ + gnrc_pktsnip_t *snip = pkt; + uint8_t lqi = 0; + if (pkt->next) { + if (pkt->next->type == GNRC_NETTYPE_NETIF) { + gnrc_netif_hdr_t *netif_hdr = pkt->next->data; + lqi = netif_hdr->lqi; + pkt = gnrc_pktbuf_remove_snip(pkt, pkt->next); + } + } + uint64_t now_us = ztimer64_now(ZTIMER64_USEC); + + print_str("rftest-rx --- len "); + print_u32_hex((uint32_t)gnrc_pkt_len(pkt)); + print_str(" lqi "); + print_byte_hex(lqi); + print_str(" rx_time "); + print_u64_hex(now_us); + print_str("\n"); + while (snip) { + for (size_t i = 0; i < snip->size; i++) { + print_byte_hex(((uint8_t *)(snip->data))[i]); + print_str(" "); + } + snip = snip->next; + } + print_str("\n\n"); + + gnrc_pktbuf_release(pkt); +} + +/** + * @brief Event loop of the RAW dump thread + * + * @param[in] arg unused parameter + */ +void *rawdump(void *arg) +{ + msg_t msg_q[RAWDUMP_MSG_Q_SIZE]; + + (void)arg; + msg_init_queue(msg_q, RAWDUMP_MSG_Q_SIZE); + while (1) { + msg_t msg; + + msg_receive(&msg); + switch (msg.type) { + case GNRC_NETAPI_MSG_TYPE_RCV: + dump_pkt((gnrc_pktsnip_t *)msg.content.ptr); + break; + default: + /* do nothing */ + break; + } + } + + /* never reached */ + return NULL; +} + +/** + * @brief Maybe you are a golfer?! + */ +int main(void) +{ + gnrc_netreg_entry_t dump; + + puts("RIOT sniffer application"); + + /* start and register rawdump thread */ + puts("Run the rawdump thread and register it"); + dump.target.pid = thread_create(rawdmp_stack, sizeof(rawdmp_stack), RAWDUMP_PRIO, + THREAD_CREATE_STACKTEST, rawdump, NULL, "rawdump"); + dump.demux_ctx = GNRC_NETREG_DEMUX_CTX_ALL; + gnrc_netreg_register(GNRC_NETTYPE_UNDEF, &dump); + + /* start the shell */ + puts("All ok, starting the shell now"); + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(NULL, line_buf, SHELL_DEFAULT_BUFSIZE); + + return 0; +} diff --git a/examples/sniffer/tools/README.md b/examples/sniffer/tools/README.md new file mode 100644 index 0000000000..7a8b2310f9 --- /dev/null +++ b/examples/sniffer/tools/README.md @@ -0,0 +1,114 @@ +# RIOT Sniffer Application + + +## About + +This sniffer script can be used to monitor and capture network traffic using +a RIOT based node. It is primarily designed for sniffing wireless data traffic, +but can also well be used for wired network traffic, as long as used network +devices support promiscuous mode and output of raw data. + +The python script `sniffer.py` requires a RIOT node running the sniffer app, its +source code is located in this repository (see main folder). This node outputs +received network traffic via a serial port or a network socket in the common +Wireshark/libpcap (pcap) format. This output is then parsed by the `sniffer.py` +script included in this folder run on a host computer. + +The `sniffer.py` script is a modified version of [malvira's script](https://github.com/malvira/libmc1322x/blob/master/tools/rftestrx2pcap.py) +for the Redbee Ecotag (https://github.com/malvira/libmc1322x/wiki/wireshark). + +## Dependencies + +The `sniffer.py` script is written in Python and needs [pyserial](https://pypi.python.org/pypi/pyserial). + +Installing the dependencies: + + +#### Debuntu + apt-get install python-serial + +#### PIP + pip install pyserial + + +## Usage + +General usage: + +1. Flash an applicable RIOT node with the sniffer application (insert path to + RIOT source and board name), as follows: +``` +$ git clone https://github.com/RIOT-OS/applications/ +$ cd applications/sniffer +$ BOARD= make clean all flash +``` + +2. Run the `sniffer.py` script (change to subfolder `tools/`) as follows : + For serial port: +``` +$ ./sniffer.py [-b baudrate] [outfile] +``` +For network socket: +``` +$ ./sniffer.py : [outfile] +``` + +You should see output like below: +``` +ifconfig 3 set chan 26 +ifconfig 3 raw +ifconfig 3 promisc +RX: 0 +``` + +For detailed information on the parameters use the scripts on-line help: + +``` +./sniffer.py -h +``` + +### Examples + +The following examples are made when using the sniffer application together with +an `iotlab-m3` node that is connected to `/dev/ttyUSB1`(or COM1) (`serial` connection type) +and runs per default with a baudrate of 500000. For the `socket` connection type port 20000 +is used. + +#### Linux (serial) + +Dump packets to a file: +``` +$ ./sniffer.py -b 500000 /dev/ttyUSB1 17 foo.pcap +``` + +This .pcap can then be opened in Wireshark. + +Alternatively for live captures, you can pipe directly into Wireshark with: +``` +$ ./sniffer.py -b 500000 /dev/ttyUSB1 17 | wireshark -k -i - +``` + +#### Windows (serial) + +For windows you can use the optional third argument to output to a +.pcap: + +``` +$ ./sniffer.py -b 500000 COM1 17 foo.pcap +``` + +#### IoT-Lab Testbed (socket) + +Start an experiment either via the website provided by the IoT-Lab testbed or +by using the RIOT specific iotlab Makefile with 3 neighboring `iotlab-m3` nodes, +where one of them runs the sniffer application and the others run the `gnrc_networking` application. + +Now you can bind the sniffer node to localhost: +ssh -L 20000:_node-id_:20000 _user_@_site_.iot-lab.info + +Then you can dump or observe the traffic generated by the other nodes running the `gnrc_networking` +application via one of the following commands: +``` +$ ./sniffer.py localhost:20000 26 foo.pcap +$ ./sniffer.py localhost:20000 26 | wireshark -k -i - +``` diff --git a/examples/sniffer/tools/sniffer.py b/examples/sniffer/tools/sniffer.py new file mode 100755 index 0000000000..07ea50e013 --- /dev/null +++ b/examples/sniffer/tools/sniffer.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +(C) 2012, Mariano Alvira +(C) 2014, Oliver Hahm +(C) 2015, Hauke Petersen +(C) 2015, Martine Lenders +(C) 2015, Cenk Gündoğan + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the Institute nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +""" + +from __future__ import print_function +import argparse +import sys +import re +import socket +from time import sleep, time +from struct import pack +from serial import Serial + +# PCAP setup +MAGIC = 0xA1B2C3D4 +MAJOR = 2 +MINOR = 4 +ZONE = 0 +SIG = 0 +SNAPLEN = 0xFFFF +NETWORK = 230 # 802.15.4 no FCS + +DEFAULT_BAUDRATE = 115200 + + +def configure_interface(port, channel): + line = "" + iface = 0 + port.write("ifconfig\n".encode()) + while True: + line = port.readline() + if line == b"": + print("Application has no network interface defined", file=sys.stderr) + sys.exit(2) + match = re.search(r"^Iface +(\d+)", line.decode(errors="ignore")) + if match is not None: + iface = int(match.group(1)) + break + + # set channel, raw mode, and promiscuous mode + print("ifconfig %d set chan %d" % (iface, channel), file=sys.stderr) + print("ifconfig %d raw" % iface, file=sys.stderr) + print("ifconfig %d promisc" % iface, file=sys.stderr) + port.write(("ifconfig %d set chan %d\n" % (iface, channel)).encode()) + port.write(("ifconfig %d raw\n" % iface).encode()) + port.write(("ifconfig %d promisc\n" % iface).encode()) + + +def generate_pcap(port, out): + # count incoming packets + count = 0 + # output overall PCAP header + out.write(pack("? *rftest-rx --- len (\w+).*", line.decode(errors="ignore") + ) + if pkt_header: + now = time() + sec = int(now) + usec = int((now - sec) * 1000000) + length = int(pkt_header.group(1), 16) + out.write(pack(" (3,): + default_outfile = sys.stdout.buffer + else: + default_outfile = sys.stdout + p = argparse.ArgumentParser() + p.add_argument( + "-b", + "--baudrate", + type=int, + default=DEFAULT_BAUDRATE, + help="Baudrate of the serial port (only evaluated " + "for non TCP-terminal, default: %d)" % DEFAULT_BAUDRATE, + ) + p.add_argument( + "conn", + metavar="tty/host:port", + type=str, + help="Serial port or TCP (host, port) tuple to " + "terminal with sniffer application", + ) + p.add_argument("channel", type=int, help="Channel to sniff on") + p.add_argument( + "outfile", + type=argparse.FileType("w+b"), + default=default_outfile, + nargs="?", + help="PCAP file to output to (default: stdout)", + ) + args = p.parse_args() + + conn = connect(args) + + sleep(1) + configure_interface(conn, args.channel) + sleep(1) + + try: + generate_pcap(conn, args.outfile) + except KeyboardInterrupt: + conn.close() + print() + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/examples/spectrum-scanner/Makefile b/examples/spectrum-scanner/Makefile new file mode 100644 index 0000000000..c59bd76b51 --- /dev/null +++ b/examples/spectrum-scanner/Makefile @@ -0,0 +1,21 @@ +# Set the name of your application: +APPLICATION = spectrum-scanner + +# If no BOARD is found in the environment, use this default: +BOARD ?= samr21-xpro + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# Define modules that are used +USEMODULE += gnrc +USEMODULE += netdev_default +USEMODULE += auto_init_gnrc_netif +USEMODULE += xtimer +USEMODULE += ztimer64_xtimer_compat +USEMODULE += fmt + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/spectrum-scanner/README.md b/examples/spectrum-scanner/README.md new file mode 100644 index 0000000000..39ec342a8a --- /dev/null +++ b/examples/spectrum-scanner/README.md @@ -0,0 +1,36 @@ +About +===== + +This application is designed to run together with the script +`tools/plot_rssi.py` to monitor energy levels on all +available wireless channels. This application works with any board with a +network device that supports the gnrc network stack (or precisely the gnrc parts +up to the link-layer). Further, in order to get any measurements from the +device, the network device and its driver needs to support the netopt options +for executing a manual CCA and getting the last ED level +(NETOPT_IS_CHANNEL_CLR, and NETOPT_LAST_ED_LEVEL, respectively). Finally the +board needs to include auto-initialization code for the targeted network device. + +Usage +===== + +Compile and flash this application to the board of your choice. You can check if +everything on the RIOT side works by connecting to the board via UART and +looking at the output. + +Data format +=========== + +The format of the data on the UART is: + +``` +[interface_number, timestamp, count] channel: ed_level, channel: ed_level, ... +``` + +where interface_number is the interface number (in RIOT's `ifconfig`), timestamp +is the time since power on in microseconds, count is the number of measurements +that were averaged, channel is a channel number, ed_level is the average energy +level for that channel since the last update. + +For further information on setting up the host part, see +`tools/README.md`. diff --git a/examples/spectrum-scanner/main.c b/examples/spectrum-scanner/main.c new file mode 100644 index 0000000000..c95cd8dc7a --- /dev/null +++ b/examples/spectrum-scanner/main.c @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2017 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup app_spectrum_scanner + * @brief Scanner application to find free channels + * @{ + * + * @file + * @brief Spectrum scanner application for RIOT + * + * @author Joakim Nohlgård + * + * @} + */ + +#include +#include + +#include "fmt.h" +#include "thread.h" +#include "xtimer.h" +#include "net/ieee802154.h" +#include "net/gnrc.h" + +/* Scanning interval */ +#define INTERVAL (500U * US_PER_MS) + +/** + * @brief Measure the radio energy spectrum and print on stdout + * + * Algorithm description: + * + * The process will repeat as many measurements as possible during the + * measurement interval, before the average power is computed. This reduces the + * noise in the measurement and will yield a better image of what the radio + * environment contains. + * + * Still, 122 measurements per second (frdm-kw41z) and 128 us per measurement + * will only give a time coverage of about 1.5%, but because the measurements are + * spread out over time they should still give a good representation of which + * channels are free. + * + * Note that because the ED values are given in decibels, the average radio + * power is not the same as the arithmetic mean of the ED measurements. To + * compute the average of the dB measurements this algorithm requires both + * logarithm and exponentiation, quite heavy operations on the kinds of CPUs + * that RIOT targets. Increasing the CPU clock frequency may therefore reduce + * the noise in the output, because of the more frequent energy measurements + * possible. + */ +void spectrum_scanner(unsigned long interval_us) +{ + size_t netif_numof = gnrc_netif_numof(); + + /* Using expf(x) (natural exponent) gives quicker computations on Cortex-M0+, + * compared to using powf(10, x). */ + /* + * This was optimized by testing different combinations of expf, powf, logf, log10f: + * + * functions used | measurement iterations per 0.5 s on reference system (frdm-kw41z) + * ------------------------------------------------------------------ + * expf, logf | 64 + * powf, log10f | 46 + * expf, log10f | 61 + * no-op (baseline) | 83 (but the measurements are useless) + */ + + while (1) { + /* Stack optimization, statically allocate this buffer */ + float ed_average[netif_numof][IEEE802154_CHANNEL_MAX + 1]; + + memset(ed_average, 0, sizeof(ed_average)); + + uint64_t last_wakeup = xtimer_now_usec64(); + uint64_t target = last_wakeup + interval_us; + /* We spin and try to do as many measurements as possible in the + * interval time */ + unsigned int count = 0; + do { + gnrc_netif_t *netif = NULL; + for (unsigned int k = 0; (netif = gnrc_netif_iter(netif)); k++) { + for (unsigned int ch = IEEE802154_CHANNEL_MIN; ch <= IEEE802154_CHANNEL_MAX; ++ch) { + uint16_t tmp_ch = ch; + int res; + res = gnrc_netapi_set(netif->pid, NETOPT_CHANNEL, 0, &tmp_ch, sizeof(uint16_t)); + if (res < 0) { + continue; + } + netopt_enable_t tmp; + /* Perform CCA to update ED level */ + res = gnrc_netapi_get(netif->pid, NETOPT_IS_CHANNEL_CLR, 0, &tmp, sizeof(netopt_enable_t)); + if (res < 0) { + continue; + } + int8_t level = 0; + res = gnrc_netapi_get(netif->pid, NETOPT_LAST_ED_LEVEL, 0, &level, sizeof(int8_t)); + if (res < 0) { + continue; + } + /* Convert dB to pseudo-energy before summing together the + * measurements. "Pseudo" because we use the natural + * exponential function e^x instead of computing 10^x which + * would be required if we needed the real measured energy. + * There is no need to know the real energy level because we + * will be converting back to dB again before printing. */ + ed_average[k][ch] += expf((float)level / 128.f); + } + } + ++count; + thread_yield(); + } while (xtimer_now_usec64() < target); + for (unsigned int k = 0; k < netif_numof; ++k) { + print("[", 1); + print_u32_dec(k); + print(", ", 2); + print_u64_dec(target); + print(", ", 2); + print_u32_dec(count); + print("] ", 2); + for (unsigned int ch = IEEE802154_CHANNEL_MIN; ch <= IEEE802154_CHANNEL_MAX; ++ch) { + /* Compute the average pseudo-energy and convert back to dB */ + ed_average[k][ch] = logf(ed_average[k][ch] / count) * 128.f; + print_u32_dec(ch); + print(": ", 2); + print_float(ed_average[k][ch], 4); + print(", ", 2); + } + print("\n", 1); + } + } +} + +int main(void) +{ + puts("RIOT scanner application"); + + spectrum_scanner(INTERVAL); + return 0; +} diff --git a/examples/spectrum-scanner/tools/README.md b/examples/spectrum-scanner/tools/README.md new file mode 100644 index 0000000000..1f1832e59e --- /dev/null +++ b/examples/spectrum-scanner/tools/README.md @@ -0,0 +1,40 @@ +# RIOT Spectrum Scanner Application + +## About + +This script can be used to plot the radio signal spectrum when a connected node +is running the spectrum-scanner application application located in the parent +directory. +This node scans over the available radio channels performing CCA measurements +and outputting the measured ED level via a serial port. This output is then +parsed by the `plot_rssi.py` script included in this folder run on a host computer. + +## Dependencies + +The `plot_rssi.py` script requires [pyserial](https://pypi.python.org/pypi/pyserial) +for the serial port access, and [matplotlib](https://matplotlib.org) and +[numpy](http://www.numpy.org) for the plotting functionality. + +Installing the dependencies: + +#### Debian/Ubuntu + apt-get install python-serial python-matplotlib python-numpy + +#### PIP + pip install pyserial matplotlib numpy + +## Usage + +General usage: + +1. Flash a RIOT node with the spectrum-scanner application from +(https://github.com/RIOT-OS/applications/tree/master/spectrum-scanner) + +2. Run the `plot_rssi.py` script +``` +$ ./plot_rssi.py -b +``` + +### Examples + +![Example screenshot](https://github.com/RIOT-OS/applications/tree/master/spectrum-scanner/tools/example.png) diff --git a/examples/spectrum-scanner/tools/example.png b/examples/spectrum-scanner/tools/example.png new file mode 100644 index 0000000000..b7c538ec03 Binary files /dev/null and b/examples/spectrum-scanner/tools/example.png differ diff --git a/examples/spectrum-scanner/tools/plot_rssi.py b/examples/spectrum-scanner/tools/plot_rssi.py new file mode 100755 index 0000000000..9db9b23d54 --- /dev/null +++ b/examples/spectrum-scanner/tools/plot_rssi.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Eistec AB +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import re +import logging +import argparse +import serial +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + + +class SpectrumEmitter(object): + def __init__(self, port): + self.port = port + + def data_gen(self): + logging.info("Begin collecting data from serial port") + while True: + # Read one line from the spectrum device + line = self.port.readline().rstrip() + pkt_data = re.match( + r"\[([-+]?\d+),\s*([-+]?\d+),\s*([-+]?\d+)\]\s*(.*)", + line.decode(errors="replace"), + ) + if pkt_data: + ed = {} + try: + iface_id = int(pkt_data.group(1)) + timestamp = int(pkt_data.group(2)) + count = int(pkt_data.group(3)) + except ValueError: + # Incorrect data received, probably UART noise or debugging + # messages from the device, not much else we can do other + # than try again with the next line + continue + logging.debug("data: if=%d cnt=%d t=%d", iface_id, count, timestamp) + raw = pkt_data.group(4) + for ch_ed in raw.split(","): + try: + pair = ch_ed.split(":") + ch = int(pair[0]) + ed[ch] = float(pair[1]) + except (ValueError, IndexError): + continue + yield ed + + +class RSSIPlot(object): + def __init__(self, ax, *args, tlen=120, dt=0.5, nchannels=27): + self.ax = ax + self.count = 0 + self.dt = dt + self.tlen = tlen + # Generate mesh for plotting, this creates a grid of nchannel rows and + # (tlen / dt) columns + self.Y, self.X = np.mgrid[ + slice(0 - 0.5, nchannels + 0.5, 1), + slice(-self.tlen - self.dt / 2, 0 + 1 - self.dt / 2, self.dt), + ] + Z = np.zeros_like(self.X) + # X and Y are the bounds, so Z should be the value *inside* those bounds. + # Therefore, remove the last row and column from the Z array. + self.Z = Z[:-1, :-1] + self.pcm = self.ax.pcolormesh( + self.X, self.Y, self.Z, vmin=-100, vmax=-20, cmap=plt.cm.get_cmap("jet") + ) + self.ax.get_figure().colorbar(self.pcm, label="Measured signal level [dB]") + self.ax.set_ylabel("Channel number") + self.ax.set_xlabel("Time [s]") + self.ch_min = nchannels + self.ch_max = 0 + + def update(self, ed): + resize = False + for ch in ed.keys(): + if ch < self.ch_min: + self.ch_min = ch + resize = True + if ch > self.ch_max: + self.ch_max = ch + resize = True + col = np.zeros((self.Z.shape[0], 1)) + for ch in ed.keys(): + col[ch, 0] = ed[ch] + self.Z = np.hstack((self.Z[:, 1:], col)) + if resize: + self.ax.set_ylim([self.ch_min - 0.5, self.ch_max + 0.5]) + self.ax.set_yticks(range(self.ch_min, self.ch_max + 1)) + self.pcm.set_array(self.Z.ravel()) + return (self.pcm,) + + +def main(argv): + loglevels = [ + logging.CRITICAL, + logging.ERROR, + logging.WARN, + logging.INFO, + logging.DEBUG, + ] + parser = argparse.ArgumentParser(argv) + parser.add_argument( + "-v", + "--verbosity", + type=int, + default=4, + help="set logging verbosity, 1=CRITICAL, 5=DEBUG", + ) + parser.add_argument("tty", help="Serial port device file name") + parser.add_argument( + "-b", "--baudrate", default=115200, type=int, help="Serial port baudrate" + ) + args = parser.parse_args() + # logging setup + logging.basicConfig(level=loglevels[args.verbosity - 1]) + + # open serial port + try: + logging.debug("Open serial port %s, baud=%d", args.tty, args.baudrate) + port = serial.Serial( + port=args.tty, baudrate=9600, dsrdtr=0, rtscts=0, timeout=0.3 + ) + # This baudrate reconfiguration is necessary for certain USB to serial + # adapters, the Linux cdc_acm driver will keep repeating stale buffer + # contents otherwise. No idea about the cause, but this fixes the symptom. + port.baudrate = args.baudrate + except IOError: + logging.critical("error opening serial port", file=sys.stderr) + sys.exit(2) + + try: + logging.debug("Creating figure") + fig, ax = plt.subplots() + graph = RSSIPlot(ax) + emitter = SpectrumEmitter(port) + animation.FuncAnimation( + fig, graph.update, emitter.data_gen, interval=10, blit=True + ) + plt.show() + except KeyboardInterrupt: + port.close() + sys.exit(2) + + +if __name__ == "__main__": + main(sys.argv)