aprsd 4.1.2__py3-none-any.whl → 4.2.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.
@@ -2,6 +2,7 @@ import datetime
2
2
  import logging
3
3
  import threading
4
4
  import time
5
+ from typing import Callable
5
6
 
6
7
  import aprslib
7
8
  import wrapt
@@ -15,7 +16,7 @@ CONF = cfg.CONF
15
16
  LOG = logging.getLogger('APRSD')
16
17
 
17
18
 
18
- class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
19
+ class APRSDFakeDriver(metaclass=trace.TraceWrapperMetaclass):
19
20
  """Fake client for testing."""
20
21
 
21
22
  # flag to tell us to stop
@@ -28,17 +29,40 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
28
29
  path = []
29
30
 
30
31
  def __init__(self):
31
- LOG.info('Starting APRSDFakeClient client.')
32
+ LOG.info('Starting APRSDFakeDriver driver.')
32
33
  self.path = ['WIDE1-1', 'WIDE2-1']
33
34
 
34
- def stop(self):
35
- self.thread_stop = True
36
- LOG.info('Shutdown APRSDFakeClient client.')
35
+ @staticmethod
36
+ def is_enabled():
37
+ if CONF.fake_client.enabled:
38
+ return True
39
+ return False
40
+
41
+ @staticmethod
42
+ def is_configured():
43
+ return APRSDFakeDriver.is_enabled
37
44
 
38
45
  def is_alive(self):
39
46
  """If the connection is alive or not."""
40
47
  return not self.thread_stop
41
48
 
49
+ def close(self):
50
+ self.thread_stop = True
51
+ LOG.info('Shutdown APRSDFakeDriver driver.')
52
+
53
+ def setup_connection(self):
54
+ # It's fake....
55
+ pass
56
+
57
+ def set_filter(self, filter: str) -> None:
58
+ pass
59
+
60
+ def login_success(self) -> bool:
61
+ return True
62
+
63
+ def login_failure(self) -> str:
64
+ return None
65
+
42
66
  @wrapt.synchronized(lock)
43
67
  def send(self, packet: core.Packet):
44
68
  """Send an APRS Message object."""
@@ -61,13 +85,37 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
61
85
  f'\'{packet.from_call}\' with PATH "{self.path}"',
62
86
  )
63
87
 
64
- def consumer(self, callback, blocking=False, immortal=False, raw=False):
88
+ def consumer(self, callback: Callable, raw: bool = False):
65
89
  LOG.debug('Start non blocking FAKE consumer')
66
90
  # Generate packets here?
67
- raw = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
68
- pkt_raw = aprslib.parse(raw)
69
- pkt = core.factory(pkt_raw)
91
+ raw_str = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
70
92
  self.aprsd_keepalive = datetime.datetime.now()
71
- callback(packet=pkt)
93
+ if raw:
94
+ callback(raw=raw_str)
95
+ else:
96
+ pkt_raw = aprslib.parse(raw_str)
97
+ pkt = core.factory(pkt_raw)
98
+ callback(packet=pkt)
99
+
72
100
  LOG.debug(f'END blocking FAKE consumer {self}')
73
- time.sleep(8)
101
+ time.sleep(1)
102
+
103
+ def decode_packet(self, *args, **kwargs):
104
+ """APRS lib already decodes this."""
105
+ if not kwargs:
106
+ return None
107
+
108
+ if kwargs.get('packet'):
109
+ return kwargs.get('packet')
110
+
111
+ if kwargs.get('raw'):
112
+ pkt_raw = aprslib.parse(kwargs.get('raw'))
113
+ pkt = core.factory(pkt_raw)
114
+ return pkt
115
+
116
+ def stats(self, serializable: bool = False) -> dict:
117
+ return {
118
+ 'driver': self.__class__.__name__,
119
+ 'is_alive': self.is_alive(),
120
+ 'transport': 'fake',
121
+ }
File without changes
@@ -0,0 +1,296 @@
1
+ import datetime
2
+ import logging
3
+ import select
4
+ import socket
5
+ import threading
6
+
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
+ )
18
+
19
+ import aprsd
20
+ from aprsd.packets import core
21
+
22
+ LOG = logging.getLogger('APRSD')
23
+
24
+
25
+ class APRSLibClient(aprslib.IS):
26
+ """Extend the aprslib class so we can exit properly.
27
+
28
+ This is a modified version of the aprslib.IS class that adds a stop method to
29
+ allow the client to exit cleanly.
30
+
31
+ The aprsis driver uses this class to connect to the APRS-IS server.
32
+ """
33
+
34
+ # flag to tell us to stop
35
+ thread_stop = False
36
+
37
+ # date for last time we heard from the server
38
+ aprsd_keepalive = datetime.datetime.now()
39
+
40
+ # Which server we are connected to?
41
+ server_string = 'None'
42
+
43
+ # timeout in seconds
44
+ select_timeout = 1
45
+ lock = threading.Lock()
46
+
47
+ def stop(self):
48
+ self.thread_stop = True
49
+ LOG.warning('Shutdown Aprsdis client.')
50
+
51
+ def close(self):
52
+ LOG.warning('Closing Aprsdis client.')
53
+ super().close()
54
+
55
+ @wrapt.synchronized(lock)
56
+ def send(self, packet: core.Packet):
57
+ """Send an APRS Message object."""
58
+ self.sendall(packet.raw)
59
+
60
+ def is_alive(self):
61
+ """If the connection is alive or not."""
62
+ return self._connected
63
+
64
+ def _connect(self):
65
+ """
66
+ Attemps connection to the server
67
+ """
68
+
69
+ self.logger.info(
70
+ 'Attempting connection to %s:%s', self.server[0], self.server[1]
71
+ )
72
+
73
+ try:
74
+ self._open_socket()
75
+
76
+ peer = self.sock.getpeername()
77
+
78
+ self.logger.info('Connected to %s', str(peer))
79
+
80
+ # 5 second timeout to receive server banner
81
+ self.sock.setblocking(1)
82
+ self.sock.settimeout(5)
83
+
84
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
85
+ # MACOS doesn't have TCP_KEEPIDLE
86
+ if hasattr(socket, 'TCP_KEEPIDLE'):
87
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
88
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
89
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
90
+
91
+ banner = self.sock.recv(512)
92
+ if is_py3:
93
+ banner = banner.decode('latin-1')
94
+
95
+ if banner[0] == '#':
96
+ self.logger.debug('Banner: %s', banner.rstrip())
97
+ else:
98
+ raise ConnectionError('invalid banner from server')
99
+
100
+ except ConnectionError as e:
101
+ self.logger.error(str(e))
102
+ self.close()
103
+ raise
104
+ except (socket.error, socket.timeout) as e:
105
+ self.close()
106
+
107
+ self.logger.error('Socket error: %s' % str(e))
108
+ if str(e) == 'timed out':
109
+ raise ConnectionError('no banner from server') from e
110
+ else:
111
+ raise ConnectionError(e) from e
112
+
113
+ self._connected = True
114
+
115
+ def _socket_readlines(self, blocking=False):
116
+ """
117
+ Generator for complete lines, received from the server
118
+ """
119
+ try:
120
+ self.sock.setblocking(0)
121
+ except OSError as e:
122
+ self.logger.error(f'socket error when setblocking(0): {str(e)}')
123
+ raise aprslib.ConnectionDrop('connection dropped') from e
124
+
125
+ while not self.thread_stop:
126
+ short_buf = b''
127
+ newline = b'\r\n'
128
+
129
+ # set a select timeout, so we get a chance to exit
130
+ # when user hits CTRL-C
131
+ readable, writable, exceptional = select.select(
132
+ [self.sock],
133
+ [],
134
+ [],
135
+ self.select_timeout,
136
+ )
137
+ if not readable:
138
+ if not blocking:
139
+ break
140
+ else:
141
+ continue
142
+
143
+ try:
144
+ short_buf = self.sock.recv(4096)
145
+
146
+ # sock.recv returns empty if the connection drops
147
+ if not short_buf:
148
+ if not blocking:
149
+ # We could just not be blocking, so empty is expected
150
+ continue
151
+ else:
152
+ self.logger.error('socket.recv(): returned empty')
153
+ raise aprslib.ConnectionDrop('connection dropped')
154
+ except OSError as e:
155
+ # self.logger.error("socket error on recv(): %s" % str(e))
156
+ if 'Resource temporarily unavailable' in str(e):
157
+ if not blocking:
158
+ if len(self.buf) == 0:
159
+ break
160
+
161
+ self.buf += short_buf
162
+
163
+ while newline in self.buf:
164
+ line, self.buf = self.buf.split(newline, 1)
165
+
166
+ yield line
167
+
168
+ def _send_login(self):
169
+ """
170
+ Sends login string to server
171
+ """
172
+ login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
173
+ login_str = login_str.format(
174
+ self.callsign,
175
+ self.passwd,
176
+ (' filter ' + self.filter) if self.filter != '' else '',
177
+ aprsd.__version__,
178
+ )
179
+
180
+ self.logger.debug('Sending login information')
181
+
182
+ try:
183
+ self._sendall(login_str)
184
+ self.sock.settimeout(5)
185
+ test = self.sock.recv(len(login_str) + 100)
186
+ if is_py3:
187
+ test = test.decode('latin-1')
188
+ test = test.rstrip()
189
+
190
+ self.logger.debug("Server: '%s'", test)
191
+
192
+ if not test:
193
+ raise LoginError(f"Server Response Empty: '{test}'")
194
+
195
+ _, _, callsign, status, e = test.split(' ', 4)
196
+ s = e.split(',')
197
+ if len(s):
198
+ server_string = s[0].replace('server ', '')
199
+ else:
200
+ server_string = e.replace('server ', '')
201
+
202
+ if callsign == '':
203
+ raise LoginError('Server responded with empty callsign???')
204
+ if callsign != self.callsign:
205
+ raise LoginError(f'Server: {test}')
206
+ if status != 'verified,' and self.passwd != '-1':
207
+ raise LoginError('Password is incorrect')
208
+
209
+ if self.passwd == '-1':
210
+ self.logger.info('Login successful (receive only)')
211
+ else:
212
+ self.logger.info('Login successful')
213
+
214
+ self.logger.info(f'Connected to {server_string}')
215
+ self.server_string = server_string
216
+
217
+ except LoginError as e:
218
+ self.logger.error(str(e))
219
+ self.close()
220
+ raise
221
+ except Exception as e:
222
+ self.close()
223
+ self.logger.error(f"Failed to login '{e}'")
224
+ self.logger.exception(e)
225
+ raise LoginError('Failed to login') from e
226
+
227
+ def consumer(self, callback, blocking=True, immortal=False, raw=False):
228
+ """
229
+ When a position sentence is received, it will be passed to the callback function
230
+
231
+ blocking: if true (default), runs forever, otherwise will return after one sentence
232
+ You can still exit the loop, by raising StopIteration in the callback function
233
+
234
+ immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
235
+ if false (default), consumer will return
236
+
237
+ raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
238
+ """
239
+
240
+ if not self._connected:
241
+ raise ConnectionError('not connected to a server')
242
+
243
+ line = b''
244
+
245
+ while not self.thread_stop:
246
+ try:
247
+ for line in self._socket_readlines(blocking):
248
+ if line[0:1] != b'#':
249
+ self.aprsd_keepalive = datetime.datetime.now()
250
+ if raw:
251
+ callback(line)
252
+ else:
253
+ callback(self._parse(line))
254
+ else:
255
+ self.logger.debug('Server: %s', line.decode('utf8'))
256
+ self.aprsd_keepalive = datetime.datetime.now()
257
+ except ParseError as exp:
258
+ self.logger.log(
259
+ 11,
260
+ "%s Packet: '%s'",
261
+ exp,
262
+ exp.packet,
263
+ )
264
+ except UnknownFormat as exp:
265
+ self.logger.log(
266
+ 9,
267
+ "%s Packet: '%s'",
268
+ exp,
269
+ exp.packet,
270
+ )
271
+ except LoginError as exp:
272
+ self.logger.error('%s: %s', exp.__class__.__name__, exp)
273
+ except (KeyboardInterrupt, SystemExit):
274
+ raise
275
+ except (ConnectionDrop, ConnectionError):
276
+ self.close()
277
+
278
+ if not immortal:
279
+ raise
280
+ else:
281
+ self.connect(blocking=blocking)
282
+ continue
283
+ except GenericError:
284
+ pass
285
+ except StopIteration:
286
+ break
287
+ except IOError:
288
+ if not self.thread_stop:
289
+ self.logger.error('IOError')
290
+ break
291
+ except Exception:
292
+ self.logger.error('APRS Packet: %s', line)
293
+ raise
294
+
295
+ if not blocking:
296
+ break
@@ -0,0 +1,86 @@
1
+ from typing import Callable, Protocol, runtime_checkable
2
+
3
+ from aprsd.packets import core
4
+ from aprsd.utils import singleton, trace
5
+
6
+
7
+ @runtime_checkable
8
+ class ClientDriver(Protocol):
9
+ """Protocol for APRSD client drivers.
10
+
11
+ This protocol defines the methods that must be
12
+ implemented by APRSD client drivers.
13
+ """
14
+
15
+ @staticmethod
16
+ def is_enabled(self) -> bool:
17
+ pass
18
+
19
+ @staticmethod
20
+ def is_configured(self) -> bool:
21
+ pass
22
+
23
+ def is_alive(self) -> bool:
24
+ pass
25
+
26
+ def close(self) -> None:
27
+ pass
28
+
29
+ def send(self, packet: core.Packet) -> bool:
30
+ pass
31
+
32
+ def setup_connection(self) -> None:
33
+ pass
34
+
35
+ def set_filter(self, filter: str) -> None:
36
+ pass
37
+
38
+ def login_success(self) -> bool:
39
+ pass
40
+
41
+ def login_failure(self) -> str:
42
+ pass
43
+
44
+ def consumer(self, callback: Callable, raw: bool = False) -> None:
45
+ pass
46
+
47
+ def decode_packet(self, *args, **kwargs) -> core.Packet:
48
+ pass
49
+
50
+ def stats(self, serializable: bool = False) -> dict:
51
+ pass
52
+
53
+
54
+ @singleton
55
+ class DriverRegistry(metaclass=trace.TraceWrapperMetaclass):
56
+ """Registry for APRSD client drivers.
57
+
58
+ This registry is used to register and unregister APRSD client drivers.
59
+
60
+ This allows us to dynamically load the configured driver at runtime.
61
+
62
+ All drivers are registered, then when aprsd needs the client, the
63
+ registry provides the configured driver for the single instance of the
64
+ single APRSD client.
65
+ """
66
+
67
+ def __init__(self):
68
+ self.drivers = []
69
+
70
+ def register(self, driver: Callable):
71
+ if not isinstance(driver, ClientDriver):
72
+ raise ValueError('Driver must be of ClientDriver type')
73
+ self.drivers.append(driver)
74
+
75
+ def unregister(self, driver: Callable):
76
+ if driver in self.drivers:
77
+ self.drivers.remove(driver)
78
+ else:
79
+ raise ValueError(f'Driver {driver} not found')
80
+
81
+ def get_driver(self) -> ClientDriver:
82
+ """Get the first enabled driver."""
83
+ for driver in self.drivers:
84
+ if driver.is_enabled() and driver.is_configured():
85
+ return driver()
86
+ raise ValueError('No enabled driver found')