commit 7089e0473a6881e6099105435657536ab5f7856a
Author: silhavyj <jakub.sil@seznam.cz>
Date:   Thu Apr 7 17:08:56 2022 +0200

    re #9422 Generated a documentation using pdoc

diff --git a/client/doc/pdoc/usb_detector/api_client.html b/client/doc/pdoc/usb_detector/api_client.html
new file mode 100644
index 0000000..ad906c1
--- /dev/null
+++ b/client/doc/pdoc/usb_detector/api_client.html
@@ -0,0 +1,309 @@
+<!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>usb_detector.api_client API documentation</title>
+<meta name="description" content="" />
+<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>usb_detector.api_client</code></h1>
+</header>
+<section id="section-intro">
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">import json
+import requests
+import logging
+from time import sleep
+from diskcache import Deque
+from requests import HTTPError, ConnectionError
+from requests.exceptions import InvalidSchema
+
+
+_uri = None     # server uri (url, port, and endpoint)
+_cache = None   # cache (failed payloads)
+_config = None  # instance of Config
+
+
+def api_client_set_config(config):
+    &#34;&#34;&#34;Initializes the client API module.
+
+    This function is meant to be called prior to calling any other function
+    of the API module. It stores the instance of Config (config manager)
+    into a private variable. It also initializes the cache for unsuccessful
+    payloads and constructs a URI (endpoint on the server side).
+
+    :param config: instance of Config which holds all values defined
+                   in the configuration file.
+    &#34;&#34;&#34;
+    # Store the variables globally within the module (file).
+    global _config, _cache, _uri
+
+    # Store the instance of Config and initialize the cache.
+    _config = config
+    _cache = _init_cache()
+
+    # Creates the URI which is made of the server url, port, and path (endpoint).
+    _uri = config.server_url + &#34;:&#34; + config.server_port + config.server_endpoint
+
+
+def _init_cache():
+    &#34;&#34;&#34; Initializes and returns a disk-based cache.
+
+    The cache holds payloads that the application failed
+    to send to the server. It periodically attempts to resend
+    them to the server. All parameters can be seen in the
+    configuration file.
+
+    :return: instance of a new cache (Deque - FIFO)
+    &#34;&#34;&#34;
+    return Deque(directory=_config.cache_dir)
+
+
+def send_data(payload: dict):
+    &#34;&#34;&#34;Sends a payload off to the server.
+
+    This function is called whenever a USB is connected
+    or disconnected. If there is no internet connection or the
+    server is not up and running, the payload will be stored
+    into the disk cache.
+
+    :param payload: payload to be sent to the server
+    &#34;&#34;&#34;
+    # Make sure that the URI has been constructed properly.
+    # It is supposed to be done by calling the api_client_set_config function
+    # with appropriate parameters.
+    if _uri is None:
+        logging.warning(f&#34;sending payload = {payload} failed because uri is set to None&#34;)
+        _cache_failed_payload(payload)
+        return
+    try:
+        logging.info(f&#34;sending payload = {payload} to {_uri}&#34;)
+        response = requests.post(url=_uri, data=json.dumps(payload))
+        logging.info(f&#34;response text: {response.text}&#34;)
+    except (ConnectionError, InvalidSchema):
+        logging.warning(f&#34;sending payload = {payload} to {_uri} failed&#34;)
+        _cache_failed_payload(payload)
+    except HTTPError as error:
+        logging.error(f&#34;HTTP Error ({_uri}) payload = {payload}, {error}&#34;)
+        _cache_failed_payload(payload)
+
+
+def _cache_failed_payload(payload: dict):
+    &#34;&#34;&#34; Caches a payload.
+
+    This function is called when the application fails to send a payload
+    to the server. The payload gets stored into a file-based cache from which
+    it will be periodically retrieved as the client will attempt to send
+    it to the server again. All parameters regarding the cache can be found
+    in the configuration file.
+
+    :param payload: payload to be cached
+    &#34;&#34;&#34;
+    # If the cache is &#34;full&#34;, discard the oldest record.
+    if len(_cache) &gt;= _config.cache_max_entries:
+        oldest_payload = _cache.pop()
+        logging.warning(f&#34;cache is full - discarding payload = {oldest_payload}&#34;)
+
+    # Store the payload into the cache.
+    logging.info(f&#34;adding payload = {payload} into cache&#34;)
+    _cache.append(payload)
+
+
+def _resend_cached_payloads():
+    &#34;&#34;&#34;Reattempts to send cached payloads to the server (API).
+
+    In the configuration file, there is a predefined number of
+    payloads that can be sent to the server with each call of this function.
+    This function is called periodically from api_client_run in order
+    to resend failed payloads to the server.
+
+    &#34;&#34;&#34;
+    # Calculate how many payload will be sent to the server
+    retries = min(_config.cache_max_retries, len(_cache))
+    logging.info(f&#34;emptying the cache ({retries} records)&#34;)
+
+    # Send the payloads to the server one by one.
+    for _ in range(0, retries):
+        payload = _cache.pop()
+        send_data(payload)
+
+
+def api_client_run():
+    &#34;&#34;&#34; Keeps resending failed payloads to the server.
+
+    This function is instantiated as a thread that periodically
+    calls the _resend_cached_payloads function in order to empty
+    the cache (failed payloads). The period can be set in the
+    configuration file.
+    &#34;&#34;&#34;
+    while True:
+        # Resend a predefined amount of failed payloads to the server.
+        _resend_cached_payloads()
+
+        # Sleep for a predefined amount of seconds.
+        sleep(_config.cache_retry_period_seconds)</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-functions">Functions</h2>
+<dl>
+<dt id="usb_detector.api_client.api_client_run"><code class="name flex">
+<span>def <span class="ident">api_client_run</span></span>(<span>)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Keeps resending failed payloads to the server.</p>
+<p>This function is instantiated as a thread that periodically
+calls the _resend_cached_payloads function in order to empty
+the cache (failed payloads). The period can be set in the
+configuration file.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def api_client_run():
+    &#34;&#34;&#34; Keeps resending failed payloads to the server.
+
+    This function is instantiated as a thread that periodically
+    calls the _resend_cached_payloads function in order to empty
+    the cache (failed payloads). The period can be set in the
+    configuration file.
+    &#34;&#34;&#34;
+    while True:
+        # Resend a predefined amount of failed payloads to the server.
+        _resend_cached_payloads()
+
+        # Sleep for a predefined amount of seconds.
+        sleep(_config.cache_retry_period_seconds)</code></pre>
+</details>
+</dd>
+<dt id="usb_detector.api_client.api_client_set_config"><code class="name flex">
+<span>def <span class="ident">api_client_set_config</span></span>(<span>config)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Initializes the client API module.</p>
+<p>This function is meant to be called prior to calling any other function
+of the API module. It stores the instance of Config (config manager)
+into a private variable. It also initializes the cache for unsuccessful
+payloads and constructs a URI (endpoint on the server side).</p>
+<p>:param config: instance of Config which holds all values defined
+in the configuration file.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def api_client_set_config(config):
+    &#34;&#34;&#34;Initializes the client API module.
+
+    This function is meant to be called prior to calling any other function
+    of the API module. It stores the instance of Config (config manager)
+    into a private variable. It also initializes the cache for unsuccessful
+    payloads and constructs a URI (endpoint on the server side).
+
+    :param config: instance of Config which holds all values defined
+                   in the configuration file.
+    &#34;&#34;&#34;
+    # Store the variables globally within the module (file).
+    global _config, _cache, _uri
+
+    # Store the instance of Config and initialize the cache.
+    _config = config
+    _cache = _init_cache()
+
+    # Creates the URI which is made of the server url, port, and path (endpoint).
+    _uri = config.server_url + &#34;:&#34; + config.server_port + config.server_endpoint</code></pre>
+</details>
+</dd>
+<dt id="usb_detector.api_client.send_data"><code class="name flex">
+<span>def <span class="ident">send_data</span></span>(<span>payload: dict)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Sends a payload off to the server.</p>
+<p>This function is called whenever a USB is connected
+or disconnected. If there is no internet connection or the
+server is not up and running, the payload will be stored
+into the disk cache.</p>
+<p>:param payload: payload to be sent to the server</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def send_data(payload: dict):
+    &#34;&#34;&#34;Sends a payload off to the server.
+
+    This function is called whenever a USB is connected
+    or disconnected. If there is no internet connection or the
+    server is not up and running, the payload will be stored
+    into the disk cache.
+
+    :param payload: payload to be sent to the server
+    &#34;&#34;&#34;
+    # Make sure that the URI has been constructed properly.
+    # It is supposed to be done by calling the api_client_set_config function
+    # with appropriate parameters.
+    if _uri is None:
+        logging.warning(f&#34;sending payload = {payload} failed because uri is set to None&#34;)
+        _cache_failed_payload(payload)
+        return
+    try:
+        logging.info(f&#34;sending payload = {payload} to {_uri}&#34;)
+        response = requests.post(url=_uri, data=json.dumps(payload))
+        logging.info(f&#34;response text: {response.text}&#34;)
+    except (ConnectionError, InvalidSchema):
+        logging.warning(f&#34;sending payload = {payload} to {_uri} failed&#34;)
+        _cache_failed_payload(payload)
+    except HTTPError as error:
+        logging.error(f&#34;HTTP Error ({_uri}) payload = {payload}, {error}&#34;)
+        _cache_failed_payload(payload)</code></pre>
+</details>
+</dd>
+</dl>
+</section>
+<section>
+</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="usb_detector" href="index.html">usb_detector</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-functions">Functions</a></h3>
+<ul class="">
+<li><code><a title="usb_detector.api_client.api_client_run" href="#usb_detector.api_client.api_client_run">api_client_run</a></code></li>
+<li><code><a title="usb_detector.api_client.api_client_set_config" href="#usb_detector.api_client.api_client_set_config">api_client_set_config</a></code></li>
+<li><code><a title="usb_detector.api_client.send_data" href="#usb_detector.api_client.send_data">send_data</a></code></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>
\ No newline at end of file
diff --git a/client/doc/pdoc/usb_detector/detector.html b/client/doc/pdoc/usb_detector/detector.html
new file mode 100644
index 0000000..4f3a18f
--- /dev/null
+++ b/client/doc/pdoc/usb_detector/detector.html
@@ -0,0 +1,381 @@
+<!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>usb_detector.detector API documentation</title>
+<meta name="description" content="" />
+<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>usb_detector.detector</code></h1>
+</header>
+<section id="section-intro">
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">import json
+import logging
+from time import sleep
+
+from .usb_reader import read_connected_devices
+
+
+_listeners_connected = []       # list of listeners (USB devices is connected)
+_listeners_disconnected = []    # list of listeners (USB devices is disconnected)
+
+_last_connected_devices = []    # list of the lastly connected USB devices
+_config = None                  # instance of Config (config manager)
+
+
+def usb_detector_set_config(config):
+    &#34;&#34;&#34;Initializes the usb detector module (file).
+
+    This function is meant to be called prior to calling
+    any other function of the detector module. It stores
+    an instance of the Config class which is then used
+    by other functions within this file.
+
+    :param config: instance of Config (config manager)
+    &#34;&#34;&#34;
+    # Store the instance into the global variable.
+    global _config
+    _config = config
+
+
+def register_listener(callback, connected: bool = True):
+    &#34;&#34;&#34;Registers a new event listener.
+
+    The caller is supposed to pass in a function they
+    wish to be called whenever an event occurs. What kind
+    of event will trigger (call) the callback function is
+    determined by the second parameter.
+
+    :param callback: Function that is called whenever the desired event happens.
+    :param connected: If the value is set to True, the callback function
+                      will be called whenever a USB device is connected.
+                      If it is set to False, it will be triggered whenever a
+                      USB device is disconnected.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Registering callback: {callback}.&#34;)
+
+    if connected is True:
+        # Register the callback for &#34;connected devices&#34;
+        _listeners_connected.append(callback)
+    else:
+        # Register the callback for &#34;disconnected devices&#34;
+        _listeners_disconnected.append(callback)
+
+
+def _notify_listeners(listeners: list, devices: list):
+    &#34;&#34;&#34; Notifies (calls) all listeners based on the even that just occurred.
+
+    This function is called whenever a USB device is plugged or unplugged.
+    Based on the type of the event, the corresponding list of listeners
+    is passed in along with a list of all devices involved in the event.
+
+    :param listeners: list of listeners registered for the event
+    :param devices: list of all USB devices involved in the event
+                    (usually a single device)
+    &#34;&#34;&#34;
+    # Make sure both lists are not None.
+    if listeners is None or devices is None:
+        return
+
+    # Iterate over the listeners and notify them
+    # of all USB devices involved in the event.
+    for callback in listeners:
+        for device in devices:
+            callback(device)
+
+
+def _store_connected_devices(devices: list):
+    &#34;&#34;&#34;Stores the list of the currently connected USB devices into a file.
+
+    This function is called whenever a device is connected or disconnected.
+    Its main purpose is to dump the list of the currently plugged devices
+    on the disk (so it is not kept in RAM when the computer shuts down). The
+    list is then loaded upon every start of the application.
+
+    :param devices: list of the devices that are currently connected to the PC
+    &#34;&#34;&#34;
+    logging.debug(&#34;storing newly connected devices&#34;)
+
+    # Dump the list into a JSON format.
+    with open(_config.connected_devices_filename, &#34;w&#34;) as file:
+        json.dump(devices, file)
+
+
+def _load_last_connected_devices() -&gt; list:
+    &#34;&#34;&#34;Loads the list of the connected devices from the disk.
+
+    This function is called with every start of the application.
+    It ensures that the application remembers the USB devices
+    that were connected to the PC before it was turned off
+    (persistent memory).
+
+    :return: list of the lastly connected USB devices
+    &#34;&#34;&#34;
+    logging.debug(&#34;loading last connected devices&#34;)
+    try:
+        with open(_config.connected_devices_filename, &#34;r&#34;) as file:
+            return json.loads(file.read())
+    except IOError:
+        logging.error(&#34;loading of last connected devices failed&#34;)
+        return []
+
+
+def _get_connected_devices(detected_devices: list, last_connected_devices: list) -&gt; list:
+    &#34;&#34;&#34;Returns a list of USB devices that were just plugged into the computer.
+
+    Using the two lists passed in as parameters, it figures out what devices
+    were just connected to the PC. Essentially, any device in the detected_devices list
+    that does not apper in the last_connected_devices list must have been just plugged in.
+
+    :param detected_devices: list of the currently plugged USB devices
+    :param last_connected_devices: list of the lastly connected USB devices
+    :return: list of all USB devices that were just plugged into the computer
+    &#34;&#34;&#34;
+    # If there is no previous record of what USB devices where connected to the PC,
+    # all newly-connected devices are treated as if they were just plugged in.
+    if last_connected_devices is None and detected_devices is not None:
+        return detected_devices
+
+    # Return an empty list if no devices were detected.
+    if detected_devices is None:
+        return []
+
+    # Return a list of all devices that were just plugged into the PC.
+    return [device for device in detected_devices if device not in last_connected_devices]
+
+
+def _get_disconnected_devices(detected_devices: list, last_connected_devices: list) -&gt; list:
+    &#34;&#34;&#34;Returns a list of USB devices that were just disconnected from the computer.
+
+    Using the two lists passed in as parameters, it figures out what devices where just
+    disconnected from the computer. Basically, any device that was seen connected to the PC
+    and does not apper in the detected_devices list must have been just unplugged from the PC.
+
+    :param detected_devices: list of the currently plugged USB devices
+    :param last_connected_devices: list of the lastly connected USB devices
+    :return: list of all USB devices that were just disconnected from the PC
+    &#34;&#34;&#34;
+    # If there is no previous record of what USB devices where connected to the PC,
+    # no devices were unplugged
+    if last_connected_devices is None:
+        return []
+
+    # Return last_connected_devices if no devices were detected.
+    if detected_devices is None:
+        return last_connected_devices
+
+    # Return a list of all devices that were just disconnected.
+    return [device for device in last_connected_devices if device not in detected_devices]
+
+
+def _update():
+    &#34;&#34;&#34;Updates the USB detector.
+
+    This function is periodically called from the usb_detector_run function.
+    It uses the other functions of this file to figure out if there have
+    been any changes since the last time this function was called - what
+    USB devices have been connected/disconnected.
+    &#34;&#34;&#34;
+    # Retrieve a list of the currently plugged USB devices
+    # and store it globally within the file.
+    global _last_connected_devices
+    detected_devices = read_connected_devices()
+
+    # Figure out what USB devices were connected to the PC since
+    # the last time this function was called.
+    connected_devices = _get_connected_devices(detected_devices, _last_connected_devices)
+
+    # Figure out what USB devices were disconnected from the PC since
+    # the last time this function was called.
+    disconnected_devices = _get_disconnected_devices(detected_devices, _last_connected_devices)
+
+    # Notify both kinds of listeners (call the registered callback functions).
+    _notify_listeners(_listeners_connected, connected_devices)
+    _notify_listeners(_listeners_disconnected, disconnected_devices)
+
+    # If there have been any changes, update the file on the disk (persistent memory).
+    if len(connected_devices) &gt; 0 or len(disconnected_devices) &gt; 0:
+        _store_connected_devices(detected_devices)
+        _last_connected_devices = detected_devices
+
+
+def usb_detector_run():
+    &#34;&#34;&#34;Keeps detecting what USB devices were plugged/unplugged.
+
+    This function is instantiated as a thread that periodically scans
+    what USB devices are connected to the PC. The scan period can be
+    found and modified in the configuration file of the application.
+    &#34;&#34;&#34;
+    logging.info(&#34;USB device detector is now running&#34;)
+
+    # Read the list of the lastly connected USB devices from the disk (once).
+    global _last_connected_devices
+    _last_connected_devices = _load_last_connected_devices()
+
+    while True:
+        # Update the USB detector.
+        _update()
+
+        # Sleep for a predefined amount of seconds.
+        sleep(_config.scan_period_seconds)</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-functions">Functions</h2>
+<dl>
+<dt id="usb_detector.detector.register_listener"><code class="name flex">
+<span>def <span class="ident">register_listener</span></span>(<span>callback, connected: bool = True)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Registers a new event listener.</p>
+<p>The caller is supposed to pass in a function they
+wish to be called whenever an event occurs. What kind
+of event will trigger (call) the callback function is
+determined by the second parameter.</p>
+<p>:param callback: Function that is called whenever the desired event happens.
+:param connected: If the value is set to True, the callback function
+will be called whenever a USB device is connected.
+If it is set to False, it will be triggered whenever a
+USB device is disconnected.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def register_listener(callback, connected: bool = True):
+    &#34;&#34;&#34;Registers a new event listener.
+
+    The caller is supposed to pass in a function they
+    wish to be called whenever an event occurs. What kind
+    of event will trigger (call) the callback function is
+    determined by the second parameter.
+
+    :param callback: Function that is called whenever the desired event happens.
+    :param connected: If the value is set to True, the callback function
+                      will be called whenever a USB device is connected.
+                      If it is set to False, it will be triggered whenever a
+                      USB device is disconnected.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Registering callback: {callback}.&#34;)
+
+    if connected is True:
+        # Register the callback for &#34;connected devices&#34;
+        _listeners_connected.append(callback)
+    else:
+        # Register the callback for &#34;disconnected devices&#34;
+        _listeners_disconnected.append(callback)</code></pre>
+</details>
+</dd>
+<dt id="usb_detector.detector.usb_detector_run"><code class="name flex">
+<span>def <span class="ident">usb_detector_run</span></span>(<span>)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Keeps detecting what USB devices were plugged/unplugged.</p>
+<p>This function is instantiated as a thread that periodically scans
+what USB devices are connected to the PC. The scan period can be
+found and modified in the configuration file of the application.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def usb_detector_run():
+    &#34;&#34;&#34;Keeps detecting what USB devices were plugged/unplugged.
+
+    This function is instantiated as a thread that periodically scans
+    what USB devices are connected to the PC. The scan period can be
+    found and modified in the configuration file of the application.
+    &#34;&#34;&#34;
+    logging.info(&#34;USB device detector is now running&#34;)
+
+    # Read the list of the lastly connected USB devices from the disk (once).
+    global _last_connected_devices
+    _last_connected_devices = _load_last_connected_devices()
+
+    while True:
+        # Update the USB detector.
+        _update()
+
+        # Sleep for a predefined amount of seconds.
+        sleep(_config.scan_period_seconds)</code></pre>
+</details>
+</dd>
+<dt id="usb_detector.detector.usb_detector_set_config"><code class="name flex">
+<span>def <span class="ident">usb_detector_set_config</span></span>(<span>config)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Initializes the usb detector module (file).</p>
+<p>This function is meant to be called prior to calling
+any other function of the detector module. It stores
+an instance of the Config class which is then used
+by other functions within this file.</p>
+<p>:param config: instance of Config (config manager)</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def usb_detector_set_config(config):
+    &#34;&#34;&#34;Initializes the usb detector module (file).
+
+    This function is meant to be called prior to calling
+    any other function of the detector module. It stores
+    an instance of the Config class which is then used
+    by other functions within this file.
+
+    :param config: instance of Config (config manager)
+    &#34;&#34;&#34;
+    # Store the instance into the global variable.
+    global _config
+    _config = config</code></pre>
+</details>
+</dd>
+</dl>
+</section>
+<section>
+</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="usb_detector" href="index.html">usb_detector</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-functions">Functions</a></h3>
+<ul class="">
+<li><code><a title="usb_detector.detector.register_listener" href="#usb_detector.detector.register_listener">register_listener</a></code></li>
+<li><code><a title="usb_detector.detector.usb_detector_run" href="#usb_detector.detector.usb_detector_run">usb_detector_run</a></code></li>
+<li><code><a title="usb_detector.detector.usb_detector_set_config" href="#usb_detector.detector.usb_detector_set_config">usb_detector_set_config</a></code></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>
\ No newline at end of file
diff --git a/client/doc/pdoc/usb_detector/event_listener.html b/client/doc/pdoc/usb_detector/event_listener.html
new file mode 100644
index 0000000..284119a
--- /dev/null
+++ b/client/doc/pdoc/usb_detector/event_listener.html
@@ -0,0 +1,206 @@
+<!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>usb_detector.event_listener API documentation</title>
+<meta name="description" content="" />
+<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>usb_detector.event_listener</code></h1>
+</header>
+<section id="section-intro">
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">import platform
+import logging
+import getpass
+from datetime import datetime
+
+from .api_client import send_data
+
+
+def _get_metadata() -&gt; dict:
+    &#34;&#34;&#34;Returns metadata associated with the computer.
+
+    This metadata is sent to the server as a part
+    of each payload. It includes the username, hostname,
+    and timestamp.
+
+    :return: metadata associated with the PC
+    &#34;&#34;&#34;
+    logging.debug(&#34;getting computer metadata&#34;)
+    return {
+        &#34;username&#34;: getpass.getuser(),                  # username
+        &#34;hostname&#34;: platform.uname().node,              # hostname
+        &#34;timestamp&#34;: str(datetime.now()).split(&#39;.&#39;)[0]  # timestamp (format: 2022-04-07 12:11:02)
+    }
+
+
+def _send_payload_to_server(device: dict, status: str):
+    &#34;&#34;&#34; Creates a payload and calls the send_data function to send it to the server.
+
+    Each payload consists of metadata, status (connected/disconnected), and device,
+    which contains a vendor id, product id, and serial number.
+
+    :param device: USB device that has been detected
+    :param status: status of the USB device (connected/disconnected)
+    &#34;&#34;&#34;
+    logging.debug(&#34;payload send preparation&#34;)
+
+    # Get metadata that will be put into the payload.
+    payload = _get_metadata()
+
+    # Add information about the USB device.
+    payload[&#34;device&#34;] = device
+
+    # Add the status of the USB device (connected/disconnected).
+    payload[&#34;status&#34;] = status
+
+    # Send the payload off to the server.
+    send_data(payload)
+
+
+def usb_connected_callback(device: dict):
+    &#34;&#34;&#34;Callback function for a connected USB device.
+
+    This function gets called whenever a USB device is
+    plugged into the computer (it is registered as a listener in
+    the USB detector). The device consists of a vendor id, product id,
+    and serial number.
+
+    :param device: USB device that was just plugged into the PC.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Device {device} has been connected&#34;)
+
+    # Create a payload and send it to the API (server).
+    _send_payload_to_server(device, &#34;connected&#34;)
+
+
+def usb_disconnected_callback(device: dict):
+    &#34;&#34;&#34;Callback function for a disconnected USB device.
+
+    This function gets called whenever a USB device is
+    disconnected from the computer (it is registered as a listener in
+    the USB detector). The device consists of a vendor id, product id,
+    and serial number.
+
+    :param device: USB device that was just disconnected from the PC.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Device {device} has been disconnected&#34;)
+
+    # Create a payload and send it to the API (server).
+    _send_payload_to_server(device, &#34;disconnected&#34;)</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-functions">Functions</h2>
+<dl>
+<dt id="usb_detector.event_listener.usb_connected_callback"><code class="name flex">
+<span>def <span class="ident">usb_connected_callback</span></span>(<span>device: dict)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Callback function for a connected USB device.</p>
+<p>This function gets called whenever a USB device is
+plugged into the computer (it is registered as a listener in
+the USB detector). The device consists of a vendor id, product id,
+and serial number.</p>
+<p>:param device: USB device that was just plugged into the PC.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def usb_connected_callback(device: dict):
+    &#34;&#34;&#34;Callback function for a connected USB device.
+
+    This function gets called whenever a USB device is
+    plugged into the computer (it is registered as a listener in
+    the USB detector). The device consists of a vendor id, product id,
+    and serial number.
+
+    :param device: USB device that was just plugged into the PC.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Device {device} has been connected&#34;)
+
+    # Create a payload and send it to the API (server).
+    _send_payload_to_server(device, &#34;connected&#34;)</code></pre>
+</details>
+</dd>
+<dt id="usb_detector.event_listener.usb_disconnected_callback"><code class="name flex">
+<span>def <span class="ident">usb_disconnected_callback</span></span>(<span>device: dict)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Callback function for a disconnected USB device.</p>
+<p>This function gets called whenever a USB device is
+disconnected from the computer (it is registered as a listener in
+the USB detector). The device consists of a vendor id, product id,
+and serial number.</p>
+<p>:param device: USB device that was just disconnected from the PC.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def usb_disconnected_callback(device: dict):
+    &#34;&#34;&#34;Callback function for a disconnected USB device.
+
+    This function gets called whenever a USB device is
+    disconnected from the computer (it is registered as a listener in
+    the USB detector). The device consists of a vendor id, product id,
+    and serial number.
+
+    :param device: USB device that was just disconnected from the PC.
+    &#34;&#34;&#34;
+    logging.info(f&#34;Device {device} has been disconnected&#34;)
+
+    # Create a payload and send it to the API (server).
+    _send_payload_to_server(device, &#34;disconnected&#34;)</code></pre>
+</details>
+</dd>
+</dl>
+</section>
+<section>
+</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="usb_detector" href="index.html">usb_detector</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-functions">Functions</a></h3>
+<ul class="">
+<li><code><a title="usb_detector.event_listener.usb_connected_callback" href="#usb_detector.event_listener.usb_connected_callback">usb_connected_callback</a></code></li>
+<li><code><a title="usb_detector.event_listener.usb_disconnected_callback" href="#usb_detector.event_listener.usb_disconnected_callback">usb_disconnected_callback</a></code></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>
\ No newline at end of file
diff --git a/client/doc/pdoc/usb_detector/index.html b/client/doc/pdoc/usb_detector/index.html
new file mode 100644
index 0000000..bdba2ce
--- /dev/null
+++ b/client/doc/pdoc/usb_detector/index.html
@@ -0,0 +1,75 @@
+<!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>usb_detector API documentation</title>
+<meta name="description" content="" />
+<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">Package <code>usb_detector</code></h1>
+</header>
+<section id="section-intro">
+</section>
+<section>
+<h2 class="section-title" id="header-submodules">Sub-modules</h2>
+<dl>
+<dt><code class="name"><a title="usb_detector.api_client" href="api_client.html">usb_detector.api_client</a></code></dt>
+<dd>
+<div class="desc"></div>
+</dd>
+<dt><code class="name"><a title="usb_detector.detector" href="detector.html">usb_detector.detector</a></code></dt>
+<dd>
+<div class="desc"></div>
+</dd>
+<dt><code class="name"><a title="usb_detector.event_listener" href="event_listener.html">usb_detector.event_listener</a></code></dt>
+<dd>
+<div class="desc"></div>
+</dd>
+<dt><code class="name"><a title="usb_detector.usb_reader" href="usb_reader.html">usb_detector.usb_reader</a></code></dt>
+<dd>
+<div class="desc"></div>
+</dd>
+</dl>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3><a href="#header-submodules">Sub-modules</a></h3>
+<ul>
+<li><code><a title="usb_detector.api_client" href="api_client.html">usb_detector.api_client</a></code></li>
+<li><code><a title="usb_detector.detector" href="detector.html">usb_detector.detector</a></code></li>
+<li><code><a title="usb_detector.event_listener" href="event_listener.html">usb_detector.event_listener</a></code></li>
+<li><code><a title="usb_detector.usb_reader" href="usb_reader.html">usb_detector.usb_reader</a></code></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>
\ No newline at end of file
diff --git a/client/doc/pdoc/usb_detector/usb_reader.html b/client/doc/pdoc/usb_detector/usb_reader.html
new file mode 100644
index 0000000..f961011
--- /dev/null
+++ b/client/doc/pdoc/usb_detector/usb_reader.html
@@ -0,0 +1,202 @@
+<!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>usb_detector.usb_reader API documentation</title>
+<meta name="description" content="" />
+<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>usb_detector.usb_reader</code></h1>
+</header>
+<section id="section-intro">
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">import logging
+
+import usb.core
+import usb.util
+
+
+# list of devices from which the application
+# could not retrieve a serial number
+_invalid_devices = []
+
+
+def read_connected_devices():
+    &#34;&#34;&#34;Reads and returns all USB devices that are currently connected to the computer.
+
+    It iterates over devices connected to individual buses and for each of
+    them, it tries to retrieve its vendor id, product id, and serial number.
+    If the application fails to retrieve the serial number of a device, it
+    will store it into an in-RAM list to prevent &#34;spam&#34; logs. Once the application
+    manages to read the serial number, the device will be removed from the list.
+
+    :return: list of all USB devices connected to the PC
+    &#34;&#34;&#34;
+    logging.debug(&#34;reading all currently connected devices&#34;)
+
+    # Create an empty list of USB devices.
+    detected_devices = []
+
+    # Get a list of all buses.
+    busses = usb.busses()
+
+    for bus in busses:
+        # Get all devices connected to the current bus.
+        devices = bus.devices
+        for dev in devices:
+            # Create a record of the device.
+            device = {
+                &#34;vendor_id&#34;: dev.idVendor,
+                &#34;product_id&#34;: dev.idProduct
+            }
+
+            # Try to retrieve the serial number of the device.
+            serial_number = None
+            device_info = usb.core.find(idProduct=dev.idProduct)
+            try:
+                serial_number = usb.util.get_string(device_info, device_info.iSerialNumber)
+            except ValueError:
+                # If you fail, store the device into the in-RAM list (if it is not already there).
+                if device not in _invalid_devices:
+                    logging.warning(f&#34;Could not retrieve serial number from device {device}&#34;)
+                    _invalid_devices.append(device)
+
+            if serial_number is not None:
+                # If you manage to read the serial number of a USB device
+                # that was previously stored into the list of &#34;failures&#34;, remove it.
+                if device in _invalid_devices:
+                    _invalid_devices.remove(device)
+
+                # Add the serial number into to USB device record.
+                device[&#34;serial_number&#34;] = serial_number
+
+                # Append the record into the list of the connected USB devices.
+                detected_devices.append(device)
+
+    # Return the list of currently plugged USB devices.
+    return detected_devices</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-functions">Functions</h2>
+<dl>
+<dt id="usb_detector.usb_reader.read_connected_devices"><code class="name flex">
+<span>def <span class="ident">read_connected_devices</span></span>(<span>)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Reads and returns all USB devices that are currently connected to the computer.</p>
+<p>It iterates over devices connected to individual buses and for each of
+them, it tries to retrieve its vendor id, product id, and serial number.
+If the application fails to retrieve the serial number of a device, it
+will store it into an in-RAM list to prevent "spam" logs. Once the application
+manages to read the serial number, the device will be removed from the list.</p>
+<p>:return: list of all USB devices connected to the PC</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def read_connected_devices():
+    &#34;&#34;&#34;Reads and returns all USB devices that are currently connected to the computer.
+
+    It iterates over devices connected to individual buses and for each of
+    them, it tries to retrieve its vendor id, product id, and serial number.
+    If the application fails to retrieve the serial number of a device, it
+    will store it into an in-RAM list to prevent &#34;spam&#34; logs. Once the application
+    manages to read the serial number, the device will be removed from the list.
+
+    :return: list of all USB devices connected to the PC
+    &#34;&#34;&#34;
+    logging.debug(&#34;reading all currently connected devices&#34;)
+
+    # Create an empty list of USB devices.
+    detected_devices = []
+
+    # Get a list of all buses.
+    busses = usb.busses()
+
+    for bus in busses:
+        # Get all devices connected to the current bus.
+        devices = bus.devices
+        for dev in devices:
+            # Create a record of the device.
+            device = {
+                &#34;vendor_id&#34;: dev.idVendor,
+                &#34;product_id&#34;: dev.idProduct
+            }
+
+            # Try to retrieve the serial number of the device.
+            serial_number = None
+            device_info = usb.core.find(idProduct=dev.idProduct)
+            try:
+                serial_number = usb.util.get_string(device_info, device_info.iSerialNumber)
+            except ValueError:
+                # If you fail, store the device into the in-RAM list (if it is not already there).
+                if device not in _invalid_devices:
+                    logging.warning(f&#34;Could not retrieve serial number from device {device}&#34;)
+                    _invalid_devices.append(device)
+
+            if serial_number is not None:
+                # If you manage to read the serial number of a USB device
+                # that was previously stored into the list of &#34;failures&#34;, remove it.
+                if device in _invalid_devices:
+                    _invalid_devices.remove(device)
+
+                # Add the serial number into to USB device record.
+                device[&#34;serial_number&#34;] = serial_number
+
+                # Append the record into the list of the connected USB devices.
+                detected_devices.append(device)
+
+    # Return the list of currently plugged USB devices.
+    return detected_devices</code></pre>
+</details>
+</dd>
+</dl>
+</section>
+<section>
+</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="usb_detector" href="index.html">usb_detector</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-functions">Functions</a></h3>
+<ul class="">
+<li><code><a title="usb_detector.usb_reader.read_connected_devices" href="#usb_detector.usb_reader.read_connected_devices">read_connected_devices</a></code></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>
\ No newline at end of file
