aprsd 3.4.3__py3-none-any.whl → 4.0.0__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 (117) hide show
  1. aprsd/cli_helper.py +12 -5
  2. aprsd/client/aprsis.py +68 -17
  3. aprsd/client/base.py +60 -12
  4. aprsd/client/drivers/aprsis.py +11 -5
  5. aprsd/client/drivers/fake.py +15 -20
  6. aprsd/client/factory.py +6 -3
  7. aprsd/client/fake.py +5 -4
  8. aprsd/client/kiss.py +43 -7
  9. aprsd/client/stats.py +2 -22
  10. aprsd/cmds/completion.py +7 -4
  11. aprsd/cmds/dev.py +39 -43
  12. aprsd/cmds/fetch_stats.py +221 -69
  13. aprsd/cmds/healthcheck.py +7 -5
  14. aprsd/cmds/list_plugins.py +140 -134
  15. aprsd/cmds/listen.py +102 -11
  16. aprsd/cmds/server.py +71 -37
  17. aprsd/conf/__init__.py +1 -2
  18. aprsd/conf/client.py +3 -4
  19. aprsd/conf/common.py +29 -92
  20. aprsd/conf/log.py +2 -4
  21. aprsd/conf/opts.py +5 -4
  22. aprsd/conf/plugin_common.py +11 -121
  23. aprsd/exception.py +2 -0
  24. aprsd/log/log.py +7 -46
  25. aprsd/main.py +19 -9
  26. aprsd/packets/__init__.py +14 -4
  27. aprsd/packets/core.py +82 -91
  28. aprsd/packets/log.py +8 -8
  29. aprsd/packets/packet_list.py +18 -25
  30. aprsd/plugin.py +33 -15
  31. aprsd/plugins/fortune.py +2 -2
  32. aprsd/plugins/notify.py +1 -4
  33. aprsd/plugins/weather.py +10 -8
  34. aprsd/stats/__init__.py +0 -2
  35. aprsd/stats/collector.py +11 -3
  36. aprsd/threads/__init__.py +3 -2
  37. aprsd/threads/aprsd.py +57 -10
  38. aprsd/threads/{keep_alive.py → keepalive.py} +14 -37
  39. aprsd/threads/registry.py +3 -3
  40. aprsd/threads/rx.py +48 -17
  41. aprsd/threads/stats.py +2 -2
  42. aprsd/threads/tx.py +34 -10
  43. aprsd/utils/__init__.py +62 -15
  44. aprsd/utils/counter.py +15 -12
  45. aprsd/utils/json.py +9 -4
  46. aprsd/utils/keepalive_collector.py +55 -0
  47. aprsd/utils/trace.py +4 -4
  48. aprsd-4.0.0.dist-info/AUTHORS +1 -0
  49. aprsd-4.0.0.dist-info/METADATA +293 -0
  50. aprsd-4.0.0.dist-info/RECORD +74 -0
  51. {aprsd-3.4.3.dist-info → aprsd-4.0.0.dist-info}/WHEEL +1 -1
  52. aprsd/cmds/webchat.py +0 -674
  53. aprsd/conf/plugin_email.py +0 -105
  54. aprsd/plugins/email.py +0 -709
  55. aprsd/plugins/location.py +0 -179
  56. aprsd/threads/log_monitor.py +0 -121
  57. aprsd/web/__init__.py +0 -0
  58. aprsd/web/admin/__init__.py +0 -0
  59. aprsd/web/admin/static/css/index.css +0 -84
  60. aprsd/web/admin/static/css/prism.css +0 -4
  61. aprsd/web/admin/static/css/tabs.css +0 -35
  62. aprsd/web/admin/static/images/Untitled.png +0 -0
  63. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  64. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  65. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  66. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  67. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  68. aprsd/web/admin/static/js/charts.js +0 -235
  69. aprsd/web/admin/static/js/echarts.js +0 -465
  70. aprsd/web/admin/static/js/logs.js +0 -26
  71. aprsd/web/admin/static/js/main.js +0 -231
  72. aprsd/web/admin/static/js/prism.js +0 -12
  73. aprsd/web/admin/static/js/send-message.js +0 -114
  74. aprsd/web/admin/static/js/tabs.js +0 -28
  75. aprsd/web/admin/templates/index.html +0 -196
  76. aprsd/web/chat/static/css/chat.css +0 -115
  77. aprsd/web/chat/static/css/index.css +0 -66
  78. aprsd/web/chat/static/css/style.css.map +0 -1
  79. aprsd/web/chat/static/css/tabs.css +0 -41
  80. aprsd/web/chat/static/css/upstream/bootstrap.min.css +0 -6
  81. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  82. aprsd/web/chat/static/css/upstream/google-fonts.css +0 -23
  83. aprsd/web/chat/static/css/upstream/jquery-ui.css +0 -1311
  84. aprsd/web/chat/static/css/upstream/jquery.toast.css +0 -28
  85. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  86. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  87. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  88. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  89. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  90. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  91. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  92. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  93. aprsd/web/chat/static/images/Untitled.png +0 -0
  94. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  95. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  96. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  97. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  98. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  99. aprsd/web/chat/static/images/globe.svg +0 -3
  100. aprsd/web/chat/static/js/gps.js +0 -84
  101. aprsd/web/chat/static/js/main.js +0 -45
  102. aprsd/web/chat/static/js/send-message.js +0 -585
  103. aprsd/web/chat/static/js/tabs.js +0 -28
  104. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +0 -7
  105. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +0 -2
  106. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +0 -13
  107. aprsd/web/chat/static/js/upstream/jquery.toast.js +0 -374
  108. aprsd/web/chat/static/js/upstream/semantic.min.js +0 -11
  109. aprsd/web/chat/static/js/upstream/socket.io.min.js +0 -7
  110. aprsd/web/chat/templates/index.html +0 -139
  111. aprsd/wsgi.py +0 -315
  112. aprsd-3.4.3.dist-info/AUTHORS +0 -13
  113. aprsd-3.4.3.dist-info/METADATA +0 -793
  114. aprsd-3.4.3.dist-info/RECORD +0 -133
  115. {aprsd-3.4.3.dist-info → aprsd-4.0.0.dist-info}/LICENSE +0 -0
  116. {aprsd-3.4.3.dist-info → aprsd-4.0.0.dist-info}/entry_points.txt +0 -0
  117. {aprsd-3.4.3.dist-info → aprsd-4.0.0.dist-info}/top_level.txt +0 -0
aprsd/cli_helper.py CHANGED
@@ -1,7 +1,7 @@
1
- from functools import update_wrapper
2
1
  import logging
3
- from pathlib import Path
4
2
  import typing as t
3
+ from functools import update_wrapper
4
+ from pathlib import Path
5
5
 
6
6
  import click
7
7
  from oslo_config import cfg
@@ -11,7 +11,6 @@ from aprsd import conf # noqa: F401
11
11
  from aprsd.log import log
12
12
  from aprsd.utils import trace
13
13
 
14
-
15
14
  CONF = cfg.CONF
16
15
  home = str(Path.home())
17
16
  DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
@@ -58,6 +57,7 @@ class AliasedGroup(click.Group):
58
57
  calling into :meth:`add_command`.
59
58
  Copied from `click` and extended for `aliases`.
60
59
  """
60
+
61
61
  def decorator(f):
62
62
  aliases = kwargs.pop("aliases", [])
63
63
  cmd = click.decorators.command(*args, **kwargs)(f)
@@ -65,6 +65,7 @@ class AliasedGroup(click.Group):
65
65
  for alias in aliases:
66
66
  self.add_command(cmd, name=alias)
67
67
  return cmd
68
+
68
69
  return decorator
69
70
 
70
71
  def group(self, *args, **kwargs):
@@ -74,6 +75,7 @@ class AliasedGroup(click.Group):
74
75
  calling into :meth:`add_command`.
75
76
  Copied from `click` and extended for `aliases`.
76
77
  """
78
+
77
79
  def decorator(f):
78
80
  aliases = kwargs.pop("aliases", [])
79
81
  cmd = click.decorators.group(*args, **kwargs)(f)
@@ -81,6 +83,7 @@ class AliasedGroup(click.Group):
81
83
  for alias in aliases:
82
84
  self.add_command(cmd, name=alias)
83
85
  return cmd
86
+
84
87
  return decorator
85
88
 
86
89
 
@@ -89,6 +92,7 @@ def add_options(options):
89
92
  for option in reversed(options):
90
93
  func = option(func)
91
94
  return func
95
+
92
96
  return _add_options
93
97
 
94
98
 
@@ -103,7 +107,9 @@ def process_standard_options(f: F) -> F:
103
107
  default_config_files = None
104
108
  try:
105
109
  CONF(
106
- [], project="aprsd", version=aprsd.__version__,
110
+ [],
111
+ project="aprsd",
112
+ version=aprsd.__version__,
107
113
  default_config_files=default_config_files,
108
114
  )
109
115
  except cfg.ConfigFilesNotFoundError:
@@ -119,7 +125,7 @@ def process_standard_options(f: F) -> F:
119
125
  trace.setup_tracing(["method", "api"])
120
126
 
121
127
  if not config_file_found:
122
- LOG = logging.getLogger("APRSD") # noqa: N806
128
+ LOG = logging.getLogger("APRSD") # noqa: N806
123
129
  LOG.error("No config file found!! run 'aprsd sample-config'")
124
130
 
125
131
  del kwargs["loglevel"]
@@ -132,6 +138,7 @@ def process_standard_options(f: F) -> F:
132
138
 
133
139
  def process_standard_options_no_config(f: F) -> F:
134
140
  """Use this as a decorator when config isn't needed."""
141
+
135
142
  def new_func(*args, **kwargs):
136
143
  ctx = args[0]
137
144
  ctx.ensure_object(dict)
aprsd/client/aprsis.py CHANGED
@@ -2,7 +2,9 @@ import datetime
2
2
  import logging
3
3
  import time
4
4
 
5
+ import timeago
5
6
  from aprslib.exceptions import LoginError
7
+ from loguru import logger
6
8
  from oslo_config import cfg
7
9
 
8
10
  from aprsd import client, exception
@@ -10,30 +12,55 @@ from aprsd.client import base
10
12
  from aprsd.client.drivers import aprsis
11
13
  from aprsd.packets import core
12
14
 
13
-
14
15
  CONF = cfg.CONF
15
16
  LOG = logging.getLogger("APRSD")
17
+ LOGU = logger
16
18
 
17
19
 
18
20
  class APRSISClient(base.APRSClient):
19
-
20
21
  _client = None
22
+ _checks = False
21
23
 
22
24
  def __init__(self):
23
25
  max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
24
26
  self.max_delta = datetime.timedelta(**max_timeout)
25
27
 
26
- def stats(self) -> dict:
28
+ def stats(self, serializable=False) -> dict:
27
29
  stats = {}
28
30
  if self.is_configured():
31
+ if self._client:
32
+ keepalive = self._client.aprsd_keepalive
33
+ server_string = self._client.server_string
34
+ if serializable:
35
+ keepalive = keepalive.isoformat()
36
+ else:
37
+ keepalive = "None"
38
+ server_string = "None"
29
39
  stats = {
30
- "server_string": self._client.server_string,
31
- "sever_keepalive": self._client.aprsd_keepalive,
40
+ "connected": self.is_connected,
32
41
  "filter": self.filter,
42
+ "login_status": self.login_status,
43
+ "connection_keepalive": keepalive,
44
+ "server_string": server_string,
45
+ "transport": self.transport(),
33
46
  }
34
47
 
35
48
  return stats
36
49
 
50
+ def keepalive_check(self):
51
+ # Don't check the first time through.
52
+ if not self.is_alive() and self._checks:
53
+ LOG.warning("Resetting client. It's not alive.")
54
+ self.reset()
55
+ self._checks = True
56
+
57
+ def keepalive_log(self):
58
+ if ka := self._client.aprsd_keepalive:
59
+ keepalive = timeago.format(ka)
60
+ else:
61
+ keepalive = "N/A"
62
+ LOGU.opt(colors=True).info(f"<green>Client keepalive {keepalive}</green>")
63
+
37
64
  @staticmethod
38
65
  def is_enabled():
39
66
  # Defaults to True if the enabled flag is non existent
@@ -70,13 +97,13 @@ class APRSISClient(base.APRSClient):
70
97
  if delta > self.max_delta:
71
98
  LOG.error(f"Connection is stale, last heard {delta} ago.")
72
99
  return True
100
+ return False
73
101
 
74
102
  def is_alive(self):
75
- if self._client:
76
- return self._client.is_alive() and not self._is_stale_connection()
77
- else:
103
+ if not self._client:
78
104
  LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
79
105
  return False
106
+ return self._client.is_alive() and not self._is_stale_connection()
80
107
 
81
108
  def close(self):
82
109
  if self._client:
@@ -99,22 +126,35 @@ class APRSISClient(base.APRSClient):
99
126
  self.connected = False
100
127
  backoff = 1
101
128
  aprs_client = None
129
+ retries = 3
130
+ retry_count = 0
102
131
  while not self.connected:
132
+ retry_count += 1
133
+ if retry_count >= retries:
134
+ break
103
135
  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)
136
+ LOG.info(
137
+ f"Creating aprslib client({host}:{port}) and logging in {user}."
138
+ )
139
+ aprs_client = aprsis.Aprsdis(
140
+ user, passwd=password, host=host, port=port
141
+ )
106
142
  # Force the log to be the same
107
143
  aprs_client.logger = LOG
108
144
  aprs_client.connect()
109
- self.connected = True
145
+ self.connected = self.login_status["success"] = True
146
+ self.login_status["message"] = aprs_client.server_string
110
147
  backoff = 1
111
148
  except LoginError as e:
112
149
  LOG.error(f"Failed to login to APRS-IS Server '{e}'")
113
- self.connected = False
150
+ self.connected = self.login_status["success"] = False
151
+ self.login_status["message"] = e.message
152
+ LOG.error(e.message)
114
153
  time.sleep(backoff)
115
154
  except Exception as e:
116
155
  LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
117
- self.connected = False
156
+ self.connected = self.login_status["success"] = False
157
+ self.login_status["message"] = e.message
118
158
  time.sleep(backoff)
119
159
  # Don't allow the backoff to go to inifinity.
120
160
  if backoff > 5:
@@ -126,7 +166,18 @@ class APRSISClient(base.APRSClient):
126
166
  return aprs_client
127
167
 
128
168
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
129
- self._client.consumer(
130
- callback, blocking=blocking,
131
- immortal=immortal, raw=raw,
132
- )
169
+ if self._client:
170
+ try:
171
+ self._client.consumer(
172
+ callback,
173
+ blocking=blocking,
174
+ immortal=immortal,
175
+ raw=raw,
176
+ )
177
+ except Exception as e:
178
+ LOG.error(e)
179
+ LOG.info(e.__cause__)
180
+ raise e
181
+ else:
182
+ LOG.warning("client is None, might be resetting.")
183
+ self.connected = False
aprsd/client/base.py CHANGED
@@ -2,11 +2,11 @@ import abc
2
2
  import logging
3
3
  import threading
4
4
 
5
- from oslo_config import cfg
6
5
  import wrapt
6
+ from oslo_config import cfg
7
7
 
8
8
  from aprsd.packets import core
9
-
9
+ from aprsd.utils import keepalive_collector
10
10
 
11
11
  CONF = cfg.CONF
12
12
  LOG = logging.getLogger("APRSD")
@@ -19,6 +19,10 @@ class APRSClient:
19
19
  _client = None
20
20
 
21
21
  connected = False
22
+ login_status = {
23
+ "success": False,
24
+ "message": None,
25
+ }
22
26
  filter = None
23
27
  lock = threading.Lock()
24
28
 
@@ -26,13 +30,40 @@ class APRSClient:
26
30
  """This magic turns this into a singleton."""
27
31
  if cls._instance is None:
28
32
  cls._instance = super().__new__(cls)
33
+ keepalive_collector.KeepAliveCollector().register(cls)
29
34
  # Put any initialization here.
30
35
  cls._instance._create_client()
31
36
  return cls._instance
32
37
 
33
38
  @abc.abstractmethod
34
39
  def stats(self) -> dict:
35
- pass
40
+ """Return statistics about the client connection.
41
+
42
+ Returns:
43
+ dict: Statistics about the connection and packet handling
44
+ """
45
+
46
+ @abc.abstractmethod
47
+ def keepalive_check(self) -> None:
48
+ """Called during keepalive run to check status."""
49
+ ...
50
+
51
+ @abc.abstractmethod
52
+ def keepalive_log(self) -> None:
53
+ """Log any keepalive information."""
54
+ ...
55
+
56
+ @property
57
+ def is_connected(self):
58
+ return self.connected
59
+
60
+ @property
61
+ def login_success(self):
62
+ return self.login_status.get("success", False)
63
+
64
+ @property
65
+ def login_failure(self):
66
+ return self.login_status["message"]
36
67
 
37
68
  def set_filter(self, filter):
38
69
  self.filter = filter
@@ -46,22 +77,31 @@ class APRSClient:
46
77
  return self._client
47
78
 
48
79
  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)
80
+ try:
81
+ self._client = self.setup_connection()
82
+ if self.filter:
83
+ LOG.info("Creating APRS client filter")
84
+ self._client.set_filter(self.filter)
85
+ except Exception as e:
86
+ LOG.error(f"Failed to create APRS client: {e}")
87
+ self._client = None
88
+ raise
53
89
 
54
90
  def stop(self):
55
91
  if self._client:
56
92
  LOG.info("Stopping client connection.")
57
93
  self._client.stop()
58
94
 
59
- def send(self, packet: core.Packet):
60
- """Send a packet to the network."""
95
+ def send(self, packet: core.Packet) -> None:
96
+ """Send a packet to the network.
97
+
98
+ Args:
99
+ packet: The APRS packet to send
100
+ """
61
101
  self.client.send(packet)
62
102
 
63
103
  @wrapt.synchronized(lock)
64
- def reset(self):
104
+ def reset(self) -> None:
65
105
  """Call this to force a rebuild/reconnect."""
66
106
  LOG.info("Resetting client connection.")
67
107
  if self._client:
@@ -76,7 +116,11 @@ class APRSClient:
76
116
 
77
117
  @abc.abstractmethod
78
118
  def setup_connection(self):
79
- pass
119
+ """Initialize and return the underlying APRS connection.
120
+
121
+ Returns:
122
+ object: The initialized connection object
123
+ """
80
124
 
81
125
  @staticmethod
82
126
  @abc.abstractmethod
@@ -90,7 +134,11 @@ class APRSClient:
90
134
 
91
135
  @abc.abstractmethod
92
136
  def decode_packet(self, *args, **kwargs):
93
- pass
137
+ """Decode raw APRS packet data into a Packet object.
138
+
139
+ Returns:
140
+ Packet: Decoded APRS packet
141
+ """
94
142
 
95
143
  @abc.abstractmethod
96
144
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
@@ -4,17 +4,20 @@ import select
4
4
  import threading
5
5
 
6
6
  import aprslib
7
+ import wrapt
7
8
  from aprslib import is_py3
8
9
  from aprslib.exceptions import (
9
- ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
10
+ ConnectionDrop,
11
+ ConnectionError,
12
+ GenericError,
13
+ LoginError,
14
+ ParseError,
10
15
  UnknownFormat,
11
16
  )
12
- import wrapt
13
17
 
14
18
  import aprsd
15
19
  from aprsd.packets import core
16
20
 
17
-
18
21
  LOG = logging.getLogger("APRSD")
19
22
 
20
23
 
@@ -27,6 +30,9 @@ class Aprsdis(aprslib.IS):
27
30
  # date for last time we heard from the server
28
31
  aprsd_keepalive = datetime.datetime.now()
29
32
 
33
+ # Which server we are connected to?
34
+ server_string = "None"
35
+
30
36
  # timeout in seconds
31
37
  select_timeout = 1
32
38
  lock = threading.Lock()
@@ -193,14 +199,14 @@ class Aprsdis(aprslib.IS):
193
199
  except ParseError as exp:
194
200
  self.logger.log(
195
201
  11,
196
- "%s\n Packet: %s",
202
+ "%s Packet: '%s'",
197
203
  exp,
198
204
  exp.packet,
199
205
  )
200
206
  except UnknownFormat as exp:
201
207
  self.logger.log(
202
208
  9,
203
- "%s\n Packet: %s",
209
+ "%s Packet: '%s'",
204
210
  exp,
205
211
  exp.packet,
206
212
  )
@@ -3,20 +3,19 @@ import threading
3
3
  import time
4
4
 
5
5
  import aprslib
6
- from oslo_config import cfg
7
6
  import wrapt
7
+ from oslo_config import cfg
8
8
 
9
9
  from aprsd import conf # noqa
10
10
  from aprsd.packets import core
11
11
  from aprsd.utils import trace
12
12
 
13
-
14
13
  CONF = cfg.CONF
15
- LOG = logging.getLogger("APRSD")
14
+ LOG = logging.getLogger('APRSD')
16
15
 
17
16
 
18
17
  class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
19
- '''Fake client for testing.'''
18
+ """Fake client for testing."""
20
19
 
21
20
  # flag to tell us to stop
22
21
  thread_stop = False
@@ -25,12 +24,12 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
25
24
  path = []
26
25
 
27
26
  def __init__(self):
28
- LOG.info("Starting APRSDFakeClient client.")
29
- self.path = ["WIDE1-1", "WIDE2-1"]
27
+ LOG.info('Starting APRSDFakeClient client.')
28
+ self.path = ['WIDE1-1', 'WIDE2-1']
30
29
 
31
30
  def stop(self):
32
31
  self.thread_stop = True
33
- LOG.info("Shutdown APRSDFakeClient client.")
32
+ LOG.info('Shutdown APRSDFakeClient client.')
34
33
 
35
34
  def is_alive(self):
36
35
  """If the connection is alive or not."""
@@ -39,35 +38,31 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
39
38
  @wrapt.synchronized(lock)
40
39
  def send(self, packet: core.Packet):
41
40
  """Send an APRS Message object."""
42
- LOG.info(f"Sending packet: {packet}")
41
+ LOG.info(f'Sending packet: {packet}')
43
42
  payload = None
44
43
  if isinstance(packet, core.Packet):
45
44
  packet.prepare()
46
- payload = packet.payload.encode("US-ASCII")
47
- if packet.path:
48
- packet.path
49
- else:
50
- self.path
45
+ payload = packet.payload.encode('US-ASCII')
51
46
  else:
52
- msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
47
+ msg_payload = f'{packet.raw}{{{str(packet.msgNo)}'
53
48
  payload = (
54
- ":{:<9}:{}".format(
49
+ ':{:<9}:{}'.format(
55
50
  packet.to_call,
56
51
  msg_payload,
57
52
  )
58
- ).encode("US-ASCII")
53
+ ).encode('US-ASCII')
59
54
 
60
55
  LOG.debug(
61
56
  f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
62
- f"'{packet.from_call}' with PATH \"{self.path}\"",
57
+ f'\'{packet.from_call}\' with PATH "{self.path}"',
63
58
  )
64
59
 
65
60
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
66
- LOG.debug("Start non blocking FAKE consumer")
61
+ LOG.debug('Start non blocking FAKE consumer')
67
62
  # Generate packets here?
68
- raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
63
+ raw = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
69
64
  pkt_raw = aprslib.parse(raw)
70
65
  pkt = core.factory(pkt_raw)
71
66
  callback(packet=pkt)
72
- LOG.debug(f"END blocking FAKE consumer {self}")
67
+ LOG.debug(f'END blocking FAKE consumer {self}')
73
68
  time.sleep(8)
aprsd/client/factory.py CHANGED
@@ -4,13 +4,11 @@ from typing import Callable, Protocol, runtime_checkable
4
4
  from aprsd import exception
5
5
  from aprsd.packets import core
6
6
 
7
-
8
7
  LOG = logging.getLogger("APRSD")
9
8
 
10
9
 
11
10
  @runtime_checkable
12
11
  class Client(Protocol):
13
-
14
12
  def __init__(self):
15
13
  pass
16
14
 
@@ -42,6 +40,7 @@ class Client(Protocol):
42
40
  class ClientFactory:
43
41
  _instance = None
44
42
  clients = []
43
+ client = None
45
44
 
46
45
  def __new__(cls, *args, **kwargs):
47
46
  """This magic turns this into a singleton."""
@@ -62,9 +61,13 @@ class ClientFactory:
62
61
  def create(self, key=None):
63
62
  for client in self.clients:
64
63
  if client.is_enabled():
65
- return client()
64
+ self.client = client()
65
+ return self.client
66
66
  raise Exception("No client is configured!!")
67
67
 
68
+ def client_exists(self):
69
+ return bool(self.client)
70
+
68
71
  def is_client_enabled(self):
69
72
  """Make sure at least one client is enabled."""
70
73
  enabled = False
aprsd/client/fake.py CHANGED
@@ -7,15 +7,16 @@ from aprsd.client import base
7
7
  from aprsd.client.drivers import fake as fake_driver
8
8
  from aprsd.utils import trace
9
9
 
10
-
11
10
  CONF = cfg.CONF
12
11
  LOG = logging.getLogger("APRSD")
13
12
 
14
13
 
15
14
  class APRSDFakeClient(base.APRSClient, metaclass=trace.TraceWrapperMetaclass):
16
-
17
- def stats(self) -> dict:
18
- return {}
15
+ def stats(self, serializable=False) -> dict:
16
+ return {
17
+ "transport": "Fake",
18
+ "connected": True,
19
+ }
19
20
 
20
21
  @staticmethod
21
22
  def is_enabled():
aprsd/client/kiss.py CHANGED
@@ -1,6 +1,9 @@
1
+ import datetime
1
2
  import logging
2
3
 
3
4
  import aprslib
5
+ import timeago
6
+ from loguru import logger
4
7
  from oslo_config import cfg
5
8
 
6
9
  from aprsd import client, exception
@@ -8,21 +11,31 @@ from aprsd.client import base
8
11
  from aprsd.client.drivers import kiss
9
12
  from aprsd.packets import core
10
13
 
11
-
12
14
  CONF = cfg.CONF
13
15
  LOG = logging.getLogger("APRSD")
16
+ LOGU = logger
14
17
 
15
18
 
16
19
  class KISSClient(base.APRSClient):
17
-
18
20
  _client = None
21
+ keepalive = datetime.datetime.now()
19
22
 
20
- def stats(self) -> dict:
23
+ def stats(self, serializable=False) -> dict:
21
24
  stats = {}
22
25
  if self.is_configured():
23
- return {
26
+ keepalive = self.keepalive
27
+ if serializable:
28
+ keepalive = keepalive.isoformat()
29
+ stats = {
30
+ "connected": self.is_connected,
31
+ "connection_keepalive": keepalive,
24
32
  "transport": self.transport(),
25
33
  }
34
+ if self.transport() == client.TRANSPORT_TCPKISS:
35
+ stats["host"] = CONF.kiss_tcp.host
36
+ stats["port"] = CONF.kiss_tcp.port
37
+ elif self.transport() == client.TRANSPORT_SERIALKISS:
38
+ stats["device"] = CONF.kiss_serial.device
26
39
  return stats
27
40
 
28
41
  @staticmethod
@@ -67,6 +80,20 @@ class KISSClient(base.APRSClient):
67
80
  if self._client:
68
81
  self._client.stop()
69
82
 
83
+ def keepalive_check(self):
84
+ # Don't check the first time through.
85
+ if not self.is_alive() and self._checks:
86
+ LOG.warning("Resetting client. It's not alive.")
87
+ self.reset()
88
+ self._checks = True
89
+
90
+ def keepalive_log(self):
91
+ if ka := self._client.aprsd_keepalive:
92
+ keepalive = timeago.format(ka)
93
+ else:
94
+ keepalive = "N/A"
95
+ LOGU.opt(colors=True).info(f"<green>Client keepalive {keepalive}</green>")
96
+
70
97
  @staticmethod
71
98
  def transport():
72
99
  if CONF.kiss_serial.enabled:
@@ -95,9 +122,18 @@ class KISSClient(base.APRSClient):
95
122
  return packet
96
123
 
97
124
  def setup_connection(self):
98
- self._client = kiss.KISS3Client()
99
- self.connected = True
125
+ try:
126
+ self._client = kiss.KISS3Client()
127
+ self.connected = self.login_status["success"] = True
128
+ except Exception as ex:
129
+ self.connected = self.login_status["success"] = False
130
+ self.login_status["message"] = str(ex)
100
131
  return self._client
101
132
 
102
133
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
103
- self._client.consumer(callback)
134
+ try:
135
+ self._client.consumer(callback)
136
+ self.keepalive = datetime.datetime.now()
137
+ except Exception as ex:
138
+ LOG.error(f"Consumer failed {ex}")
139
+ LOG.error(ex)
aprsd/client/stats.py CHANGED
@@ -1,38 +1,18 @@
1
1
  import threading
2
2
 
3
- from oslo_config import cfg
4
3
  import wrapt
4
+ from oslo_config import cfg
5
5
 
6
6
  from aprsd import client
7
7
  from aprsd.utils import singleton
8
8
 
9
-
10
9
  CONF = cfg.CONF
11
10
 
12
11
 
13
12
  @singleton
14
13
  class APRSClientStats:
15
-
16
14
  lock = threading.Lock()
17
15
 
18
16
  @wrapt.synchronized(lock)
19
17
  def stats(self, serializable=False):
20
- cl = client.client_factory.create()
21
- stats = {
22
- "transport": cl.transport(),
23
- "filter": cl.filter,
24
- "connected": cl.connected,
25
- }
26
-
27
- if cl.transport() == client.TRANSPORT_APRSIS:
28
- stats["server_string"] = cl.client.server_string
29
- keepalive = cl.client.aprsd_keepalive
30
- if serializable:
31
- keepalive = keepalive.isoformat()
32
- stats["server_keepalive"] = keepalive
33
- elif cl.transport() == client.TRANSPORT_TCPKISS:
34
- stats["host"] = CONF.kiss_tcp.host
35
- stats["port"] = CONF.kiss_tcp.port
36
- elif cl.transport() == client.TRANSPORT_SERIALKISS:
37
- stats["device"] = CONF.kiss_serial.device
38
- return stats
18
+ return client.client_factory.create().stats(serializable=serializable)