121 Commits
Author SHA1 Message Date
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 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 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 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 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 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
hemna 314f4da180 fix: Restore max_delta after custom stale_timeout test
The singleton's max_delta was being modified by test_init_custom_stale_timeout
and not restored, causing test_is_stale_connection_false to fail because
it expected 2 minutes but got 60 seconds.
2026-03-27 10:18:07 -04:00
hemna f6eb383caf fix: Update test to work with singleton pattern
The APRSISDriver uses @singleton decorator which transforms the class
into a function. The test was incorrectly trying to use __new__ which
doesn't work with decorated singletons. Instead, re-initialize the
existing instance after changing the config.
2026-03-27 10:13:55 -04:00
hemna 930339d4cf feat: Add configurable stale_timeout for APRS-IS connections
Add a new 'stale_timeout' configuration option to the aprs_network config
group that allows users to customize how long to wait before considering
an APRS-IS connection stale.

Problem:
The stale connection threshold was hardcoded to 2 minutes. In environments
with frequent network hiccups or when using certain APRS-IS servers that
may drop connections silently, 2 minutes can be too long to wait before
reconnecting, resulting in significant data loss.

Solution:
- Add 'stale_timeout' option to aprsd/conf/client.py with default of 120s
- Update APRSISDriver.__init__ to use the config value
- Maintain backward compatibility by defaulting to 120s if not configured
- Update tests to handle the new configuration option

Usage:
  [aprs_network]
  stale_timeout = 60  # Reconnect after 60 seconds without data

The default remains 120 seconds (2 minutes) for backward compatibility.
2026-03-27 10:05:44 -04:00
hemna bf258e4bcf chore(tests): fix unused variable linter warning in test_stats.py 2026-03-24 13:22:37 -04:00
hemna 85ebf8a274 refactor(threads): migrate TX threads to Event-based timing
- PacketSendSchedulerThread: Add daemon=False, replace time.sleep with self.wait
- AckSendSchedulerThread: Add daemon=False, replace time.sleep with self.wait
- SendPacketThread: Replace time.sleep with self.wait, remove manual loop_count
- SendAckThread: Replace time.sleep with self.wait, remove manual loop_count
- BeaconSendThread: Set self.period=CONF.beacon_interval, remove counter-based
  conditional, replace time.sleep with self.wait, remove _loop_cnt tracking
- Update tests to use new Event-based API
2026-03-24 13:22:37 -04:00
hemna bc9ce61e59 refactor(threads): migrate RX threads to Event-based timing
- APRSDRXThread: Replace time.sleep with self.wait for interruptible waits
- APRSDRXThread.stop(): Use _shutdown_event.set() instead of thread_stop
- APRSDRXThread: Error recovery waits check for shutdown signal
- APRSDFilterThread: Use queue timeout with self.period for interruptible wait
- Remove unused time import
- Update tests to use new Event-based API
2026-03-24 13:22:37 -04:00
hemna 343ec3e81c refactor(threads): migrate stats threads to Event-based timing 2026-03-24 13:22:37 -04:00
hemna 43ba69e352 feat(threads): add join_non_daemon() to APRSDThreadList
Allows graceful shutdown by waiting for non-daemon threads to complete
while allowing daemon threads to be terminated immediately.
2026-03-24 13:22:36 -04:00
hemna b7a37322e1 refactor(threads): add daemon, period, Event-based shutdown to APRSDThread
- Add daemon=True class attribute (subclasses override to False)
- Add period=1 class attribute for wait interval
- Replace thread_stop boolean with _shutdown_event (threading.Event)
- Add wait() method for interruptible sleeps
- Update tests for new Event-based API

BREAKING: thread_stop boolean replaced with _shutdown_event.
Code checking thread.thread_stop directly must use thread._shutdown_event.is_set()
2026-03-24 13:22:36 -04:00
hemna 6ea9889369 make consumer call signature consistent. 2026-02-18 14:11:25 -05:00
hemna 2b7e42802b update the keepalive for kiss 2026-02-18 14:00:52 -05:00
hemnaandClaude Sonnet 4.5 202c689658 Replace insecure pickle serialization with JSON
SECURITY FIX: Replace pickle.load() with json.load() to eliminate
remote code execution vulnerability from malicious pickle files.

Changes:
- Update ObjectStoreMixin to use JSON instead of pickle
- Add PacketJSONDecoder to reconstruct Packet objects from JSON
- Change file extension from .p to .json
- Add warning when old pickle files detected
- Add OrderedDict restoration for PacketList
- Update all tests to work with JSON format

Users with existing pickle files must run:
  aprsd dev migrate-pickle

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 16:07:55 -05:00
hemna c5ca4f11af Added new APRSDPushStatsThread
This allows an aprsd server instance to push it's to a remote
location.
2026-02-10 18:49:23 -05:00
hemna c62d0545c6 linting fix for test_tx 2026-02-05 17:04:57 -05:00
hemna 2180a52a9f added some mocks to speed up tests 2026-01-25 13:04:47 -05:00
hemna 3bcd03a514 Fix client / driver inconsistencies from protocol
The client registry defined a protocol that all drivers had
to implement.  This patch ensures that all methods are
consistent in the protocol definition.
2026-01-23 09:18:35 -05:00
hemna 24bc86424e Added --output-json for aprsd sample-config
This adds the ability to output the sample config
as json for non-human processing.
2026-01-21 15:45:48 -05:00
hemna f2bd594a89 Added owner_callsign
This adds a new option in the aprsd.conf [DEFAULT] section
that denotes who is the callsign that officially owns this APRSD
instance.  This will be used for sending the instance info to the
registry.  It's useful when the callsign used by the instance is
something useful on the APRS network, which isn't necessarily the
same as the person that owns it.
2026-01-18 23:54:43 -05:00
hemna cc8d834e5c Remove the login callsign in aprs_network
It's been confusing for a while that when we configured aprsd,
we had to enter the callsign in the [DEFAULT] section and
the [aprs_network] section.

This patch removes the login from the aprs_network section.  aprsd
will now use the main callsign in the [DEFAULT] section as the callsign
to login to the aprsis network.
2026-01-18 21:21:09 -05:00
hemna 2a8b7002f2 Added new TX Scheduler and pool.
This patch adds a new Send Packet scheduler and Ack Packet send
scheduler.  This prevents us from creating a new thread for each
packet that we send.
2026-01-16 23:38:46 -05:00
hemna ce9fc3757d updated tox.ini 2026-01-16 22:46:51 -05:00
hemna 274d5af0e9 Refactored RX thread to not parse packets
The Main RX Thread that runs the client.consumer() call used to
parse packets as soon as it got them.  This lead to an iffecient
strategy for listen and acquire packets as fast as possible.
The APRSDRXThread now gets the raw packet from the client and
shoves it on the packet_queue.  The other threads that are looking
for packets on the packet_queue will parse the raw packet with
aprslib.  This allows us to capture packets as quickly as we can,
and then process those packets in the secondary threads.
This prevents a bottleneck capturing packets.
2026-01-14 15:00:14 -05:00
hemna 0620e63e72 added more unit tests 2026-01-12 23:26:49 -05:00
hemna 6cbd6452d5 kiss consumer update
this patch updates the kiss consumer to call the callback with the frame
as arg[0] just like aprslib does.
2026-01-12 23:25:06 -05:00
hemna 26242f7d43 Added unit tests for log 2026-01-06 18:57:54 -05:00
hemna 1da92e52ef Added unit tests for packets.
Also did some code cleanup.
2026-01-05 17:00:03 -05:00
hemna f9979fa3da remove py310 testing 2025-12-29 20:49:54 -05:00
hemna 9ac881c56c Update WatchList and NotifySeenPlugin
The watchList was updating the last seen during RX time.
This happens before the NotifySeenPlugin even sees the packet,
so the callsign is never 'old'.  this patch fixes that, so the
watch list works.
2025-12-12 12:39:10 -05:00
hemna d0dfaa42e6 Added unit tests 2025-12-09 17:20:23 -05:00
hemna c34a82108b fixed tox failures 2025-11-26 20:28:25 -05:00
hemna 961d3e946a Fixed unit tests 2025-10-11 20:23:44 -04:00
hemna af0feaf9c8 Fixed some unit tests
Fixed unit tests related to the updated static method signatures
of the client and drivers.
2025-10-07 14:18:50 -04:00
hemna 1c39546bb9 Reworked the entire client and drivers
This patch includes a completely reworked client structure.
There is now only 1 client object, that loads the appropriate
drivers.  The drivers are fake, aprsis and tcpkiss.

The TCPKISS client was written from scratch to avoid using asyncio.
Asyncion is nothing but a pain in the ass.
2025-04-23 20:52:02 -04:00
hemna ec1adf4182 fixed list-plugins
This patch fixes the list-plugins and list-extensions.
Pypi changed their search page to require javascript, which
breaks python scripts....
2025-01-03 17:16:26 -05:00
hemna 72d068c0b8 Changed to ruff
This patch changes to the ruff linter.  SO MUCH quicker.
Removed grey and mypy as well.
2024-12-20 22:00:54 -05:00