aprsd 3.2.2__py2.py3-none-any.whl → 3.3.1__py2.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/cli_helper.py +35 -1
- aprsd/client.py +3 -3
- aprsd/clients/aprsis.py +0 -1
- aprsd/cmds/dev.py +31 -2
- aprsd/cmds/fetch_stats.py +5 -1
- aprsd/cmds/list_plugins.py +87 -10
- aprsd/cmds/server.py +10 -1
- aprsd/cmds/webchat.py +245 -45
- aprsd/conf/common.py +68 -1
- aprsd/conf/log.py +8 -10
- aprsd/conf/plugin_common.py +108 -0
- aprsd/log/log.py +74 -66
- aprsd/main.py +11 -11
- aprsd/packets/__init__.py +2 -2
- aprsd/packets/core.py +20 -1
- aprsd/plugin_utils.py +1 -0
- aprsd/plugins/fortune.py +4 -1
- aprsd/plugins/location.py +88 -6
- aprsd/plugins/weather.py +7 -3
- aprsd/threads/__init__.py +1 -1
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +13 -5
- aprsd/threads/tx.py +40 -0
- aprsd/utils/__init__.py +19 -0
- aprsd/utils/objectstore.py +7 -1
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/send-message.js +107 -29
- aprsd/web/chat/templates/index.html +6 -2
- aprsd/wsgi.py +4 -39
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/METADATA +106 -98
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/RECORD +36 -35
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/WHEEL +1 -1
- aprsd-3.3.1.dist-info/pbr.json +1 -0
- aprsd/log/rich.py +0 -160
- aprsd-3.2.2.dist-info/pbr.json +0 -1
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/LICENSE +0 -0
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/entry_points.txt +0 -0
- {aprsd-3.2.2.dist-info → aprsd-3.3.1.dist-info}/top_level.txt +0 -0
aprsd/log/log.py
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
import logging
|
2
|
-
from logging import
|
3
|
-
from logging.handlers import RotatingFileHandler
|
2
|
+
from logging.handlers import QueueHandler
|
4
3
|
import queue
|
5
4
|
import sys
|
6
5
|
|
6
|
+
from loguru import logger
|
7
7
|
from oslo_config import cfg
|
8
8
|
|
9
9
|
from aprsd import conf
|
10
|
-
from aprsd.log import rich as aprsd_logging
|
11
10
|
|
12
11
|
|
13
12
|
CONF = cfg.CONF
|
@@ -15,75 +14,84 @@ LOG = logging.getLogger("APRSD")
|
|
15
14
|
logging_queue = queue.Queue()
|
16
15
|
|
17
16
|
|
17
|
+
class InterceptHandler(logging.Handler):
|
18
|
+
def emit(self, record):
|
19
|
+
# get corresponding Loguru level if it exists
|
20
|
+
try:
|
21
|
+
level = logger.level(record.levelname).name
|
22
|
+
except ValueError:
|
23
|
+
level = record.levelno
|
24
|
+
|
25
|
+
# find caller from where originated the logged message
|
26
|
+
frame, depth = sys._getframe(6), 6
|
27
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
28
|
+
frame = frame.f_back
|
29
|
+
depth += 1
|
30
|
+
|
31
|
+
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
32
|
+
|
33
|
+
|
18
34
|
# Setup the log faciility
|
19
35
|
# to disable log to stdout, but still log to file
|
20
36
|
# use the --quiet option on the cmdln
|
21
|
-
def setup_logging(loglevel, quiet):
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
37
|
+
def setup_logging(loglevel=None, quiet=False):
|
38
|
+
if not loglevel:
|
39
|
+
log_level = CONF.logging.log_level
|
40
|
+
else:
|
41
|
+
log_level = conf.log.LOG_LEVELS[loglevel]
|
42
|
+
|
43
|
+
# intercept everything at the root logger
|
44
|
+
logging.root.handlers = [InterceptHandler()]
|
45
|
+
logging.root.setLevel(log_level)
|
46
|
+
|
47
|
+
imap_list = [
|
48
|
+
"imapclient.imaplib", "imaplib", "imapclient",
|
49
|
+
"imapclient.util",
|
50
|
+
]
|
51
|
+
aprslib_list = [
|
52
|
+
"aprslib",
|
53
|
+
"aprslib.parsing",
|
54
|
+
"aprslib.exceptions",
|
55
|
+
]
|
56
|
+
|
57
|
+
# We don't really want to see the aprslib parsing debug output.
|
58
|
+
disable_list = imap_list + aprslib_list
|
59
|
+
|
60
|
+
# remove every other logger's handlers
|
61
|
+
# and propagate to root logger
|
62
|
+
for name in logging.root.manager.loggerDict.keys():
|
63
|
+
logging.getLogger(name).handlers = []
|
64
|
+
if name in disable_list:
|
65
|
+
logging.getLogger(name).propagate = False
|
66
|
+
else:
|
67
|
+
logging.getLogger(name).propagate = True
|
68
|
+
|
69
|
+
handlers = [
|
70
|
+
{
|
71
|
+
"sink": sys.stdout, "serialize": False,
|
72
|
+
"format": CONF.logging.logformat,
|
73
|
+
},
|
74
|
+
]
|
75
|
+
if CONF.logging.logfile:
|
76
|
+
handlers.append(
|
77
|
+
{
|
78
|
+
"sink": CONF.logging.logfile, "serialize": False,
|
79
|
+
"format": CONF.logging.logformat,
|
80
|
+
},
|
35
81
|
)
|
36
|
-
rh.setFormatter(log_formatter)
|
37
|
-
LOG.addHandler(rh)
|
38
|
-
rich_logging = True
|
39
82
|
|
40
|
-
log_file = CONF.logging.logfile
|
41
|
-
log_format = CONF.logging.logformat
|
42
|
-
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
43
|
-
|
44
|
-
if log_file:
|
45
|
-
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
46
|
-
fh.setFormatter(log_formatter)
|
47
|
-
LOG.addHandler(fh)
|
48
|
-
|
49
|
-
imap_logger = None
|
50
83
|
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
|
51
|
-
|
52
|
-
|
53
|
-
if rh:
|
54
|
-
imap_logger.addHandler(rh)
|
55
|
-
if fh:
|
56
|
-
imap_logger.addHandler(fh)
|
84
|
+
for name in imap_list:
|
85
|
+
logging.getLogger(name).propagate = True
|
57
86
|
|
58
87
|
if CONF.admin.web_enabled:
|
59
|
-
qh =
|
60
|
-
|
61
|
-
|
62
|
-
|
88
|
+
qh = QueueHandler(logging_queue)
|
89
|
+
handlers.append(
|
90
|
+
{
|
91
|
+
"sink": qh, "serialize": False,
|
92
|
+
"format": CONF.logging.logformat,
|
93
|
+
},
|
63
94
|
)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
if not quiet and not rich_logging:
|
68
|
-
sh = logging.StreamHandler(sys.stdout)
|
69
|
-
sh.setFormatter(log_formatter)
|
70
|
-
LOG.addHandler(sh)
|
71
|
-
if imap_logger:
|
72
|
-
imap_logger.addHandler(sh)
|
73
|
-
|
74
|
-
|
75
|
-
def setup_logging_no_config(loglevel, quiet):
|
76
|
-
log_level = conf.log.LOG_LEVELS[loglevel]
|
77
|
-
LOG.setLevel(log_level)
|
78
|
-
log_format = CONF.logging.logformat
|
79
|
-
date_format = CONF.logging.date_format
|
80
|
-
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
81
|
-
fh = NullHandler()
|
82
|
-
|
83
|
-
fh.setFormatter(log_formatter)
|
84
|
-
LOG.addHandler(fh)
|
85
|
-
|
86
|
-
if not quiet:
|
87
|
-
sh = logging.StreamHandler(sys.stdout)
|
88
|
-
sh.setFormatter(log_formatter)
|
89
|
-
LOG.addHandler(sh)
|
95
|
+
|
96
|
+
# configure loguru
|
97
|
+
logger.configure(handlers=handlers)
|
aprsd/main.py
CHANGED
@@ -59,20 +59,25 @@ click_completion.core.startswith = custom_startswith
|
|
59
59
|
click_completion.init()
|
60
60
|
|
61
61
|
|
62
|
-
@click.group(context_settings=CONTEXT_SETTINGS)
|
62
|
+
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
63
63
|
@click.version_option()
|
64
64
|
@click.pass_context
|
65
65
|
def cli(ctx):
|
66
66
|
pass
|
67
67
|
|
68
68
|
|
69
|
-
def
|
70
|
-
# First import all the possible commands for the CLI
|
71
|
-
# The commands themselves live in the cmds directory
|
69
|
+
def load_commands():
|
72
70
|
from .cmds import ( # noqa
|
73
71
|
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
74
72
|
send_message, server, webchat,
|
75
73
|
)
|
74
|
+
|
75
|
+
|
76
|
+
def main():
|
77
|
+
# First import all the possible commands for the CLI
|
78
|
+
# The commands themselves live in the cmds directory
|
79
|
+
load_commands()
|
80
|
+
utils.load_entry_points("aprsd.extension")
|
76
81
|
cli(auto_envvar_prefix="APRSD")
|
77
82
|
|
78
83
|
|
@@ -120,12 +125,7 @@ def sample_config(ctx):
|
|
120
125
|
def get_namespaces():
|
121
126
|
args = []
|
122
127
|
|
123
|
-
|
124
|
-
selected = []
|
125
|
-
if "oslo.config.opts" in all:
|
126
|
-
for x in all["oslo.config.opts"]:
|
127
|
-
if x.group == "oslo.config.opts":
|
128
|
-
selected.append(x)
|
128
|
+
selected = imp.entry_points(group="oslo.config.opts")
|
129
129
|
for entry in selected:
|
130
130
|
if "aprsd" in entry.name:
|
131
131
|
args.append("--namespace")
|
@@ -146,8 +146,8 @@ def sample_config(ctx):
|
|
146
146
|
raise SystemExit
|
147
147
|
raise
|
148
148
|
LOG.warning(conf.namespace)
|
149
|
-
return
|
150
149
|
generator.generate(conf)
|
150
|
+
return
|
151
151
|
|
152
152
|
|
153
153
|
@cli.command()
|
aprsd/packets/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from aprsd.packets.core import ( # noqa: F401
|
2
|
-
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
|
3
|
-
StatusPacket, WeatherPacket,
|
2
|
+
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
|
3
|
+
RejectPacket, StatusPacket, WeatherPacket,
|
4
4
|
)
|
5
5
|
from aprsd.packets.packet_list import PacketList # noqa: F401
|
6
6
|
from aprsd.packets.seen_list import SeenList # noqa: F401
|
aprsd/packets/core.py
CHANGED
@@ -148,7 +148,6 @@ class Packet(metaclass=abc.ABCMeta):
|
|
148
148
|
self.from_call,
|
149
149
|
self.payload,
|
150
150
|
)
|
151
|
-
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
|
152
151
|
|
153
152
|
@staticmethod
|
154
153
|
def factory(raw_packet):
|
@@ -466,6 +465,26 @@ class GPSPacket(Packet):
|
|
466
465
|
)
|
467
466
|
|
468
467
|
|
468
|
+
@dataclass(unsafe_hash=True)
|
469
|
+
class BeaconPacket(GPSPacket):
|
470
|
+
def _build_payload(self):
|
471
|
+
"""The payload is the non headers portion of the packet."""
|
472
|
+
time_zulu = self._build_time_zulu()
|
473
|
+
lat = self.convert_latitude(self.latitude)
|
474
|
+
long = self.convert_longitude(self.longitude)
|
475
|
+
|
476
|
+
self.payload = (
|
477
|
+
f"@{time_zulu}z{lat}{self.symbol_table}"
|
478
|
+
f"{long}{self.symbol}APRSD Beacon"
|
479
|
+
)
|
480
|
+
|
481
|
+
def _build_raw(self):
|
482
|
+
self.raw = (
|
483
|
+
f"{self.from_call}>APZ100:"
|
484
|
+
f"{self.payload}"
|
485
|
+
)
|
486
|
+
|
487
|
+
|
469
488
|
@dataclass
|
470
489
|
class MicEPacket(GPSPacket):
|
471
490
|
messagecapable: bool = False
|
aprsd/plugin_utils.py
CHANGED
aprsd/plugins/fortune.py
CHANGED
@@ -8,6 +8,8 @@ from aprsd.utils import trace
|
|
8
8
|
|
9
9
|
LOG = logging.getLogger("APRSD")
|
10
10
|
|
11
|
+
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
12
|
+
|
11
13
|
|
12
14
|
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
13
15
|
"""Fortune."""
|
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
|
19
21
|
fortune_path = None
|
20
22
|
|
21
23
|
def setup(self):
|
22
|
-
self.fortune_path = shutil.which(
|
24
|
+
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
|
25
|
+
LOG.info(f"Fortune path {self.fortune_path}")
|
23
26
|
if not self.fortune_path:
|
24
27
|
self.enabled = False
|
25
28
|
else:
|
aprsd/plugins/location.py
CHANGED
@@ -2,7 +2,8 @@ import logging
|
|
2
2
|
import re
|
3
3
|
import time
|
4
4
|
|
5
|
-
from geopy.geocoders import
|
5
|
+
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
|
6
|
+
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
|
6
7
|
from oslo_config import cfg
|
7
8
|
|
8
9
|
from aprsd import packets, plugin, plugin_utils
|
@@ -13,6 +14,82 @@ CONF = cfg.CONF
|
|
13
14
|
LOG = logging.getLogger("APRSD")
|
14
15
|
|
15
16
|
|
17
|
+
class UsLocation:
|
18
|
+
raw = {}
|
19
|
+
|
20
|
+
def __init__(self, info):
|
21
|
+
self.info = info
|
22
|
+
|
23
|
+
def __str__(self):
|
24
|
+
return self.info
|
25
|
+
|
26
|
+
|
27
|
+
class USGov:
|
28
|
+
"""US Government geocoder that uses the geopy API.
|
29
|
+
|
30
|
+
This is a dummy class the implements the geopy reverse API,
|
31
|
+
so the factory can return an object that conforms to the API.
|
32
|
+
"""
|
33
|
+
def reverse(self, coordinates):
|
34
|
+
"""Reverse geocode a coordinate."""
|
35
|
+
LOG.info(f"USGov reverse geocode {coordinates}")
|
36
|
+
coords = coordinates.split(",")
|
37
|
+
lat = float(coords[0])
|
38
|
+
lon = float(coords[1])
|
39
|
+
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
40
|
+
# LOG.info(f"WEATHER: {result}")
|
41
|
+
# LOG.info(f"area description {result['location']['areaDescription']}")
|
42
|
+
if 'location' in result:
|
43
|
+
loc = UsLocation(result['location']['areaDescription'])
|
44
|
+
else:
|
45
|
+
loc = UsLocation("Unknown Location")
|
46
|
+
|
47
|
+
LOG.info(f"USGov reverse geocode LOC {loc}")
|
48
|
+
return loc
|
49
|
+
|
50
|
+
|
51
|
+
def geopy_factory():
|
52
|
+
"""Factory function for geopy geocoders."""
|
53
|
+
geocoder = CONF.location_plugin.geopy_geocoder
|
54
|
+
LOG.info(f"Using geocoder: {geocoder}")
|
55
|
+
user_agent = CONF.location_plugin.user_agent
|
56
|
+
LOG.info(f"Using user_agent: {user_agent}")
|
57
|
+
|
58
|
+
if geocoder == "Nominatim":
|
59
|
+
return Nominatim(user_agent=user_agent)
|
60
|
+
elif geocoder == "USGov":
|
61
|
+
return USGov()
|
62
|
+
elif geocoder == "ArcGIS":
|
63
|
+
return ArcGIS(
|
64
|
+
username=CONF.location_plugin.arcgis_username,
|
65
|
+
password=CONF.location_plugin.arcgis_password,
|
66
|
+
user_agent=user_agent,
|
67
|
+
)
|
68
|
+
elif geocoder == "AzureMaps":
|
69
|
+
return AzureMaps(
|
70
|
+
user_agent=user_agent,
|
71
|
+
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
|
72
|
+
)
|
73
|
+
elif geocoder == "Baidu":
|
74
|
+
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
|
75
|
+
elif geocoder == "Bing":
|
76
|
+
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
|
77
|
+
elif geocoder == "GoogleV3":
|
78
|
+
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
|
79
|
+
elif geocoder == "HERE":
|
80
|
+
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
|
81
|
+
elif geocoder == "OpenCage":
|
82
|
+
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
|
83
|
+
elif geocoder == "TomTom":
|
84
|
+
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
|
85
|
+
elif geocoder == "What3Words":
|
86
|
+
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
|
87
|
+
elif geocoder == "Woosmap":
|
88
|
+
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
|
89
|
+
else:
|
90
|
+
raise ValueError(f"Unknown geocoder: {geocoder}")
|
91
|
+
|
92
|
+
|
16
93
|
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
17
94
|
"""Location!"""
|
18
95
|
|
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|
57
134
|
# Get some information about their location
|
58
135
|
try:
|
59
136
|
tic = time.perf_counter()
|
60
|
-
geolocator =
|
137
|
+
geolocator = geopy_factory()
|
138
|
+
LOG.info(f"Using GEOLOCATOR: {geolocator}")
|
61
139
|
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
62
140
|
location = geolocator.reverse(coordinates)
|
63
141
|
address = location.raw.get("address")
|
142
|
+
LOG.debug(f"GEOLOCATOR address: {address}")
|
64
143
|
toc = time.perf_counter()
|
65
144
|
if address:
|
66
145
|
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
67
|
-
|
68
|
-
|
146
|
+
if address.get("country_code") == "us":
|
147
|
+
area_info = f"{address.get('county')}, {address.get('state')}"
|
148
|
+
else:
|
149
|
+
# what to do for address for non US?
|
150
|
+
area_info = f"{address.get('country'), 'Unknown'}"
|
69
151
|
else:
|
70
|
-
|
71
|
-
area_info = f"{address.get('country'), 'Unknown'}"
|
152
|
+
area_info = str(location)
|
72
153
|
except Exception as ex:
|
154
|
+
LOG.error(ex)
|
73
155
|
LOG.error(f"Failed to fetch Geopy address {ex}")
|
74
156
|
area_info = "Unknown Location"
|
75
157
|
|
aprsd/plugins/weather.py
CHANGED
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|
26
26
|
"weather" - returns weather near the calling callsign
|
27
27
|
"""
|
28
28
|
|
29
|
-
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
29
|
+
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
30
|
+
command_regex = r"^[wW]"
|
31
|
+
|
30
32
|
command_name = "USWeather"
|
31
33
|
short_description = "Provide USA only weather of GPS Beacon location"
|
32
34
|
|
@@ -189,7 +191,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
189
191
|
|
190
192
|
"""
|
191
193
|
|
192
|
-
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
194
|
+
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
195
|
+
command_regex = r"^[wW]"
|
196
|
+
|
193
197
|
command_name = "OpenWeatherMap"
|
194
198
|
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
195
199
|
|
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
211
215
|
|
212
216
|
@trace.trace
|
213
217
|
def process(self, packet):
|
214
|
-
fromcall = packet.get("
|
218
|
+
fromcall = packet.get("from_call")
|
215
219
|
message = packet.get("message_text", None)
|
216
220
|
# ack = packet.get("msgNo", "0")
|
217
221
|
LOG.info(f"OWMWeather Plugin '{message}'")
|
aprsd/threads/__init__.py
CHANGED
@@ -4,7 +4,7 @@ import queue
|
|
4
4
|
# aprsd.threads
|
5
5
|
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
6
6
|
from .keep_alive import KeepAliveThread # noqa: F401
|
7
|
-
from .rx import APRSDRXThread # noqa: F401
|
7
|
+
from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread # noqa: F401
|
8
8
|
|
9
9
|
|
10
10
|
packet_queue = queue.Queue(maxsize=20)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
import requests
|
6
|
+
|
7
|
+
import aprsd
|
8
|
+
from aprsd import threads as aprsd_threads
|
9
|
+
|
10
|
+
|
11
|
+
CONF = cfg.CONF
|
12
|
+
LOG = logging.getLogger("APRSD")
|
13
|
+
|
14
|
+
|
15
|
+
class APRSRegistryThread(aprsd_threads.APRSDThread):
|
16
|
+
"""This sends service information to the configured APRS Registry."""
|
17
|
+
_loop_cnt: int = 1
|
18
|
+
|
19
|
+
def __init__(self):
|
20
|
+
super().__init__("APRSRegistryThread")
|
21
|
+
self._loop_cnt = 1
|
22
|
+
if not CONF.aprs_registry.enabled:
|
23
|
+
LOG.error(
|
24
|
+
"APRS Registry is not enabled. ",
|
25
|
+
)
|
26
|
+
LOG.error(
|
27
|
+
"APRS Registry thread is STOPPING.",
|
28
|
+
)
|
29
|
+
self.stop()
|
30
|
+
LOG.info(
|
31
|
+
"APRS Registry thread is running and will send "
|
32
|
+
f"info every {CONF.aprs_registry.frequency_seconds} seconds "
|
33
|
+
f"to {CONF.aprs_registry.registry_url}.",
|
34
|
+
)
|
35
|
+
|
36
|
+
def loop(self):
|
37
|
+
# Only call the registry every N seconds
|
38
|
+
if self._loop_cnt % CONF.aprs_registry.frequency_seconds == 0:
|
39
|
+
info = {
|
40
|
+
"callsign": CONF.callsign,
|
41
|
+
"description": CONF.aprs_registry.description,
|
42
|
+
"service_website": CONF.aprs_registry.service_website,
|
43
|
+
"software": f"APRSD version {aprsd.__version__} "
|
44
|
+
"https://github.com/craigerl/aprsd",
|
45
|
+
}
|
46
|
+
try:
|
47
|
+
requests.post(
|
48
|
+
f"{CONF.aprs_registry.registry_url}",
|
49
|
+
json=info,
|
50
|
+
)
|
51
|
+
except Exception as e:
|
52
|
+
LOG.error(f"Failed to send registry info: {e}")
|
53
|
+
|
54
|
+
time.sleep(1)
|
55
|
+
self._loop_cnt += 1
|
56
|
+
return True
|
aprsd/threads/rx.py
CHANGED
@@ -58,12 +58,13 @@ class APRSDRXThread(APRSDThread):
|
|
58
58
|
pass
|
59
59
|
|
60
60
|
|
61
|
-
class
|
61
|
+
class APRSDDupeRXThread(APRSDRXThread):
|
62
62
|
"""Process received packets.
|
63
63
|
|
64
64
|
This is the main APRSD Server command thread that
|
65
|
-
receives packets
|
66
|
-
|
65
|
+
receives packets and makes sure the packet
|
66
|
+
hasn't been seen previously before sending it on
|
67
|
+
to be processed.
|
67
68
|
"""
|
68
69
|
|
69
70
|
def process_packet(self, *args, **kwargs):
|
@@ -118,6 +119,13 @@ class APRSDPluginRXThread(APRSDRXThread):
|
|
118
119
|
self.packet_queue.put(packet)
|
119
120
|
|
120
121
|
|
122
|
+
class APRSDPluginRXThread(APRSDDupeRXThread):
|
123
|
+
""""Process received packets.
|
124
|
+
|
125
|
+
For backwards compatibility, we keep the APRSDPluginRXThread.
|
126
|
+
"""
|
127
|
+
|
128
|
+
|
121
129
|
class APRSDProcessPacketThread(APRSDThread):
|
122
130
|
"""Base class for processing received packets.
|
123
131
|
|
@@ -157,7 +165,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|
157
165
|
|
158
166
|
def process_packet(self, packet):
|
159
167
|
"""Process a packet received from aprs-is server."""
|
160
|
-
LOG.debug(f"
|
168
|
+
LOG.debug(f"ProcessPKT-LOOP {self._loop_cnt}")
|
161
169
|
our_call = CONF.callsign.lower()
|
162
170
|
|
163
171
|
from_call = packet.from_call
|
@@ -202,7 +210,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|
202
210
|
self.process_other_packet(
|
203
211
|
packet, for_us=(to_call.lower() == our_call),
|
204
212
|
)
|
205
|
-
LOG.debug("Packet processing complete")
|
213
|
+
LOG.debug(f"Packet processing complete for pkt '{packet.key}'")
|
206
214
|
return False
|
207
215
|
|
208
216
|
@abc.abstractmethod
|
aprsd/threads/tx.py
CHANGED
@@ -37,6 +37,7 @@ msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
|
37
37
|
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
38
38
|
|
39
39
|
|
40
|
+
@msg_throttle_decorator.sleep_and_retry
|
40
41
|
def send(packet: core.Packet, direct=False, aprs_client=None):
|
41
42
|
"""Send a packet either in a thread or directly to the client."""
|
42
43
|
# prepare the packet for sending.
|
@@ -194,3 +195,42 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
|
194
195
|
time.sleep(1)
|
195
196
|
self.loop_count += 1
|
196
197
|
return True
|
198
|
+
|
199
|
+
|
200
|
+
class BeaconSendThread(aprsd_threads.APRSDThread):
|
201
|
+
"""Thread that sends a GPS beacon packet periodically.
|
202
|
+
|
203
|
+
Settings are in the [DEFAULT] section of the config file.
|
204
|
+
"""
|
205
|
+
_loop_cnt: int = 1
|
206
|
+
|
207
|
+
def __init__(self):
|
208
|
+
super().__init__("BeaconSendThread")
|
209
|
+
self._loop_cnt = 1
|
210
|
+
# Make sure Latitude and Longitude are set.
|
211
|
+
if not CONF.latitude or not CONF.longitude:
|
212
|
+
LOG.error(
|
213
|
+
"Latitude and Longitude are not set in the config file."
|
214
|
+
"Beacon will not be sent and thread is STOPPED.",
|
215
|
+
)
|
216
|
+
self.stop()
|
217
|
+
LOG.info(
|
218
|
+
"Beacon thread is running and will send "
|
219
|
+
f"beacons every {CONF.beacon_interval} seconds.",
|
220
|
+
)
|
221
|
+
|
222
|
+
def loop(self):
|
223
|
+
# Only dump out the stats every N seconds
|
224
|
+
if self._loop_cnt % CONF.beacon_interval == 0:
|
225
|
+
pkt = core.BeaconPacket(
|
226
|
+
from_call=CONF.callsign,
|
227
|
+
to_call="APRS",
|
228
|
+
latitude=float(CONF.latitude),
|
229
|
+
longitude=float(CONF.longitude),
|
230
|
+
comment="APRSD GPS Beacon",
|
231
|
+
symbol=CONF.beacon_symbol,
|
232
|
+
)
|
233
|
+
send(pkt, direct=True)
|
234
|
+
self._loop_cnt += 1
|
235
|
+
time.sleep(1)
|
236
|
+
return True
|
aprsd/utils/__init__.py
CHANGED
@@ -4,6 +4,7 @@ import errno
|
|
4
4
|
import os
|
5
5
|
import re
|
6
6
|
import sys
|
7
|
+
import traceback
|
7
8
|
|
8
9
|
import update_checker
|
9
10
|
|
@@ -131,3 +132,21 @@ def parse_delta_str(s):
|
|
131
132
|
return {key: float(val) for key, val in m.groupdict().items()}
|
132
133
|
else:
|
133
134
|
return {}
|
135
|
+
|
136
|
+
|
137
|
+
def load_entry_points(group):
|
138
|
+
"""Load all extensions registered to the given entry point group"""
|
139
|
+
print(f"Loading extensions for group {group}")
|
140
|
+
try:
|
141
|
+
import importlib_metadata
|
142
|
+
except ImportError:
|
143
|
+
# For python 3.10 and later
|
144
|
+
import importlib.metadata as importlib_metadata
|
145
|
+
|
146
|
+
eps = importlib_metadata.entry_points(group=group)
|
147
|
+
for ep in eps:
|
148
|
+
try:
|
149
|
+
ep.load()
|
150
|
+
except Exception as e:
|
151
|
+
print(f"Extension {ep.name} of group {group} failed to load with {e}", file=sys.stderr)
|
152
|
+
print(traceback.format_exc(), file=sys.stderr)
|
aprsd/utils/objectstore.py
CHANGED
@@ -28,6 +28,9 @@ class ObjectStoreMixin:
|
|
28
28
|
def __len__(self):
|
29
29
|
return len(self.data)
|
30
30
|
|
31
|
+
def __iter__(self):
|
32
|
+
return iter(self.data)
|
33
|
+
|
31
34
|
def get_all(self):
|
32
35
|
with self.lock:
|
33
36
|
return self.data
|
@@ -96,11 +99,14 @@ class ObjectStoreMixin:
|
|
96
99
|
LOG.debug(
|
97
100
|
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
|
98
101
|
)
|
99
|
-
|
102
|
+
else:
|
103
|
+
LOG.debug(f"{self.__class__.__name__}::No data to load.")
|
100
104
|
except (pickle.UnpicklingError, Exception) as ex:
|
101
105
|
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
102
106
|
LOG.error(ex)
|
103
107
|
self.data = {}
|
108
|
+
else:
|
109
|
+
LOG.debug(f"{self.__class__.__name__}::No save file found.")
|
104
110
|
|
105
111
|
def flush(self):
|
106
112
|
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
@@ -0,0 +1,3 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
|
2
|
+
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
|
3
|
+
</svg>
|