diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py
index 7c6654e..1dedd76 100644
--- a/aprsd/cmds/listen.py
+++ b/aprsd/cmds/listen.py
@@ -84,6 +84,7 @@ class ListenStatsThread(APRSDThread):
super().__init__('PacketStatsLog')
self._last_total_rx = 0
self.period = 31
+ self.start_time = time.time()
def loop(self):
if self.loop_count % self.period == 0:
@@ -95,6 +96,24 @@ class ListenStatsThread(APRSDThread):
rx_delta = total_rx - self._last_total_rx
rate = rx_delta / self.period
+ # Get unique callsigns count from packets' from_call field
+ unique_callsigns = set()
+ if 'packets' in stats and stats['packets']:
+ for packet in stats['packets']:
+ # Handle both Packet objects and dicts (if serializable)
+ if hasattr(packet, 'from_call'):
+ if packet.from_call:
+ unique_callsigns.add(packet.from_call)
+ elif isinstance(packet, dict) and 'from_call' in packet:
+ if packet['from_call']:
+ unique_callsigns.add(packet['from_call'])
+ unique_callsigns_count = len(unique_callsigns)
+
+ # Calculate uptime
+ elapsed = time.time() - self.start_time
+ elapsed_minutes = elapsed / 60
+ elapsed_hours = elapsed / 3600
+
# Log summary stats
LOGU.opt(colors=True).info(
f'RX Rate: {rate:.2f} pps '
@@ -102,14 +121,33 @@ class ListenStatsThread(APRSDThread):
f'RX Last {self.period} secs: {rx_delta} '
f'Packets in PacketListStats: {packet_count}',
)
+ LOGU.opt(colors=True).info(
+ f'Uptime: {elapsed:.0f}s ({elapsed_minutes:.1f}m / {elapsed_hours:.2f}h) '
+ f'Unique Callsigns: {unique_callsigns_count}',
+ )
self._last_total_rx = total_rx
- # Log individual type stats
- for k, v in stats['types'].items():
- thread_hex = f'fg {utils.hex_from_name(k)}'
+ # Log individual type stats, sorted by RX count (descending)
+ sorted_types = sorted(
+ stats['types'].items(), key=lambda x: x[1]['rx'], reverse=True
+ )
+ for k, v in sorted_types:
+ # Calculate percentage of this packet type compared to total RX
+ percentage = (v['rx'] / total_rx * 100) if total_rx > 0 else 0.0
+ # Format values first, then apply colors
+ packet_type_str = f'{k:<15}'
+ rx_count_str = f'{v["rx"]:6d}'
+ tx_count_str = f'{v["tx"]:6d}'
+ percentage_str = f'{percentage:5.1f}%'
+ # Use different colors for RX count based on threshold (matching mqtt_injest.py)
+ rx_color_tag = (
+ 'green' if v['rx'] > 100 else 'yellow' if v['rx'] > 10 else 'red'
+ )
LOGU.opt(colors=True).info(
- f'<{thread_hex}>{k:<15}{thread_hex}> '
- f'RX: {v["rx"]} TX: {v["tx"]}',
+ f' {packet_type_str}: '
+ f'<{rx_color_tag}>RX: {rx_count_str}{rx_color_tag}> '
+ f'TX: {tx_count_str} '
+ f'({percentage_str})',
)
time.sleep(1)
@@ -305,7 +343,10 @@ def listen(
)
LOG.debug('Start APRSDListenProcessThread')
listen_thread.start()
+
+ LOG.debug(f'enable_packet_stats: {enable_packet_stats}')
if enable_packet_stats:
+ LOG.debug('Start ListenStatsThread')
listen_stats = ListenStatsThread()
listen_stats.start()
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 39fa6da..fa49431 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,7 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements-dev.in -o requirements-dev.txt
build==1.3.0 # via pip-tools, -r requirements-dev.in
-cachetools==6.2.2 # via tox
+cachetools==6.2.4 # via tox
cfgv==3.5.0 # via pre-commit
chardet==5.2.0 # via tox
click==8.3.1 # via pip-tools
@@ -20,6 +20,8 @@ pyproject-api==1.10.0 # via tox
pyproject-hooks==1.2.0 # via build, pip-tools
pyyaml==6.0.3 # via pre-commit
setuptools==80.9.0 # via pip-tools
+tomli==2.3.0 # via build, pip-tools, pyproject-api, tox
tox==4.32.0 # via -r requirements-dev.in
+typing-extensions==4.15.0 # via tox, virtualenv
virtualenv==20.35.4 # via pre-commit, tox
wheel==0.45.1 # via pip-tools, -r requirements-dev.in
diff --git a/requirements.txt b/requirements.txt
index 9002e81..63f113e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -40,6 +40,6 @@ typing-extensions==4.15.0 # via typing-inspect
typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.3.1 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in
-urllib3==2.6.1 # via requests
+urllib3==2.6.2 # via requests
wrapt==2.0.1 # via -r requirements.in
zipp==3.23.0 # via importlib-metadata
diff --git a/uv.lock b/uv.lock
index 8e575d0..f8fa464 100644
--- a/uv.lock
+++ b/uv.lock
@@ -38,7 +38,6 @@ dependencies = [
{ name = "rfc3986" },
{ name = "rich" },
{ name = "rush" },
- { name = "setuptools" },
{ name = "stevedore" },
{ name = "thesmuggler" },
{ name = "timeago" },
@@ -64,7 +63,6 @@ dev = [
{ name = "identify" },
{ name = "nodeenv" },
{ name = "packaging" },
- { name = "pip" },
{ name = "pip-tools" },
{ name = "platformdirs" },
{ name = "pluggy" },
@@ -72,8 +70,9 @@ dev = [
{ name = "pyproject-api" },
{ name = "pyproject-hooks" },
{ name = "pyyaml" },
- { name = "setuptools" },
+ { name = "tomli" },
{ name = "tox" },
+ { name = "typing-extensions" },
{ name = "virtualenv" },
{ name = "wheel" },
]
@@ -85,7 +84,7 @@ requires-dist = [
{ name = "ax253", specifier = "==0.1.5.post1" },
{ name = "bitarray", specifier = "==3.8.0" },
{ name = "build", marker = "extra == 'dev'", specifier = "==1.3.0" },
- { name = "cachetools", marker = "extra == 'dev'", specifier = "==6.2.2" },
+ { name = "cachetools", marker = "extra == 'dev'", specifier = "==6.2.4" },
{ name = "certifi", specifier = "==2025.11.12" },
{ name = "cfgv", marker = "extra == 'dev'", specifier = "==3.5.0" },
{ name = "chardet", marker = "extra == 'dev'", specifier = "==5.2.0" },
@@ -113,7 +112,6 @@ requires-dist = [
{ name = "packaging", specifier = "==25.0" },
{ name = "packaging", marker = "extra == 'dev'", specifier = "==25.0" },
{ name = "pbr", specifier = "==7.0.3" },
- { name = "pip", marker = "extra == 'dev'", specifier = "==25.3" },
{ name = "pip-tools", marker = "extra == 'dev'", specifier = "==7.5.2" },
{ name = "platformdirs", marker = "extra == 'dev'", specifier = "==4.5.1" },
{ name = "pluggy", specifier = "==1.6.0" },
@@ -131,17 +129,17 @@ requires-dist = [
{ name = "rfc3986", specifier = "==2.0.0" },
{ name = "rich", specifier = "==14.2.0" },
{ name = "rush", specifier = "==2021.4.0" },
- { name = "setuptools", specifier = "==80.9.0" },
- { name = "setuptools", marker = "extra == 'dev'", specifier = "==80.9.0" },
{ name = "stevedore", specifier = "==5.6.0" },
{ name = "thesmuggler", specifier = "==1.0.1" },
{ name = "timeago", specifier = "==1.0.16" },
+ { name = "tomli", marker = "extra == 'dev'", specifier = "==2.3.0" },
{ name = "tox", marker = "extra == 'dev'", specifier = "==4.32.0" },
{ name = "typing-extensions", specifier = "==4.15.0" },
+ { name = "typing-extensions", marker = "extra == 'dev'", specifier = "==4.15.0" },
{ name = "typing-inspect", specifier = "==0.9.0" },
{ name = "tzlocal", specifier = "==5.3.1" },
{ name = "update-checker", specifier = "==0.18.0" },
- { name = "urllib3", specifier = "==2.6.1" },
+ { name = "urllib3", specifier = "==2.6.2" },
{ name = "virtualenv", marker = "extra == 'dev'", specifier = "==20.35.4" },
{ name = "wheel", marker = "extra == 'dev'", specifier = "==0.45.1" },
{ name = "wrapt", specifier = "==2.0.1" },
@@ -285,11 +283,11 @@ wheels = [
[[package]]
name = "cachetools"
-version = "6.2.2"
+version = "6.2.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" },
]
[[package]]
@@ -1036,11 +1034,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.6.1"
+version = "2.6.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", size = 432678, upload-time = "2025-12-08T15:25:26.773Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
]
[[package]]