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.
Files changed (141) hide show
  1. aprsd/__init__.py +6 -4
  2. aprsd/cli_helper.py +151 -0
  3. aprsd/client/__init__.py +13 -0
  4. aprsd/client/aprsis.py +132 -0
  5. aprsd/client/base.py +105 -0
  6. aprsd/client/drivers/__init__.py +0 -0
  7. aprsd/client/drivers/aprsis.py +224 -0
  8. aprsd/client/drivers/fake.py +73 -0
  9. aprsd/client/drivers/kiss.py +119 -0
  10. aprsd/client/factory.py +88 -0
  11. aprsd/client/fake.py +48 -0
  12. aprsd/client/kiss.py +103 -0
  13. aprsd/client/stats.py +38 -0
  14. aprsd/cmds/__init__.py +0 -0
  15. aprsd/cmds/completion.py +22 -0
  16. aprsd/cmds/dev.py +162 -0
  17. aprsd/cmds/fetch_stats.py +156 -0
  18. aprsd/cmds/healthcheck.py +86 -0
  19. aprsd/cmds/list_plugins.py +319 -0
  20. aprsd/cmds/listen.py +230 -0
  21. aprsd/cmds/send_message.py +174 -0
  22. aprsd/cmds/server.py +142 -0
  23. aprsd/cmds/webchat.py +681 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +302 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +191 -0
  30. aprsd/conf/plugin_email.py +105 -0
  31. aprsd/exception.py +13 -0
  32. aprsd/log/__init__.py +0 -0
  33. aprsd/log/log.py +138 -0
  34. aprsd/main.py +104 -867
  35. aprsd/messaging.py +4 -0
  36. aprsd/packets/__init__.py +12 -0
  37. aprsd/packets/collector.py +56 -0
  38. aprsd/packets/core.py +823 -0
  39. aprsd/packets/log.py +143 -0
  40. aprsd/packets/packet_list.py +116 -0
  41. aprsd/packets/seen_list.py +54 -0
  42. aprsd/packets/tracker.py +109 -0
  43. aprsd/packets/watch_list.py +122 -0
  44. aprsd/plugin.py +475 -284
  45. aprsd/plugin_utils.py +86 -0
  46. aprsd/plugins/__init__.py +0 -0
  47. aprsd/plugins/email.py +709 -0
  48. aprsd/plugins/fortune.py +61 -0
  49. aprsd/plugins/location.py +179 -0
  50. aprsd/plugins/notify.py +61 -0
  51. aprsd/plugins/ping.py +31 -0
  52. aprsd/plugins/time.py +115 -0
  53. aprsd/plugins/version.py +31 -0
  54. aprsd/plugins/weather.py +405 -0
  55. aprsd/stats/__init__.py +20 -0
  56. aprsd/stats/app.py +49 -0
  57. aprsd/stats/collector.py +38 -0
  58. aprsd/threads/__init__.py +11 -0
  59. aprsd/threads/aprsd.py +119 -0
  60. aprsd/threads/keep_alive.py +124 -0
  61. aprsd/threads/log_monitor.py +121 -0
  62. aprsd/threads/registry.py +56 -0
  63. aprsd/threads/rx.py +354 -0
  64. aprsd/threads/stats.py +44 -0
  65. aprsd/threads/tx.py +255 -0
  66. aprsd/utils/__init__.py +163 -0
  67. aprsd/utils/counter.py +51 -0
  68. aprsd/utils/json.py +80 -0
  69. aprsd/utils/objectstore.py +123 -0
  70. aprsd/utils/ring_buffer.py +40 -0
  71. aprsd/utils/trace.py +180 -0
  72. aprsd/web/__init__.py +0 -0
  73. aprsd/web/admin/__init__.py +0 -0
  74. aprsd/web/admin/static/css/index.css +84 -0
  75. aprsd/web/admin/static/css/prism.css +4 -0
  76. aprsd/web/admin/static/css/tabs.css +35 -0
  77. aprsd/web/admin/static/images/Untitled.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  82. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  83. aprsd/web/admin/static/js/charts.js +235 -0
  84. aprsd/web/admin/static/js/echarts.js +465 -0
  85. aprsd/web/admin/static/js/logs.js +26 -0
  86. aprsd/web/admin/static/js/main.js +231 -0
  87. aprsd/web/admin/static/js/prism.js +12 -0
  88. aprsd/web/admin/static/js/send-message.js +114 -0
  89. aprsd/web/admin/static/js/tabs.js +28 -0
  90. aprsd/web/admin/templates/index.html +196 -0
  91. aprsd/web/chat/static/css/chat.css +115 -0
  92. aprsd/web/chat/static/css/index.css +66 -0
  93. aprsd/web/chat/static/css/style.css.map +1 -0
  94. aprsd/web/chat/static/css/tabs.css +41 -0
  95. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  96. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  97. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  98. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  99. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  107. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  108. aprsd/web/chat/static/images/Untitled.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  113. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  114. aprsd/web/chat/static/images/globe.svg +3 -0
  115. aprsd/web/chat/static/js/gps.js +84 -0
  116. aprsd/web/chat/static/js/main.js +45 -0
  117. aprsd/web/chat/static/js/send-message.js +585 -0
  118. aprsd/web/chat/static/js/tabs.js +28 -0
  119. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  120. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  121. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  122. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  123. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  124. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  125. aprsd/web/chat/templates/index.html +139 -0
  126. aprsd/wsgi.py +315 -0
  127. aprsd-3.4.1.dist-info/AUTHORS +13 -0
  128. aprsd-3.4.1.dist-info/LICENSE +175 -0
  129. aprsd-3.4.1.dist-info/METADATA +799 -0
  130. aprsd-3.4.1.dist-info/RECORD +134 -0
  131. {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
  132. aprsd-3.4.1.dist-info/entry_points.txt +8 -0
  133. aprsd/fake_aprs.py +0 -83
  134. aprsd/utils.py +0 -166
  135. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  136. aprsd-1.0.0.dist-info/METADATA +0 -181
  137. aprsd-1.0.0.dist-info/RECORD +0 -13
  138. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  139. aprsd-1.0.0.dist-info/pbr.json +0 -1
  140. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  141. {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 pbr.version
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
16
15
 
17
- __version__ = pbr.version.VersionInfo("aprsd").version_string()
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)
@@ -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