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.
- aprsd/__init__.py +6 -4
- aprsd/cli_helper.py +151 -0
- aprsd/client/__init__.py +13 -0
- aprsd/client/aprsis.py +132 -0
- aprsd/client/base.py +105 -0
- aprsd/client/drivers/__init__.py +0 -0
- aprsd/client/drivers/aprsis.py +224 -0
- aprsd/client/drivers/fake.py +73 -0
- aprsd/client/drivers/kiss.py +119 -0
- aprsd/client/factory.py +88 -0
- aprsd/client/fake.py +48 -0
- aprsd/client/kiss.py +103 -0
- aprsd/client/stats.py +38 -0
- aprsd/cmds/__init__.py +0 -0
- aprsd/cmds/completion.py +22 -0
- aprsd/cmds/dev.py +162 -0
- aprsd/cmds/fetch_stats.py +156 -0
- aprsd/cmds/healthcheck.py +86 -0
- aprsd/cmds/list_plugins.py +319 -0
- aprsd/cmds/listen.py +230 -0
- aprsd/cmds/send_message.py +174 -0
- aprsd/cmds/server.py +142 -0
- aprsd/cmds/webchat.py +681 -0
- aprsd/conf/__init__.py +56 -0
- aprsd/conf/client.py +131 -0
- aprsd/conf/common.py +302 -0
- aprsd/conf/log.py +65 -0
- aprsd/conf/opts.py +80 -0
- aprsd/conf/plugin_common.py +191 -0
- aprsd/conf/plugin_email.py +105 -0
- aprsd/exception.py +13 -0
- aprsd/log/__init__.py +0 -0
- aprsd/log/log.py +138 -0
- aprsd/main.py +104 -867
- aprsd/messaging.py +4 -0
- aprsd/packets/__init__.py +12 -0
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +823 -0
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +116 -0
- aprsd/packets/seen_list.py +54 -0
- aprsd/packets/tracker.py +109 -0
- aprsd/packets/watch_list.py +122 -0
- aprsd/plugin.py +475 -284
- aprsd/plugin_utils.py +86 -0
- aprsd/plugins/__init__.py +0 -0
- aprsd/plugins/email.py +709 -0
- aprsd/plugins/fortune.py +61 -0
- aprsd/plugins/location.py +179 -0
- aprsd/plugins/notify.py +61 -0
- aprsd/plugins/ping.py +31 -0
- aprsd/plugins/time.py +115 -0
- aprsd/plugins/version.py +31 -0
- aprsd/plugins/weather.py +405 -0
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +49 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +11 -0
- aprsd/threads/aprsd.py +119 -0
- aprsd/threads/keep_alive.py +124 -0
- aprsd/threads/log_monitor.py +121 -0
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +354 -0
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +255 -0
- aprsd/utils/__init__.py +163 -0
- aprsd/utils/counter.py +51 -0
- aprsd/utils/json.py +80 -0
- aprsd/utils/objectstore.py +123 -0
- aprsd/utils/ring_buffer.py +40 -0
- aprsd/utils/trace.py +180 -0
- aprsd/web/__init__.py +0 -0
- aprsd/web/admin/__init__.py +0 -0
- aprsd/web/admin/static/css/index.css +84 -0
- aprsd/web/admin/static/css/prism.css +4 -0
- aprsd/web/admin/static/css/tabs.css +35 -0
- aprsd/web/admin/static/images/Untitled.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/admin/static/js/charts.js +235 -0
- aprsd/web/admin/static/js/echarts.js +465 -0
- aprsd/web/admin/static/js/logs.js +26 -0
- aprsd/web/admin/static/js/main.js +231 -0
- aprsd/web/admin/static/js/prism.js +12 -0
- aprsd/web/admin/static/js/send-message.js +114 -0
- aprsd/web/admin/static/js/tabs.js +28 -0
- aprsd/web/admin/templates/index.html +196 -0
- aprsd/web/chat/static/css/chat.css +115 -0
- aprsd/web/chat/static/css/index.css +66 -0
- aprsd/web/chat/static/css/style.css.map +1 -0
- aprsd/web/chat/static/css/tabs.css +41 -0
- aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
- aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
- aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
- aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
- aprsd/web/chat/static/images/Untitled.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/gps.js +84 -0
- aprsd/web/chat/static/js/main.js +45 -0
- aprsd/web/chat/static/js/send-message.js +585 -0
- aprsd/web/chat/static/js/tabs.js +28 -0
- aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
- aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
- aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
- aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
- aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
- aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
- aprsd/web/chat/templates/index.html +139 -0
- aprsd/wsgi.py +315 -0
- aprsd-3.4.1.dist-info/AUTHORS +13 -0
- aprsd-3.4.1.dist-info/LICENSE +175 -0
- aprsd-3.4.1.dist-info/METADATA +799 -0
- aprsd-3.4.1.dist-info/RECORD +134 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
- aprsd-3.4.1.dist-info/entry_points.txt +8 -0
- aprsd/fake_aprs.py +0 -83
- aprsd/utils.py +0 -166
- aprsd-1.0.0.dist-info/AUTHORS +0 -6
- aprsd-1.0.0.dist-info/METADATA +0 -181
- aprsd-1.0.0.dist-info/RECORD +0 -13
- aprsd-1.0.0.dist-info/entry_points.txt +0 -4
- aprsd-1.0.0.dist-info/pbr.json +0 -1
- /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/top_level.txt +0 -0
aprsd/packets/log.py
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from loguru import logger
|
5
|
+
from oslo_config import cfg
|
6
|
+
|
7
|
+
from aprsd.packets.core import AckPacket, RejectPacket
|
8
|
+
|
9
|
+
|
10
|
+
LOG = logging.getLogger()
|
11
|
+
LOGU = logger
|
12
|
+
CONF = cfg.CONF
|
13
|
+
|
14
|
+
FROM_COLOR = "fg #C70039"
|
15
|
+
TO_COLOR = "fg #D033FF"
|
16
|
+
TX_COLOR = "red"
|
17
|
+
RX_COLOR = "green"
|
18
|
+
PACKET_COLOR = "cyan"
|
19
|
+
|
20
|
+
|
21
|
+
def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
22
|
+
"""LOG a packet to the logfile."""
|
23
|
+
if not CONF.enable_packet_logging:
|
24
|
+
return
|
25
|
+
if CONF.log_packet_format == "compact":
|
26
|
+
return
|
27
|
+
|
28
|
+
# asdict(packet)
|
29
|
+
logit = ["\n"]
|
30
|
+
name = packet.__class__.__name__
|
31
|
+
|
32
|
+
if isinstance(packet, AckPacket):
|
33
|
+
pkt_max_send_count = CONF.default_ack_send_count
|
34
|
+
else:
|
35
|
+
pkt_max_send_count = CONF.default_packet_send_count
|
36
|
+
|
37
|
+
if header:
|
38
|
+
if tx:
|
39
|
+
header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
|
40
|
+
logit.append(
|
41
|
+
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
|
42
|
+
f"TX:{packet.send_count + 1} of {pkt_max_send_count}",
|
43
|
+
)
|
44
|
+
else:
|
45
|
+
header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
|
46
|
+
logit.append(
|
47
|
+
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
|
48
|
+
)
|
49
|
+
|
50
|
+
else:
|
51
|
+
header_str = ""
|
52
|
+
logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
53
|
+
# log_list.append(f" Packet : {packet.__class__.__name__}")
|
54
|
+
if packet.msgNo:
|
55
|
+
logit.append(f" Msg # : {packet.msgNo}")
|
56
|
+
if packet.from_call:
|
57
|
+
logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
|
58
|
+
if packet.to_call:
|
59
|
+
logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
|
60
|
+
if hasattr(packet, "path") and packet.path:
|
61
|
+
logit.append(f" Path : {'=>'.join(packet.path)}")
|
62
|
+
if hasattr(packet, "via") and packet.via:
|
63
|
+
logit.append(f" VIA : {packet.via}")
|
64
|
+
|
65
|
+
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
66
|
+
msg = packet.human_info
|
67
|
+
|
68
|
+
if msg:
|
69
|
+
msg = msg.replace("<", "\\<")
|
70
|
+
logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
|
71
|
+
|
72
|
+
if hasattr(packet, "comment") and packet.comment:
|
73
|
+
logit.append(f" Comment : {packet.comment}")
|
74
|
+
|
75
|
+
raw = packet.raw.replace("<", "\\<")
|
76
|
+
logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
|
77
|
+
logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
78
|
+
|
79
|
+
LOGU.opt(colors=True).info("\n".join(logit))
|
80
|
+
LOG.debug(repr(packet))
|
81
|
+
|
82
|
+
|
83
|
+
def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
84
|
+
if not CONF.enable_packet_logging:
|
85
|
+
return
|
86
|
+
if CONF.log_packet_format == "multiline":
|
87
|
+
log_multiline(packet, tx, header)
|
88
|
+
return
|
89
|
+
|
90
|
+
logit = []
|
91
|
+
name = packet.__class__.__name__
|
92
|
+
if isinstance(packet, AckPacket):
|
93
|
+
pkt_max_send_count = CONF.default_ack_send_count
|
94
|
+
else:
|
95
|
+
pkt_max_send_count = CONF.default_packet_send_count
|
96
|
+
|
97
|
+
if header:
|
98
|
+
if tx:
|
99
|
+
via_color = "red"
|
100
|
+
arrow = f"<{via_color}>-></{via_color}>"
|
101
|
+
logit.append(
|
102
|
+
f"<red>TX {arrow}</red> "
|
103
|
+
f"<cyan>{name}</cyan>"
|
104
|
+
f":{packet.msgNo}"
|
105
|
+
f" ({packet.send_count + 1} of {pkt_max_send_count})",
|
106
|
+
)
|
107
|
+
else:
|
108
|
+
via_color = "fg #828282"
|
109
|
+
arrow = f"<{via_color}>-></{via_color}>"
|
110
|
+
left_arrow = f"<{via_color}><-</{via_color}>"
|
111
|
+
logit.append(
|
112
|
+
f"<fg #1AA730>RX</fg #1AA730> {left_arrow} "
|
113
|
+
f"<cyan>{name}</cyan>"
|
114
|
+
f":{packet.msgNo}",
|
115
|
+
)
|
116
|
+
else:
|
117
|
+
via_color = "green"
|
118
|
+
arrow = f"<{via_color}>-></{via_color}>"
|
119
|
+
logit.append(
|
120
|
+
f"<cyan>{name}</cyan>"
|
121
|
+
f":{packet.msgNo}",
|
122
|
+
)
|
123
|
+
|
124
|
+
tmp = None
|
125
|
+
if packet.path:
|
126
|
+
tmp = f"{arrow}".join(packet.path) + f"{arrow} "
|
127
|
+
|
128
|
+
logit.append(
|
129
|
+
f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
|
130
|
+
f"{tmp if tmp else ' '}"
|
131
|
+
f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
|
132
|
+
)
|
133
|
+
|
134
|
+
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
135
|
+
logit.append(":")
|
136
|
+
msg = packet.human_info
|
137
|
+
|
138
|
+
if msg:
|
139
|
+
msg = msg.replace("<", "\\<")
|
140
|
+
logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
|
141
|
+
|
142
|
+
LOGU.opt(colors=True).info(" ".join(logit))
|
143
|
+
log_multiline(packet, tx, header)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from collections import OrderedDict
|
2
|
+
import logging
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
from aprsd.packets import collector, core
|
7
|
+
from aprsd.utils import objectstore
|
8
|
+
|
9
|
+
|
10
|
+
CONF = cfg.CONF
|
11
|
+
LOG = logging.getLogger("APRSD")
|
12
|
+
|
13
|
+
|
14
|
+
class PacketList(objectstore.ObjectStoreMixin):
|
15
|
+
"""Class to keep track of the packets we tx/rx."""
|
16
|
+
_instance = None
|
17
|
+
_total_rx: int = 0
|
18
|
+
_total_tx: int = 0
|
19
|
+
maxlen: int = 100
|
20
|
+
|
21
|
+
def __new__(cls, *args, **kwargs):
|
22
|
+
if cls._instance is None:
|
23
|
+
cls._instance = super().__new__(cls)
|
24
|
+
cls._instance.maxlen = CONF.packet_list_maxlen
|
25
|
+
cls._instance._init_data()
|
26
|
+
return cls._instance
|
27
|
+
|
28
|
+
def _init_data(self):
|
29
|
+
self.data = {
|
30
|
+
"types": {},
|
31
|
+
"packets": OrderedDict(),
|
32
|
+
}
|
33
|
+
|
34
|
+
def rx(self, packet: type[core.Packet]):
|
35
|
+
"""Add a packet that was received."""
|
36
|
+
with self.lock:
|
37
|
+
self._total_rx += 1
|
38
|
+
self._add(packet)
|
39
|
+
ptype = packet.__class__.__name__
|
40
|
+
if ptype not in self.data["types"]:
|
41
|
+
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
42
|
+
self.data["types"][ptype]["rx"] += 1
|
43
|
+
|
44
|
+
def tx(self, packet: type[core.Packet]):
|
45
|
+
"""Add a packet that was received."""
|
46
|
+
with self.lock:
|
47
|
+
self._total_tx += 1
|
48
|
+
self._add(packet)
|
49
|
+
ptype = packet.__class__.__name__
|
50
|
+
if ptype not in self.data["types"]:
|
51
|
+
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
52
|
+
self.data["types"][ptype]["tx"] += 1
|
53
|
+
|
54
|
+
def add(self, packet):
|
55
|
+
with self.lock:
|
56
|
+
self._add(packet)
|
57
|
+
|
58
|
+
def _add(self, packet):
|
59
|
+
if not self.data.get("packets"):
|
60
|
+
self._init_data()
|
61
|
+
if packet.key in self.data["packets"]:
|
62
|
+
self.data["packets"].move_to_end(packet.key)
|
63
|
+
elif len(self.data["packets"]) == self.maxlen:
|
64
|
+
self.data["packets"].popitem(last=False)
|
65
|
+
self.data["packets"][packet.key] = packet
|
66
|
+
|
67
|
+
def find(self, packet):
|
68
|
+
with self.lock:
|
69
|
+
return self.data["packets"][packet.key]
|
70
|
+
|
71
|
+
def __len__(self):
|
72
|
+
with self.lock:
|
73
|
+
return len(self.data["packets"])
|
74
|
+
|
75
|
+
def total_rx(self):
|
76
|
+
with self.lock:
|
77
|
+
return self._total_rx
|
78
|
+
|
79
|
+
def total_tx(self):
|
80
|
+
with self.lock:
|
81
|
+
return self._total_tx
|
82
|
+
|
83
|
+
def stats(self, serializable=False) -> dict:
|
84
|
+
# limit the number of packets to return to 50
|
85
|
+
with self.lock:
|
86
|
+
tmp = OrderedDict(
|
87
|
+
reversed(
|
88
|
+
list(
|
89
|
+
self.data.get("packets", OrderedDict()).items(),
|
90
|
+
),
|
91
|
+
),
|
92
|
+
)
|
93
|
+
pkts = []
|
94
|
+
count = 1
|
95
|
+
for packet in tmp:
|
96
|
+
pkts.append(tmp[packet])
|
97
|
+
count += 1
|
98
|
+
if count > CONF.packet_list_stats_maxlen:
|
99
|
+
break
|
100
|
+
|
101
|
+
stats = {
|
102
|
+
"total_tracked": self._total_rx + self._total_rx,
|
103
|
+
"rx": self._total_rx,
|
104
|
+
"tx": self._total_tx,
|
105
|
+
"types": self.data.get("types", []),
|
106
|
+
"packet_count": len(self.data.get("packets", [])),
|
107
|
+
"maxlen": self.maxlen,
|
108
|
+
"packets": pkts,
|
109
|
+
}
|
110
|
+
return stats
|
111
|
+
|
112
|
+
|
113
|
+
# Now register the PacketList with the collector
|
114
|
+
# every packet we RX and TX goes through the collector
|
115
|
+
# for processing for whatever reason is needed.
|
116
|
+
collector.PacketCollector().register(PacketList)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
from aprsd.packets import collector, core
|
7
|
+
from aprsd.utils import objectstore
|
8
|
+
|
9
|
+
|
10
|
+
CONF = cfg.CONF
|
11
|
+
LOG = logging.getLogger("APRSD")
|
12
|
+
|
13
|
+
|
14
|
+
class SeenList(objectstore.ObjectStoreMixin):
|
15
|
+
"""Global callsign seen list."""
|
16
|
+
|
17
|
+
_instance = None
|
18
|
+
data: dict = {}
|
19
|
+
|
20
|
+
def __new__(cls, *args, **kwargs):
|
21
|
+
if cls._instance is None:
|
22
|
+
cls._instance = super().__new__(cls)
|
23
|
+
cls._instance.data = {}
|
24
|
+
return cls._instance
|
25
|
+
|
26
|
+
def stats(self, serializable=False):
|
27
|
+
"""Return the stats for the PacketTrack class."""
|
28
|
+
with self.lock:
|
29
|
+
return self.data
|
30
|
+
|
31
|
+
def rx(self, packet: type[core.Packet]):
|
32
|
+
"""When we get a packet from the network, update the seen list."""
|
33
|
+
with self.lock:
|
34
|
+
callsign = None
|
35
|
+
if packet.from_call:
|
36
|
+
callsign = packet.from_call
|
37
|
+
else:
|
38
|
+
LOG.warning(f"Can't find FROM in packet {packet}")
|
39
|
+
return
|
40
|
+
if callsign not in self.data:
|
41
|
+
self.data[callsign] = {
|
42
|
+
"last": None,
|
43
|
+
"count": 0,
|
44
|
+
}
|
45
|
+
self.data[callsign]["last"] = datetime.datetime.now()
|
46
|
+
self.data[callsign]["count"] += 1
|
47
|
+
|
48
|
+
def tx(self, packet: type[core.Packet]):
|
49
|
+
"""We don't care about TX packets."""
|
50
|
+
|
51
|
+
|
52
|
+
# Register with the packet collector so we can process the packet
|
53
|
+
# when we get it off the client (network)
|
54
|
+
collector.PacketCollector().register(SeenList)
|
aprsd/packets/tracker.py
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
from aprsd.packets import collector, core
|
7
|
+
from aprsd.utils import objectstore
|
8
|
+
|
9
|
+
|
10
|
+
CONF = cfg.CONF
|
11
|
+
LOG = logging.getLogger("APRSD")
|
12
|
+
|
13
|
+
|
14
|
+
class PacketTrack(objectstore.ObjectStoreMixin):
|
15
|
+
"""Class to keep track of outstanding text messages.
|
16
|
+
|
17
|
+
This is a thread safe class that keeps track of active
|
18
|
+
messages.
|
19
|
+
|
20
|
+
When a message is asked to be sent, it is placed into this
|
21
|
+
class via it's id. The TextMessage class's send() method
|
22
|
+
automatically adds itself to this class. When the ack is
|
23
|
+
recieved from the radio, the message object is removed from
|
24
|
+
this class.
|
25
|
+
"""
|
26
|
+
|
27
|
+
_instance = None
|
28
|
+
_start_time = None
|
29
|
+
|
30
|
+
data: dict = {}
|
31
|
+
total_tracked: int = 0
|
32
|
+
|
33
|
+
def __new__(cls, *args, **kwargs):
|
34
|
+
if cls._instance is None:
|
35
|
+
cls._instance = super().__new__(cls)
|
36
|
+
cls._instance._start_time = datetime.datetime.now()
|
37
|
+
cls._instance._init_store()
|
38
|
+
return cls._instance
|
39
|
+
|
40
|
+
def __getitem__(self, name):
|
41
|
+
with self.lock:
|
42
|
+
return self.data[name]
|
43
|
+
|
44
|
+
def __iter__(self):
|
45
|
+
with self.lock:
|
46
|
+
return iter(self.data)
|
47
|
+
|
48
|
+
def keys(self):
|
49
|
+
with self.lock:
|
50
|
+
return self.data.keys()
|
51
|
+
|
52
|
+
def items(self):
|
53
|
+
with self.lock:
|
54
|
+
return self.data.items()
|
55
|
+
|
56
|
+
def values(self):
|
57
|
+
with self.lock:
|
58
|
+
return self.data.values()
|
59
|
+
|
60
|
+
def stats(self, serializable=False):
|
61
|
+
with self.lock:
|
62
|
+
stats = {
|
63
|
+
"total_tracked": self.total_tracked,
|
64
|
+
}
|
65
|
+
pkts = {}
|
66
|
+
for key in self.data:
|
67
|
+
last_send_time = self.data[key].last_send_time
|
68
|
+
pkts[key] = {
|
69
|
+
"last_send_time": last_send_time,
|
70
|
+
"send_count": self.data[key].send_count,
|
71
|
+
"retry_count": self.data[key].retry_count,
|
72
|
+
"message": self.data[key].raw,
|
73
|
+
}
|
74
|
+
stats["packets"] = pkts
|
75
|
+
return stats
|
76
|
+
|
77
|
+
def rx(self, packet: type[core.Packet]) -> None:
|
78
|
+
"""When we get a packet from the network, check if we should remove it."""
|
79
|
+
if isinstance(packet, core.AckPacket):
|
80
|
+
self._remove(packet.msgNo)
|
81
|
+
elif isinstance(packet, core.RejectPacket):
|
82
|
+
self._remove(packet.msgNo)
|
83
|
+
elif hasattr(packet, "ackMsgNo"):
|
84
|
+
# Got a piggyback ack, so remove the original message
|
85
|
+
self._remove(packet.ackMsgNo)
|
86
|
+
|
87
|
+
def tx(self, packet: type[core.Packet]) -> None:
|
88
|
+
"""Add a packet that was sent."""
|
89
|
+
with self.lock:
|
90
|
+
key = packet.msgNo
|
91
|
+
packet.send_count = 0
|
92
|
+
self.data[key] = packet
|
93
|
+
self.total_tracked += 1
|
94
|
+
|
95
|
+
def remove(self, key):
|
96
|
+
self._remove(key)
|
97
|
+
|
98
|
+
def _remove(self, key):
|
99
|
+
with self.lock:
|
100
|
+
try:
|
101
|
+
del self.data[key]
|
102
|
+
except KeyError:
|
103
|
+
pass
|
104
|
+
|
105
|
+
|
106
|
+
# Now register the PacketList with the collector
|
107
|
+
# every packet we RX and TX goes through the collector
|
108
|
+
# for processing for whatever reason is needed.
|
109
|
+
collector.PacketCollector().register(PacketTrack)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
from aprsd import utils
|
7
|
+
from aprsd.packets import collector, core
|
8
|
+
from aprsd.utils import objectstore
|
9
|
+
|
10
|
+
|
11
|
+
CONF = cfg.CONF
|
12
|
+
LOG = logging.getLogger("APRSD")
|
13
|
+
|
14
|
+
|
15
|
+
class WatchList(objectstore.ObjectStoreMixin):
|
16
|
+
"""Global watch list and info for callsigns."""
|
17
|
+
|
18
|
+
_instance = None
|
19
|
+
data = {}
|
20
|
+
|
21
|
+
def __new__(cls, *args, **kwargs):
|
22
|
+
if cls._instance is None:
|
23
|
+
cls._instance = super().__new__(cls)
|
24
|
+
return cls._instance
|
25
|
+
|
26
|
+
def __init__(self):
|
27
|
+
super().__init__()
|
28
|
+
self._update_from_conf()
|
29
|
+
|
30
|
+
def _update_from_conf(self, config=None):
|
31
|
+
with self.lock:
|
32
|
+
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
33
|
+
for callsign in CONF.watch_list.callsigns:
|
34
|
+
call = callsign.replace("*", "")
|
35
|
+
# FIXME(waboring) - we should fetch the last time we saw
|
36
|
+
# a beacon from a callsign or some other mechanism to find
|
37
|
+
# last time a message was seen by aprs-is. For now this
|
38
|
+
# is all we can do.
|
39
|
+
if call not in self.data:
|
40
|
+
self.data[call] = {
|
41
|
+
"last": None,
|
42
|
+
"packet": None,
|
43
|
+
}
|
44
|
+
|
45
|
+
def stats(self, serializable=False) -> dict:
|
46
|
+
stats = {}
|
47
|
+
with self.lock:
|
48
|
+
for callsign in self.data:
|
49
|
+
stats[callsign] = {
|
50
|
+
"last": self.data[callsign]["last"],
|
51
|
+
"packet": self.data[callsign]["packet"],
|
52
|
+
"age": self.age(callsign),
|
53
|
+
"old": self.is_old(callsign),
|
54
|
+
}
|
55
|
+
return stats
|
56
|
+
|
57
|
+
def is_enabled(self):
|
58
|
+
return CONF.watch_list.enabled
|
59
|
+
|
60
|
+
def callsign_in_watchlist(self, callsign):
|
61
|
+
with self.lock:
|
62
|
+
return callsign in self.data
|
63
|
+
|
64
|
+
def rx(self, packet: type[core.Packet]) -> None:
|
65
|
+
"""Track when we got a packet from the network."""
|
66
|
+
callsign = packet.from_call
|
67
|
+
|
68
|
+
if self.callsign_in_watchlist(callsign):
|
69
|
+
with self.lock:
|
70
|
+
self.data[callsign]["last"] = datetime.datetime.now()
|
71
|
+
self.data[callsign]["packet"] = packet
|
72
|
+
|
73
|
+
def tx(self, packet: type[core.Packet]) -> None:
|
74
|
+
"""We don't care about TX packets."""
|
75
|
+
|
76
|
+
def last_seen(self, callsign):
|
77
|
+
with self.lock:
|
78
|
+
if self.callsign_in_watchlist(callsign):
|
79
|
+
return self.data[callsign]["last"]
|
80
|
+
|
81
|
+
def age(self, callsign):
|
82
|
+
now = datetime.datetime.now()
|
83
|
+
last_seen_time = self.last_seen(callsign)
|
84
|
+
if last_seen_time:
|
85
|
+
return str(now - last_seen_time)
|
86
|
+
else:
|
87
|
+
return None
|
88
|
+
|
89
|
+
def max_delta(self, seconds=None):
|
90
|
+
if not seconds:
|
91
|
+
seconds = CONF.watch_list.alert_time_seconds
|
92
|
+
max_timeout = {"seconds": seconds}
|
93
|
+
return datetime.timedelta(**max_timeout)
|
94
|
+
|
95
|
+
def is_old(self, callsign, seconds=None):
|
96
|
+
"""Watch list callsign last seen is old compared to now?
|
97
|
+
|
98
|
+
This tests to see if the last time we saw a callsign packet,
|
99
|
+
if that is older than the allowed timeout in the config.
|
100
|
+
|
101
|
+
We put this here so any notification plugin can use this
|
102
|
+
same test.
|
103
|
+
"""
|
104
|
+
if not self.callsign_in_watchlist(callsign):
|
105
|
+
return False
|
106
|
+
|
107
|
+
age = self.age(callsign)
|
108
|
+
if age:
|
109
|
+
delta = utils.parse_delta_str(age)
|
110
|
+
d = datetime.timedelta(**delta)
|
111
|
+
|
112
|
+
max_delta = self.max_delta(seconds=seconds)
|
113
|
+
|
114
|
+
if d > max_delta:
|
115
|
+
return True
|
116
|
+
else:
|
117
|
+
return False
|
118
|
+
else:
|
119
|
+
return False
|
120
|
+
|
121
|
+
|
122
|
+
collector.PacketCollector().register(WatchList)
|