Files
python/docs/meshtastic/util.html
2021-12-07 15:12:21 -08:00

518 lines
23 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="generator" content="pdoc 0.10.0" />
<title>meshtastic.util API documentation</title>
<meta name="description" content="Utility functions." />
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
</head>
<body>
<main>
<article id="content">
<header>
<h1 class="title">Module <code>meshtastic.util</code></h1>
</header>
<section id="section-intro">
<p>Utility functions.</p>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">&#34;&#34;&#34; Utility functions.
&#34;&#34;&#34;
import traceback
from queue import Queue
import sys
import time
import platform
import logging
import threading
import serial
import serial.tools.list_ports
import pkg_resources
&#34;&#34;&#34;Some devices such as a seger jlink we never want to accidentally open&#34;&#34;&#34;
blacklistVids = dict.fromkeys([0x1366])
def pskToString(psk: bytes):
&#34;&#34;&#34;Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string&#34;&#34;&#34;
if len(psk) == 0:
return &#34;unencrypted&#34;
elif len(psk) == 1:
b = psk[0]
if b == 0:
return &#34;unencrypted&#34;
elif b == 1:
return &#34;default&#34;
else:
return f&#34;simple{b - 1}&#34;
else:
return &#34;secret&#34;
def stripnl(s):
&#34;&#34;&#34;remove newlines from a string (and remove extra whitespace)&#34;&#34;&#34;
s = str(s).replace(&#34;\n&#34;, &#34; &#34;)
return &#39; &#39;.join(s.split())
def fixme(message):
&#34;&#34;&#34;raise an exception for things that needs to be fixed&#34;&#34;&#34;
raise Exception(f&#34;FIXME: {message}&#34;)
def catchAndIgnore(reason, closure):
&#34;&#34;&#34;Call a closure but if it throws an excpetion print it and continue&#34;&#34;&#34;
try:
closure()
except BaseException as ex:
logging.error(f&#34;Exception thrown in {reason}: {ex}&#34;)
def findPorts():
&#34;&#34;&#34;Find all ports that might have meshtastic devices
Returns:
list -- a list of device paths
&#34;&#34;&#34;
l = list(map(lambda port: port.device,
filter(lambda port: port.vid is not None and port.vid not in blacklistVids,
serial.tools.list_ports.comports())))
l.sort()
return l
class dotdict(dict):
&#34;&#34;&#34;dot.notation access to dictionary attributes&#34;&#34;&#34;
__getattr__ = dict.get
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
class Timeout:
&#34;&#34;&#34;Timeout class&#34;&#34;&#34;
def __init__(self, maxSecs=20):
self.expireTime = 0
self.sleepInterval = 0.1
self.expireTimeout = maxSecs
def reset(self):
&#34;&#34;&#34;Restart the waitForSet timer&#34;&#34;&#34;
self.expireTime = time.time() + self.expireTimeout
def waitForSet(self, target, attrs=()):
&#34;&#34;&#34;Block until the specified attributes are set. Returns True if config has been received.&#34;&#34;&#34;
self.reset()
while time.time() &lt; self.expireTime:
if all(map(lambda a: getattr(target, a, None), attrs)):
return True
time.sleep(self.sleepInterval)
return False
class DeferredExecution():
&#34;&#34;&#34;A thread that accepts closures to run, and runs them as they are received&#34;&#34;&#34;
def __init__(self, name=None):
self.queue = Queue()
self.thread = threading.Thread(target=self._run, args=(), name=name)
self.thread.daemon = True
self.thread.start()
def queueWork(self, runnable):
&#34;&#34;&#34; Queue up the work&#34;&#34;&#34;
self.queue.put(runnable)
def _run(self):
while True:
try:
o = self.queue.get()
o()
except:
logging.error(
f&#34;Unexpected error in deferred execution {sys.exc_info()[0]}&#34;)
print(traceback.format_exc())
def our_exit(message, return_value = 1):
&#34;&#34;&#34;Print the message and return a value.
return_value defaults to 1 (non-successful)
&#34;&#34;&#34;
print(message)
sys.exit(return_value)
def support_info():
&#34;&#34;&#34;Print out info that is helping in support of the cli.&#34;&#34;&#34;
print(&#39;If having issues with meshtastic cli or python library&#39;)
print(&#39;or wish to make feature requests, visit:&#39;)
print(&#39;https://github.com/meshtastic/Meshtastic-python/issues&#39;)
print(&#39;When adding an issue, be sure to include the following info:&#39;)
print(&#39; System: {0}&#39;.format(platform.system()))
print(&#39; Platform: {0}&#39;.format(platform.platform()))
print(&#39; Release: {0}&#39;.format(platform.uname().release))
print(&#39; Machine: {0}&#39;.format(platform.uname().machine))
print(&#39; meshtastic: v{0}&#39;.format(pkg_resources.require(&#39;meshtastic&#39;)[0].version))
print(&#39; Executable: {0}&#39;.format(sys.argv[0]))
print(&#39; Python: {0} {1} {2}&#39;.format(platform.python_version(),
platform.python_implementation(), platform.python_compiler()))
print(&#39;&#39;)
print(&#39;Please add the output from the command: meshtastic --info&#39;)
print(&#39;&#39;)</code></pre>
</details>
</section>
<section>
</section>
<section>
</section>
<section>
<h2 class="section-title" id="header-functions">Functions</h2>
<dl>
<dt id="meshtastic.util.catchAndIgnore"><code class="name flex">
<span>def <span class="ident">catchAndIgnore</span></span>(<span>reason, closure)</span>
</code></dt>
<dd>
<div class="desc"><p>Call a closure but if it throws an excpetion print it and continue</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def catchAndIgnore(reason, closure):
&#34;&#34;&#34;Call a closure but if it throws an excpetion print it and continue&#34;&#34;&#34;
try:
closure()
except BaseException as ex:
logging.error(f&#34;Exception thrown in {reason}: {ex}&#34;)</code></pre>
</details>
</dd>
<dt id="meshtastic.util.findPorts"><code class="name flex">
<span>def <span class="ident">findPorts</span></span>(<span>)</span>
</code></dt>
<dd>
<div class="desc"><p>Find all ports that might have meshtastic devices</p>
<h2 id="returns">Returns</h2>
<p>list &ndash; a list of device paths</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def findPorts():
&#34;&#34;&#34;Find all ports that might have meshtastic devices
Returns:
list -- a list of device paths
&#34;&#34;&#34;
l = list(map(lambda port: port.device,
filter(lambda port: port.vid is not None and port.vid not in blacklistVids,
serial.tools.list_ports.comports())))
l.sort()
return l</code></pre>
</details>
</dd>
<dt id="meshtastic.util.fixme"><code class="name flex">
<span>def <span class="ident">fixme</span></span>(<span>message)</span>
</code></dt>
<dd>
<div class="desc"><p>raise an exception for things that needs to be fixed</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def fixme(message):
&#34;&#34;&#34;raise an exception for things that needs to be fixed&#34;&#34;&#34;
raise Exception(f&#34;FIXME: {message}&#34;)</code></pre>
</details>
</dd>
<dt id="meshtastic.util.our_exit"><code class="name flex">
<span>def <span class="ident">our_exit</span></span>(<span>message, return_value=1)</span>
</code></dt>
<dd>
<div class="desc"><p>Print the message and return a value.
return_value defaults to 1 (non-successful)</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def our_exit(message, return_value = 1):
&#34;&#34;&#34;Print the message and return a value.
return_value defaults to 1 (non-successful)
&#34;&#34;&#34;
print(message)
sys.exit(return_value)</code></pre>
</details>
</dd>
<dt id="meshtastic.util.pskToString"><code class="name flex">
<span>def <span class="ident">pskToString</span></span>(<span>psk: bytes)</span>
</code></dt>
<dd>
<div class="desc"><p>Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def pskToString(psk: bytes):
&#34;&#34;&#34;Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string&#34;&#34;&#34;
if len(psk) == 0:
return &#34;unencrypted&#34;
elif len(psk) == 1:
b = psk[0]
if b == 0:
return &#34;unencrypted&#34;
elif b == 1:
return &#34;default&#34;
else:
return f&#34;simple{b - 1}&#34;
else:
return &#34;secret&#34;</code></pre>
</details>
</dd>
<dt id="meshtastic.util.stripnl"><code class="name flex">
<span>def <span class="ident">stripnl</span></span>(<span>s)</span>
</code></dt>
<dd>
<div class="desc"><p>remove newlines from a string (and remove extra whitespace)</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def stripnl(s):
&#34;&#34;&#34;remove newlines from a string (and remove extra whitespace)&#34;&#34;&#34;
s = str(s).replace(&#34;\n&#34;, &#34; &#34;)
return &#39; &#39;.join(s.split())</code></pre>
</details>
</dd>
<dt id="meshtastic.util.support_info"><code class="name flex">
<span>def <span class="ident">support_info</span></span>(<span>)</span>
</code></dt>
<dd>
<div class="desc"><p>Print out info that is helping in support of the cli.</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def support_info():
&#34;&#34;&#34;Print out info that is helping in support of the cli.&#34;&#34;&#34;
print(&#39;If having issues with meshtastic cli or python library&#39;)
print(&#39;or wish to make feature requests, visit:&#39;)
print(&#39;https://github.com/meshtastic/Meshtastic-python/issues&#39;)
print(&#39;When adding an issue, be sure to include the following info:&#39;)
print(&#39; System: {0}&#39;.format(platform.system()))
print(&#39; Platform: {0}&#39;.format(platform.platform()))
print(&#39; Release: {0}&#39;.format(platform.uname().release))
print(&#39; Machine: {0}&#39;.format(platform.uname().machine))
print(&#39; meshtastic: v{0}&#39;.format(pkg_resources.require(&#39;meshtastic&#39;)[0].version))
print(&#39; Executable: {0}&#39;.format(sys.argv[0]))
print(&#39; Python: {0} {1} {2}&#39;.format(platform.python_version(),
platform.python_implementation(), platform.python_compiler()))
print(&#39;&#39;)
print(&#39;Please add the output from the command: meshtastic --info&#39;)
print(&#39;&#39;)</code></pre>
</details>
</dd>
</dl>
</section>
<section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="meshtastic.util.DeferredExecution"><code class="flex name class">
<span>class <span class="ident">DeferredExecution</span></span>
<span>(</span><span>name=None)</span>
</code></dt>
<dd>
<div class="desc"><p>A thread that accepts closures to run, and runs them as they are received</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class DeferredExecution():
&#34;&#34;&#34;A thread that accepts closures to run, and runs them as they are received&#34;&#34;&#34;
def __init__(self, name=None):
self.queue = Queue()
self.thread = threading.Thread(target=self._run, args=(), name=name)
self.thread.daemon = True
self.thread.start()
def queueWork(self, runnable):
&#34;&#34;&#34; Queue up the work&#34;&#34;&#34;
self.queue.put(runnable)
def _run(self):
while True:
try:
o = self.queue.get()
o()
except:
logging.error(
f&#34;Unexpected error in deferred execution {sys.exc_info()[0]}&#34;)
print(traceback.format_exc())</code></pre>
</details>
<h3>Methods</h3>
<dl>
<dt id="meshtastic.util.DeferredExecution.queueWork"><code class="name flex">
<span>def <span class="ident">queueWork</span></span>(<span>self, runnable)</span>
</code></dt>
<dd>
<div class="desc"><p>Queue up the work</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def queueWork(self, runnable):
&#34;&#34;&#34; Queue up the work&#34;&#34;&#34;
self.queue.put(runnable)</code></pre>
</details>
</dd>
</dl>
</dd>
<dt id="meshtastic.util.Timeout"><code class="flex name class">
<span>class <span class="ident">Timeout</span></span>
<span>(</span><span>maxSecs=20)</span>
</code></dt>
<dd>
<div class="desc"><p>Timeout class</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class Timeout:
&#34;&#34;&#34;Timeout class&#34;&#34;&#34;
def __init__(self, maxSecs=20):
self.expireTime = 0
self.sleepInterval = 0.1
self.expireTimeout = maxSecs
def reset(self):
&#34;&#34;&#34;Restart the waitForSet timer&#34;&#34;&#34;
self.expireTime = time.time() + self.expireTimeout
def waitForSet(self, target, attrs=()):
&#34;&#34;&#34;Block until the specified attributes are set. Returns True if config has been received.&#34;&#34;&#34;
self.reset()
while time.time() &lt; self.expireTime:
if all(map(lambda a: getattr(target, a, None), attrs)):
return True
time.sleep(self.sleepInterval)
return False</code></pre>
</details>
<h3>Methods</h3>
<dl>
<dt id="meshtastic.util.Timeout.reset"><code class="name flex">
<span>def <span class="ident">reset</span></span>(<span>self)</span>
</code></dt>
<dd>
<div class="desc"><p>Restart the waitForSet timer</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def reset(self):
&#34;&#34;&#34;Restart the waitForSet timer&#34;&#34;&#34;
self.expireTime = time.time() + self.expireTimeout</code></pre>
</details>
</dd>
<dt id="meshtastic.util.Timeout.waitForSet"><code class="name flex">
<span>def <span class="ident">waitForSet</span></span>(<span>self, target, attrs=())</span>
</code></dt>
<dd>
<div class="desc"><p>Block until the specified attributes are set. Returns True if config has been received.</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def waitForSet(self, target, attrs=()):
&#34;&#34;&#34;Block until the specified attributes are set. Returns True if config has been received.&#34;&#34;&#34;
self.reset()
while time.time() &lt; self.expireTime:
if all(map(lambda a: getattr(target, a, None), attrs)):
return True
time.sleep(self.sleepInterval)
return False</code></pre>
</details>
</dd>
</dl>
</dd>
<dt id="meshtastic.util.dotdict"><code class="flex name class">
<span>class <span class="ident">dotdict</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>dot.notation access to dictionary attributes</p></div>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class dotdict(dict):
&#34;&#34;&#34;dot.notation access to dictionary attributes&#34;&#34;&#34;
__getattr__ = dict.get
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__</code></pre>
</details>
<h3>Ancestors</h3>
<ul class="hlist">
<li>builtins.dict</li>
</ul>
</dd>
</dl>
</section>
</article>
<nav id="sidebar">
<h1>Index</h1>
<div class="toc">
<ul></ul>
</div>
<ul id="index">
<li><h3>Super-module</h3>
<ul>
<li><code><a title="meshtastic" href="index.html">meshtastic</a></code></li>
</ul>
</li>
<li><h3><a href="#header-functions">Functions</a></h3>
<ul class="two-column">
<li><code><a title="meshtastic.util.catchAndIgnore" href="#meshtastic.util.catchAndIgnore">catchAndIgnore</a></code></li>
<li><code><a title="meshtastic.util.findPorts" href="#meshtastic.util.findPorts">findPorts</a></code></li>
<li><code><a title="meshtastic.util.fixme" href="#meshtastic.util.fixme">fixme</a></code></li>
<li><code><a title="meshtastic.util.our_exit" href="#meshtastic.util.our_exit">our_exit</a></code></li>
<li><code><a title="meshtastic.util.pskToString" href="#meshtastic.util.pskToString">pskToString</a></code></li>
<li><code><a title="meshtastic.util.stripnl" href="#meshtastic.util.stripnl">stripnl</a></code></li>
<li><code><a title="meshtastic.util.support_info" href="#meshtastic.util.support_info">support_info</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="meshtastic.util.DeferredExecution" href="#meshtastic.util.DeferredExecution">DeferredExecution</a></code></h4>
<ul class="">
<li><code><a title="meshtastic.util.DeferredExecution.queueWork" href="#meshtastic.util.DeferredExecution.queueWork">queueWork</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="meshtastic.util.Timeout" href="#meshtastic.util.Timeout">Timeout</a></code></h4>
<ul class="">
<li><code><a title="meshtastic.util.Timeout.reset" href="#meshtastic.util.Timeout.reset">reset</a></code></li>
<li><code><a title="meshtastic.util.Timeout.waitForSet" href="#meshtastic.util.Timeout.waitForSet">waitForSet</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="meshtastic.util.dotdict" href="#meshtastic.util.dotdict">dotdict</a></code></h4>
</li>
</ul>
</li>
</ul>
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p>
</footer>
</body>
</html>