diff --git a/README.md b/README.md index 78a542a..311362b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ A: Wifibroadcast is not tied to any GPU - it operates with UDP packets. But to g ## Theory Wifibroadcast puts the wifi cards into monitor mode. This mode allows to send and receive arbitrary packets without association and waiting for ACK packets. [Analysis of Injection Capabilities and Media Access of IEEE 802.11 Hardware in Monitor Mode](https://github.com/svpcom/wifibroadcast/blob/master/patches/Analysis%20of%20Injection%20Capabilities%20and%20Media%20Access%20of%20IEEE%20802.11%20Hardware%20in%20Monitor%20Mode.pdf) - +[802.11 timings](https://github.com/ewa/802.11-data) Sample usage chain: ------------------- ``` diff --git a/patches/mimo_for_dummies.pdf b/patches/mimo_for_dummies.pdf new file mode 100644 index 0000000..f8fc4f2 Binary files /dev/null and b/patches/mimo_for_dummies.pdf differ diff --git a/scripts/wfb-server.sh b/scripts/wfb-server.sh deleted file mode 100755 index 9756af5..0000000 --- a/scripts/wfb-server.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -e - -if [ $# -lt 2 ] -then echo "Usage: $0 [wlan2] ..." - exit 1 -fi - -PROFILE=$1 -shift 1 -WLANS=$@ -CHANNEL5G="149" # Freq: 5805 (5795–5815) BW 40 MHz - -for WLAN in $WLANS -do -echo "Setting $WLAN to channel $CHANNEL5G" -ifconfig $WLAN down -iw dev $WLAN set monitor otherbss -iw reg set BO -ifconfig $WLAN up -iw dev $WLAN set channel $CHANNEL5G HT40+ -done - -exec python -m telemetry.server $PROFILE $WLANS diff --git a/scripts/wifibroadcast@.service b/scripts/wifibroadcast@.service index d00354e..073d11f 100644 --- a/scripts/wifibroadcast@.service +++ b/scripts/wifibroadcast@.service @@ -6,7 +6,7 @@ ReloadPropagatedFrom=wifibroadcast.service [Service] Type=simple EnvironmentFile=/etc/default/wifibroadcast -ExecStart=/usr/bin/wfb-server.sh %i ${WFB_NICS} +ExecStart=/usr/bin/python -m telemetry.server %i ${WFB_NICS} TimeoutStopSec=5s Restart=on-failure RestartSec=5s diff --git a/setup.py b/setup.py index 92283fa..30c1038 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ setup( packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), zip_safe=False, entry_points={'console_scripts': ['wfb-cli=telemetry.cli:main']}, - scripts=['scripts/wfb-server.sh'], package_data={'telemetry.conf': ['master.cfg', 'site.cfg']}, data_files = [('/usr/bin', ['wfb_tx', 'wfb_rx', 'wfb_keygen']), ('/lib/systemd/system', ['scripts/wifibroadcast.service', diff --git a/telemetry/cli.py b/telemetry/cli.py index c40d624..80ab987 100644 --- a/telemetry/cli.py +++ b/telemetry/cli.py @@ -135,7 +135,7 @@ def init(stdscr, profile): i.scrollok(1) if cfg_video.stats_port is not None: - reactor.connectTCP('127.0.0.1', cfg_video.stats_port, AntennaStatClientFactory(status_win1, cfg_video.listen is not None)) + reactor.connectTCP('127.0.0.1', cfg_video.stats_port, AntennaStatClientFactory(status_win1, cfg_video.peer.startswith('listen:'))) else: status_win1.addstr(0, 0, '[statistics disabled]', curses.A_REVERSE) status_win1.refresh() diff --git a/telemetry/conf/master.cfg b/telemetry/conf/master.cfg index a463dc4..488a79a 100644 --- a/telemetry/conf/master.cfg +++ b/telemetry/conf/master.cfg @@ -3,6 +3,7 @@ conf_dir = '/etc' bin_dir = '/usr/bin' tmp_dir = '/tmp' + [common] debug = False version = '0.0.1.trunk' @@ -11,6 +12,9 @@ commit = None radio_mtu = 1446 # MAX_PAYLOAD_SIZE, don't change if doubt mavlink_agg_timeout = 0.1 # aggragate mavlink packets if less than radio_mtu but no longer than 100ms tx_sel_delta = 3 # hysteresis for antenna selection, [dB] +wifi_channel = 165 # radio channel @5825 MHz, range: 5815–5835 MHz, width 20MHz +wifi_region = 'BO' # Set CRDA region + [gs_mavlink] keypair = 'gs.key' # keypair generated by wfb-keygen @@ -19,21 +23,30 @@ stream_tx = 1 # radio port for mavlink tx stream_rx = 2 # radio port for mavlink rx port_rx = 14600 # udp port for internal use port_tx = 14601 # udp port range (from port_tx to port_tx + number of wlans) for internal use -listen = None # udp port for incoming connection, conflicts with 'connect' -connect = 14550 # udp port for outgoing connection, conflicts with 'listen' + +peer = 'connect://127.0.0.1:14550' # outgoing connection +# peer = 'listen://0.0.0.0:14550' # incoming connection + inject_rssi = True # inject RADIO_STATUS packets + +# Radio settings for TX and RX +bandwidth = 20 # bandwidth 20 or 40 MHz + # Radiotap flags for TX: -bandwidth = 40 # bandwidth 20 or 40 MHz -short_gi = True # use short GI or not +short_gi = False # use short GI or not stbc = 1 # stbc streams: 1, 2, 3 or 0 if unused mcs_index = 1 # mcs index + [gs_video] keypair = 'gs.key' # keypair generated by wfb-keygen stats_port = 8002 # used by wfb-cli stream = 3 # radio port for video stream -listen = None # udp port for video source (drone) or None for GS -connect = 5600 # udp port for video sink (GS) or None for drone +peer = 'connect://127.0.0.1:5600' # outgoing connection for video sink (GS) + +# Radio settings for RX +bandwidth = 20 # bandwidth 20 or 40 MHz + [drone_mavlink] keypair = 'drone.key' @@ -42,23 +55,31 @@ stream_tx = 2 stream_rx = 1 port_rx = 14700 port_tx = 14701 -listen = None -connect = 14560 + +peer = 'listen://0.0.0.0:14560' # incoming connection +#peer = 'connect://127.0.0.1:14560' # outgoing connection + inject_rssi = True # inject RADIO_STATUS packets + +# Radio settings for TX and RX +bandwidth = 20 # bandwidth 20 or 40 MHz + # Radiotap flags for TX: -bandwidth = 40 # bandwidth 20 or 40 MHz -short_gi = True # use short GI or not +short_gi = False # use short GI or not stbc = 1 # stbc streams: 1, 2, 3 or 0 if unused mcs_index = 1 # mcs index + [drone_video] keypair = 'drone.key' stats_port = None stream = 3 -listen = 5602 -connect = None +peer = 'listen://0.0.0.0:5602' # listen for video stream (drone) + +# Radio settings for TX and RX +bandwidth = 20 # bandwidth 20 or 40 MHz + # Radiotap flags for TX: -bandwidth = 40 # bandwidth 20 or 40 MHz -short_gi = True # use short GI or not +short_gi = False # use short GI or not stbc = 1 # stbc streams: 1, 2, 3 or 0 if unused mcs_index = 1 # mcs index diff --git a/telemetry/server.py b/telemetry/server.py index 1a45712..e4a3f51 100644 --- a/telemetry/server.py +++ b/telemetry/server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2018 Vasily Evseenko +# Copyright (C) 2018, 2019 Vasily Evseenko # # This program is free software; you can redistribute it and/or modify @@ -22,10 +22,11 @@ import sys import time import json import os +import re from itertools import groupby from twisted.python import log -from twisted.internet import reactor, defer +from twisted.internet import reactor, defer, utils from twisted.internet.protocol import ProcessProtocol, DatagramProtocol, Protocol, Factory from twisted.protocols.basic import LineReceiver from twisted.internet.error import ReactorNotRunning @@ -34,6 +35,36 @@ from telemetry.common import abort_on_crash, exit_status from telemetry.proxy import UDPProxyProtocol from telemetry.conf import settings +connect_re = re.compile(r'^connect://(?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):(?P[0-9]+)$', re.IGNORECASE) +listen_re = re.compile(r'^listen://(?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):(?P[0-9]+)$', re.IGNORECASE) + + +class ExecError(Exception): + pass + + +def call_and_check_rc(cmd, *args): + def _check_rc((stdout, stderr, rc)): + if rc != 0: + err = ExecError('RC %d: %s %s' % (rc, cmd, ' '.join(args))) + err.stdout = stdout.strip() + err.stderr = stderr.strip() + raise err + + log.msg('# %s' % (' '.join((cmd,) + args),)) + if stdout: + log.msg(stdout) + + def _got_signal(f): + f.trap(tuple) + stdout, stderr, signum = f.value + err = ExecError('Got signal %d: %s %s' % (signum, cmd, ' '.join(args))) + err.stdout = stdout.strip() + err.stderr = stderr.strip() + raise err + + return utils.getProcessOutputAndValue(cmd, args, env=os.environ).addCallbacks(_check_rc, _got_signal) + class BadTelemetry(Exception): pass @@ -242,10 +273,38 @@ class TXProtocol(ProcessProtocol): return df.addCallback(lambda _: self.df) +@defer.inlineCallbacks +def init_wlans(profile, wlans): + max_bw = max(getattr(getattr(settings, '%s_mavlink' % profile), 'bandwidth'), + getattr(getattr(settings, '%s_video' % profile), 'bandwidth')) + + if max_bw == 20: + ht_mode = 'HT20' + elif max_bw == 40: + ht_mode = 'HT40+' + else: + raise Exception('Unsupported bandwith %d MHz' % (max_bw,)) + + try: + yield call_and_check_rc('iw', 'reg', 'set', settings.common.wifi_region) + for wlan in wlans: + yield call_and_check_rc('ifconfig', wlan, 'down') + yield call_and_check_rc('iw', 'dev', wlan, 'set', 'monitor', 'otherbss') + yield call_and_check_rc('ifconfig', wlan, 'up') + yield call_and_check_rc('iw', 'dev', wlan, 'set', 'channel', str(settings.common.wifi_channel), ht_mode) + except ExecError as v: + if v.stdout: + log.msg(v.stdout, isError=1) + if v.stderr: + log.msg(v.stderr, isError=1) + raise + def init(profile, wlans): - return defer.gatherResults([defer.maybeDeferred(init_mavlink, profile, wlans), - defer.maybeDeferred(init_video, profile, wlans)])\ - .addErrback(lambda f: f.trap(defer.FirstError) and f.value.subFailure) + def _init_services(_): + return defer.gatherResults([defer.maybeDeferred(init_mavlink, profile, wlans), + defer.maybeDeferred(init_video, profile, wlans)])\ + .addErrback(lambda f: f.trap(defer.FirstError) and f.value.subFailure) + return init_wlans(profile, wlans).addCallback(_init_services) def init_mavlink(profile, wlans): @@ -260,19 +319,27 @@ def init_mavlink(profile, wlans): cfg.stream_tx, cfg.port_tx, os.path.join(settings.path.conf_dir, cfg.keypair), cfg.bandwidth, "short" if cfg.short_gi else "long", cfg.stbc, cfg.mcs_index)).split() + wlans - if cfg.listen: + if connect_re.match(cfg.peer): + m = connect_re.match(cfg.peer) + connect = m.group('addr'), int(m.group('port')) + listen = None + log.msg('Connect telem stream %d(RX), %d(TX) to %s:%d' % (cfg.stream_rx, cfg.stream_tx, connect[0], connect[1])) + elif listen_re.match(cfg.peer): + m = listen_re.match(cfg.peer) + listen = m.group('addr'), int(m.group('port')) connect = None - listen = cfg.listen + log.msg('Listen for telem stream %d(RX), %d(TX) on %s:%d' % (cfg.stream_rx, cfg.stream_tx, listen[0], listen[1])) else: - connect = ('127.0.0.1', cfg.connect) - listen = 0 + raise Exception('Unsupport peer address: %s' % (cfg.peer,)) + # The first argument is not None only if we initiate mavlink connection p_in = UDPProxyProtocol(connect, agg_max_size=settings.common.radio_mtu, agg_timeout=settings.common.mavlink_agg_timeout, inject_rssi=cfg.inject_rssi) p_tx_l = [UDPProxyProtocol(('127.0.0.1', cfg.port_tx + i)) for i, _ in enumerate(wlans)] p_rx = UDPProxyProtocol() p_rx.peer = p_in - sockets = [ reactor.listenUDP(listen, p_in), reactor.listenUDP(cfg.port_rx, p_rx) ] + sockets = [ reactor.listenUDP(listen[1] if listen else 0, p_in), + reactor.listenUDP(cfg.port_rx, p_rx) ] sockets += [ reactor.listenUDP(0, p_tx) for p_tx in p_tx_l ] log.msg('Telem RX: %s' % (' '.join(cmd_rx),)) @@ -296,33 +363,44 @@ def init_mavlink(profile, wlans): def init_video(profile, wlans): cfg = getattr(settings, '%s_video' % (profile,)) - if cfg.listen: + if listen_re.match(cfg.peer): + m = listen_re.match(cfg.peer) + listen = m.group('addr'), int(m.group('port')) + log.msg('Listen for video stream %d on %s:%d' % (cfg.stream, listen[0], listen[1])) + # We don't use TX diversity for video streaming due to only one transmitter on the vehichle cmd = ('%s -p %d -u %d -K %s -B %d -G %s -S %d -M %d %s' % \ (os.path.join(settings.path.bin_dir, 'wfb_tx'), cfg.stream, - cfg.listen, os.path.join(settings.path.conf_dir, cfg.keypair), + listen[1], os.path.join(settings.path.conf_dir, cfg.keypair), cfg.bandwidth, "short" if cfg.short_gi else "long", cfg.stbc, cfg.mcs_index, wlans[0])).split() df = TXProtocol(cmd, 'video tx').start() - else: + elif connect_re.match(cfg.peer): + m = connect_re.match(cfg.peer) + connect = m.group('addr'), int(m.group('port')) + log.msg('Send video stream %d to %s:%d' % (cfg.stream, connect[0], connect[1])) + ant_f = AntennaFactory(None, None) if cfg.stats_port: reactor.listenTCP(cfg.stats_port, ant_f) - cmd = ('%s -p %d -u %d -K %s' % \ - (os.path.join(settings.path.bin_dir, 'wfb_rx'), cfg.stream, cfg.connect, + cmd = ('%s -p %d -c %s -u %d -K %s' % \ + (os.path.join(settings.path.bin_dir, 'wfb_rx'), + cfg.stream, connect[0], connect[1], os.path.join(settings.path.conf_dir, cfg.keypair))).split() + wlans df = RXProtocol(ant_f, cmd, 'video rx').start() + else: + raise Exception('Unsupport peer address: %s' % (cfg.peer,)) log.msg('Video: %s' % (' '.join(cmd),)) return df def main(): log.startLogging(sys.stdout) - - reactor.callWhenRunning(lambda: defer.maybeDeferred(init, sys.argv[1], sys.argv[2:])\ + profile, wlans = sys.argv[1], sys.argv[2:] + reactor.callWhenRunning(lambda: defer.maybeDeferred(init, profile, wlans)\ .addErrback(abort_on_crash)) reactor.run()