1418 Commits
Author SHA1 Message Date
github-actions[bot] 8d709196b6 chore: update AUTHORS [skip ci] 2026-09-09 03:15:35 +00:00
hemna 007d98b9a4 feat: optionally piggyback APRS acknowledgements (#285)
* feat: add configurable APRS piggyback acknowledgements

* fix: keep ACKs immediate when piggybacking is disabled

* docs: update changelog for piggyback acknowledgements
5.0.1
2026-09-08 23:15:25 -04:00
github-actions[bot] 63c73e381b chore: update AUTHORS [skip ci] 2026-09-02 19:23:52 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c145aa7446 build(deps): bump pip from 26.1.2 to 26.2 (#282)
Bumps [pip](https://github.com/pypa/pip) from 26.1.2 to 26.2.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/26.1.2...26.2)

---
updated-dependencies:
- dependency-name: pip
  dependency-version: '26.2'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 15:23:44 -04:00
hemna d5fc5b2744 fix: remove obsolete pickle migration instruction (#283) 2026-09-02 15:23:01 -04:00
github-actions[bot] a8f3372cfd chore: update AUTHORS [skip ci] 2026-08-31 19:40:05 +00:00
hemna d6017b4e3a docs: build and navigate the complete changelog (#281) 2026-08-31 15:39:56 -04:00
github-actions[bot] 1d9bb41674 chore: update AUTHORS [skip ci] 2026-08-31 19:33:07 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4a7a75a2c4 build(deps): bump mistune from 0.8.4 to 3.3.0 (#279)
Bumps [mistune](https://github.com/lepture/mistune) from 0.8.4 to 3.3.0.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v0.8.4...v3.3.0)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 15:32:58 -04:00
hemna 1a13a80660 build: run docs build from project environment (#280) 2026-08-31 15:31:06 -04:00
hemna e9f1965881 build: use pyproject extras for documentation (#278) 2026-08-31 15:04:39 -04:00
hemna 078315d773 docs: generate changelog in docs source directory (#277)
* docs: update changelog for atomic object store writes

* docs: generate changelog in docs source directory

* build: install development dependencies from pyproject
2026-08-31 14:56:34 -04:00
hemna 1a37265d93 docs: update changelog for atomic object store writes (#276) 2026-08-31 14:29:45 -04:00
hemna 832e235c9b fix: write object stores atomically (#275) 2026-08-31 14:23:58 -04:00
hemna 1239426dc7 fix: clarify _send_packet()/_send_ack() scheduling contract (#263) (#273)
Replace misleading bare 'pass' with explicit comment explaining that
PacketTrack polling is handled by PacketSendSchedulerThread /
AckSendSchedulerThread.  Restructure condition to
'if not (scheduler and scheduler.is_alive()):' so the fallback
thread is only started when the scheduler is genuinely unavailable.

Closes #244
2026-08-28 16:11:36 -04:00
hemna 6e45e4f501 fix: pass lock arg to @wrapt.synchronized on pause_all/unpause_all (#267)
@wrapt.synchronized without an argument acquires the lock on the
wrapped object itself, not the class-level lock.  For APRSDThreadList
the intent is always to hold the shared class-level lock.

Pass the class-level lock to both decorators:
  @wrapt.synchronized  ->  @wrapt.synchronized(lock)

Closes #248
2026-08-28 15:59:42 -04:00
hemna 9a9704c720 fix: rename 'loging_queue' → 'logging_queue' in APRSDStats.stats() (#266)
The key in the stats dictionary was misspelled 'loging_queue' (single g).
Callers reading stats['logging_queue'] would get a KeyError.

Closes #247
2026-08-28 15:52:04 -04:00
hemna 71d0cf5031 fix: WeatherPacket.key returns None when both timestamps absent (#265)
The key property had no fallback return when neither raw_timestamp
nor wx_raw_timestamp was set, silently returning None.  This causes
KeyErrors when callers use the key as a dict key.

Add an explicit fallback: return self.from_call.

Closes #246
2026-08-28 15:48:02 -04:00
hemna d6ab5c91eb fix: rename RejectPacket.__post__init__ to __post_init__ (#264)
The method was silently ignored because __post__init__ (double underscores
in the middle) is not a dataclass lifecycle hook.  Rename to __post_init__
so the warning is emitted when response is set.

Closes #245
2026-08-28 15:16:20 -04:00
hemna a1c28bf46f fix: clarify _send_packet()/_send_ack() scheduling contract (#263)
Replace misleading bare 'pass' with explicit comment explaining that
PacketTrack polling is handled by PacketSendSchedulerThread /
AckSendSchedulerThread.  Restructure condition to
'if not (scheduler and scheduler.is_alive()):' so the fallback
thread is only started when the scheduler is genuinely unavailable.

Closes #244
2026-08-28 14:50:18 -04:00
hemna 08deaab94e fix: replace deprecated datetime.utcfromtimestamp()/utcnow() calls (#268)
Both APIs are deprecated since Python 3.12 and scheduled for removal.

- aprsd/packets/core.py: datetime.utcfromtimestamp(ts)
    -> datetime.fromtimestamp(ts, tz=timezone.utc)

- aprsd/plugins/time.py: pytz.datetime.datetime.utcnow()
    -> datetime.now(timezone.utc).replace(tzinfo=None)
  (naive UTC returned to preserve pytz.utc.localize() contract)

- tests/plugins/test_time.py: same fix in test setup

Closes #249
2026-08-28 14:41:36 -04:00
hemna 868cb8c3dc fix: remove mutable class-level data: list = [] from RingBuffer (#269)
The class-level annotation 'data: list = []' creates a single list shared
among all instances.  While __init__ already rebinds self.data = [], the
class-level default is confusing and fragile.

Remove the class-level default; move the annotation to __init__.

Closes #250
2026-08-28 14:36:59 -04:00
hemna 7a2ed79759 fix: remove dead-code Python version guard in utils/__init__.py (#270)
The guard:
    if sys.version_info.major == 3 and sys.version_info.minor >= 3:
        from collections.abc import MutableMapping
    else:
        from collections.abc import MutableMapping

has identical branches — both import from collections.abc (the correct
Python 3.3+ location).  Remove the guard; keep the bare import.

Closes #251
2026-08-28 14:23:58 -04:00
hemna 0063cc13c0 fix: enable ruff isort (I) ruleset in pyproject.toml (#271)
The 'I' (isort) ruleset was commented out. The codebase now has zero
isort violations so the ruleset can be enabled to enforce import order
going forward.

Closes #252
2026-08-28 14:19:36 -04:00
hemna f83154b6e9 fix: remove no-op self-assignment 'fromcall = fromcall' in USMetarPlugin (#272)
In the else-branch of USMetarPlugin.process(), 'fromcall = fromcall'
is a self-assignment that does nothing.  It was likely a leftover from
an edit that intended to reassign fromcall but forgot to write the RHS.
Remove the dead line.  The variable is still used correctly on the next
line (get_aprs_fi call).

Closes #253
2026-08-28 14:13:05 -04:00
hemna aa43f60728 fix: add reset() to @singleton decorator to allow test isolation (#262)
The @singleton decorator stored its instance in wrapper_singleton.instance
inside a closure. Tests could reset __new__-based singletons via
ClassName._instance = None, but @singleton classes had no equivalent reset
mechanism, causing state to leak between tests.

Add wrapper_singleton.reset() to every @singleton-decorated class. The
method clears wrapper_singleton.instance so the next call creates a fresh
instance, matching the __new__ singleton pattern.

Update all tests to call ClassName.reset() instead of manually setting
ClassName.instance = None (tests/client/test_client.py x5,
tests/client/test_registry.py x1).

Tests added (tests/utils/test_utils.py):
- test_singleton_has_reset: asserts reset is callable on the wrapper
- test_singleton_reset_clears_instance: verifies a new instance is created
  after reset(), not the cached one
- test_singleton_instance_is_none_before_first_call: verifies the instance
  lifecycle — None → populated → None after reset

Closes #240
2026-08-28 13:51:33 -04:00
hemna 28d0ac0e6b fix: initialise StatsStore.data = {} in __init__ (#260)
ObjectStoreMixin.save() calls len(self) -> len(self.data) immediately.
StatsStore never set self.data in __init__, so any call to save() before
add() raised AttributeError: 'StatsStore' object has no attribute 'data',
crashing APRSDStatsStoreThread on the first tick if the path where
enable_save is True is hit.

Tests updated/added (tests/threads/test_stats.py):
- test_init: now asserts data exists and equals {} (was asserting absence)
- test_save_before_add_does_not_raise: regression guard — save() must not
  raise AttributeError when called before add()

Closes #241
2026-08-28 13:45:43 -04:00
hemna ec1f221867 fix: PacketTrack.keys/items/values return list snapshots instead of live dict views (#261)
keys(), items(), values() each acquired self.lock then returned a live
dict view (dict_keys, dict_items, dict_values). The lock was released
on return, leaving callers with an unsynchronised view that races with
concurrent tx()/rx()/remove() calls on other threads.

Wrap each return in list() so a snapshot is taken while the lock is held.

Tests added (tests/packets/test_tracker.py):
- test_keys_returns_snapshot_not_view: asserts isinstance list and that
  a subsequent mutation does not appear in the returned value
- test_items_returns_snapshot_not_view: same for items()
- test_values_returns_snapshot_not_view: same for values()

Closes #242
2026-08-28 12:56:13 -04:00
hemna 16d497fd35 fix: validate _type against allowlist in factory() before globals() lookup (#259)
factory() was calling globals()[raw['_type']] on a value read from a
persisted JSON file on disk without validation, allowing an attacker who
can write to ~/.config/aprsd/ to reference arbitrary names in the module
global namespace.

Add an allowlist (_known_packet_type_names) derived lazily from
TYPE_LOOKUP. Any _type value not in the set raises ValueError before
globals() is ever called.

Tests added (tests/packets/test_packet.py):
- test_factory_known_type_roundtrip: valid known _type still deserialises
- test_factory_unknown_type_raises: module-global name ('os') is rejected
- test_factory_arbitrary_string_raises: arbitrary strings are rejected
- test_factory_empty_type_raises: empty string is rejected
- test_factory_allowlist_covers_all_type_lookup_classes: allowlist stays
  in sync with TYPE_LOOKUP automatically

Closes #239
2026-08-28 12:42:34 -04:00
hemna c975dd85d8 fix: WatchList.stats() — remove early return that bypassed lock (#258)
stats() had a bare 'return self.data' on the first line that:
- returned the raw internal dict without holding self.lock (race condition)
- made the locked loop below unreachable dead code
- returned the wrong shape (callers expect age/old/packet/last keys, not
  the raw was_old_before_update internal key)

Remove the early return so the existing with self.lock: loop executes.

Tests added (tests/packets/test_watch_list.py):
- test_stats_empty: empty watch list returns {}
- test_stats_returns_enriched_shape: verifies the four expected keys are
  present and the raw-internal 'was_old_before_update' key is absent
- test_stats_not_raw_internal_dict: returned dict must not be wl.data itself
- setUp/tearDown: reset class-level data and initialized to prevent leakage

Closes #237
2026-08-28 12:35:58 -04:00
hemna 3f505b6043 test: add unit tests for APRSDClient._checks initialisation (#238 follow-up) (#257)
- test_checks_initialised_to_false: asserts _checks is False right after
  __init__, catching any regression that removes the initialisation
- test_keepalive_check_first_call_no_reset: verifies no AttributeError and
  no spurious reset on the very first keepalive_check() call even when the
  driver is already dead
- test_keepalive_check: remove the manual _checks = False setup that masked
  the original bug; rely on the __init__ value instead
2026-08-28 12:28:42 -04:00
hemna f9d27208f0 fix: initialise APRSDClient._checks = False in __init__ (#254)
keepalive_check() reads self._checks on its first call but the
attribute was never set in __init__, causing AttributeError which
kills the KeepAliveThread on first invocation.

Closes #238
2026-08-28 12:23:52 -04:00
hemna 4ea9e33e20 fix: APRSISDriver.is_configured() returns False when driver is disabled (#236)
The final `return True` in is_configured() should be `return False`.
When APRS-IS is not enabled, the method always returned True, so the
startup guard in server.py never fired for misconfigured instances.

Closes #2
2026-08-28 12:10:50 -04:00
hemna 3a231264b1 fix: correct PacketList.tx() docstring from 'received' to 'transmitted' (#235)
Closes #21
2026-08-28 11:51:12 -04:00
hemna 7a81be06bb fix: add aprsd/~/ to .gitignore and remove stale local directory (#234)
The aprsd/~/ directory (unexpanded tilde) contained a stale pickle
save file at aprsd/~/.config/aprsd/listen/statsstore.p. Runtime data
should never be committed. Added explicit .gitignore entry to prevent
accidental future commits.

Closes #17
2026-08-28 11:44:20 -04:00
hemna 4642035306 fix: rename 'list' variable in HelpPlugin to avoid shadowing Python builtin (#233)
* fix: rename 'list' variable in HelpPlugin to avoid shadowing Python builtin

Closes #15

aprsd/plugin.py HelpPlugin.process() used 'list' as a local variable name,
shadowing the Python built-in list type. Renamed to 'plugin_names'.

* ci: fix CI workflow for Forgejo self-hosted runners

- Switch runs-on from ubuntu-latest to docker (node:20-bookworm)
  ubuntu-latest runner has no Node.js so actions/checkout@v4 fails
- Replace actions/setup-python@v5 (broken on self-hosted) with uv
- Use tox-uv instead of pip-installed tox for faster installs

* ci: use catthehacker/ubuntu:act-latest container (matches haminfo working CI)

* ci: fix master-build.yml for Forgejo self-hosted runners

Use catthehacker/ubuntu:act-latest container + uv for tox,
matching the working pattern from haminfo. Removes broken
actions/setup-python@v2 + ubuntu-latest bare runner combo.

* ci: revert workflow changes - Forgejo-specific, not for GitHub Actions

* ci: restrict master-build to master branch and tags only

The Docker build job clones from GitHub by branch name, which fails
for feature branches (sanitized slash → name mismatch). This workflow
should only run on master pushes and version tags, not on PRs.

* docs: add unreleased changelog entries for PR #233

* docs: update changelog for 5.0.1 release
2026-08-28 11:18:26 -04:00
hemna dcd1294b2f docs: add 5.0.0 changelog entry 5.0.0 2026-06-11 10:26:52 -04:00
hemna 77201f1e3d fix: upgrade uv.lock to resolve 9 Dependabot security alerts
Packages upgraded:
- filelock 3.20.0 -> 3.29.3 (TOCTOU symlink vulnerabilities)
- pip 25.3 -> 26.1.2 (path traversal, untrusted control sphere)
- pytest 9.0.2 -> 9.0.3 (vulnerable tmpdir handling)
- uv 0.11.6 -> 0.11.20 (arbitrary file write via entry points)
- virtualenv 20.35.4 -> 21.4.2 (TOCTOU in directory creation)
- wheel 0.45.1 -> 0.47.0 (arbitrary file permission modification)
2026-06-11 10:24:10 -04:00
hemna 4f6824f671 chore(deps): upgrade all pinned dependencies to latest (#229)
Key security upgrades:
- urllib3: 2.6.3 → 2.7.0 (fixes CVE-2026-decompression bypass)
- requests: 2.32.5 → 2.34.2 (fixes CVE-2026-25645 insecure temp file)
- idna: 3.11 → 3.18 (fixes CVE-2026-45409 crafted input DoS)
- pygments: 2.19.2 → 2.20.0 (fixes CVE-2026-4539 ReDoS)

Also bumps: attrs, bitarray, certifi, charset-normalizer, click,
importlib-metadata, markdown-it-py, oslo-config, oslo-i18n, packaging,
pytz, rich, stevedore, update-checker, wrapt, zipp
2026-06-10 15:58:05 -04:00
hemna d06390add8 Merge pull request #228 from craigerl/fix/dupe-filter-processed-flag
Fix DupePacketFilter checking wrong packet's processed flag
2026-06-02 15:47:38 -04:00
hemna db3a0428c0 Fix DupePacketFilter checking wrong packet's processed flag
DupePacketFilter.filter() was checking packet.processed (the newly
arrived duplicate) instead of found.processed (the previously stored
packet). Since all freshly decoded packets have processed=False by
default, the dupe detection branch was never reachable — every
duplicate message was passed through and re-processed.

This caused KM6LYW's retransmit of msg:9028 (via a different
digipeater path 63s after first receipt) to trigger NearestPlugin
a second time, sending 4 reply packets instead of 2 and resetting
the AckPacket retry counter back to (1 of 3).

Fix: check found.processed instead of packet.processed.
Update existing tests to reflect the correct variable under test.
Add regression test test_filter_aprs_retransmit_via_different_digi
that reproduces the exact production scenario.
2026-06-02 11:17:24 -04:00
hemna b064ac97a4 feat: add APRS Chat bulletin script 2026-05-21 14:52:03 -04:00
hemna 5cc918e5c2 fix: add socket lock to prevent TX/RX race causing stream corruption on retransmits
The RX reader thread sets setblocking(0) and the TX writer (via aprslib
sendall) sets setblocking(1) on the same socket without synchronization.
This race condition causes partial writes where other stations' APRS-IS
stream data gets concatenated onto retransmitted packets.

Add a shared _socket_lock between send() and _socket_readlines() so the
socket blocking mode is never changed by one thread while the other is
mid-operation.
2026-05-14 16:55:57 -04:00
hemna 9146ff76c9 fix: remove stale .client attribute access in send_message command
APRSDClient no longer has a .client property after the driver refactor
(commit 1c39546). Instantiating APRSDClient() is sufficient to trigger
connection via auto_connect=True.
2026-05-13 23:17:38 -04:00
hemna 0c515d45fe fix: filter stale BeaconPackets from PacketTrack on load from disk
Older versions persisted BeaconPackets to packettrack.json. On restart
these zombie beacons would be retransmitted by the scheduler. Now
PacketTrack.load() strips any BeaconPackets from the persisted data.

Workaround: delete ~/.config/aprsd/packettrack.json before restarting.
2026-05-13 12:14:21 -04:00
hemna d3281cff0b test: add tests for beacon/ack flood prevention and scheduler timing guards
Tests cover:
- BeaconPacket skipped in PacketTrack.tx() (fire-and-forget)
- AckPacket send_count not reset when same ack already tracked
- Heavy traffic scenario with 5 digi paths for same message
- Scheduler timing guards prevent threadpool race conditions
- Scheduler cleanup of max-retry packets
- MessagePacket still allows re-send (existing behavior preserved)
2026-05-13 11:44:49 -04:00
hemna d8134c4531 fix: prevent beacon and ack packet floods from PacketTrack retries
BeaconPackets are now skipped in PacketTrack — they are fire-and-forget
and never receive an ack, so tracking them caused the scheduler to
re-transmit them as unwanted duplicates.

AckPackets already being tracked are no longer reset when the same
message arrives via multiple digipeater paths, which was restarting
the retry counter and flooding RF with duplicate acks.

Added timing guards in both scheduler loops to prevent threadpool race
conditions where multiple workers could fire before send_count was
incremented.
2026-05-13 11:36:25 -04:00
hemna 3a47571b60 Merge pull request #224 from craigerl/dependabot/uv/uv-0.11.6
Bump uv from 0.9.26 to 0.11.6
2026-04-29 09:16:53 -04:00
dependabot[bot] 4ecdec0aa4 Bump uv from 0.9.26 to 0.11.6
Bumps [uv](https://github.com/astral-sh/uv) from 0.9.26 to 0.11.6.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.26...0.11.6)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.11.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 19:49:05 +00:00
hemna 490ff41cdc feat: Add configurable stats_store_interval for stats file saves
Add stats_store_interval config option to control how frequently
the statsstore.json file is written to disk. Default remains 10
seconds for backward compatibility.

This allows reducing disk I/O in production deployments and
can help avoid potential file corruption issues when external
processes read the stats file.
2026-03-27 17:37:16 -04:00