def _get_time(log):
    """Parse timestamp from log line"""
    tstamp = log[:19]
    return datetime.strptime(tstamp, "%Y-%m-%d %H:%M:%S")

def
_get_time
(
log
)
:
tstamp
=
log
[
:
19
]
return
datetime
.
strptime
(
tstamp
,
"%Y-%m-%d %H:%M:%S"
)


Parse logs in the format from monitor . py

def _parse(log, start_time):
    """Parse logs in the format from monitor.py

    Args:
        log        : String containing the logged line.
        start_time : Log file's starting time.

    Returns:
        Dictionary with the receiver metrics.

    """
    log_time = _get_time(log)
    info = {'date': log_time, 'time': (log_time - start_time).total_seconds()}
    for entry in log[19:-1].split(";"):
        key, val = entry.split("=")
        key = key.strip()
        val = val.strip()

        if (key == "Lock"):
            val = "Locked" if val == "True" else "Unlocked"  # categorial plot
        elif (key == "Level" or key == "SNR"):
            unit = re.sub('[0-9.-]', '', val)
            val = val.replace(unit, '')
            # The logs from the S400 could return NaN, in which case "val" is
            # empty here. Skip these values.
            if (val == ''):
                continue
            val = float(val)
        elif (key == "BER" or key == "Packet Errors"):
            val = float(val)
        else:
            raise ValueError("Unknown key")

        info[key] = val

    return info


def _plot(ds, ds_name):
    """Plot all results from list of dictionaries

    Args:
        ds      : Dataset (list of dictionaries)
        ds_name : Dataset name

    """
    keys = set()
    for elem in ds:
        keys.update(list(ds[0].keys()))
    keys.remove("time")
    keys.remove("date")

    formatter = DateFormatter('%m/%d %H:%M')

    path = os.path.join("figs", ds_name)
    if not os.path.isdir(path):
        os.makedirs(path)

    for key in keys:
        logger.info("Plotting {}".format(key))

        x = [r[key] for r in ds if key in r]
        t = [r["date"] for r in ds if key in r]

        # Add unit to y-label
        if (key == "SNR"):
            ylabel = "SNR (dB)"
        elif (key == "Level"):
            ylabel = "Signal Level"
            # NOTE: the SDR logs positive signal levels rather than dBm.
            if (all([val < 0 for val in x])):
                ylabel += " (dBm)"
        else:
            ylabel = key

        fig, ax = plt.subplots()
        plt.plot_date(t, x, ms=2)
        plt.ylabel(ylabel)
        plt.xlabel("Time (UTC)")

        # Plot BER in log scale as it is not zero throughout the acquisition
        if (key == "BER" and any([val > 0 for val in x])):
            ax.set_yscale('log')

        ax.xaxis.set_major_formatter(formatter)
        ax.xaxis.set_tick_params(rotation=30, labelsize=10)
        plt.tight_layout()

        savepath = os.path.join(path, key.replace("/", "_").lower() + ".png")
        plt.savefig(savepath, dpi=300)
        plt.close()


def _analyze(logs, ds_name):
    """Parse and plot receiver metrics"""
    start_time = _get_time(logs[0])
    ds = list()
    for log in logs:
        logger.debug(log)
        res = _parse(log, start_time)
        logger.debug(res)
        if (res is not None):
            ds.append(res)
    _plot(ds, ds_name)

def _analyze ( logs , ds_name ) : start_time = _get_time ( logs [ 0 ] ) ds = list ( ) for log in logs : logger . debug ( log ) res = _parse ( log , start_time ) logger . debug ( res ) if ( res is not None ) : ds . append ( res ) _plot ( ds , ds_name )


Convert footnotes from github - flavored markdown into extended markdown

def convert_footnotes(text, doc):
    """Convert footnotes from github-flavored markdown into extended markdown

    The extended markdown format provides a more specific syntax for footnotes,
    which pandoc can process and convert into LaTeX footnotes.

    This function assumes each document has a unique set of footnotes, and that
    multiple docs can adopt the same footnote numbering. It keeps a global
    record of footnotes and increases the footnote number such that the
    concatenated text does not have duplicate footnotes.

    """
    lines = text.splitlines()
    footnote = None

    for i, line in enumerate(lines):
        if "" in line:
            footnote = re.search(r"(.*?)", line).group(1)

            if doc not in footnote_map:
                footnote_map[doc] = {}

            # Assign a global footnote number
            if footnote in footnote_map[doc]:
                i_footnote = footnote_map[doc][footnote]['idx']
            else:
                i_footnote = sum([len(x) for x in footnote_map.values()]) + 1
                footnote_map[doc][footnote] = {
                    'idx': i_footnote,
                    'ref_missing': True
                }

            if footnote_map[doc][footnote]['ref_missing']:
                # Footnote reference (without colon)
                lines[i] = line.replace("" + footnote + "",
                                        "[^{}]".format(i_footnote))
                footnote_map[doc][footnote]['ref_missing'] = False
            else:
                # Footnote definition (with colon)
                lines[i] = line.replace("" + footnote + "",
                                        "[^{}]:".format(i_footnote))
    return "\n".join(lines)

def
convert_footnotes
(
text
,
doc
)
:
lines
=
text
.
splitlines
(
)
footnote
=
for
i
,
line
in
enumerate
(
lines
)
:
if
""
in
line
:
footnote
=
re
.
search
(
r"(.*?)"
,
line
)
.
group
(
1
)
if
doc
not
in
footnote_map
:
footnote_map
[
doc
]
=
{
}
# Assign a global footnote number
if
footnote
in
footnote_map
[
doc
]
:
i_footnote
=
footnote_map
[
doc
]
[
footnote
]
[
'idx'
]
else
:
i_footnote
=
sum
(
[
len
(
x
)
for
x
in
footnote_map
.
values
(
)
]
)
+
1
footnote_map
[
doc
]
[
footnote
]
=
{
'idx'
:
i_footnote
,
'ref_missing'
:
True
}
if
footnote_map
[
doc
]
[
footnote
]
[
'ref_missing'
]
:
# Footnote reference (without colon)
lines
[
i
]
=
line
.
replace
(
""
+
footnote
+
""
,
"[^{}]"
.
format
(
i_footnote
)
)
footnote_map
[
doc
]
[
footnote
]
[
'ref_missing'
]
=
False
else
:
# Footnote definition (with colon)
lines
[
i
]
=
line
.
replace
(
""
+
footnote
+
""
,
"[^{}]:"
.
format
(
i_footnote
)
)
return
"\n"
.
join
(
lines
)


def remove_yaml_front_matter(text):
    """Remove the YAML front matter of a Markdown document"""
    if text[0:4] == "---\n":
        # Find where the front matter ends
        lines = text.splitlines()
        i_end = 1
        while (i_end < 10):  # front matter should use less than 10 lines
            if lines[i_end] == "---":
                break
            i_end += 1
        assert (i_end < 10), "Failed to find YAML front matter closure"

        # Remove also the extra line break after the front matter
        if (lines[i_end + 1] == ""):
            i_end += 1

        text = "\n".join(lines[(i_end + 1):])

    return text


def fix_links(concat_text, docs, title_map, absent_docs):
    """Change document links to anchor links on the concatenated text

    The original Markdown docs are linked to each other based on file
    names. However, once they are concatenated, the original file links do not
    work anymore. This function fixes the problem by changing the file links
    into the corresponding in-document anchor links, based on the document's
    title heading.

    The other problem is that not all markdown docs are included on the
    compiled pdf version. This function removes links to the docs are not
    present in the concatenated text.

    """
    # Replace all document links with the corresponding anchor link
    for doc in docs:
        # Convert title to the corresponding markdown anchor link
        anchor_link = title_map[doc].lower().replace(" ", "-")

        # Replace direct links
        original_link = "({})".format(doc)
        new_link = "(#{})".format(anchor_link)
        concat_text = concat_text.replace(original_link, new_link)

        # Fix links pointing to a particular subsection
        concat_text = concat_text.replace("({}#".format(doc), "(#")

    # Replace links to absent docs
    lines = concat_text.splitlines()
    for doc in absent_docs:
        original_link = "({})".format(doc)
        for i, line in enumerate(lines):
            if original_link in line:
                match = re.search(r"\[(.*?)\]\(" + doc + r"\)", line)
                lines[i] = line.replace(match.group(0), match.group(1))

    return "\n".join(lines)


def _get_iptables_rules(net_if):
    """Get iptables rules that are specifically applied to a target interface

    Args:
        net_if : network interface name

    Returns:
        list of dictionaries with information of the individual matched rules

    """
    # Unfortunately, root privileges are required to read the current firewall
    # rules. Hence, we can't read the current rules without eventually asking
    # the root password in dry-run mode. As a workaround, return an empty list
    # as if the rule was not set yet.
    if (runner.dry):
        return []

    # Get rules
    cmd = ["iptables", "-L", "-v", "--line-numbers"]
    res = runner.run(cmd, root=True, capture_output=True).stdout

    # Parse
    header1 = ""
    header2 = ""
    rules = list()
    for line in res.splitlines():
        if ("Chain INPUT" in line.decode()):
            header1 = line.decode()

        if ("destination" in line.decode()):
            header2 = line.decode()

        if (net_if in line.decode()):
            rules.append({
                'rule': line.decode().split(),
                'header1': header1,
                'header2': header2
            })

    return rules


def _is_iptables_igmp_rule_set(net_if, cmd):
    """Check if an iptables rule for IGMP is already configured

    Args:
        net_if : network interface name
        cmd    : list with iptables command

    Returns:
        True if rule is already set, False otherwise.

    """
    assert (cmd[0] != "sudo")
    for rule in _get_iptables_rules(net_if):
        if (rule['rule'][3] == "ACCEPT" and rule['rule'][6] == cmd[6]
                and rule['rule'][4] == "igmp"):
            print("\nFirewall rule for IGMP already configured\n")
            print(rule['header1'])
            print(rule['header2'])
            print(" ".join(rule['rule']))
            print("\nSkipping...")
            return True

    return False


def _is_iptables_udp_rule_set(net_if, cmd):
    """Check if an iptables rule for UDP is already configured

    Args:
        net_if : network interface name
        cmd    : list with iptables command

    Returns:
        True if rule is already set, False otherwise.

    """
    assert (cmd[0] != "sudo")
    for rule in _get_iptables_rules(net_if):
        if (rule['rule'][3] == "ACCEPT" and rule['rule'][6] == cmd[6]
                and rule['rule'][4] == "udp" and rule['rule'][12] == cmd[10]):
            print("\nFirewall rule already configured\n")
            print(rule['header1'])
            print(rule['header2'])
            print(" ".join(rule['rule']))
            print("\nSkipping...")
            return True

    return False

def
_is_iptables_udp_rule_set
(
net_if
,
cmd
)
:
assert
(
cmd
[
0
]
!=
"sudo"
)
for
rule
in
_get_iptables_rules
(
net_if
)
:
if
(
rule
[
'rule'
]
[
3
]
==
"ACCEPT"
and
rule
[
'rule'
]
[
6
]
==
cmd
[
6
]
and
rule
[
'rule'
]
[
4
]
==
"udp"
and
rule
[
'rule'
]
[
12
]
==
cmd
[
10
]
)
:
print
(
"\nFirewall rule already configured\n"
)
print
(
rule
[
'header1'
]
)
print
(
rule
[
'header2'
]
)
print
(
" "
.
join
(
rule
[
'rule'
]
)
)
print
(
"\nSkipping..."
)
return
True
return
False


def _add_iptables_rule(net_if, cmd):
    """Add iptables rule

    Args:
        net_if : network interface name
        cmd    : list with iptables command

    """
    assert (cmd[0] != "sudo")

    # Set up the iptables rules
    runner.run(cmd, root=True)

    for rule in _get_iptables_rules(net_if):
        print_rule = False

        if (rule['rule'][3] == "ACCEPT" and rule['rule'][6] == cmd[6]
                and rule['rule'][4] == cmd[4]):
            if (cmd[4] == "igmp"):
                print_rule = True
            elif (cmd[4] == "udp" and rule['rule'][12] == cmd[10]):
                print_rule = True

            if (print_rule):
                print("Added iptables rule:\n")
                print(rule['header1'])
                print(rule['header2'])
                print(" ".join(rule['rule']) + "\n")

def
_add_iptables_rule
(
net_if
,
cmd
)
:
assert
(
cmd
[
0
]
!=
"sudo"
)
# Set up the iptables rules
runner
.
run
(
cmd
,
root
=
True
)
for
rule
in
_get_iptables_rules
(
net_if
)
:
print_rule
=
False
if
(
rule
[
'rule'
]
[
3
]
==
"ACCEPT"
and
rule
[
'rule'
]
[
6
]
==
cmd
[
6
]
and
rule
[
'rule'
]
[
4
]
==
cmd
[
4
]
)
:
if
(
cmd
[
4
]
==
"igmp"
)
:
print_rule
=
True
elif
(
cmd
[
4
]
==
"udp"
and
rule
[
'rule'
]
[
12
]
==
cmd
[
10
]
)
:
print_rule
=
True
if
(
print_rule
)
:
print
(
"Added iptables rule:\n"
)
print
(
rule
[
'header1'
]
)
print
(
rule
[
'header2'
]
)
print
(
" "
.
join
(
rule
[
'rule'
]
)
+
"\n"
)


Configure iptables rules to accept blocksat traffic on a DVB - S2 interface

def _configure_iptables(net_if, ports, igmp=False, prompt=True):
    """Configure iptables rules to accept blocksat traffic on a DVB-S2 interface

    Args:
        net_if : DVB network interface name
        ports  : ports used for blocks traffic and API traffic
        igmp   : Whether or not to configure rule to accept IGMP queries
        prompt : Ask yes/no before applying any changes

    """

    cmd = [
        "iptables",
        "-I",
        "INPUT",
        "-p",
        "udp",
        "-i",
        net_if,
        "--match",
        "multiport",
        "--dports",
        ",".join(ports),
        "-j",
        "ACCEPT",
    ]

    util.fill_print(
        "A firewall rule is required to accept Blocksat traffic arriving " +
        "through interface {} towards UDP ports {}.".format(
            net_if, ",".join(ports)))

    if (runner.dry):
        util.fill_print("The following command would be executed:")

    if (not _is_iptables_udp_rule_set(net_if, cmd)):
        if (runner.dry or (not prompt) or util.ask_yes_or_no(
                "Add the corresponding ACCEPT firewall rule?")):
            _add_iptables_rule(net_if, cmd)
        else:
            print("\nFirewall configuration cancelled")

    # We're done, unless we also need to configure an IGMP rule
    if (not igmp):
        return

    # IGMP rule supports standalone DVB receivers. The host in this case will
    # need to periodically send IGMP membership reports in order for upstream
    # switches between itself and the DVB receiver to continue delivering the
    # multicast-addressed traffic. This overcomes the scenario where group
    # membership timeouts are implemented by the intermediate switches.
    cmd = [
        "iptables",
        "-I",
        "INPUT",
        "-p",
        "igmp",
        "-i",
        net_if,
        "-j",
        "ACCEPT",
    ]

    print()
    util.fill_print(
        "A firewall rule is required to accept IGMP queries arriving from the "
        "standalone DVB-S2 receiver on interface {}.".format(net_if))

    if (runner.dry):
        util.fill_print("The following command would be executed:")

    if (not _is_iptables_igmp_rule_set(net_if, cmd)):
        if (runner.dry or (not prompt) or util.ask_yes_or_no(
                "Add the corresponding ACCEPT firewall rule?")):
            _add_iptables_rule(net_if, cmd)
        else:
            print("\nIGMP firewall rule cancelled")


def is_firewalld():
    """Check if the firewall is based on firewalld"""
    if (which('firewall-cmd') is None):
        return False

    if (which('systemctl') is None):
        # Can't check whether firewalld is running
        return False

    # Run the following commands even in dry-run mode
    res1 = runner.run(['systemctl', 'is-active', '--quiet', 'firewalld'],
                      nodry=True,
                      nocheck=True)
    res2 = runner.run(['systemctl', 'is-active', '--quiet', 'iptables'],
                      nodry=True,
                      nocheck=True)

    # If running (active), 'is-active' returns 0
    if (res1.returncode == 0 and res2.returncode == 0):
        raise ValueError(
            "Failed to detect firewall system (firewalld or iptables)")

    return (res1.returncode == 0)


def _configure_firewalld(net_if, ports, src_ip, igmp, prompt):
    """Configure firewalld for blocksat and IGMP traffic

    Add one rich rule for blocksat traffic coming specifically from the
    satellite of choice (corresponding to the given source IP) and another rich
    rule (if necessary) for IGMP traffic.

    NOTE: unlike the iptables configuration, the current firewalld
    configuration disregards the network interface. The alternative to consider
    the interface is to use firewalld zones. However, because the network
    interface may not be dedicated to DVB-S2 traffic (e.g., when using a
    standalone receiver), it can be undesirable to assign the interface
    receiving satellite traffic to a dedicated zone just for blocksat. In
    contrast, the rich rule approach is more generic and works for all types of
    receivers.

    """

    if len(ports) > 1:
        portrange = "{}-{}".format(min(ports), max(ports))
    else:
        portrange = ports

    util.fill_print(
        "- Configure the firewall to accept Blocksat traffic arriving " +
        "from {} towards address {} on UDP ports {}:\n".format(
            src_ip, defs.mcast_ip, portrange))

    if (not runner.dry and prompt):
        if (not util.ask_yes_or_no("Add firewalld rule?")):
            print("\nFirewall configuration cancelled")
            return

    rich_rule = ("rule "
                 "family=ipv4 "
                 "source address={} "
                 "destination address={}/32 "
                 "port port={} protocol=udp accept".format(
                     src_ip, defs.mcast_ip, portrange))
    runner.run(['firewall-cmd', '--add-rich-rule', "{}".format(rich_rule)],
               root=True)

    if (runner.dry):
        print()
        util.fill_print(
            "NOTE: Add \"--permanent\" to make it persistent. In this case, "
            "remember to reload firewalld afterwards.")

    # We're done, unless we also need to configure an IGMP rule
    if (not igmp):
        return

    util.fill_print(
        "- Allow IGMP packets. This is necessary when using a standalone "
        "DVB-S2 receiver connected through a switch:\n")

    if (not runner.dry and prompt):
        if (not util.ask_yes_or_no("Enable IGMP on the firewall?")):
            print("\nFirewall configuration cancelled")
            return

    runner.run(['firewall-cmd', '--add-protocol=igmp'], root=True)
    print()


def configure(net_ifs, ports, src_ip, igmp=False, prompt=True, dry=False):
    """Configure firewallrules to accept blocksat traffic via DVB interface

    Args:
        net_ifs : List of DVB network interface names
        ports   : UDP ports used by satellite traffic
        src_ip  : Source IP to whitelist (unique to each satellite)
        igmp    : Whether or not to configure rule to accept IGMP queries
        prompt  : Prompt user to accept configurations before executing them
        dry     : Dry run mode

    """
    assert (isinstance(net_ifs, list))
    runner.set_dry(dry)
    util.print_header("Firewall Rules")

    for i, net_if in enumerate(net_ifs):
        if (is_firewalld()):
            _configure_firewalld(net_if, ports, src_ip, igmp, prompt)
        else:
            _configure_iptables(net_if, ports, igmp, prompt)

        if (i < len(net_ifs) - 1):
            print("")

def
configure
(
net_ifs
,
ports
,
src_ip
,
igmp
=
False
,
prompt
=
True
,
dry
=
False
)
:
assert
(
isinstance
(
net_ifs
,
list
)
)
runner
.
set_dry
(
dry
)
util
.
print_header
(
"Firewall Rules"
)
for
i
,
net_if
in
enumerate
(
net_ifs
)
:
if
(
is_firewalld
(
)
)
:
_configure_firewalld
(
net_if
,
ports
,
src_ip
,
igmp
,
prompt
)
else
:
_configure_iptables
(
net_if
,
ports
,
igmp
,
prompt
)
if
(
i
<
len
(
net_ifs
)
1
)
:
print
(
""
)


def subparser(subparsers):
    """Parser for firewall command"""
    p = subparsers.add_parser('firewall',
                              description="Set firewall rules",
                              help='Set firewall rules',
                              formatter_class=ArgumentDefaultsHelpFormatter)
    p.add_argument('-i',
                   '--interface',
                   required=True,
                   help='Network interface')
    p.add_argument(
        '--standalone',
        default=False,
        action='store_true',
        help='Apply configurations for a standalone DVB-S2 receiver')
    p.add_argument('-y',
                   '--yes',
                   default=False,
                   action='store_true',
                   help="Default to answering Yes to configuration prompts")
    p.add_argument("--dry-run",
                   action='store_true',
                   default=False,
                   help="Print all commands but do not execute them")
    p.set_defaults(func=firewall_subcommand)
    return p


def firewall_subcommand(args):
    """Call function that sets firewall rules

    Handles the firewall subcommand

    """
    user_info = config.read_cfg_file(args.cfg, args.cfg_dir)

    if (user_info is None):
        return

    configure([args.interface],
              defs.src_ports,
              user_info['sat']['ip'],
              igmp=args.standalone,
              prompt=(not args.yes),
              dry=args.dry_run)

def firewall_subcommand ( args ) : user_info = config . read_cfg_file ( args . cfg , args . cfg_dir ) if ( user_info is None ) : return configure ( [ args . interface ] , defs . src_ports , user_info [ 'sat' ] [ 'ip' ] , igmp = args . standalone , prompt = ( not args . yes ) , dry = args . dry_run )


def _check_pip_updates(cfg_dir, cli_version):
    """Check if the CLI has updates available via pip

    Args:
        cfg_dir     : Directory where the .update is located/created
        cli_version : Current version of the CLI

    """
    logger.debug("Checking blocksat-cli updates")

    # Save the new pip verification timestamp on the cache file
    update_cache = UpdateCache(cfg_dir)
    update_cache.save()

    # Is blocksat-cli installed?
    res = subprocess.run(["pip3", "show", "blocksat-cli"],
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)
    found = (res.returncode == 0)

    # If the CLI was not installed via pip, don't check updates
    if (not found):
        logger.debug("Could not find blocksat-cli installed via pip3")
        return

    # Is the CLI outdated?
    try:
        outdated_list = json.loads(
            subprocess.check_output(
                ["pip3", "list", "--outdated", "--format", "json"],
                stderr=subprocess.DEVNULL).decode())
    except subprocess.CalledProcessError:
        # Don't break if the command fails. It could be a problem on pip. For
        # example, there is a bug in which pip throws a TypeError on the "list
        # --outdated" command saying: '>' not supported between instances of
        # 'Version' and 'Version'
        logger.warning("Failed to check blocksat-cli updates.")
        return

    cli_outdated = False
    for package in outdated_list:
        if (package['name'] == "blocksat-cli"):
            cli_outdated = True
            # Save the available update on the cache file
            update_cache.save((package['version'], package['latest_version']))
            logger.debug("Update available for blocksat-cli")

    if (not cli_outdated):
        logger.debug("blocksat-cli is up-to-date")
        # Clear a potential update flag on the cache file
        update_cache.save()


def check_cli_updates(args, cli_version):
    """Check if the CLI has updates available

    Check the cache file first. If the cache is old enough, check updates
    through the pip interface. The cache file can be verified very quickly,
    whereas the verification through pip can be rather slow.

    Args:
        args        : Arguments from parent argument parser
        cli_version : Current version of the CLI

    """

    # If pip is not available, assume the CLI was not installed via pip
    if (not which("pip3")):
        logger.debug("pip3 unavailable")
        return

    # Create the configuration directory if it does not exist
    if not os.path.exists(args.cfg_dir):
        os.makedirs(args.cfg_dir, exist_ok=True)

    # Check the update cache file first
    update_cache = UpdateCache(args.cfg_dir)

    # If the cache file says there is an update available, check whether the
    # current version is already the updated one. If not, recommend the update.
    if (update_cache.has_update()):
        if (update_cache.new_version() > StrictVersion(cli_version)):
            # Not updated yet. Recommend it.
            update_cache.recommend_update()
        else:
            # Already updated. Clear the update flag.
            update_cache.save()
        return

    # If the last update verification was still recent (less than a day ago),
    # do not check pip. Avoid the unnecessary spawning of the pip-checking
    # thread.
    if (update_cache.data):
        time_since_last_check = (datetime.now() - update_cache.last_check())
        if (time_since_last_check < timedelta(days=1)):
            logger.debug(
                "Last update check was less than a day ago: {}".format(
                    str(time_since_last_check)))
            return

    # Check pip updates on a thread. This verification can be slow, so it is
    # better not to block.
    t = threading.Thread(target=_check_pip_updates,
                         args=(args.cfg_dir, cli_version))
    t.start()


def __init__(self, cfg_dir):
        """UpdateCache constructor

        Args:
            cfg_dir : Directory where the .update is located/created

        """
        super().__init__(cfg_dir, filename=".update")

def __init__ ( self , cfg_dir ) : super ( ) . __init__ ( cfg_dir , filename = ".update" )


def last_check(self):
        """Return the datetime corresponding to the last update verification"""
        return datetime.strptime(self.data['last_check'],
                                 '%Y-%m-%d %H:%M:%S.%f')

def
last_check
(
self
)
:
return
datetime
.
strptime
(
self
.
data
[
'last_check'
]
,
'%Y-%m-%d %H:%M:%S.%f'
)


def save(self, cli_update=None):
        """Save information into the update cache file

        Args:
            cli_update : Version of a new CLI version available for update,
                         if any

        """
        self.data['last_check'] = datetime.now().strftime(
            '%Y-%m-%d %H:%M:%S.%f')
        self.data['cli_update'] = cli_update
        super().save()

def
save
(
self
,
cli_update
=
)
:
self
.
data
[
'last_check'
]
=
datetime
.
now
(
)
.
strftime
(
'%Y-%m-%d %H:%M:%S.%f'
)
self
.
data
[
'cli_update'
]
=
cli_update
super
(
)
.
save
(
)


def has_update(self):
        """Returns whether there is an update available for the CLI"""
        return 'cli_update' in self.data and \
            self.data['cli_update'] is not None

def has_update ( self ) : return 'cli_update' in self . data and self . data [ 'cli_update' ] is not None


def new_version(self):
        """Get the new version available for an update

        Note: This method should be called only if there is a new update, as
            confirmed by "has_update". Otherwise, it will throw an exception.

        """
        return StrictVersion(self.data['cli_update'][1])

def new_version ( self ) : return StrictVersion ( self . data [ 'cli_update' ] [ 1 ] )


def recommend_update(self):
        """Print information regarding the recommended update"""
        if (self.data['cli_update'] is None):
            return
        print("\nUpdate available for blocksat-cli")
        print("Current version: {}.\nLatest version: {}.".format(
            self.data['cli_update'][0], self.data['cli_update'][1]))
        print("Please run:\n\n    pip3 install blocksat-cli --upgrade\n\n")


def __init__(self, cfg_dir, filename=".cache"):
        """Cache constructor

        Args:
            cfg_dir : Directory where the .update is located/created

        """
        self.path = os.path.join(cfg_dir, filename)
        self.data = {}
        self.load()

def __init__ ( self , cfg_dir , filename = ".cache" ) : self . path = os . path . join ( cfg_dir , filename ) self . data = { } self . load ( )


def load(self):
        """Load data from the cache file"""
        if (not os.path.exists(self.path)):
            return

        try:
            with open(self.path, 'r') as fd:
                self.data = json.load(fd)
        except json.decoder.JSONDecodeError:
            return

def load ( self ) : if ( not os . path . exists ( self . path ) ) : return try : with open ( self . path , 'r' ) as fd : self . data = json . load ( fd ) except json . decoder . JSONDecodeError : return


def save(self):
        """Save data into the cache file

        Args:
            cli_update : Version of a new CLI version available for update,
                         if any

        """
        with open(self.path, 'w') as fd:
            json.dump(self.data, fd)

def save ( self ) : with open ( self . path , 'w' ) as fd : json . dump ( self . data , fd )


def get(self, field):
        """Get using dot notation"""
        nested_keys = field.split('.')
        tmp = self.data
        for i, key in enumerate(nested_keys):
            if key not in tmp:
                return None
            if i == len(nested_keys) - 1:
                return tmp[key]
            tmp = tmp[key]

def get ( self , field ) : nested_keys = field . split ( '.' ) tmp = self . data for i , key in enumerate ( nested_keys ) : if key not in tmp : return None if i == len ( nested_keys ) - 1 : return tmp [ key ] tmp = tmp [ key ]


def set(self, field, val, overwrite=True):
        """Set using dot notation"""
        nested_keys = field.split('.')
        tmp = self.data
        for i, key in enumerate(nested_keys):
            if key not in tmp:
                tmp[key] = {}
            if i == len(nested_keys) - 1:
                if (key in tmp and not overwrite):
                    return
                tmp[key] = val
            tmp = tmp[key]

def set ( self , field , val , overwrite = True ) : nested_keys = field . split ( '.' ) tmp = self . data for i , key in enumerate ( nested_keys ) : if key not in tmp : tmp [ key ] = { } if i == len ( nested_keys ) - 1 : if ( key in tmp and not overwrite ) : return tmp [ key ] = val tmp = tmp [ key ]


def typed_input(msg, hint=None, in_type=int, default=None):
    """Ask for user input of a specific type"""
    res = None
    while (res is None):
        try:
            if (default is not None):
                assert (isinstance(default, in_type))
                input_val = _input(msg + ": [{}] ".format(default)) or default
            else:
                input_val = _input(msg + ": ")
            res = in_type(input_val)
        except ValueError:
            if (hint is None):
                if (in_type == int):
                    print("Please enter an integer number")
                elif (in_type == float):
                    print("Please enter a number")
                elif (in_type == IPv4Address):
                    print("Please enter a valid IPv4 address")
                else:
                    type_str = in_type.__name__
                    print("Please enter a {}".format(type_str))
            else:
                print(hint)
    assert (res is not None)
    assert (isinstance(res, in_type))
    return res

def
typed_input
(
msg
,
hint
=
,
in_type
=
int
,
default
=
)
:
res
=
while
(
res
is
)
:
try
:
if
(
default
is
not
)
:
assert
(
isinstance
(
default
,
in_type
)
)
input_val
=
_input
(
msg
+
": [{}] "
.
format
(
default
)
)
or
default
else
:
input_val
=
_input
(
msg
+
": "
)
res
=
in_type
(
input_val
)
except
ValueError
:
if
(
hint
is
)
:
if
(
in_type
==
int
)
:
print
(
"Please enter an integer number"
)
elif
(
in_type
==
float
)
:
print
(
"Please enter a number"
)
elif
(
in_type
==
IPv4Address
)
:
print
(
"Please enter a valid IPv4 address"
)
else
:
type_str
=
in_type
.
__name__
print
(
"Please enter a {}"
.
format
(
type_str
)
)
else
:
print
(
hint
)
assert
(
res
is
not
)
assert
(
isinstance
(
res
,
in_type
)
)
return
res


Ask for a non - null string input with an optional default value

def string_input(msg, default=None):
    """Ask for a non-null string input with an optional default value"""
    res = ""
    while (len(res) == 0):
        if (default is not None):
            assert (isinstance(default, str))
            res = _input(msg + ": [{}] ".format(default)) or default
        else:
            res = _input(msg + ": ")
    return res

def
string_input
(
msg
,
default
=
)
:
res
=
""
while
(
len
(
res
)
==
0
)
:
if
(
default
is
not
)
:
assert
(
isinstance
(
default
,
str
)
)
res
=
_input
(
msg
+
": [{}] "
.
format
(
default
)
)
or
default
else
:
res
=
_input
(
msg
+
": "
)
return
res


def ask_yes_or_no(msg, default="y", help_msg=None):
    """Yes or no question

    Args:
        msg      : the message or question to ask the user
        default  : default response
        help_msg : Optional help message

    Returns:
        True if answer is yes, False otherwise.

    """
    response = None

    if (default == "y"):
        options = "[Y/n]"
    else:
        options = "[N/y]"

    while response not in {"y", "n"}:
        if (help_msg is None):
            question = msg + " " + options + " "
            raw_resp = _input(question) or default
        else:
            print(textwrap.fill(msg))
            print()
            print(textwrap.fill(help_msg))
            print()
            raw_resp = _input("Answer " + options + " ") or default

        response = raw_resp.lower()

        if (response not in ["y", "n"]):
            print("Please enter \"y\" or \"n\"")

    return (response == "y")

def
ask_yes_or_no
(
msg
,
default
=
"y"
,
help_msg
=
)
:
response
=
if
(
default
==
"y"
)
:
options
=
"[Y/n]"
else
:
options
=
"[N/y]"
while
response
not
in
{
"y"
,
"n"
}
:
if
(
help_msg
is
)
:
question
=
msg
+
" "
+
options
+
" "
raw_resp
=
_input
(
question
)
or
default
else
:
print
(
textwrap
.
fill
(
msg
)
)
print
(
)
print
(
textwrap
.
fill
(
help_msg
)
)
print
(
)
raw_resp
=
_input
(
"Answer "
+
options
+
" "
)
or
default
response
=
raw_resp
.
lower
(
)
if
(
response
not
in
[
"y"
,
"n"
]
)
:
print
(
"Please enter \"y\" or \"n\""
)
return
(
response
==
"y"
)


def ask_multiple_choice(vec,
                        msg,
                        label,
                        to_str,
                        help_msg=None,
                        none_option=False,
                        none_str="None of the above"):
    """Multiple choice question

    Args:
        vec         : Vector with elements to choose from
        msg         : Msg to prompt user for choice
        label       : Description/label of what "vec" holdes
        to_str      : Function that prints information about elements
        help_msg    : Optional help message
        none_option : Whether to display a "none of the above" option
        none_str    : What do display as "none of the above" message

    Returns:
        Chosen element

    """
    if (none_option):
        assert (len(vec) > 0)
    else:
        assert (len(vec) > 1)

    print(msg)

    for i_elem, elem in enumerate(vec):
        elem_str = to_str(elem)
        print("[%2u] %s" % (i_elem, elem_str))

    if (none_option):
        print("[%2u] %s" % (len(vec), none_str))

    if (help_msg is not None):
        print()
        print(help_msg)

    resp = None
    while (not isinstance(resp, int)):
        try:
            resp = int(_input("\n%s number: " % (label)))
        except ValueError:
            print("Please choose a number")
            continue

        max_resp = len(vec) + 1 if none_option else len(vec)
        if (resp >= max_resp):
            print("Please choose number from 0 to %u" % (max_resp - 1))
            resp = None
            continue

        if (none_option and resp == len(vec)):
            choice = None
            print(none_str)
        else:
            choice = copy.deepcopy(vec[resp])
            # NOTE: deep copy to prevent the caller from changing the original
            # element.
            print(to_str(choice))
        print()

        return choice


def print_header(header, target_len=80):
    """Print section header"""

    prefix = ""
    suffix = ""
    header_len = len(header) + 2
    remaining = target_len - header_len
    prefix_len = int(remaining / 2)
    suffix_len = int(remaining / 2)

    if (remaining % 1 == 1):
        prefix_len += 1

    for i in range(0, prefix_len):
        prefix += "-"

    for i in range(0, suffix_len):
        suffix += "-"

    print("\n" + prefix + " " + header + " " + suffix)

def
print_header
(
header
,
target_len
=
80
)
:
prefix
=
""
suffix
=
""
header_len
=
len
(
header
)
+
2
remaining
=
target_len
header_len
prefix_len
=
int
(
remaining
/
2
)
suffix_len
=
int
(
remaining
/
2
)
if
(
remaining
%
1
==
1
)
:
prefix_len
+=
1
for
i
in
range
(
0
,
prefix_len
)
:
prefix
+=
"-"
for
i
in
range
(
0
,
suffix_len
)
:
suffix
+=
"-"
print
(
"\n"
+
prefix
+
" "
+
header
+
" "
+
suffix
)


def print_sub_header(header, target_len=60):
    """Print sub-section header"""
    print_header(header, target_len=target_len)

def print_sub_header ( header , target_len = 60 ) : print_header ( header , target_len = target_len )


Get the user s home directory even if running with sudo

def get_home_dir():
    """Get the user's home directory even if running with sudo"""
    sudo_user = os.environ.get('SUDO_USER')
    user = sudo_user if sudo_user is not None else ""
    return os.path.expanduser("~" + user)

def get_home_dir ( ) : sudo_user = os . environ . get ( 'SUDO_USER' ) user = sudo_user if sudo_user is not None else "" return os . path . expanduser ( "~" + user )


Print progress on urlretrieve s reporthook

def _print_urlretrieve_progress(block_num, block_size, total_size):
    """Print progress on urlretrieve's reporthook"""
    downloaded = block_num * block_size
    progress = 100 * downloaded / total_size
    if (total_size > 0):
        print("Download progress: {:4.1f}%".format(progress), end='\r')
        if (progress >= 100):
            print("")

def
_print_urlretrieve_progress
(
block_num
,
block_size
,
total_size
)
:
downloaded
=
block_num
*
block_size
progress
=
100
*
downloaded
/
total_size
if
(
total_size
>
0
)
:
print
(
"Download progress: {:4.1f}%"
.
format
(
progress
)
,
end
=
'\r'
)
if
(
progress
>=
100
)
:
print
(
""
)


def download_file(url, destdir, dry_run, logger=None):
    """Download file from a given URL

    Args:
        url     : Download URL.
        destdir : Destination directory.
        dry_run : Dry-run mode.
        logger  : Optional logger to print messages.

    Returns:
        Path to downloaded file if the download is successful. None in
        dry-run mode or if the download fails.

    """
    filename = url.split('/')[-1]
    local_path = os.path.join(destdir, filename)

    if (dry_run):
        print("Download: {}".format(url))
        print("Save at: {}".format(destdir))
        return

    if (logger is not None):
        logger.debug("Download {} and save at {}".format(url, destdir))

    try:
        urlretrieve(url, local_path, _print_urlretrieve_progress)
    except HTTPError as e:
        logger.error(str(e))
        return

    return local_path

def
download_file
(
url
,
destdir
,
dry_run
,
logger
=
)
:
filename
=
url
.
split
(
'/'
)
[
1
]
local_path
=
os
.
path
.
join
(
destdir
,
filename
)
if
(
dry_run
)
:
print
(
"Download: {}"
.
format
(
url
)
)
print
(
"Save at: {}"
.
format
(
destdir
)
)
return
if
(
logger
is
not
)
:
logger
.
debug
(
"Download {} and save at {}"
.
format
(
url
,
destdir
)
)
try
:
urlretrieve
(
url
,
local_path
,
_print_urlretrieve_progress
)
except
HTTPError
as
e
:
logger
.
error
(
str
(
e
)
)
return
return
local_path


def _get_cmd_str(self, orig_cmd):
        """Generate string for command

        If any argument is supposed to be quoted (i.e., has spaces), add the
        quotes.

        """
        quoted_cmd = orig_cmd.copy()
        for i, elem in enumerate(quoted_cmd):
            if (" " in elem):
                quoted_cmd[i] = "\'{}\'".format(elem)
        return "> " + " ".join(quoted_cmd)

def
_get_cmd_str
(
self
,
orig_cmd
)
:
quoted_cmd
=
orig_cmd
.
copy
(
)
for
i
,
elem
in
enumerate
(
quoted_cmd
)
:
if
(
" "
in
elem
)
:
quoted_cmd
[
i
]
=
"\'{}\'"
.
format
(
elem
)
return
"> "
+
" "
.
join
(
quoted_cmd
)


def create_file(self, content, path, **kwargs):
        """Create file with given content on a specified path

        If the target path requires root privileges, this function can be
        executed with option root=True.

        """
        if (self.dry):
            # In dry-run mode, run an equivalent echo command.
            cmd = ["echo", "-e", repr(content), ">", path]
        else:
            tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=False)
            with tmp_file as fd:
                fd.write(content)
            cmd = ["mv", tmp_file.name, path]
        self.run(cmd, **kwargs)
        if (not self.dry):
            self.logger.info("Created file {}".format(path))


def __init__(self):
        """Create unnamed pipe"""
        r_fd, w_fd = os.pipe()

        self.r_fd = r_fd  # read file descriptor
        self.r_fo = os.fdopen(r_fd, "r")  # read file object

        self.w_fd = w_fd  # write file descriptor
        self.w_fo = os.fdopen(w_fd, "w")

def
__init__
(
self
)
:
r_fd
,
w_fd
=
os
.
pipe
(
)
self
.
r_fd
=
r_fd
# read file descriptor
self
.
r_fo
=
os
.
fdopen
(
r_fd
,
"r"
)
# read file object
self
.
w_fd
=
w_fd
# write file descriptor
self
.
w_fo
=
os
.
fdopen
(
w_fd
,
"w"
)


def __del__(self):
        """Close pipe"""
        self.r_fo.close()
        self.w_fo.close()

def __del__ ( self ) : self . r_fo . close ( ) self . w_fo . close ( )


def readline(self):
        """Read line from pipe file"""
        return self.r_fo.readline()

def readline ( self ) : return self . r_fo . readline ( )


def write(self, data):
        """Write data to pipe file

        Args:
            data : The data to write into the pipe file

        """
        self.w_fo.write(data)

def write ( self , data ) : self . w_fo . write ( data )


def _tune_max_pipe_size(pipesize):
    """Tune the maximum size of pipes"""
    if (not which("sysctl")):
        logging.error("Couldn't tune max-pipe-size. Please check how to tune "
                      "it in your OS.")
        return False

    ret = subprocess.check_output(["sysctl", "fs.pipe-max-size"])
    current_max = int(ret.decode().split()[-1])

    if (current_max < pipesize):
        cmd = ["sysctl", "-w", "fs.pipe-max-size=" + str(pipesize)]
        print(
            textwrap.fill("The maximum pipe size that is currently "
                          "configured in your OS is of {} bytes, which is "
                          "not sufficient for the receiver application. "
                          "It will be necessary to run the following command "
                          "as root:".format(current_max),
                          width=80))
        print("\n" + " ".join(cmd) + "\n")

        if (not util.ask_yes_or_no("Is that OK?", default="y")):
            print("Abort")
            return False

        res = runner.run(cmd, root=True)
        return (res.returncode == 0)
    else:
        return True


def _get_monitor(args):
    """Create an object of the Monitor class

    Args:
        args      : SDR parser arguments
        sat_name  : Satellite name

    """
    # If debugging leandvb, don't echo the logs to stdout, otherwise the
    # logs get mixed on the console and it becomes hard to read them.
    echo = (args.debug_dvbs2 == 0)

    # Force scrolling logs if tsp is configured to print to stdout
    scrolling = tsp.prints_to_stdout(args) or args.log_scrolling

    return monitoring.Monitor(args.cfg_dir,
                              logfile=args.log_file,
                              scroll=scrolling,
                              echo=echo,
                              min_interval=args.log_interval,
                              server=args.monitoring_server,
                              port=args.monitoring_port,
                              report=args.report,
                              report_opts=monitoring.get_report_opts(args),
                              utc=args.utc)


Monitor leandvb s demodulator information

def _monitor_demod_info(info_pipe, monitor):
    """Monitor leandvb's demodulator information

    Continuously read the demodulator information printed by leandvb into the
    descriptor pointed by --fd-info, which is tied to an unnamed pipe
    file. Then, feed the information into the monitor object.

    Args:
        info_pipe : Pipe object pointing to the pipe file that is used as
                    leandvb's --fd-info descriptor
        monitor   : Object of the Monitor class used to handle receiver
                    monitoring and logging

    """
    assert (isinstance(info_pipe, util.Pipe))

    # "Standard" status format accepted by the Monitor class
    status = {'lock': (False, None), 'level': None, 'snr': None, 'ber': None}

    while True:
        line = info_pipe.readline()

        if "FRAMELOCK" in line:
            status['lock'] = (line.split()[-1] == "1", None)

        for metric in rx_stat_map:
            if metric in line:
                val = float(line.split()[-1])
                unit = rx_stat_map[metric]['unit']
                key = rx_stat_map[metric]['key']
                status[key] = (val, unit)

        # If unlocked, clear the status metrics that depend on receiver locking
        # (they will show garbage if unlocked). If locked, just make sure that
        # all the required metrics are filled in the status dictionary.
        ready = True
        for metric in rx_stat_map:
            key = rx_stat_map[metric]['key']
            if status['lock'][0]:
                if key not in status or status[key] is None:
                    ready = False
            else:
                if (key in status):
                    del status[key]

        if (not ready):
            continue

        monitor.update(status)


def subparser(subparsers):
    """Parser for sdr command"""
    p = subparsers.add_parser('sdr',
                              description="Launch SDR receiver",
                              help='Launch SDR receiver',
                              formatter_class=ArgumentDefaultsHelpFormatter)

    # Monitoring options
    monitoring.add_to_parser(p)

    rtl_p = p.add_argument_group('rtl_sdr options')
    rtl_p.add_argument('--sps',
                       default=2.0,
                       type=float,
                       help='Samples per symbol, or, equivalently, the '
                       'target oversampling ratio')
    rtl_p.add_argument('--rtl-idx',
                       default=0,
                       type=int,
                       help='RTL-SDR device index')
    rtl_p.add_argument('-g',
                       '--gain',
                       default=40,
                       type=float,
                       help='RTL-SDR Rx gain')
    rtl_p.add_argument('-f',
                       '--iq-file',
                       default=None,
                       help='File to read IQ samples from instead of reading '
                       'from the RTL-SDR in real-time')

    ldvb_p = p.add_argument_group('leandvb options')
    ldvb_p.add_argument(
        '-n',
        '--n-helpers',
        default=6,
        type=int,
        help='Number of instances of the external LDPC decoder \
                        to spawn as child processes')
    ldvb_p.add_argument(
        '-d',
        '--debug-dvbs2',
        action='count',
        default=0,
        help="Debug leandvb's DVB-S2 decoding. Use it multiple "
        "times to increase the debugging level")
    ldvb_p.add_argument('-v',
                        '--verbose',
                        default=False,
                        action='store_true',
                        help='leandvb in verbose mode')
    ldvb_p.add_argument('--gui',
                        default=False,
                        action='store_true',
                        help='GUI mode')
    ldvb_p.add_argument('--derotate',
                        default=0,
                        type=float,
                        help='Frequency offset correction to apply in kHz')
    ldvb_p.add_argument('--fastlock',
                        default=False,
                        action='store_true',
                        help='leandvb fast lock mode')
    ldvb_p.add_argument('--rrc-rej',
                        default=30,
                        type=int,
                        help='leandvb RRC rej parameter')
    ldvb_p.add_argument('-m',
                        '--modcod',
                        choices=defs.modcods.keys(),
                        default='qpsk3/5',
                        metavar='',
                        help="DVB-S2 modulation and coding (MODCOD) scheme. "
                        "Choose from: " + ", ".join(defs.modcods.keys()))
    ldvb_p.add_argument('--ldpc-dec',
                        default="ext",
                        choices=["int", "ext"],
                        help="LDPC decoder to use (internal or external)")
    ldvb_p.add_argument('--ldpc-bf',
                        default=100,
                        help='Max number of iterations used by the internal \
                        LDPC decoder when not using an external LDPC tool')
    ldvb_p.add_argument('--ldpc-iterations',
                        default=25,
                        help='Max number of iterations used by the external \
                        LDPC decoder when using an external LDPC tool')
    ldvb_p.add_argument('--framesizes',
                        type=int,
                        default=1,
                        choices=[0, 1, 2, 3],
                        help="Bitmask of desired frame sizes (1=normal, \
                        2=short)")
    ldvb_p.add_argument('--no-tsp',
                        default=False,
                        action='store_true',
                        help='Feed leandvb output to stdout instead of tsp')
    ldvb_p.add_argument('--pipe-size',
                        default=32,
                        type=int,
                        help='Size in Mbytes of the input pipe file read by \
                        leandvb')

    # TSDuck Options
    tsp.add_to_parser(p)

    p.set_defaults(func=run, record=False)

    subsubparsers = p.add_subparsers(title='subcommands',
                                     help='Target sub-command')
    # IQ recording
    p2 = subsubparsers.add_parser(
        'rec',
        description="Record IQ samples instead of "
        "feeding them into leandvb",
        help='Record IQ samples',
        formatter_class=ArgumentDefaultsHelpFormatter)
    p2.add_argument('-f',
                    '--iq-file',
                    default="blocksat.iq",
                    help='File on which to save IQ samples received with '
                    'the RTL-SDR.')
    p2.set_defaults(record=True)

    return p


Disable reverse - path ( RP ) filtering for the DVB interface

def _set_filters(dvb_ifs):
    """Disable reverse-path (RP) filtering for the DVB interface

    There are two layers of RP filters, one specific to the network interface
    and a higher level that controls the configurations for all network
    interfaces. This function disables RP filtering on the top layer (for all
    interfaces) but then enables RP filtering individually on all interfaces,
    except the DVB-S2 interface of interst. This way, in the end, only the
    DVB-S2 interface has RP filtering disabled.

    If the top layer RP filter (the "all" rule) is already disabled, this
    function disables only the target DVB-S2 interface. This strategy is
    especially useful when multiple DVB-S2 interfaces are attached to the
    host. If one of them has already disabled the "all" rule, this function
    disables only the incoming DVB-S2 interface and doesn't re-enable the RP
    filters of the other interfaces, otherwise it would end up reactivating the
    filter for the other DVB-S2 interface(s).

    Args:
        dvb_ifs : List of target DVB-S2 network interfaces.

    """
    assert (isinstance(dvb_ifs, list))

    # If the "all" rule is already disabled, disable only the target DVB-S2
    # interface.
    if (_read_filter("all", nodry=True) == 0):
        if (not runner.dry):
            print("RP filter for \"all\" interfaces is already disabled")

        for dvb_if in dvb_ifs:
            _rm_filter(dvb_if)

    # If the "all" rule is enabled, disable it, and manually enable the RP
    # filtering on all other interfaces.
    else:
        # Check interfaces
        ifs = os.listdir("/proc/sys/net/ipv4/conf/")

        # Enable all RP filters
        for interface in ifs:
            if (interface == "all" or interface == "lo"
                    or interface in dvb_ifs):
                continue

            # Enable the RP filter if not enabled already.
            if (_read_filter(interface, nodry=True) > 0):
                if (not runner.dry):
                    print("RP filter is already enabled on interface %s" %
                          (interface))
            else:
                _add_filter(interface)

        # Disable the overall RP filter
        _rm_filter("all")

        # And disable RP filtering on the DVB interface
        for dvb_if in dvb_ifs:
            _rm_filter(dvb_if)


Disable reverse - path ( RP ) filtering for the DVB interfaces

def set_filters(dvb_ifs, prompt=True, dry=False):
    """Disable reverse-path (RP) filtering for the DVB interfaces

    Args:
        dvb_ifs : list of DVB network interfaces
        prompt  : Whether to prompt user before applying a configuration
        dry     : Dry run mode

    """
    assert (isinstance(dvb_ifs, list))
    runner.set_dry(dry)
    util.print_header("Reverse Path Filters")

    # Check if the RP filters are already configured properly
    rp_filters_set = list()
    rp_filters_set.append(_read_filter("all", nodry=True) == 0)
    for dvb_if in dvb_ifs:
        rp_filters_set.append(_read_filter(dvb_if, nodry=True) == 0)

    if (all(rp_filters_set)):
        print("Current RP filtering configurations are already OK")
        print("Skipping...")
        return

    util.fill_print("It will be necessary to reconfigure some reverse path \
    (RP) filtering rules applied by the Linux kernel. This is required to \
    prevent the filtering of the one-way Blockstream Satellite traffic.")

    if (runner.dry):
        util.fill_print("The following command(s) would be executed to \
        reconfigure the RP filters:")

    if (runner.dry or (not prompt) or util.ask_yes_or_no("OK to proceed?")):
        _set_filters(dvb_ifs)
    else:
        print("RP filtering configuration cancelled")


def subparser(subparsers):
    """Parser for rp command"""
    p = subparsers.add_parser('reverse-path',
                              aliases=['rp'],
                              description="Set reverse path filters",
                              help='Set reverse path filters',
                              formatter_class=ArgumentDefaultsHelpFormatter)
    p.add_argument('-i',
                   '--interface',
                   required=True,
                   help='Network interface')
    p.add_argument('-y',
                   '--yes',
                   default=False,
                   action='store_true',
                   help="Default to answering Yes to configuration prompts")
    p.add_argument("--dry-run",
                   action='store_true',
                   default=False,
                   help="Print all commands but do not execute them")
    p.set_defaults(func=run)
    return p


def run(args):
    """Call function that sets reverse path filters

    Handles the reverse-path subcommand

    """
    set_filters([args.interface], prompt=(not args.yes), dry=args.dry_run)

def run ( args ) : set_filters ( [ args . interface ] , prompt = ( not args . yes ) , dry = args . dry_run )


def prints_to_stdout(args):
    """Check if tsp is configured to print to stdout"""
    return args.ts_monitor_bitrate or args.ts_monitor_sequence or \
        args.ts_dump

def prints_to_stdout ( args ) : return args . ts_monitor_bitrate or args . ts_monitor_sequence or args . ts_dump


def gen_cmd(self, args, in_plugin=None):
        """Generate the tsp command

        Args:
            in_plugin : List with input plugin command to be used by tsp.

        Rerturns:
            Boolean indicating whether a valid command was generated.

        """
        # cache the request for TS dumping
        self.ts_dump = args.ts_dump
        self.ts_dump_opts = args.ts_dump_opts

        self.cmd = cmd = [
            "tsp", "--realtime", "--buffer-size-mb",
            str(args.tsp_buffer_size_mb), "--max-flushed-packets",
            str(args.tsp_max_flushed_packets), "--max-input-packets",
            str(args.tsp_max_input_packets)
        ]

        if (in_plugin):
            cmd.extend(in_plugin)

        if (args.ts_analysis):
            logger.info("MPEG-TS analysis will be saved on file {}".format(
                args.ts_analysis))
            if (not util.ask_yes_or_no("Proceed?", default="y")):
                return False
            cmd.extend(["-P", "analyze", "-o", args.ts_analysis])

        if (args.ts_monitor_bitrate):
            cmd.extend([
                "-P", "bitrate_monitor", "-p",
                str(args.ts_monitor_bitrate), "--min", "0"
            ])

        if (args.ts_monitor_sequence):
            cmd.extend(["-P", "continuity"])

        cmd.extend([
            "-P", "mpe", "--pid", "-".join([str(pid) for pid in defs.pids]),
            "--udp-forward", "--local-address", args.local_address
        ])

        # Output the MPEG TS stream to one of the following:
        #   a) a file;
        #   b) stdout (if using --ts-dump);
        #   c) /dev/null (default).
        if (args.ts_file is not None):
            logger.info("MPEG TS output will be saved on file {}".format(
                args.ts_file))
            if (not util.ask_yes_or_no("Proceed?", default="y")):
                return False
            cmd.extend(["-O", "file", args.ts_file])
        elif (not args.ts_dump):
            cmd.extend(["-O", "drop"])

        return True


def run(self, stdin=None):
        """Run tsp

        Args:
            in_plugin : List with input plugin command to be used by tsp.
            stdin : Stdin to attach to the tsp process.

        Rerturns:
            Boolean indicating whether the process is running succesfully.

        """
        # Create a .tsduck.lastcheck file on the home directory to skip the
        # TSDuck version check.
        Path(os.path.join(util.get_home_dir(), ".tsduck.lastcheck")).touch()

        # When tsdump is enabled, hook tsp and tspdump through a pipe file (tsp
        # on the write side and tsdump on the read side)
        if (self.ts_dump):
            info_pipe = util.Pipe()
            stdout = info_pipe.w_fo
        else:
            stdout = None

        self.proc = subprocess.Popen(self.cmd, stdin=stdin, stdout=stdout)

        if (self.ts_dump):
            tsdump_cmd = ['tsdump']
            tsdump_cmd.extend(self.ts_dump_opts)
            self.dump_proc = subprocess.Popen(tsdump_cmd, stdin=info_pipe.r_fo)


def get_report_opts(args):
    """Extract the parser fields needed to construct a Reporter object"""
    return {
        'cfg': args.cfg,
        'cfg_dir': args.cfg_dir,
        'dest_addr': args.report_dest,
        'hostname': args.report_hostname,
        'tls_cert': args.report_cert,
        'tls_key': args.report_key,
        'gnupghome': args.report_gnupghome,
        'passphrase': args.report_passphrase
    }

def get_report_opts ( args ) : return { 'cfg' : args . cfg , 'cfg_dir' : args . cfg_dir , 'dest_addr' : args . report_dest , 'hostname' : args . report_hostname , 'tls_cert' : args . report_cert , 'tls_key' : args . report_key , 'gnupghome' : args . report_gnupghome , 'passphrase' : args . report_passphrase }


def add_to_parser(parser):
    """Add receiver monitoring options to parser"""
    m_p = parser.add_argument_group('receiver logging options')
    m_p.add_argument(
        '--log-scrolling',
        default=False,
        action='store_true',
        help='Print receiver logs line-by-line rather than repeatedly on the \
        same line')
    m_p.add_argument('--log-file',
                     default=False,
                     action='store_true',
                     help='Save receiver logs on a file')
    m_p.add_argument('--log-interval',
                     type=float,
                     default=1.0,
                     help="Logging interval in seconds")

    ms_p = parser.add_argument_group('receiver monitoring server options')
    ms_p.add_argument('--monitoring-server',
                      default=False,
                      action='store_true',
                      help='Run HTTP server to monitor the receiver')
    ms_p.add_argument('--monitoring-port',
                      default=defs.monitor_port,
                      type=int,
                      help='Monitoring server\'s port')

    r_p = parser.add_argument_group('receiver reporting options')
    r_p.add_argument('--report',
                     default=False,
                     action='store_true',
                     help='Report receiver metrics to a remote HTTP server')
    r_p.add_argument(
        '--report-dest',
        default=monitoring_api.metric_endpoint,
        help='Destination address in http://ip:port format. By default, '
        'report to Blockstream\'s Satellite Monitoring API')
    r_p.add_argument('--report-hostname', help='Reporter\'s hostname')
    r_p.add_argument(
        '--report-cert',
        default=None,
        help="Certificate for client-side authentication with the destination")
    r_p.add_argument(
        '--report-key',
        default=None,
        help="Private key for client-side authentication with the destination")
    r_p.add_argument(
        '--report-gnupghome',
        default=".gnupg",
        help="GnuPG home directory, by default created inside the config "
        "directory specified via --cfg-dir option. This option is used when "
        "reporting to Blockstream's Satellite Monitoring API only, where it "
        "determines the name of the directory (inside --cfg-dir) that holds "
        "the key for authentication with the API")
    r_p.add_argument(
        '--report-passphrase',
        default=None,
        help="Passphrase to the private GnuPG key used to sign receiver "
        "status reports sent to Blockstream's Satellite Monitoring API. If "
        "undefined (default), the program prompts for this passphrase instead."
    )


def __init__(self,
                 cfg,
                 cfg_dir,
                 dest_addr,
                 hostname=None,
                 tls_cert=None,
                 tls_key=None,
                 gnupghome=None,
                 passphrase=None):
        """Reporter Constructor

        Args:
            cfg       : User configuration
            cfg_dir   : Configuration directory
            dest_addr : Remote server address
            hostname  : Hostname used to identify the reports
            tls_cert  : Optional client side certificate for TLS authentication
            tls_key   : Key associated with the client side certificate
            gnupghome : GnuPG home directory used when reporting to
                        Blockstream's monitoring API
            passphrase : Passphrase to the private key used when reporting to
                         Blockstream's monitoring API. If None, it will be
                         obtained by prompting the user.

        """
        info = config.read_cfg_file(cfg, cfg_dir)
        assert (info is not None)

        # Validate the satellite
        satellite = info['sat']['alias']
        assert (satellite is not None), "Reporting satellite undefined"
        assert (satellite in sats), "Invalid satellite"
        self.satellite = satellite

        # Destination address, hostname, and client side cert
        self.dest_addr = dest_addr
        self.hostname = hostname
        self.tls_cert = tls_cert
        self.tls_key = tls_key

        # If the report destination address corresponds to Blockstream's
        # Monitoring API (the default), create the object to handle the
        # interaction with this API (e.g., registration).
        if (dest_addr == monitoring_api.metric_endpoint):
            self.bs_monitoring = monitoring_api.BsMonitoring(
                cfg, cfg_dir, gnupghome, passphrase)
        else:
            self.bs_monitoring = None

        logger.info("Reporting Rx status to {} ".format(self.dest_addr))


def send(self, metrics):
        """Save measurement on database

        Args:
            metrics : Dictionary with receiver metrics to send over the report
        """
        # When the receiver is not registered with the monitoring API, the
        # sign-up procedure runs. However, this procedure can only work if the
        # receiver is locked, given that it requires reception of a validation
        # code sent exclusively over satellite. Hence, the registration routine
        # (running on a thread) waits until a threading event is set below to
        # indicate the Rx lock:
        if (self.bs_monitoring and not self.bs_monitoring.registered):
            if ('lock' in metrics and metrics['lock']):
                self.bs_monitoring.rx_lock_event.set()

            # If the registration procedure has already stopped and failed, do
            # not proceed. Reporting would otherwise fail anyway, given that
            # the monitoring server does not accept reports from non-registered
            # receivers. Just warn the user and stop right here.
            if (self.bs_monitoring.registration_failure):
                print()
                logger.error("Report failed: registration incomplete "
                             "(relaunch receiver and try again)")

            # Don't send reports to the monitoring API until the receiver is
            # properly registered and verified
            return

        data = {}
        data.update(metrics)

        # When reporting to Blockstream's Monitoring API, sign every set of
        # reported data using the local GPG key and don't send the satellite
        # nor the hostname on the requests. This information is already
        # associated with the account registered with the Monitoring API. In
        # contrast, when reporting to a general-purpose server, do include the
        # satellite and hostname information if defined.
        if (self.bs_monitoring is None):
            data["satellite"] = self.satellite,
            if (self.hostname):
                data['hostname'] = self.hostname
        else:
            self.bs_monitoring.sign_report(data)

        logger.debug("Report {} to {}".format(data, self.dest_addr))

        try:
            r = requests.post(self.dest_addr,
                              json=data,
                              cert=(self.tls_cert, self.tls_key))
            if (r.status_code != requests.codes.ok):
                print()
                logger.error("Report failed: " + r.text)
        except requests.exceptions.ConnectionError as e:
            print()
            logger.error("Report failed: " + str(e))


def __init__(self,
                 cfg_dir,
                 logfile=False,
                 scroll=False,
                 echo=True,
                 min_interval=1.0,
                 server=False,
                 port=defs.monitor_port,
                 report=False,
                 report_opts={},
                 utc=True):
        """Monitor Constructor

        Args:
            cfg_dir      : Configuration directory where logs are saved
            logfile      : Whether to dump logs into a file
            scroll       : Whether to print logs with scrolling or continuously
                           overwrite the same line
            echo         : Whether to echo (print) every update to receiver
                           metrics to stdout.
            min_interval : Minimum interval in seconds between logs echoed or
                           saved.
            server       : Launch server to reply the receiver status via HTTP.
            port         : Server's HTTP port, if enabled.
            report       : Whether to report the receiver status over HTTP to
                           a remote address.
            report_opts  : Reporter options.
            utc          : Whether to print logs in UTC time.

        """
        self.cfg_dir = cfg_dir
        self.logfile = None
        self.scroll = scroll
        self.echo = echo
        self.min_interval = min_interval
        self.report = report
        self.utc = utc
        if (logfile):
            self._setup_logfile()

        # Supported receiver metrics, with their labels and printing formats
        self._metrics = {
            'lock': {
                'label': 'Lock',
                'format_str': ''
            },
            'level': {
                'label': 'Level',
                'format_str': '.2f'
            },
            'snr': {
                'label': 'SNR',
                'format_str': '.2f'
            },
            'ber': {
                'label': 'BER',
                'format_str': '.2e'
            },
            'quality': {
                'label': 'Signal Quality',
                'format_str': '.1f'
            },
            'pkt_err': {
                'label': 'Packet Errors',
                'format_str': 'd'
            }
        }
        self.stats = {}

        # Reporter sessions
        if (report):
            self.reporter = Reporter(**report_opts)

        # State
        self.t_last_print = time.time()

        # Launch HTTP server on a daemon thread
        if (server):
            self.sever_thread = threading.Thread(target=self._run_server,
                                                 args=(port, ),
                                                 daemon=True)
            self.sever_thread.start()


def _run_server(self, port):
        """Run HTTP server with access to the Monitor object"""
        server_address = ('', port)
        Server.monitor = self
        self.httpd = HTTPServer(server_address, Server)
        self.httpd.serve_forever()

def _run_server ( self , port ) : server_address = ( '' , port ) Server . monitor = self self . httpd = HTTPServer ( server_address , Server ) self . httpd . serve_forever ( )


def __str__(self):
        """Return string containing the receiver stats"""
        if (self.utc):
            timestamp = time.gmtime()
        else:
            timestamp = time.localtime()
        t_now = time.strftime("%Y-%m-%d %H:%M:%S", timestamp)

        string = "{} ".format(t_now)

        for key in self._metrics.keys():
            if key in self.stats:
                val = self.stats[key]
                # Label=Value
                string += " {} = {:{fmt_str}}".format(
                    self._metrics[key]['label'],
                    val[0],
                    fmt_str=self._metrics[key]['format_str'],
                )
                # Unit
                if (not math.isnan(val[0]) and val[1]):
                    string += val[1]

                string += ";"

        return string

def
__str__
(
self
)
:
if
(
self
.
utc
)
:
timestamp
=
time
.
gmtime
(
)
else
:
timestamp
=
time
.
localtime
(
)
t_now
=
time
.
strftime
(
"%Y-%m-%d %H:%M:%S"
,
timestamp
)
string
=
"{} "
.
format
(
t_now
)
for
key
in
self
.
_metrics
.
keys
(
)
:
if
key
in
self
.
stats
:
val
=
self
.
stats
[
key
]
# Label=Value
string
+=
" {} = {:{fmt_str}}"
.
format
(
self
.
_metrics
[
key
]
[
'label'
]
,
val
[
0
]
,
fmt_str
=
self
.
_metrics
[
key
]
[
'format_str'
]
,
)
# Unit
if
(
not
math
.
isnan
(
val
[
0
]
)
and
val
[
1
]
)
:
string
+=
val
[
1
]
string
+=
";"
return
string


def _setup_logfile(self):
        """Setup directory and file for logs"""
        log_dir = os.path.join(self.cfg_dir, "logs")
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        name = time.strftime("%Y%m%d-%H%M%S") + ".log"
        self.logfile = os.path.join(log_dir, name)
        logger.info("Saving logs at {}".format(self.logfile))

def
_setup_logfile
(
self
)
:
log_dir
=
os
.
path
.
join
(
self
.
cfg_dir
,
"logs"
)
if
not
os
.
path
.
exists
(
log_dir
)
:
os
.
makedirs
(
log_dir
)
name
=
time
.
strftime
(
"%Y%m%d-%H%M%S"
)
+
".log"
self
.
logfile
=
os
.
path
.
join
(
log_dir
,
name
)
logger
.
info
(
"Saving logs at {}"
.
format
(
self
.
logfile
)
)


def get_stats(self, strip_unit=True):
        """Get dictionary with the receiver stats

        Args:
            strip unit : Return the values directly, instead of tuples
                         containing the value and its unit.

        """
        if (not strip_unit):
            res = self.stats.copy()
        else:
            res = {}
            for key, val in self.stats.items():
                res[key] = val[0]

        return res

def get_stats ( self , strip_unit = True ) : if ( not strip_unit ) : res = self . stats . copy ( ) else : res = { } for key , val in self . stats . items ( ) : res [ key ] = val [ 0 ] return res


def update(self, data):
        """Update the receiver stats kept internally

        Args:
            data : Dictionary of tuples corresponding to the receiver metrics.
                   Each tuple should consist of the value and its unit.

        """

        # The input should be a dictionary of tuples
        assert (isinstance(data, dict))
        assert (all([isinstance(x, tuple) for x in data.values()]))

        # All keys in the input dictionary must be supported receiver metrics
        assert (all([k in self._metrics for k in data.keys()]))

        # Copy all the data
        self.stats = data

        # Is it time to log a new line?
        t_now = time.time()
        if (t_now - self.t_last_print) < self.min_interval:
            return

        self.t_last_print = t_now

        # Append metrics to log file
        if (self.logfile):
            with open(self.logfile, 'a') as fd:
                fd.write(str(self) + "\n")

        # Report over HTTP to a remote address
        if (self.report):
            self.reporter.send(self.get_stats())
            # If the reporter object is configured to report to the Monitoring
            # API but, at this point, it is still waiting for registration to
            # complete, don't log the status to the console yet. Otherwise, the
            # user would likely miss the logs indicating completion of the
            # registration procedure. On the other hand, if a log file is
            # enabled, it is OK to log into it throughout the registration.
            #
            # Besides, if the reporter is configured to report to the
            # Monitoring API and the registration is not complete yet, the
            # above call does not really send the report yet. Nevertheless, it
            # is still required. Refer to the implementation.
            #
            # As soon as the registration procedure ends (as indicated by the
            # "registration_running" flag), console logs are reactivated, even
            # if the registration fails.
            if (self.reporter.bs_monitoring is not None
                    and not self.reporter.bs_monitoring.registered
                    and self.reporter.bs_monitoring.registration_running):
                return

        # Print to console
        if (self.echo):
            print_end = '\n' if self.scroll else '\r'
            if (not self.scroll):
                sys.stdout.write("\033[K")
            print(str(self), end=print_end)


def parse_http_header(header, header_key):
    """Parse HTTP header value

    Parse the value of a specific header from a RAW HTTP response.

    :param header: String containing the RAW HTTP response and headers
    :type header: str
    :param header_key: The header name of which to extract a value from
    :type header_key: str
    :return: The value of the header
    :rtype: str

    """
    split_headers = header.split('\r\n')

    for entry in split_headers:
        header = entry.strip().split(':', 1)

        if header[0].strip().lower() == header_key.strip().lower():
            return ''.join(header[1::]).split()[0]

def parse_http_header ( header , header_key ) : split_headers = header . split ( '\r\n' ) for entry in split_headers : header = entry . strip ( ) . split ( ':' , 1 ) if header [ 0 ] . strip ( ) . lower ( ) == header_key . strip ( ) . lower ( ) : return '' . join ( header [ 1 : : ] ) . split ( ) [ 0 ]


def make_http_request(url, data=None, headers=None):
    """Helper function for making HTTP requests

    Helper function for making HTTP requests using urllib.

    :param url: The URL to which a request should be made
    :type url: str
    :param data: Provide data for the request. Request method will be set to
                 POST if data is provided
    :type data: str
    :param headers: Provide headers to send with the request
    :type headers: dict
    :return: A urllib.Request.urlopen object
    :rtype: urllib.Request.urlopen

    """
    if not headers:
        headers = {}

    # If data is provided the request method will automatically be set to POST
    # by urllib
    request = urllib.request.Request(url, data=data, headers=headers)
    return urllib.request.urlopen(request)


Decorator for checking whether the device description is available on a device .

def _device_description_required(func):
    """Decorator for checking whether the device description is available on a
    device.

    """
    @wraps(func)
    def wrapper(device, *args, **kwargs):
        if device.description is None:
            raise NotRetrievedError(
                'No device description retrieved for this device.')
        elif device.description == NotAvailableError:
            return
        return func(device, *args, **kwargs)

    return wrapper


def _get_if_index(ifname, fd):
    """Get the index corresponding to interface ifname used by socket fd"""
    ifreq = struct.pack('16si', ifname.encode(), 0)
    res = fcntl.ioctl(fd, SIOCGIFINDEX, ifreq)
    return int(struct.unpack('16si', res)[1])

def _get_if_index ( ifname , fd ) : ifreq = struct . pack ( '16si' , ifname . encode ( ) , 0 ) res = fcntl . ioctl ( fd , SIOCGIFINDEX , ifreq ) return int ( struct . unpack ( '16si' , res ) [ 1 ] )


def get_friendly_name(self):
        """Get the friendly name for the device

        Gets the device's friendly name

        :return: Friendly name of the device
        :rtype: str

        """
        return self.friendly_name

def get_friendly_name ( self ) : return self . friendly_name


def __init__(self, **headers):
        """
        Example M-SEARCH header:
        ------------------------------------------------------------------------
        M-SEARCH * HTTP/1.1          SSDP method for search requests
        HOST: 239.255.255.250:1900   SSDP multicast address and port (REQUIRED)
        MAN: "ssdp:discover"         HTTP Extension Framework scope (REQUIRED)
        MX: 2                        Maximum wait time in seconds (REQUIRED)
        ST: upnp:rootdevice          Search target (REQUIRED)
        ------------------------------------------------------------------------
        """
        self.headers = {}
        self.set_headers(**headers)

        self._available_methods = ['M-SEARCH']

        self.method = None
        self.host = self.headers.get('HOST')
        self.man = self.headers.get('MAN')
        self.mx = self.headers.get('MX')
        self.st = self.headers.get('ST')

def __init__ ( self , * * headers ) : self . headers = { } self . set_headers ( * * headers ) self . _available_methods = [ 'M-SEARCH' ] self . method = None self . host = self . headers . get ( 'HOST' ) self . man = self . headers . get ( 'MAN' ) self . mx = self . headers . get ( 'MX' ) self . st = self . headers . get ( 'ST' )


def m_search(self, discover_delay=2, st='ssdp:all', **headers):
        """Perform an M-SEARCH SSDP request

        Send an SSDP M-SEARCH request for finding UPnP devices on the network.

        :param discover_delay: Device discovery delay in seconds
        :type discover_delay: int
        :param st: Specify device Search Target
        :type st: str
        :param headers: Specify M-SEARCH specific headers
        :type headers: str
        :return: List of device that replied
        :rtype: list

        """
        self.set_method('M-SEARCH')

        self.set_header('MAN', '"ssdp:discover"')
        self.set_header('MX', discover_delay)
        self.set_header('ST', st)
        self.set_headers(**headers)

        self.socket.settimeout(discover_delay)

        devices = self._send_request(self._get_raw_request())

        for device in devices:
            yield device

def m_search ( self , discover_delay = 2 , st = 'ssdp:all' , * * headers ) : self . set_method ( 'M-SEARCH' ) self . set_header ( 'MAN' , '"ssdp:discover"' ) self . set_header ( 'MX' , discover_delay ) self . set_header ( 'ST' , st ) self . set_headers ( * * headers ) self . socket . settimeout ( discover_delay ) devices = self . _send_request ( self . _get_raw_request ( ) ) for device in devices : yield device


def _get_raw_request(self):
        """Get raw request data to send to server"""

        final_request_data = ''

        if self.method is not None:
            ssdp_start_line = '{} * HTTP/1.1'.format(self.method)
        else:
            ssdp_start_line = 'HTTP/1.1 200 OK'

        final_request_data += '{}\r\n'.format(ssdp_start_line)

        for header, value in self.headers.items():
            final_request_data += '{}: {}\r\n'.format(header, value)

        final_request_data += '\r\n'

        return final_request_data

def
_get_raw_request
(
self
)
:
final_request_data
=
''
if
self
.
method
is
not
:
ssdp_start_line
=
'{} * HTTP/1.1'
.
format
(
self
.
method
)
else
:
ssdp_start_line
=
'HTTP/1.1 200 OK'
final_request_data
+=
'{}\r\n'
.
format
(
ssdp_start_line
)
for
header
,
value
in
self
.
headers
.
items
(
)
:
final_request_data
+=
'{}: {}\r\n'
.
format
(
header
,
value
)
final_request_data
+=
'\r\n'
return
final_request_data


def discover(self, delay=2, **headers):
        """Find UPnP devices on the network

        Find available UPnP devices on the network by sending an M-SEARCH
        request.

        :param delay: Discovery delay, amount of time in seconds to wait for a
                      reply from devices
        :type delay: int
        :param headers: Optional headers for the request
        :return: List of discovered devices
        :rtype: list

        """

        discovered_devices = []
        for device in self.ssdp.m_search(discover_delay=delay,
                                         st='upnp:rootdevice',
                                         **headers):
            discovered_devices.append(device)

        self.discovered_devices = discovered_devices
        return self.discovered_devices

def discover ( self , delay = 2 , * * headers ) : discovered_devices = [ ] for device in self . ssdp . m_search ( discover_delay = delay , st = 'upnp:rootdevice' , * * headers ) : discovered_devices . append ( device ) self . discovered_devices = discovered_devices return self . discovered_devices


def _cfg_satellite():
    """Configure satellite covering the user"""
    os.system('clear')
    util.print_header("Satellite")

    help_msg = "Not sure? Check the coverage map at:\n" \
               "https://blockstream.com/satellite/#satellite_network-coverage"

    question = "Which satellite below covers your location?"
    sat = util.ask_multiple_choice(
        defs.satellites, question, "Satellite",
        lambda sat: '{} ({})'.format(sat['name'], sat['alias']), help_msg)
    return sat


Get the marketed receiver name ( including satellite kit name )

def _get_rx_marketing_name(rx):
    """Get the marketed receiver name (including satellite kit name)"""
    model = (rx['vendor'] + " " + rx['model']).strip()
    if model == "Novra S400":
        model += " (pro kit)"  # append
    elif model == "TBS 5927":
        model += " (basic kit)"  # append
    elif model == "Selfsat IP22":
        model = "Blockstream Base Station"  # overwrite
    elif model == "RTL-SDR":
        model += " software-defined"
    return model

def
_get_rx_marketing_name
(
rx
)
:
model
=
(
rx
[
'vendor'
]
+
" "
+
rx
[
'model'
]
)
.
strip
(
)
if
model
==
"Novra S400"
:
model
+=
" (pro kit)"
# append
elif
model
==
"TBS 5927"
:
model
+=
" (basic kit)"
# append
elif
model
==
"Selfsat IP22"
:
model
=
"Blockstream Base Station"
# overwrite
elif
model
==
"RTL-SDR"
:
model
+=
" software-defined"
return
model


def _get_antenna_name(x):
    """Get antenna name for the configuration menu"""
    if x['type'] == 'dish':
        s = 'Satellite Dish ({})'.format(x['label'])
    elif x['type'] == 'sat-ip':
        s = 'Blockstream Base Station (Legacy Output)'
    else:
        s = 'Blockstream Flat-Panel Antenna'
    return s


def _ask_antenna():
    """Configure antenna"""
    os.system('clear')
    util.print_header("Antenna")

    question = "What kind of antenna are you using?"
    antenna = util.ask_multiple_choice(defs.antennas,
                                       question,
                                       "Antenna",
                                       _get_antenna_name,
                                       none_option=True,
                                       none_str="Other")

    if (antenna is None):
        size = util.typed_input("Enter size in cm",
                                "Please enter an integer number in cm",
                                in_type=int)
        antenna = {'label': "custom", 'type': 'dish', 'size': size}

    return antenna

def
_ask_antenna
(
)
:
os
.
system
(
'clear'
)
util
.
print_header
(
"Antenna"
)
question
=
"What kind of antenna are you using?"
antenna
=
util
.
ask_multiple_choice
(
defs
.
antennas
,
question
,
"Antenna"
,
_get_antenna_name
,
none_option
=
True
,
none_str
=
"Other"
)
if
(
antenna
is
)
:
size
=
util
.
typed_input
(
"Enter size in cm"
,
"Please enter an integer number in cm"
,
in_type
=
int
)
antenna
=
{
'label'
:
"custom"
,
'type'
:
'dish'
,
'size'
:
size
}
return
antenna


def _cfg_rx_setup():
    """Configure Rx setup - which receiver user is using """
    os.system('clear')
    util.print_header("Receiver")

    # Receiver
    question = "Select your DVB-S2 receiver:"
    setup = util.ask_multiple_choice(
        defs.demods, question, "Receiver",
        lambda x: '{} receiver'.format(_get_rx_marketing_name(x)))

    # Network interface connected to the standalone receiver
    if (setup['type'] == defs.standalone_setup_type):
        try:
            devices = os.listdir('/sys/class/net/')
        except FileNotFoundError:
            devices = None
            pass

        question = "Which network interface is connected to the receiver?"
        if (devices is not None):
            netdev = util.ask_multiple_choice(devices, question, "Interface",
                                              lambda x: '{}'.format(x))
        else:
            netdev = input(question + " ")

        setup['netdev'] = netdev.strip()

        custom_ip = util.ask_yes_or_no(
            "Have you manually assigned a custom IP address to the receiver?",
            default="n")
        if (custom_ip):
            ipv4_addr = util.typed_input("Which IPv4 address?",
                                         in_type=IPv4Address)
            setup["rx_ip"] = str(ipv4_addr)
        else:
            setup["rx_ip"] = defs.default_standalone_ip_addr

    # Define the Sat-IP antenna directly without asking the user
    if (setup['type'] == defs.sat_ip_setup_type):
        # NOTE: this works as long as there is a single Sat-IP antenna in the
        # list. In the future, if other Sat-IP antennas are included, change to
        # a multiple-choice prompt.
        for antenna in defs.antennas:
            if antenna['type'] == 'sat-ip':
                setup['antenna'] = antenna
                return setup
        # We should have returned in the preceding line.
        raise RuntimeError("Failed to find antenna of {} receiver".format(
            defs.sat_ip_setup_type))

    # Antenna
    setup['antenna'] = _ask_antenna()

    return setup


Prompt the user for the LNB s input frequency range

def _ask_lnb_freq_range():
    """Prompt the user for the LNB's input frequency range"""
    in_range = []
    while (True):
        while (len(in_range) != 2):
            try:
                resp = input(
                    "Inform the two extreme frequencies (in MHz) of "
                    "your LNB's frequency range, separated by comma: ")
                in_range = [float(x) for x in resp.split(",")]
            except ValueError:
                continue

        if (in_range[1] < in_range[0]):
            in_range = []
            print("Please, provide the lowest frequency first, followed by "
                  "the highest.")
            continue

        if (in_range[0] < 3000 or in_range[1] < 3000):
            in_range = []
            print("Please, provide the frequencies in MHz.")
            continue

        break

    return in_range


Prompt the user for the LNB s LO frequencies

def _ask_lnb_lo(single_lo=True):
    """Prompt the user for the LNB's LO frequencies"""
    if (single_lo):
        while (True):
            lo_freq = util.typed_input("LNB LO frequency in MHz",
                                       in_type=float)

            if (lo_freq < 3000):
                print("Please, provide the frequencies in MHz.")
                continue

            break
        return lo_freq

    lo_freq = []
    while (True):
        while (len(lo_freq) != 2):
            try:
                resp = input("Inform the two LO frequencies in MHz, separated "
                             "by comma: ")
                lo_freq = [float(x) for x in resp.split(",")]
            except ValueError:
                continue

        if (lo_freq[1] < lo_freq[0]):
            lo_freq = []
            print("Please, provide the low LO frequency first, followed by "
                  "the high LO frequency.")
            continue

        if (lo_freq[0] < 3000 or lo_freq[1] < 3000):
            lo_freq = []
            print("Please, provide the frequencies in MHz.")
            continue

        break

    return lo_freq


Configure custom LNB based on user - entered specs

def _cfg_custom_lnb(sat):
    """Configure custom LNB based on user-entered specs

    Args:
        sat : user's satellite info

    """

    print("\nPlease, inform the specifications of your LNB:")

    bands = ["C", "Ku"]
    question = "Frequency band:"
    custom_lnb_band = util.ask_multiple_choice(bands, question, "Band",
                                               lambda x: '{}'.format(x))

    if (sat['band'].lower() != custom_lnb_band.lower()):
        logging.error("You must use a %s band LNB to receive from %s" %
                      (sat['band'], sat['name']))
        sys.exit(1)

    if (custom_lnb_band == "Ku"):
        custom_lnb_universal = util.ask_yes_or_no(
            "Is it a Universal Ku band LNB?")

        if (custom_lnb_universal):
            # Input frequency range
            print()
            util.fill_print("""A Universal Ku band LNB typically covers an
            input frequency range from 10.7 to 12.75 GHz.""")
            if (util.ask_yes_or_no("Does your LNB cover this input range "
                                   "from 10.7 to 12.75 GHz?")):
                custom_lnb_in_range = [10700.0, 12750.0]
            else:
                custom_lnb_in_range = _ask_lnb_freq_range()

            # LO
            print()
            util.fill_print("""A Universal Ku band LNB has two LO (local
            oscillator) frequencies. Typically the two frequencies are 9750
            MHz and 10600 MHz.""")
            if (util.ask_yes_or_no("Does your LNB have LO frequencies 9750 "
                                   "MHz and 10600 MHz?")):
                custom_lnb_lo_freq = [9750.0, 10600.0]
            else:
                custom_lnb_lo_freq = _ask_lnb_lo(single_lo=False)
        else:
            # Non-universal Ku-band LNB
            custom_lnb_in_range = _ask_lnb_freq_range()
            custom_lnb_lo_freq = _ask_lnb_lo()
    else:
        # C-band LNB
        custom_lnb_universal = False
        custom_lnb_in_range = _ask_lnb_freq_range()
        custom_lnb_lo_freq = _ask_lnb_lo()

    # Polarization
    question = "Choose the LNB polarization:"
    options = [{
        'id': "Dual",
        'text': "Dual polarization (horizontal and vertical)"
    }, {
        'id': "H",
        'text': "Horizontal"
    }, {
        'id': "V",
        'text': "Vertical"
    }]
    pol = util.ask_multiple_choice(options, question, "Polarization",
                                   lambda x: '{}'.format(x['text']))

    return {
        'vendor': "",
        'model': "",
        'in_range': custom_lnb_in_range,
        'lo_freq': custom_lnb_lo_freq,
        'universal': custom_lnb_universal,
        'band': custom_lnb_band,
        'pol': pol['id']
    }


def _sat_freq_in_lnb_range(sat, lnb):
    """Check if the satellite signal is within the LNB input range"""
    return sat['dl_freq'] > lnb['in_range'][0] and \
        sat['dl_freq'] < lnb['in_range'][1]

def _sat_freq_in_lnb_range ( sat , lnb ) : return sat [ 'dl_freq' ] > lnb [ 'in_range' ] [ 0 ] and sat [ 'dl_freq' ] < lnb [ 'in_range' ] [ 1 ]


def _ask_lnb(sat):
    """Ask the user's LNB"""
    question = "Please, inform your LNB model:"
    lnb_options = [
        lnb for lnb in defs.lnbs
        if _sat_freq_in_lnb_range(sat, lnb) and lnb['vendor'] != 'Selfsat'
    ]
    lnb = util.ask_multiple_choice(
        lnb_options,
        question,
        "LNB",
        lambda x: "{} {} {}".format(
            x['vendor'], x['model'], "(Universal Ku band LNBF)"
            if x['universal'] else ""),
        none_option=True,
        none_str="Other")

    if (lnb is None):
        lnb = _cfg_custom_lnb(sat)

    return lnb

def
_ask_lnb
(
sat
)
:
question
=
"Please, inform your LNB model:"
lnb_options
=
[
lnb
for
lnb
in
defs
.
lnbs
if
_sat_freq_in_lnb_range
(
sat
,
lnb
)
and
lnb
[
'vendor'
]
!=
'Selfsat'
]
lnb
=
util
.
ask_multiple_choice
(
lnb_options
,
question
,
"LNB"
,
lambda
x
:
"{} {} {}"
.
format
(
x
[
'vendor'
]
,
x
[
'model'
]
,
"(Universal Ku band LNBF)"
if
x
[
'universal'
]
else
""
)
,
none_option
=
True
,
none_str
=
"Other"
)
if
(
lnb
is
)
:
lnb
=
_cfg_custom_lnb
(
sat
)
return
lnb


def _ask_psu_voltage(question):
    """Prompt for the power inserter model or voltage"""
    psu = util.ask_multiple_choice(defs.psus,
                                   question,
                                   "Power inserter",
                                   lambda x: "{}".format(x['model']),
                                   none_option=True,
                                   none_str="No - another model")
    if (psu is None):
        voltage = util.typed_input("What is the voltage supplied to the "
                                   "LNB by your power inserter?")
    else:
        voltage = psu["voltage"]

    return voltage

def
_ask_psu_voltage
(
question
)
:
psu
=
util
.
ask_multiple_choice
(
defs
.
psus
,
question
,
"Power inserter"
,
lambda
x
:
"{}"
.
format
(
x
[
'model'
]
)
,
none_option
=
True
,
none_str
=
"No - another model"
)
if
(
psu
is
)
:
voltage
=
util
.
typed_input
(
"What is the voltage supplied to the "
"LNB by your power inserter?"
)
else
:
voltage
=
psu
[
"voltage"
]
return
voltage


def _cfg_lnb(sat, setup):
    """Configure LNB - either from preset or from custom specs

    Configure also the LNB power supply, if applicable.

    Args:
        sat   : user's satellite info
        setup : user's setup info

    """

    # For flat-panel and Sat-IP antennas, the LNB is the integrated one
    if (setup['antenna']['type'] in ['flat', 'sat-ip']):
        for lnb in defs.lnbs:
            if lnb['vendor'] == 'Selfsat':
                lnb["v1_pointed"] = False
                return lnb

    os.system('clear')
    util.print_header("LNB")

    lnb = _ask_lnb(sat)

    if (not _sat_freq_in_lnb_range(sat, lnb)):
        logging.warning("Your LNB's input frequency range does not cover the "
                        "frequency of {} ({} MHz)".format(
                            sat['name'], sat['dl_freq']))
        if not util.ask_yes_or_no("Continue anyway?", default="n"):
            logging.error("Invalid LNB")
            sys.exit(1)

    if (sat['band'].lower() != lnb['band'].lower()):
        logging.error("The LNB you chose cannot operate " +
                      "in %s band (band of satellite %s)" %
                      (sat['band'], sat['alias']))
        sys.exit(1)

    # When configuring a non-SDR receiver with a dual polarization LNB, we must
    # know whether the LNB was pointed before on an SDR setup in order to
    # define the polarization on channels.conf. When configuring an SDR
    # receiver, collect the power inserter voltage too for future use.
    if ((lnb['pol'].lower() == "dual" and setup['type'] != defs.sdr_setup_type)
            or setup['type'] == defs.sdr_setup_type):
        os.system('clear')
        util.print_header("Power Supply")

    if (lnb['pol'].lower() == "dual" and setup['type'] != defs.sdr_setup_type):
        prev_sdr_setup = util.ask_yes_or_no(
            "Are you reusing an LNB that is already pointed and that was used "
            "before by an SDR receiver?",
            default='n',
            help_msg="NOTE: this information is helpful to determine the "
            "polarization required for the LNB.")

        lnb["v1_pointed"] = prev_sdr_setup

        if (prev_sdr_setup):
            print()
            question = (
                "In the pre-existing SDR setup, did you use one of the "
                "LNB power inserters below?")
            lnb["v1_psu_voltage"] = _ask_psu_voltage(question)
    elif (setup['type'] == defs.sdr_setup_type):
        question = ("Are you using one of the LNB power inserters below?")
        lnb["psu_voltage"] = _ask_psu_voltage(question)

    return lnb