aprsd 4.1.1__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.
- aprsd/client/__init__.py +5 -13
- aprsd/client/client.py +141 -0
- aprsd/client/drivers/__init__.py +10 -0
- aprsd/client/drivers/aprsis.py +174 -255
- aprsd/client/drivers/fake.py +59 -11
- aprsd/client/drivers/lib/__init__.py +0 -0
- aprsd/client/drivers/lib/aprslib.py +296 -0
- aprsd/client/drivers/registry.py +86 -0
- aprsd/client/drivers/tcpkiss.py +408 -0
- aprsd/client/stats.py +2 -2
- aprsd/cmds/dev.py +0 -3
- aprsd/cmds/listen.py +3 -3
- aprsd/cmds/send_message.py +4 -4
- aprsd/cmds/server.py +4 -10
- aprsd/log/log.py +5 -2
- aprsd/main.py +0 -7
- aprsd/packets/core.py +168 -169
- aprsd/packets/log.py +69 -59
- aprsd/plugin.py +3 -2
- aprsd/plugin_utils.py +2 -2
- aprsd/plugins/weather.py +2 -2
- aprsd/stats/collector.py +5 -4
- aprsd/threads/rx.py +12 -10
- aprsd/threads/tx.py +32 -31
- aprsd/utils/keepalive_collector.py +7 -5
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info}/METADATA +48 -48
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info}/RECORD +32 -33
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info}/WHEEL +1 -1
- aprsd/client/aprsis.py +0 -183
- aprsd/client/base.py +0 -156
- aprsd/client/drivers/kiss.py +0 -144
- aprsd/client/factory.py +0 -91
- aprsd/client/fake.py +0 -49
- aprsd/client/kiss.py +0 -143
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info}/entry_points.txt +0 -0
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info/licenses}/AUTHORS +0 -0
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info/licenses}/LICENSE +0 -0
- {aprsd-4.1.1.dist-info → aprsd-4.2.0.dist-info}/top_level.txt +0 -0
aprsd/client/drivers/fake.py
CHANGED
@@ -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
|
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
|
32
|
+
LOG.info('Starting APRSDFakeDriver driver.')
|
32
33
|
self.path = ['WIDE1-1', 'WIDE2-1']
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
88
|
+
def consumer(self, callback: Callable, raw: bool = False):
|
65
89
|
LOG.debug('Start non blocking FAKE consumer')
|
66
90
|
# Generate packets here?
|
67
|
-
|
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
|
-
|
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(
|
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')
|