aprsd 1.0.0__py3-none-any.whl → 3.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aprsd/__init__.py +6 -4
- aprsd/cli_helper.py +151 -0
- aprsd/client/__init__.py +13 -0
- aprsd/client/aprsis.py +132 -0
- aprsd/client/base.py +105 -0
- aprsd/client/drivers/__init__.py +0 -0
- aprsd/client/drivers/aprsis.py +224 -0
- aprsd/client/drivers/fake.py +73 -0
- aprsd/client/drivers/kiss.py +119 -0
- aprsd/client/factory.py +88 -0
- aprsd/client/fake.py +48 -0
- aprsd/client/kiss.py +103 -0
- aprsd/client/stats.py +38 -0
- aprsd/cmds/__init__.py +0 -0
- aprsd/cmds/completion.py +22 -0
- aprsd/cmds/dev.py +162 -0
- aprsd/cmds/fetch_stats.py +156 -0
- aprsd/cmds/healthcheck.py +86 -0
- aprsd/cmds/list_plugins.py +319 -0
- aprsd/cmds/listen.py +230 -0
- aprsd/cmds/send_message.py +174 -0
- aprsd/cmds/server.py +142 -0
- aprsd/cmds/webchat.py +681 -0
- aprsd/conf/__init__.py +56 -0
- aprsd/conf/client.py +131 -0
- aprsd/conf/common.py +302 -0
- aprsd/conf/log.py +65 -0
- aprsd/conf/opts.py +80 -0
- aprsd/conf/plugin_common.py +191 -0
- aprsd/conf/plugin_email.py +105 -0
- aprsd/exception.py +13 -0
- aprsd/log/__init__.py +0 -0
- aprsd/log/log.py +138 -0
- aprsd/main.py +104 -867
- aprsd/messaging.py +4 -0
- aprsd/packets/__init__.py +12 -0
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +823 -0
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +116 -0
- aprsd/packets/seen_list.py +54 -0
- aprsd/packets/tracker.py +109 -0
- aprsd/packets/watch_list.py +122 -0
- aprsd/plugin.py +475 -284
- aprsd/plugin_utils.py +86 -0
- aprsd/plugins/__init__.py +0 -0
- aprsd/plugins/email.py +709 -0
- aprsd/plugins/fortune.py +61 -0
- aprsd/plugins/location.py +179 -0
- aprsd/plugins/notify.py +61 -0
- aprsd/plugins/ping.py +31 -0
- aprsd/plugins/time.py +115 -0
- aprsd/plugins/version.py +31 -0
- aprsd/plugins/weather.py +405 -0
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +49 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +11 -0
- aprsd/threads/aprsd.py +119 -0
- aprsd/threads/keep_alive.py +124 -0
- aprsd/threads/log_monitor.py +121 -0
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +354 -0
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +255 -0
- aprsd/utils/__init__.py +163 -0
- aprsd/utils/counter.py +51 -0
- aprsd/utils/json.py +80 -0
- aprsd/utils/objectstore.py +123 -0
- aprsd/utils/ring_buffer.py +40 -0
- aprsd/utils/trace.py +180 -0
- aprsd/web/__init__.py +0 -0
- aprsd/web/admin/__init__.py +0 -0
- aprsd/web/admin/static/css/index.css +84 -0
- aprsd/web/admin/static/css/prism.css +4 -0
- aprsd/web/admin/static/css/tabs.css +35 -0
- aprsd/web/admin/static/images/Untitled.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/admin/static/js/charts.js +235 -0
- aprsd/web/admin/static/js/echarts.js +465 -0
- aprsd/web/admin/static/js/logs.js +26 -0
- aprsd/web/admin/static/js/main.js +231 -0
- aprsd/web/admin/static/js/prism.js +12 -0
- aprsd/web/admin/static/js/send-message.js +114 -0
- aprsd/web/admin/static/js/tabs.js +28 -0
- aprsd/web/admin/templates/index.html +196 -0
- aprsd/web/chat/static/css/chat.css +115 -0
- aprsd/web/chat/static/css/index.css +66 -0
- aprsd/web/chat/static/css/style.css.map +1 -0
- aprsd/web/chat/static/css/tabs.css +41 -0
- aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
- aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
- aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
- aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
- aprsd/web/chat/static/images/Untitled.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/gps.js +84 -0
- aprsd/web/chat/static/js/main.js +45 -0
- aprsd/web/chat/static/js/send-message.js +585 -0
- aprsd/web/chat/static/js/tabs.js +28 -0
- aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
- aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
- aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
- aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
- aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
- aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
- aprsd/web/chat/templates/index.html +139 -0
- aprsd/wsgi.py +315 -0
- aprsd-3.4.1.dist-info/AUTHORS +13 -0
- aprsd-3.4.1.dist-info/LICENSE +175 -0
- aprsd-3.4.1.dist-info/METADATA +799 -0
- aprsd-3.4.1.dist-info/RECORD +134 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
- aprsd-3.4.1.dist-info/entry_points.txt +8 -0
- aprsd/fake_aprs.py +0 -83
- aprsd/utils.py +0 -166
- aprsd-1.0.0.dist-info/AUTHORS +0 -6
- aprsd-1.0.0.dist-info/METADATA +0 -181
- aprsd-1.0.0.dist-info/RECORD +0 -13
- aprsd-1.0.0.dist-info/entry_points.txt +0 -4
- aprsd-1.0.0.dist-info/pbr.json +0 -1
- /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/top_level.txt +0 -0
aprsd/__init__.py
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
1
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
4
2
|
# not use this file except in compliance with the License. You may obtain
|
5
3
|
# a copy of the License at
|
@@ -12,6 +10,10 @@
|
|
12
10
|
# License for the specific language governing permissions and limitations
|
13
11
|
# under the License.
|
14
12
|
|
15
|
-
import
|
13
|
+
from importlib.metadata import PackageNotFoundError, version
|
14
|
+
|
16
15
|
|
17
|
-
|
16
|
+
try:
|
17
|
+
__version__ = version("aprsd")
|
18
|
+
except PackageNotFoundError:
|
19
|
+
pass
|
aprsd/cli_helper.py
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
from functools import update_wrapper
|
2
|
+
import logging
|
3
|
+
from pathlib import Path
|
4
|
+
import typing as t
|
5
|
+
|
6
|
+
import click
|
7
|
+
from oslo_config import cfg
|
8
|
+
|
9
|
+
import aprsd
|
10
|
+
from aprsd import conf # noqa: F401
|
11
|
+
from aprsd.log import log
|
12
|
+
from aprsd.utils import trace
|
13
|
+
|
14
|
+
|
15
|
+
CONF = cfg.CONF
|
16
|
+
home = str(Path.home())
|
17
|
+
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
|
18
|
+
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
|
19
|
+
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf"
|
20
|
+
|
21
|
+
|
22
|
+
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
23
|
+
|
24
|
+
common_options = [
|
25
|
+
click.option(
|
26
|
+
"--loglevel",
|
27
|
+
default="INFO",
|
28
|
+
show_default=True,
|
29
|
+
type=click.Choice(
|
30
|
+
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
31
|
+
case_sensitive=False,
|
32
|
+
),
|
33
|
+
show_choices=True,
|
34
|
+
help="The log level to use for aprsd.log",
|
35
|
+
),
|
36
|
+
click.option(
|
37
|
+
"-c",
|
38
|
+
"--config",
|
39
|
+
"config_file",
|
40
|
+
show_default=True,
|
41
|
+
default=DEFAULT_CONFIG_FILE,
|
42
|
+
help="The aprsd config file to use for options.",
|
43
|
+
),
|
44
|
+
click.option(
|
45
|
+
"--quiet",
|
46
|
+
is_flag=True,
|
47
|
+
default=False,
|
48
|
+
help="Don't log to stdout",
|
49
|
+
),
|
50
|
+
]
|
51
|
+
|
52
|
+
|
53
|
+
class AliasedGroup(click.Group):
|
54
|
+
def command(self, *args, **kwargs):
|
55
|
+
"""A shortcut decorator for declaring and attaching a command to
|
56
|
+
the group. This takes the same arguments as :func:`command` but
|
57
|
+
immediately registers the created command with this instance by
|
58
|
+
calling into :meth:`add_command`.
|
59
|
+
Copied from `click` and extended for `aliases`.
|
60
|
+
"""
|
61
|
+
def decorator(f):
|
62
|
+
aliases = kwargs.pop("aliases", [])
|
63
|
+
cmd = click.decorators.command(*args, **kwargs)(f)
|
64
|
+
self.add_command(cmd)
|
65
|
+
for alias in aliases:
|
66
|
+
self.add_command(cmd, name=alias)
|
67
|
+
return cmd
|
68
|
+
return decorator
|
69
|
+
|
70
|
+
def group(self, *args, **kwargs):
|
71
|
+
"""A shortcut decorator for declaring and attaching a group to
|
72
|
+
the group. This takes the same arguments as :func:`group` but
|
73
|
+
immediately registers the created command with this instance by
|
74
|
+
calling into :meth:`add_command`.
|
75
|
+
Copied from `click` and extended for `aliases`.
|
76
|
+
"""
|
77
|
+
def decorator(f):
|
78
|
+
aliases = kwargs.pop("aliases", [])
|
79
|
+
cmd = click.decorators.group(*args, **kwargs)(f)
|
80
|
+
self.add_command(cmd)
|
81
|
+
for alias in aliases:
|
82
|
+
self.add_command(cmd, name=alias)
|
83
|
+
return cmd
|
84
|
+
return decorator
|
85
|
+
|
86
|
+
|
87
|
+
def add_options(options):
|
88
|
+
def _add_options(func):
|
89
|
+
for option in reversed(options):
|
90
|
+
func = option(func)
|
91
|
+
return func
|
92
|
+
return _add_options
|
93
|
+
|
94
|
+
|
95
|
+
def process_standard_options(f: F) -> F:
|
96
|
+
def new_func(*args, **kwargs):
|
97
|
+
ctx = args[0]
|
98
|
+
ctx.ensure_object(dict)
|
99
|
+
config_file_found = True
|
100
|
+
if kwargs["config_file"]:
|
101
|
+
default_config_files = [kwargs["config_file"]]
|
102
|
+
else:
|
103
|
+
default_config_files = None
|
104
|
+
try:
|
105
|
+
CONF(
|
106
|
+
[], project="aprsd", version=aprsd.__version__,
|
107
|
+
default_config_files=default_config_files,
|
108
|
+
)
|
109
|
+
except cfg.ConfigFilesNotFoundError:
|
110
|
+
config_file_found = False
|
111
|
+
ctx.obj["loglevel"] = kwargs["loglevel"]
|
112
|
+
# ctx.obj["config_file"] = kwargs["config_file"]
|
113
|
+
ctx.obj["quiet"] = kwargs["quiet"]
|
114
|
+
log.setup_logging(
|
115
|
+
ctx.obj["loglevel"],
|
116
|
+
ctx.obj["quiet"],
|
117
|
+
)
|
118
|
+
if CONF.trace_enabled:
|
119
|
+
trace.setup_tracing(["method", "api"])
|
120
|
+
|
121
|
+
if not config_file_found:
|
122
|
+
LOG = logging.getLogger("APRSD") # noqa: N806
|
123
|
+
LOG.error("No config file found!! run 'aprsd sample-config'")
|
124
|
+
|
125
|
+
del kwargs["loglevel"]
|
126
|
+
del kwargs["config_file"]
|
127
|
+
del kwargs["quiet"]
|
128
|
+
return f(*args, **kwargs)
|
129
|
+
|
130
|
+
return update_wrapper(t.cast(F, new_func), f)
|
131
|
+
|
132
|
+
|
133
|
+
def process_standard_options_no_config(f: F) -> F:
|
134
|
+
"""Use this as a decorator when config isn't needed."""
|
135
|
+
def new_func(*args, **kwargs):
|
136
|
+
ctx = args[0]
|
137
|
+
ctx.ensure_object(dict)
|
138
|
+
ctx.obj["loglevel"] = kwargs["loglevel"]
|
139
|
+
ctx.obj["config_file"] = kwargs["config_file"]
|
140
|
+
ctx.obj["quiet"] = kwargs["quiet"]
|
141
|
+
log.setup_logging(
|
142
|
+
ctx.obj["loglevel"],
|
143
|
+
ctx.obj["quiet"],
|
144
|
+
)
|
145
|
+
|
146
|
+
del kwargs["loglevel"]
|
147
|
+
del kwargs["config_file"]
|
148
|
+
del kwargs["quiet"]
|
149
|
+
return f(*args, **kwargs)
|
150
|
+
|
151
|
+
return update_wrapper(t.cast(F, new_func), f)
|
aprsd/client/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from aprsd.client import aprsis, factory, fake, kiss
|
2
|
+
|
3
|
+
|
4
|
+
TRANSPORT_APRSIS = "aprsis"
|
5
|
+
TRANSPORT_TCPKISS = "tcpkiss"
|
6
|
+
TRANSPORT_SERIALKISS = "serialkiss"
|
7
|
+
TRANSPORT_FAKE = "fake"
|
8
|
+
|
9
|
+
|
10
|
+
client_factory = factory.ClientFactory()
|
11
|
+
client_factory.register(aprsis.APRSISClient)
|
12
|
+
client_factory.register(kiss.KISSClient)
|
13
|
+
client_factory.register(fake.APRSDFakeClient)
|
aprsd/client/aprsis.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import time
|
4
|
+
|
5
|
+
from aprslib.exceptions import LoginError
|
6
|
+
from oslo_config import cfg
|
7
|
+
|
8
|
+
from aprsd import client, exception
|
9
|
+
from aprsd.client import base
|
10
|
+
from aprsd.client.drivers import aprsis
|
11
|
+
from aprsd.packets import core
|
12
|
+
|
13
|
+
|
14
|
+
CONF = cfg.CONF
|
15
|
+
LOG = logging.getLogger("APRSD")
|
16
|
+
|
17
|
+
|
18
|
+
class APRSISClient(base.APRSClient):
|
19
|
+
|
20
|
+
_client = None
|
21
|
+
|
22
|
+
def __init__(self):
|
23
|
+
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
|
24
|
+
self.max_delta = datetime.timedelta(**max_timeout)
|
25
|
+
|
26
|
+
def stats(self) -> dict:
|
27
|
+
stats = {}
|
28
|
+
if self.is_configured():
|
29
|
+
stats = {
|
30
|
+
"server_string": self._client.server_string,
|
31
|
+
"sever_keepalive": self._client.aprsd_keepalive,
|
32
|
+
"filter": self.filter,
|
33
|
+
}
|
34
|
+
|
35
|
+
return stats
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def is_enabled():
|
39
|
+
# Defaults to True if the enabled flag is non existent
|
40
|
+
try:
|
41
|
+
return CONF.aprs_network.enabled
|
42
|
+
except KeyError:
|
43
|
+
return False
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def is_configured():
|
47
|
+
if APRSISClient.is_enabled():
|
48
|
+
# Ensure that the config vars are correctly set
|
49
|
+
if not CONF.aprs_network.login:
|
50
|
+
LOG.error("Config aprs_network.login not set.")
|
51
|
+
raise exception.MissingConfigOptionException(
|
52
|
+
"aprs_network.login is not set.",
|
53
|
+
)
|
54
|
+
if not CONF.aprs_network.password:
|
55
|
+
LOG.error("Config aprs_network.password not set.")
|
56
|
+
raise exception.MissingConfigOptionException(
|
57
|
+
"aprs_network.password is not set.",
|
58
|
+
)
|
59
|
+
if not CONF.aprs_network.host:
|
60
|
+
LOG.error("Config aprs_network.host not set.")
|
61
|
+
raise exception.MissingConfigOptionException(
|
62
|
+
"aprs_network.host is not set.",
|
63
|
+
)
|
64
|
+
|
65
|
+
return True
|
66
|
+
return True
|
67
|
+
|
68
|
+
def _is_stale_connection(self):
|
69
|
+
delta = datetime.datetime.now() - self._client.aprsd_keepalive
|
70
|
+
if delta > self.max_delta:
|
71
|
+
LOG.error(f"Connection is stale, last heard {delta} ago.")
|
72
|
+
return True
|
73
|
+
|
74
|
+
def is_alive(self):
|
75
|
+
if self._client:
|
76
|
+
return self._client.is_alive() and not self._is_stale_connection()
|
77
|
+
else:
|
78
|
+
LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
|
79
|
+
return False
|
80
|
+
|
81
|
+
def close(self):
|
82
|
+
if self._client:
|
83
|
+
self._client.stop()
|
84
|
+
self._client.close()
|
85
|
+
|
86
|
+
@staticmethod
|
87
|
+
def transport():
|
88
|
+
return client.TRANSPORT_APRSIS
|
89
|
+
|
90
|
+
def decode_packet(self, *args, **kwargs):
|
91
|
+
"""APRS lib already decodes this."""
|
92
|
+
return core.factory(args[0])
|
93
|
+
|
94
|
+
def setup_connection(self):
|
95
|
+
user = CONF.aprs_network.login
|
96
|
+
password = CONF.aprs_network.password
|
97
|
+
host = CONF.aprs_network.host
|
98
|
+
port = CONF.aprs_network.port
|
99
|
+
self.connected = False
|
100
|
+
backoff = 1
|
101
|
+
aprs_client = None
|
102
|
+
while not self.connected:
|
103
|
+
try:
|
104
|
+
LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.")
|
105
|
+
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
106
|
+
# Force the log to be the same
|
107
|
+
aprs_client.logger = LOG
|
108
|
+
aprs_client.connect()
|
109
|
+
self.connected = True
|
110
|
+
backoff = 1
|
111
|
+
except LoginError as e:
|
112
|
+
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
113
|
+
self.connected = False
|
114
|
+
time.sleep(backoff)
|
115
|
+
except Exception as e:
|
116
|
+
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
117
|
+
self.connected = False
|
118
|
+
time.sleep(backoff)
|
119
|
+
# Don't allow the backoff to go to inifinity.
|
120
|
+
if backoff > 5:
|
121
|
+
backoff = 5
|
122
|
+
else:
|
123
|
+
backoff += 1
|
124
|
+
continue
|
125
|
+
self._client = aprs_client
|
126
|
+
return aprs_client
|
127
|
+
|
128
|
+
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
129
|
+
self._client.consumer(
|
130
|
+
callback, blocking=blocking,
|
131
|
+
immortal=immortal, raw=raw,
|
132
|
+
)
|
aprsd/client/base.py
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
import abc
|
2
|
+
import logging
|
3
|
+
import threading
|
4
|
+
|
5
|
+
from oslo_config import cfg
|
6
|
+
import wrapt
|
7
|
+
|
8
|
+
from aprsd.packets import core
|
9
|
+
|
10
|
+
|
11
|
+
CONF = cfg.CONF
|
12
|
+
LOG = logging.getLogger("APRSD")
|
13
|
+
|
14
|
+
|
15
|
+
class APRSClient:
|
16
|
+
"""Singleton client class that constructs the aprslib connection."""
|
17
|
+
|
18
|
+
_instance = None
|
19
|
+
_client = None
|
20
|
+
|
21
|
+
connected = False
|
22
|
+
filter = None
|
23
|
+
lock = threading.Lock()
|
24
|
+
|
25
|
+
def __new__(cls, *args, **kwargs):
|
26
|
+
"""This magic turns this into a singleton."""
|
27
|
+
if cls._instance is None:
|
28
|
+
cls._instance = super().__new__(cls)
|
29
|
+
# Put any initialization here.
|
30
|
+
cls._instance._create_client()
|
31
|
+
return cls._instance
|
32
|
+
|
33
|
+
@abc.abstractmethod
|
34
|
+
def stats(self) -> dict:
|
35
|
+
pass
|
36
|
+
|
37
|
+
def set_filter(self, filter):
|
38
|
+
self.filter = filter
|
39
|
+
if self._client:
|
40
|
+
self._client.set_filter(filter)
|
41
|
+
|
42
|
+
@property
|
43
|
+
def client(self):
|
44
|
+
if not self._client:
|
45
|
+
self._create_client()
|
46
|
+
return self._client
|
47
|
+
|
48
|
+
def _create_client(self):
|
49
|
+
self._client = self.setup_connection()
|
50
|
+
if self.filter:
|
51
|
+
LOG.info("Creating APRS client filter")
|
52
|
+
self._client.set_filter(self.filter)
|
53
|
+
|
54
|
+
def stop(self):
|
55
|
+
if self._client:
|
56
|
+
LOG.info("Stopping client connection.")
|
57
|
+
self._client.stop()
|
58
|
+
|
59
|
+
def send(self, packet: core.Packet):
|
60
|
+
"""Send a packet to the network."""
|
61
|
+
self.client.send(packet)
|
62
|
+
|
63
|
+
@wrapt.synchronized(lock)
|
64
|
+
def reset(self):
|
65
|
+
"""Call this to force a rebuild/reconnect."""
|
66
|
+
LOG.info("Resetting client connection.")
|
67
|
+
if self._client:
|
68
|
+
self._client.close()
|
69
|
+
del self._client
|
70
|
+
self._create_client()
|
71
|
+
else:
|
72
|
+
LOG.warning("Client not initialized, nothing to reset.")
|
73
|
+
|
74
|
+
# Recreate the client
|
75
|
+
LOG.info(f"Creating new client {self.client}")
|
76
|
+
|
77
|
+
@abc.abstractmethod
|
78
|
+
def setup_connection(self):
|
79
|
+
pass
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
@abc.abstractmethod
|
83
|
+
def is_enabled():
|
84
|
+
pass
|
85
|
+
|
86
|
+
@staticmethod
|
87
|
+
@abc.abstractmethod
|
88
|
+
def transport():
|
89
|
+
pass
|
90
|
+
|
91
|
+
@abc.abstractmethod
|
92
|
+
def decode_packet(self, *args, **kwargs):
|
93
|
+
pass
|
94
|
+
|
95
|
+
@abc.abstractmethod
|
96
|
+
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
97
|
+
pass
|
98
|
+
|
99
|
+
@abc.abstractmethod
|
100
|
+
def is_alive(self):
|
101
|
+
pass
|
102
|
+
|
103
|
+
@abc.abstractmethod
|
104
|
+
def close(self):
|
105
|
+
pass
|
File without changes
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import select
|
4
|
+
import threading
|
5
|
+
|
6
|
+
import aprslib
|
7
|
+
from aprslib import is_py3
|
8
|
+
from aprslib.exceptions import (
|
9
|
+
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
|
10
|
+
UnknownFormat,
|
11
|
+
)
|
12
|
+
import wrapt
|
13
|
+
|
14
|
+
import aprsd
|
15
|
+
from aprsd.packets import core
|
16
|
+
|
17
|
+
|
18
|
+
LOG = logging.getLogger("APRSD")
|
19
|
+
|
20
|
+
|
21
|
+
class Aprsdis(aprslib.IS):
|
22
|
+
"""Extend the aprslib class so we can exit properly."""
|
23
|
+
|
24
|
+
# flag to tell us to stop
|
25
|
+
thread_stop = False
|
26
|
+
|
27
|
+
# date for last time we heard from the server
|
28
|
+
aprsd_keepalive = datetime.datetime.now()
|
29
|
+
|
30
|
+
# timeout in seconds
|
31
|
+
select_timeout = 1
|
32
|
+
lock = threading.Lock()
|
33
|
+
|
34
|
+
def stop(self):
|
35
|
+
self.thread_stop = True
|
36
|
+
LOG.info("Shutdown Aprsdis client.")
|
37
|
+
|
38
|
+
@wrapt.synchronized(lock)
|
39
|
+
def send(self, packet: core.Packet):
|
40
|
+
"""Send an APRS Message object."""
|
41
|
+
self.sendall(packet.raw)
|
42
|
+
|
43
|
+
def is_alive(self):
|
44
|
+
"""If the connection is alive or not."""
|
45
|
+
return self._connected
|
46
|
+
|
47
|
+
def _socket_readlines(self, blocking=False):
|
48
|
+
"""
|
49
|
+
Generator for complete lines, received from the server
|
50
|
+
"""
|
51
|
+
try:
|
52
|
+
self.sock.setblocking(0)
|
53
|
+
except OSError as e:
|
54
|
+
self.logger.error(f"socket error when setblocking(0): {str(e)}")
|
55
|
+
raise aprslib.ConnectionDrop("connection dropped")
|
56
|
+
|
57
|
+
while not self.thread_stop:
|
58
|
+
short_buf = b""
|
59
|
+
newline = b"\r\n"
|
60
|
+
|
61
|
+
# set a select timeout, so we get a chance to exit
|
62
|
+
# when user hits CTRL-C
|
63
|
+
readable, writable, exceptional = select.select(
|
64
|
+
[self.sock],
|
65
|
+
[],
|
66
|
+
[],
|
67
|
+
self.select_timeout,
|
68
|
+
)
|
69
|
+
if not readable:
|
70
|
+
if not blocking:
|
71
|
+
break
|
72
|
+
else:
|
73
|
+
continue
|
74
|
+
|
75
|
+
try:
|
76
|
+
short_buf = self.sock.recv(4096)
|
77
|
+
|
78
|
+
# sock.recv returns empty if the connection drops
|
79
|
+
if not short_buf:
|
80
|
+
if not blocking:
|
81
|
+
# We could just not be blocking, so empty is expected
|
82
|
+
continue
|
83
|
+
else:
|
84
|
+
self.logger.error("socket.recv(): returned empty")
|
85
|
+
raise aprslib.ConnectionDrop("connection dropped")
|
86
|
+
except OSError as e:
|
87
|
+
# self.logger.error("socket error on recv(): %s" % str(e))
|
88
|
+
if "Resource temporarily unavailable" in str(e):
|
89
|
+
if not blocking:
|
90
|
+
if len(self.buf) == 0:
|
91
|
+
break
|
92
|
+
|
93
|
+
self.buf += short_buf
|
94
|
+
|
95
|
+
while newline in self.buf:
|
96
|
+
line, self.buf = self.buf.split(newline, 1)
|
97
|
+
|
98
|
+
yield line
|
99
|
+
|
100
|
+
def _send_login(self):
|
101
|
+
"""
|
102
|
+
Sends login string to server
|
103
|
+
"""
|
104
|
+
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
105
|
+
login_str = login_str.format(
|
106
|
+
self.callsign,
|
107
|
+
self.passwd,
|
108
|
+
(" filter " + self.filter) if self.filter != "" else "",
|
109
|
+
aprsd.__version__,
|
110
|
+
)
|
111
|
+
|
112
|
+
self.logger.debug("Sending login information")
|
113
|
+
|
114
|
+
try:
|
115
|
+
self._sendall(login_str)
|
116
|
+
self.sock.settimeout(5)
|
117
|
+
test = self.sock.recv(len(login_str) + 100)
|
118
|
+
if is_py3:
|
119
|
+
test = test.decode("latin-1")
|
120
|
+
test = test.rstrip()
|
121
|
+
|
122
|
+
self.logger.debug("Server: '%s'", test)
|
123
|
+
|
124
|
+
if not test:
|
125
|
+
raise LoginError(f"Server Response Empty: '{test}'")
|
126
|
+
|
127
|
+
_, _, callsign, status, e = test.split(" ", 4)
|
128
|
+
s = e.split(",")
|
129
|
+
if len(s):
|
130
|
+
server_string = s[0].replace("server ", "")
|
131
|
+
else:
|
132
|
+
server_string = e.replace("server ", "")
|
133
|
+
|
134
|
+
if callsign == "":
|
135
|
+
raise LoginError("Server responded with empty callsign???")
|
136
|
+
if callsign != self.callsign:
|
137
|
+
raise LoginError(f"Server: {test}")
|
138
|
+
if status != "verified," and self.passwd != "-1":
|
139
|
+
raise LoginError("Password is incorrect")
|
140
|
+
|
141
|
+
if self.passwd == "-1":
|
142
|
+
self.logger.info("Login successful (receive only)")
|
143
|
+
else:
|
144
|
+
self.logger.info("Login successful")
|
145
|
+
|
146
|
+
self.logger.info(f"Connected to {server_string}")
|
147
|
+
self.server_string = server_string
|
148
|
+
|
149
|
+
except LoginError as e:
|
150
|
+
self.logger.error(str(e))
|
151
|
+
self.close()
|
152
|
+
raise
|
153
|
+
except Exception as e:
|
154
|
+
self.close()
|
155
|
+
self.logger.error(f"Failed to login '{e}'")
|
156
|
+
self.logger.exception(e)
|
157
|
+
raise LoginError("Failed to login")
|
158
|
+
|
159
|
+
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
160
|
+
"""
|
161
|
+
When a position sentence is received, it will be passed to the callback function
|
162
|
+
|
163
|
+
blocking: if true (default), runs forever, otherwise will return after one sentence
|
164
|
+
You can still exit the loop, by raising StopIteration in the callback function
|
165
|
+
|
166
|
+
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
167
|
+
if false (default), consumer will return
|
168
|
+
|
169
|
+
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
170
|
+
"""
|
171
|
+
|
172
|
+
if not self._connected:
|
173
|
+
raise ConnectionError("not connected to a server")
|
174
|
+
|
175
|
+
line = b""
|
176
|
+
|
177
|
+
while True and not self.thread_stop:
|
178
|
+
try:
|
179
|
+
for line in self._socket_readlines(blocking):
|
180
|
+
if line[0:1] != b"#":
|
181
|
+
self.aprsd_keepalive = datetime.datetime.now()
|
182
|
+
if raw:
|
183
|
+
callback(line)
|
184
|
+
else:
|
185
|
+
callback(self._parse(line))
|
186
|
+
else:
|
187
|
+
self.logger.debug("Server: %s", line.decode("utf8"))
|
188
|
+
self.aprsd_keepalive = datetime.datetime.now()
|
189
|
+
except ParseError as exp:
|
190
|
+
self.logger.log(
|
191
|
+
11,
|
192
|
+
"%s\n Packet: %s",
|
193
|
+
exp,
|
194
|
+
exp.packet,
|
195
|
+
)
|
196
|
+
except UnknownFormat as exp:
|
197
|
+
self.logger.log(
|
198
|
+
9,
|
199
|
+
"%s\n Packet: %s",
|
200
|
+
exp,
|
201
|
+
exp.packet,
|
202
|
+
)
|
203
|
+
except LoginError as exp:
|
204
|
+
self.logger.error("%s: %s", exp.__class__.__name__, exp)
|
205
|
+
except (KeyboardInterrupt, SystemExit):
|
206
|
+
raise
|
207
|
+
except (ConnectionDrop, ConnectionError):
|
208
|
+
self.close()
|
209
|
+
|
210
|
+
if not immortal:
|
211
|
+
raise
|
212
|
+
else:
|
213
|
+
self.connect(blocking=blocking)
|
214
|
+
continue
|
215
|
+
except GenericError:
|
216
|
+
pass
|
217
|
+
except StopIteration:
|
218
|
+
break
|
219
|
+
except Exception:
|
220
|
+
self.logger.error("APRS Packet: %s", line)
|
221
|
+
raise
|
222
|
+
|
223
|
+
if not blocking:
|
224
|
+
break
|