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/log/log.py CHANGED
@@ -1,13 +1,12 @@
1
1
  import logging
2
- from logging import NullHandler
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
- log_level = conf.log.LOG_LEVELS[loglevel]
23
- LOG.setLevel(log_level)
24
- date_format = CONF.logging.date_format
25
- rh = None
26
- fh = None
27
-
28
- rich_logging = False
29
- if CONF.logging.get("rich_logging", False) and not quiet:
30
- log_format = "%(message)s"
31
- log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
32
- rh = aprsd_logging.APRSDRichHandler(
33
- show_thread=True, thread_width=20,
34
- rich_tracebacks=True, omit_repeated_times=False,
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
- imap_logger = logging.getLogger("imapclient.imaplib")
52
- imap_logger.setLevel(log_level)
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 = logging.handlers.QueueHandler(logging_queue)
60
- q_log_formatter = logging.Formatter(
61
- fmt=CONF.logging.logformat,
62
- datefmt=CONF.logging.date_format,
88
+ qh = QueueHandler(logging_queue)
89
+ handlers.append(
90
+ {
91
+ "sink": qh, "serialize": False,
92
+ "format": CONF.logging.logformat,
93
+ },
63
94
  )
64
- qh.setFormatter(q_log_formatter)
65
- LOG.addHandler(qh)
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 main():
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
- all = imp.entry_points()
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, RejectPacket,
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
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
76
76
  exclude,
77
77
  )
78
78
  )
79
+ LOG.debug(f"Fetching OWM weather '{url}'")
79
80
  response = requests.get(url)
80
81
  except Exception as e:
81
82
  LOG.error(e)
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("fortune")
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 Nominatim
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 = Nominatim(user_agent="APRSD")
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
- if address.get("country_code") == "us":
68
- area_info = f"{address.get('county')}, {address.get('state')}"
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
- # what to do for address for non US?
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("from")
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 APRSDPluginRXThread(APRSDRXThread):
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 from APRIS and then sends them for
66
- processing in the PluginProcessPacketThread.
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"RXPKT-LOOP {self._loop_cnt}")
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)
@@ -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
- LOG.debug(f"{self.data}")
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>