aprsd 4.1.2__py3-none-any.whl → 4.2.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 (43) hide show
  1. aprsd/client/__init__.py +5 -13
  2. aprsd/client/client.py +156 -0
  3. aprsd/client/drivers/__init__.py +10 -0
  4. aprsd/client/drivers/aprsis.py +174 -255
  5. aprsd/client/drivers/fake.py +59 -11
  6. aprsd/client/drivers/lib/__init__.py +0 -0
  7. aprsd/client/drivers/lib/aprslib.py +296 -0
  8. aprsd/client/drivers/registry.py +86 -0
  9. aprsd/client/drivers/tcpkiss.py +423 -0
  10. aprsd/client/stats.py +2 -2
  11. aprsd/cmds/dev.py +6 -4
  12. aprsd/cmds/fetch_stats.py +2 -0
  13. aprsd/cmds/list_plugins.py +6 -133
  14. aprsd/cmds/listen.py +5 -3
  15. aprsd/cmds/send_message.py +8 -5
  16. aprsd/cmds/server.py +7 -11
  17. aprsd/conf/common.py +7 -1
  18. aprsd/exception.py +7 -0
  19. aprsd/log/log.py +1 -1
  20. aprsd/main.py +0 -7
  21. aprsd/packets/core.py +168 -169
  22. aprsd/packets/log.py +69 -59
  23. aprsd/plugin.py +3 -2
  24. aprsd/plugin_utils.py +2 -2
  25. aprsd/plugins/weather.py +2 -2
  26. aprsd/stats/collector.py +5 -4
  27. aprsd/threads/rx.py +13 -11
  28. aprsd/threads/tx.py +32 -31
  29. aprsd/utils/keepalive_collector.py +7 -5
  30. aprsd/utils/package.py +176 -0
  31. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/METADATA +48 -48
  32. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/RECORD +37 -37
  33. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/WHEEL +1 -1
  34. aprsd/client/aprsis.py +0 -183
  35. aprsd/client/base.py +0 -156
  36. aprsd/client/drivers/kiss.py +0 -144
  37. aprsd/client/factory.py +0 -91
  38. aprsd/client/fake.py +0 -49
  39. aprsd/client/kiss.py +0 -143
  40. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/entry_points.txt +0 -0
  41. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/AUTHORS +0 -0
  42. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info/licenses}/LICENSE +0 -0
  43. {aprsd-4.1.2.dist-info → aprsd-4.2.1.dist-info}/top_level.txt +0 -0
aprsd/client/__init__.py CHANGED
@@ -1,13 +1,5 @@
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)
1
+ # define the client transports here
2
+ TRANSPORT_APRSIS = 'aprsis'
3
+ TRANSPORT_TCPKISS = 'tcpkiss'
4
+ TRANSPORT_SERIALKISS = 'serialkiss'
5
+ TRANSPORT_FAKE = 'fake'
aprsd/client/client.py ADDED
@@ -0,0 +1,156 @@
1
+ import logging
2
+ import threading
3
+ from typing import Callable
4
+
5
+ import timeago
6
+ import wrapt
7
+ from loguru import logger
8
+ from oslo_config import cfg
9
+
10
+ from aprsd.client import drivers # noqa - ensure drivers are registered
11
+ from aprsd.client.drivers.registry import DriverRegistry
12
+ from aprsd.packets import core
13
+ from aprsd.utils import keepalive_collector
14
+
15
+ CONF = cfg.CONF
16
+ LOG = logging.getLogger('APRSD')
17
+ LOGU = logger
18
+
19
+
20
+ class APRSDClient:
21
+ """APRSD client class.
22
+
23
+ This is a singleton class that provides a single instance of the APRSD client.
24
+ It is responsible for connecting to the appropriate APRSD client driver based on
25
+ the configuration.
26
+
27
+ """
28
+
29
+ _instance = None
30
+ driver = None
31
+ lock = threading.Lock()
32
+ filter = None
33
+
34
+ def __new__(cls, *args, **kwargs):
35
+ """This magic turns this into a singleton."""
36
+ if cls._instance is None:
37
+ cls._instance = super().__new__(cls)
38
+ keepalive_collector.KeepAliveCollector().register(cls)
39
+ return cls._instance
40
+
41
+ def __init__(self, auto_connect: bool = True):
42
+ self.auto_connect = auto_connect
43
+ self.connected = False
44
+ self.login_status = {
45
+ 'success': False,
46
+ 'message': None,
47
+ }
48
+ self.driver = DriverRegistry().get_driver()
49
+ if self.auto_connect:
50
+ self.connect()
51
+
52
+ def stats(self, serializable=False) -> dict:
53
+ stats = {}
54
+ if self.driver:
55
+ stats = self.driver.stats(serializable=serializable)
56
+ return stats
57
+
58
+ @staticmethod
59
+ def is_enabled():
60
+ for driver in DriverRegistry().drivers:
61
+ if driver.is_enabled():
62
+ return True
63
+ return False
64
+
65
+ @staticmethod
66
+ def is_configured():
67
+ """Check if ANY driver is configured."""
68
+ for driver in DriverRegistry().drivers:
69
+ if driver.is_configured():
70
+ return True
71
+ return False
72
+
73
+ # @property
74
+ # def is_connected(self):
75
+ # if not self.driver:
76
+ # return False
77
+ # return self.driver.is_connected()
78
+
79
+ @property
80
+ def login_success(self):
81
+ if not self.driver:
82
+ return False
83
+ return self.driver.login_success
84
+
85
+ @property
86
+ def login_failure(self):
87
+ if not self.driver:
88
+ return None
89
+ return self.driver.login_failure
90
+
91
+ def set_filter(self, filter):
92
+ self.filter = filter
93
+ if not self.driver:
94
+ return
95
+ self.driver.set_filter(filter)
96
+
97
+ def get_filter(self):
98
+ if not self.driver:
99
+ return None
100
+ return self.driver.filter
101
+
102
+ def is_alive(self):
103
+ return self.driver.is_alive()
104
+
105
+ def connect(self):
106
+ if not self.driver:
107
+ self.driver = DriverRegistry().get_driver()
108
+ self.driver.setup_connection()
109
+
110
+ def close(self):
111
+ if not self.driver:
112
+ return
113
+ self.driver.close()
114
+
115
+ @wrapt.synchronized(lock)
116
+ def reset(self):
117
+ """Call this to force a rebuild/reconnect."""
118
+ LOG.info('Resetting client connection.')
119
+ if self.driver:
120
+ self.driver.close()
121
+ if not self.delay_connect:
122
+ self.driver.setup_connection()
123
+ if self.filter:
124
+ self.driver.set_filter(self.filter)
125
+ else:
126
+ LOG.warning('Client not initialized, nothing to reset.')
127
+
128
+ def send(self, packet: core.Packet) -> bool:
129
+ return self.driver.send(packet)
130
+
131
+ # For the keepalive collector
132
+ def keepalive_check(self):
133
+ # Don't check the first time through.
134
+ if not self.driver.is_alive and self._checks:
135
+ LOG.warning("Resetting client. It's not alive.")
136
+ self.reset()
137
+ self._checks = True
138
+
139
+ # For the keepalive collector
140
+ def keepalive_log(self):
141
+ if ka := self.driver.keepalive:
142
+ keepalive = timeago.format(ka)
143
+ else:
144
+ keepalive = 'N/A'
145
+ LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
146
+
147
+ def consumer(self, callback: Callable, raw: bool = False):
148
+ return self.driver.consumer(callback=callback, raw=raw)
149
+
150
+ def decode_packet(self, *args, **kwargs) -> core.Packet:
151
+ try:
152
+ packet = self.driver.decode_packet(*args, **kwargs)
153
+ except Exception as e:
154
+ LOG.error(f'Error decoding packet: {e}')
155
+ return None
156
+ return packet
@@ -0,0 +1,10 @@
1
+ # All client drivers must be registered here
2
+ from aprsd.client.drivers.aprsis import APRSISDriver
3
+ from aprsd.client.drivers.fake import APRSDFakeDriver
4
+ from aprsd.client.drivers.registry import DriverRegistry
5
+ from aprsd.client.drivers.tcpkiss import TCPKISSDriver
6
+
7
+ driver_registry = DriverRegistry()
8
+ driver_registry.register(APRSDFakeDriver)
9
+ driver_registry.register(APRSISDriver)
10
+ driver_registry.register(TCPKISSDriver)
@@ -1,286 +1,205 @@
1
1
  import datetime
2
2
  import logging
3
- import select
4
- import socket
5
- import threading
3
+ import time
4
+ from typing import Callable
6
5
 
7
- import aprslib
8
- import wrapt
9
- from aprslib import is_py3
10
- from aprslib.exceptions import (
11
- ConnectionDrop,
12
- ConnectionError,
13
- GenericError,
14
- LoginError,
15
- ParseError,
16
- UnknownFormat,
17
- )
6
+ from aprslib.exceptions import LoginError
7
+ from loguru import logger
8
+ from oslo_config import cfg
18
9
 
19
- import aprsd
10
+ from aprsd import client, exception
11
+ from aprsd.client.drivers.lib.aprslib import APRSLibClient
20
12
  from aprsd.packets import core
21
13
 
14
+ CONF = cfg.CONF
22
15
  LOG = logging.getLogger('APRSD')
16
+ LOGU = logger
23
17
 
24
18
 
25
- class Aprsdis(aprslib.IS):
26
- """Extend the aprslib class so we can exit properly."""
19
+ # class APRSISDriver(metaclass=trace.TraceWrapperMetaclass):
20
+ class APRSISDriver:
21
+ """This is the APRS-IS driver for the APRSD client.
27
22
 
28
- # flag to tell us to stop
29
- thread_stop = False
23
+ This driver uses our modified aprslib.IS class to connect to the APRS-IS server.
30
24
 
31
- # date for last time we heard from the server
32
- aprsd_keepalive = datetime.datetime.now()
25
+ """
33
26
 
34
- # Which server we are connected to?
35
- server_string = 'None'
27
+ _client = None
28
+ _checks = False
29
+ connected = False
36
30
 
37
- # timeout in seconds
38
- select_timeout = 1
39
- lock = threading.Lock()
40
-
41
- def stop(self):
42
- self.thread_stop = True
43
- LOG.warning('Shutdown Aprsdis client.')
44
-
45
- def close(self):
46
- LOG.warning('Closing Aprsdis client.')
47
- super().close()
48
-
49
- @wrapt.synchronized(lock)
50
- def send(self, packet: core.Packet):
51
- """Send an APRS Message object."""
52
- self.sendall(packet.raw)
53
-
54
- def is_alive(self):
55
- """If the connection is alive or not."""
56
- return self._connected
57
-
58
- def _connect(self):
59
- """
60
- Attemps connection to the server
61
- """
62
-
63
- self.logger.info(
64
- 'Attempting connection to %s:%s', self.server[0], self.server[1]
65
- )
31
+ def __init__(self):
32
+ max_timeout = {'hours': 0.0, 'minutes': 2, 'seconds': 0}
33
+ self.max_delta = datetime.timedelta(**max_timeout)
34
+ self.login_status = {
35
+ 'success': False,
36
+ 'message': None,
37
+ }
66
38
 
39
+ @staticmethod
40
+ def is_enabled():
41
+ # Defaults to True if the enabled flag is non existent
67
42
  try:
68
- self._open_socket()
69
-
70
- peer = self.sock.getpeername()
71
-
72
- self.logger.info('Connected to %s', str(peer))
73
-
74
- # 5 second timeout to receive server banner
75
- self.sock.setblocking(1)
76
- self.sock.settimeout(5)
77
-
78
- self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
79
- # MACOS doesn't have TCP_KEEPIDLE
80
- if hasattr(socket, 'TCP_KEEPIDLE'):
81
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
82
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
83
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
84
-
85
- banner = self.sock.recv(512)
86
- if is_py3:
87
- banner = banner.decode('latin-1')
88
-
89
- if banner[0] == '#':
90
- self.logger.debug('Banner: %s', banner.rstrip())
91
- else:
92
- raise ConnectionError('invalid banner from server')
93
-
94
- except ConnectionError as e:
95
- self.logger.error(str(e))
96
- self.close()
97
- raise
98
- except (socket.error, socket.timeout) as e:
99
- self.close()
100
-
101
- self.logger.error('Socket error: %s' % str(e))
102
- if str(e) == 'timed out':
103
- raise ConnectionError('no banner from server') from e
104
- else:
105
- raise ConnectionError(e) from e
106
-
107
- self._connected = True
108
-
109
- def _socket_readlines(self, blocking=False):
110
- """
111
- Generator for complete lines, received from the server
112
- """
113
- try:
114
- self.sock.setblocking(0)
115
- except OSError as e:
116
- self.logger.error(f'socket error when setblocking(0): {str(e)}')
117
- raise aprslib.ConnectionDrop('connection dropped') from e
43
+ return CONF.aprs_network.enabled
44
+ except KeyError:
45
+ return False
46
+
47
+ @staticmethod
48
+ def is_configured():
49
+ if APRSISDriver.is_enabled():
50
+ # Ensure that the config vars are correctly set
51
+ if not CONF.aprs_network.login:
52
+ LOG.error('Config aprs_network.login not set.')
53
+ raise exception.MissingConfigOptionException(
54
+ 'aprs_network.login is not set.',
55
+ )
56
+ if not CONF.aprs_network.password:
57
+ LOG.error('Config aprs_network.password not set.')
58
+ raise exception.MissingConfigOptionException(
59
+ 'aprs_network.password is not set.',
60
+ )
61
+ if not CONF.aprs_network.host:
62
+ LOG.error('Config aprs_network.host not set.')
63
+ raise exception.MissingConfigOptionException(
64
+ 'aprs_network.host is not set.',
65
+ )
118
66
 
119
- while not self.thread_stop:
120
- short_buf = b''
121
- newline = b'\r\n'
67
+ return True
68
+ return True
122
69
 
123
- # set a select timeout, so we get a chance to exit
124
- # when user hits CTRL-C
125
- readable, writable, exceptional = select.select(
126
- [self.sock],
127
- [],
128
- [],
129
- self.select_timeout,
130
- )
131
- if not readable:
132
- if not blocking:
133
- break
134
- else:
135
- continue
70
+ @property
71
+ def is_alive(self):
72
+ if not self._client:
73
+ LOG.warning(f'APRS_CLIENT {self._client} alive? NO!!!')
74
+ return False
75
+ return self._client.is_alive() and not self._is_stale_connection()
136
76
 
77
+ def close(self):
78
+ if self._client:
79
+ self._client.stop()
80
+ self._client.close()
81
+
82
+ def send(self, packet: core.Packet) -> bool:
83
+ return self._client.send(packet)
84
+
85
+ def setup_connection(self):
86
+ user = CONF.aprs_network.login
87
+ password = CONF.aprs_network.password
88
+ host = CONF.aprs_network.host
89
+ port = CONF.aprs_network.port
90
+ self.connected = False
91
+ backoff = 1
92
+ retries = 3
93
+ retry_count = 0
94
+ while not self.connected:
95
+ retry_count += 1
96
+ if retry_count >= retries:
97
+ break
137
98
  try:
138
- short_buf = self.sock.recv(4096)
139
-
140
- # sock.recv returns empty if the connection drops
141
- if not short_buf:
142
- if not blocking:
143
- # We could just not be blocking, so empty is expected
144
- continue
145
- else:
146
- self.logger.error('socket.recv(): returned empty')
147
- raise aprslib.ConnectionDrop('connection dropped')
148
- except OSError as e:
149
- # self.logger.error("socket error on recv(): %s" % str(e))
150
- if 'Resource temporarily unavailable' in str(e):
151
- if not blocking:
152
- if len(self.buf) == 0:
153
- break
154
-
155
- self.buf += short_buf
156
-
157
- while newline in self.buf:
158
- line, self.buf = self.buf.split(newline, 1)
159
-
160
- yield line
161
-
162
- def _send_login(self):
163
- """
164
- Sends login string to server
165
- """
166
- login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
167
- login_str = login_str.format(
168
- self.callsign,
169
- self.passwd,
170
- (' filter ' + self.filter) if self.filter != '' else '',
171
- aprsd.__version__,
172
- )
173
-
174
- self.logger.debug('Sending login information')
175
-
176
- try:
177
- self._sendall(login_str)
178
- self.sock.settimeout(5)
179
- test = self.sock.recv(len(login_str) + 100)
180
- if is_py3:
181
- test = test.decode('latin-1')
182
- test = test.rstrip()
183
-
184
- self.logger.debug("Server: '%s'", test)
185
-
186
- if not test:
187
- raise LoginError(f"Server Response Empty: '{test}'")
188
-
189
- _, _, callsign, status, e = test.split(' ', 4)
190
- s = e.split(',')
191
- if len(s):
192
- server_string = s[0].replace('server ', '')
193
- else:
194
- server_string = e.replace('server ', '')
195
-
196
- if callsign == '':
197
- raise LoginError('Server responded with empty callsign???')
198
- if callsign != self.callsign:
199
- raise LoginError(f'Server: {test}')
200
- if status != 'verified,' and self.passwd != '-1':
201
- raise LoginError('Password is incorrect')
99
+ LOG.info(
100
+ f'Creating aprslib client({host}:{port}) and logging in {user}.'
101
+ )
102
+ self._client = APRSLibClient(
103
+ user, passwd=password, host=host, port=port
104
+ )
105
+ # Force the log to be the same
106
+ self._client.logger = LOG
107
+ self._client.connect()
108
+ self.connected = self.login_status['success'] = True
109
+ self.login_status['message'] = self._client.server_string
110
+ backoff = 1
111
+ except LoginError as e:
112
+ LOG.error(f"Failed to login to APRS-IS Server '{e}'")
113
+ self.connected = self.login_status['success'] = False
114
+ self.login_status['message'] = (
115
+ e.message if hasattr(e, 'message') else str(e)
116
+ )
117
+ LOG.error(self.login_status['message'])
118
+ time.sleep(backoff)
119
+ except Exception as e:
120
+ LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
121
+ self.connected = self.login_status['success'] = False
122
+ self.login_status['message'] = getattr(e, 'message', str(e))
123
+ time.sleep(backoff)
124
+ # Don't allow the backoff to go to inifinity.
125
+ if backoff > 5:
126
+ backoff = 5
127
+ else:
128
+ backoff += 1
129
+ continue
202
130
 
203
- if self.passwd == '-1':
204
- self.logger.info('Login successful (receive only)')
205
- else:
206
- self.logger.info('Login successful')
131
+ def set_filter(self, filter):
132
+ self._client.set_filter(filter)
207
133
 
208
- self.logger.info(f'Connected to {server_string}')
209
- self.server_string = server_string
134
+ def login_success(self) -> bool:
135
+ return self.login_status.get('success', False)
210
136
 
211
- except LoginError as e:
212
- self.logger.error(str(e))
213
- self.close()
214
- raise
215
- except Exception as e:
216
- self.close()
217
- self.logger.error(f"Failed to login '{e}'")
218
- self.logger.exception(e)
219
- raise LoginError('Failed to login') from e
137
+ def login_failure(self) -> str:
138
+ return self.login_status.get('message', None)
220
139
 
221
- def consumer(self, callback, blocking=True, immortal=False, raw=False):
222
- """
223
- When a position sentence is received, it will be passed to the callback function
140
+ @property
141
+ def filter(self):
142
+ return self._client.filter
224
143
 
225
- blocking: if true (default), runs forever, otherwise will return after one sentence
226
- You can still exit the loop, by raising StopIteration in the callback function
144
+ @property
145
+ def server_string(self):
146
+ return self._client.server_string
227
147
 
228
- immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
229
- if false (default), consumer will return
148
+ @property
149
+ def keepalive(self):
150
+ return self._client.aprsd_keepalive
230
151
 
231
- raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
232
- """
152
+ def _is_stale_connection(self):
153
+ delta = datetime.datetime.now() - self._client.aprsd_keepalive
154
+ if delta > self.max_delta:
155
+ LOG.error(f'Connection is stale, last heard {delta} ago.')
156
+ return True
157
+ return False
233
158
 
234
- if not self._connected:
235
- raise ConnectionError('not connected to a server')
159
+ @staticmethod
160
+ def transport():
161
+ return client.TRANSPORT_APRSIS
236
162
 
237
- line = b''
163
+ def decode_packet(self, *args, **kwargs):
164
+ """APRS lib already decodes this."""
165
+ return core.factory(args[0])
238
166
 
239
- while True and not self.thread_stop:
167
+ def consumer(self, callback: Callable, raw: bool = False):
168
+ if self._client and self.connected:
240
169
  try:
241
- for line in self._socket_readlines(blocking):
242
- if line[0:1] != b'#':
243
- self.aprsd_keepalive = datetime.datetime.now()
244
- if raw:
245
- callback(line)
246
- else:
247
- callback(self._parse(line))
248
- else:
249
- self.logger.debug('Server: %s', line.decode('utf8'))
250
- self.aprsd_keepalive = datetime.datetime.now()
251
- except ParseError as exp:
252
- self.logger.log(
253
- 11,
254
- "%s Packet: '%s'",
255
- exp,
256
- exp.packet,
257
- )
258
- except UnknownFormat as exp:
259
- self.logger.log(
260
- 9,
261
- "%s Packet: '%s'",
262
- exp,
263
- exp.packet,
170
+ self._client.consumer(
171
+ callback,
172
+ blocking=False,
173
+ immortal=False,
174
+ raw=raw,
264
175
  )
265
- except LoginError as exp:
266
- self.logger.error('%s: %s', exp.__class__.__name__, exp)
267
- except (KeyboardInterrupt, SystemExit):
268
- raise
269
- except (ConnectionDrop, ConnectionError):
270
- self.close()
271
-
272
- if not immortal:
273
- raise
274
- else:
275
- self.connect(blocking=blocking)
276
- continue
277
- except GenericError:
278
- pass
279
- except StopIteration:
280
- break
281
- except Exception:
282
- self.logger.error('APRS Packet: %s', line)
283
- raise
284
-
285
- if not blocking:
286
- break
176
+ except Exception as e:
177
+ LOG.error(e)
178
+ LOG.info(e.__cause__)
179
+ raise e
180
+ else:
181
+ self.connected = False
182
+
183
+ def stats(self, serializable: bool = False) -> dict:
184
+ stats = {}
185
+ if self.is_configured():
186
+ if self._client:
187
+ keepalive = self._client.aprsd_keepalive
188
+ server_string = self._client.server_string
189
+ if serializable:
190
+ keepalive = keepalive.isoformat()
191
+ filter = self.filter
192
+ else:
193
+ keepalive = 'None'
194
+ server_string = 'None'
195
+ filter = 'None'
196
+ stats = {
197
+ 'connected': self.is_alive,
198
+ 'filter': filter,
199
+ 'login_status': self.login_status,
200
+ 'connection_keepalive': keepalive,
201
+ 'server_string': server_string,
202
+ 'transport': self.transport(),
203
+ }
204
+
205
+ return stats