kestrel-pi 0.3.0__tar.gz
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.
- kestrel_pi-0.3.0/PKG-INFO +25 -0
- kestrel_pi-0.3.0/README.md +17 -0
- kestrel_pi-0.3.0/kestrel_pi/__init__.py +27 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/__init__.py +94 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/controller.py +1109 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/env.py +245 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/features.py +256 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/metrics.py +250 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/policies.py +283 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/readout.py +159 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/spec.py +253 -0
- kestrel_pi-0.3.0/kestrel_pi/agent/train.py +515 -0
- kestrel_pi-0.3.0/kestrel_pi/applyclient.py +40 -0
- kestrel_pi-0.3.0/kestrel_pi/capture.py +281 -0
- kestrel_pi-0.3.0/kestrel_pi/config.py +66 -0
- kestrel_pi-0.3.0/kestrel_pi/config_catalog.py +312 -0
- kestrel_pi-0.3.0/kestrel_pi/connectivity.py +220 -0
- kestrel_pi-0.3.0/kestrel_pi/context.py +151 -0
- kestrel_pi-0.3.0/kestrel_pi/daemon.py +577 -0
- kestrel_pi-0.3.0/kestrel_pi/dispatch.py +129 -0
- kestrel_pi-0.3.0/kestrel_pi/loader.py +374 -0
- kestrel_pi-0.3.0/kestrel_pi/options.py +81 -0
- kestrel_pi-0.3.0/kestrel_pi/plugins.py +259 -0
- kestrel_pi-0.3.0/kestrel_pi/ui.py +194 -0
- kestrel_pi-0.3.0/kestrel_pi/userguide.py +771 -0
- kestrel_pi-0.3.0/kestrel_pi/vendor/__init__.py +7 -0
- kestrel_pi-0.3.0/kestrel_pi/vendor/waveshare_epd/__init__.py +11 -0
- kestrel_pi-0.3.0/kestrel_pi/vendor/waveshare_epd/epd2in7.py +527 -0
- kestrel_pi-0.3.0/kestrel_pi/vendor/waveshare_epd/epd2in7_V2.py +519 -0
- kestrel_pi-0.3.0/kestrel_pi/vendor/waveshare_epd/epdconfig.py +322 -0
- kestrel_pi-0.3.0/kestrel_pi/web.py +3106 -0
- kestrel_pi-0.3.0/kestrel_pi.egg-info/PKG-INFO +25 -0
- kestrel_pi-0.3.0/kestrel_pi.egg-info/SOURCES.txt +62 -0
- kestrel_pi-0.3.0/kestrel_pi.egg-info/dependency_links.txt +1 -0
- kestrel_pi-0.3.0/kestrel_pi.egg-info/entry_points.txt +4 -0
- kestrel_pi-0.3.0/kestrel_pi.egg-info/top_level.txt +1 -0
- kestrel_pi-0.3.0/pyproject.toml +27 -0
- kestrel_pi-0.3.0/setup.cfg +4 -0
- kestrel_pi-0.3.0/tests/test_agent_capture_gate.py +207 -0
- kestrel_pi-0.3.0/tests/test_agent_env.py +215 -0
- kestrel_pi-0.3.0/tests/test_agent_features.py +108 -0
- kestrel_pi-0.3.0/tests/test_agent_metrics.py +175 -0
- kestrel_pi-0.3.0/tests/test_agent_policies.py +189 -0
- kestrel_pi-0.3.0/tests/test_agent_readout.py +93 -0
- kestrel_pi-0.3.0/tests/test_agent_spec.py +156 -0
- kestrel_pi-0.3.0/tests/test_agent_train_reward.py +137 -0
- kestrel_pi-0.3.0/tests/test_agent_training.py +496 -0
- kestrel_pi-0.3.0/tests/test_capture.py +175 -0
- kestrel_pi-0.3.0/tests/test_capture_collect.py +135 -0
- kestrel_pi-0.3.0/tests/test_config_catalog.py +65 -0
- kestrel_pi-0.3.0/tests/test_config_set.py +195 -0
- kestrel_pi-0.3.0/tests/test_connectivity.py +134 -0
- kestrel_pi-0.3.0/tests/test_context.py +82 -0
- kestrel_pi-0.3.0/tests/test_daemon.py +169 -0
- kestrel_pi-0.3.0/tests/test_dispatch.py +103 -0
- kestrel_pi-0.3.0/tests/test_eink.py +607 -0
- kestrel_pi-0.3.0/tests/test_loader.py +264 -0
- kestrel_pi-0.3.0/tests/test_options.py +35 -0
- kestrel_pi-0.3.0/tests/test_plugins_apply.py +980 -0
- kestrel_pi-0.3.0/tests/test_reference_plugins.py +2262 -0
- kestrel_pi-0.3.0/tests/test_ui.py +70 -0
- kestrel_pi-0.3.0/tests/test_update_apply.py +360 -0
- kestrel_pi-0.3.0/tests/test_updates.py +566 -0
- kestrel_pi-0.3.0/tests/test_web.py +1938 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kestrel-pi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Kestrel plugin host daemon and plugin framework (Phase 3)
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# kestrel_pi
|
|
10
|
+
|
|
11
|
+
The Phase 3 plugin host for kestrel: a small daemon
|
|
12
|
+
(`kestrel-daemon`) that loads plugins, dispatches the legacy-compatible
|
|
13
|
+
`on_*` hooks off its event loop, sources events from the Phase 1 connectivity
|
|
14
|
+
and the Phase 2 capture store, and serves an embedded Flask status page on the
|
|
15
|
+
management link.
|
|
16
|
+
|
|
17
|
+
This package is installed into the `pi` user's virtualenv (`/home/pi/.venv`) by
|
|
18
|
+
the image build. The reference plugins ship separately under
|
|
19
|
+
`/usr/local/lib/kestrel/plugins/` and operator plugins live in
|
|
20
|
+
`/etc/kestrel/plugins/`.
|
|
21
|
+
|
|
22
|
+
See `docs/design/phase-3-plugins.md` in the repository for the design, and the
|
|
23
|
+
`Plugin` base class in `kestrel_pi/plugins.py` for the API.
|
|
24
|
+
|
|
25
|
+
Run the tests with `pytest` from this directory (pytest is already in the venv).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# kestrel_pi
|
|
2
|
+
|
|
3
|
+
The Phase 3 plugin host for kestrel: a small daemon
|
|
4
|
+
(`kestrel-daemon`) that loads plugins, dispatches the legacy-compatible
|
|
5
|
+
`on_*` hooks off its event loop, sources events from the Phase 1 connectivity
|
|
6
|
+
and the Phase 2 capture store, and serves an embedded Flask status page on the
|
|
7
|
+
management link.
|
|
8
|
+
|
|
9
|
+
This package is installed into the `pi` user's virtualenv (`/home/pi/.venv`) by
|
|
10
|
+
the image build. The reference plugins ship separately under
|
|
11
|
+
`/usr/local/lib/kestrel/plugins/` and operator plugins live in
|
|
12
|
+
`/etc/kestrel/plugins/`.
|
|
13
|
+
|
|
14
|
+
See `docs/design/phase-3-plugins.md` in the repository for the design, and the
|
|
15
|
+
`Plugin` base class in `kestrel_pi/plugins.py` for the API.
|
|
16
|
+
|
|
17
|
+
Run the tests with `pytest` from this directory (pytest is already in the venv).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""kestrel plugin host daemon and plugin framework (Phase 3).
|
|
2
|
+
|
|
3
|
+
The public plugin API is re-exported here so plugins can write either
|
|
4
|
+
|
|
5
|
+
from kestrel_pi import Plugin
|
|
6
|
+
|
|
7
|
+
or the fully-qualified
|
|
8
|
+
|
|
9
|
+
from kestrel_pi.plugins import Plugin
|
|
10
|
+
|
|
11
|
+
Both are the same class. ``HOOK_API_VERSION`` is the daemon's current hook-API
|
|
12
|
+
version; a plugin declares the version it targets with ``__target_api__`` and
|
|
13
|
+
the loader compares the two (improvement 4).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.3.0"
|
|
17
|
+
|
|
18
|
+
# Bump this on any breaking change to a dispatched hook's signature. Phase 5
|
|
19
|
+
# gives ``on_ui_update`` its real second argument - a read-only device/agent
|
|
20
|
+
# state snapshot (:class:`~kestrel_pi.ui.UIState`) - now that a physical
|
|
21
|
+
# e-ink renderer exists, so this is 2. A plugin written for the new signature
|
|
22
|
+
# declares ``__target_api__ = 2``; one still pinned to 1 loads with a warning.
|
|
23
|
+
HOOK_API_VERSION = 2
|
|
24
|
+
|
|
25
|
+
from .plugins import CAPABILITIES, Plugin # noqa: E402 (re-export after constants)
|
|
26
|
+
|
|
27
|
+
__all__ = ["Plugin", "CAPABILITIES", "HOOK_API_VERSION", "__version__"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""The Phase 4 reinforcement-learning capture agent.
|
|
2
|
+
|
|
3
|
+
This subpackage is the learning agent that drives capture and learns which
|
|
4
|
+
channels, dwells and attack postures yield handshakes, replacing the Phase 2
|
|
5
|
+
scripted sweep with a policy that adapts to the RF environment. It reads its
|
|
6
|
+
observations and issues its control through the Phase 2 capture interface
|
|
7
|
+
(``docs/developer/capture-interface.md``, ``contract_version 1``) and never patches
|
|
8
|
+
AngryOxide, so it only ever runs on the capture dongle when capture is enabled.
|
|
9
|
+
|
|
10
|
+
Layout (see ``docs/design/phase-4-agent.md``):
|
|
11
|
+
|
|
12
|
+
- :mod:`.spec` - the *fixed* env contract: observation shape, the flattened
|
|
13
|
+
``Discrete`` action vocabulary (posture x dwell x transmit) and its bijective
|
|
14
|
+
decode, the reward, and channel ranking. Pure stdlib + numpy; no torch/gym,
|
|
15
|
+
no hardware, so it is the unit-test seam and the thing a model is bound to.
|
|
16
|
+
- :mod:`.features` - builds the observation vector from the capture status and
|
|
17
|
+
the append-only store delta (the stable Output surface).
|
|
18
|
+
- :mod:`.env` - the Gymnasium environment wrapping the capture interface.
|
|
19
|
+
- :mod:`.policies` - the config-selected SB3 algorithm map and the scripted
|
|
20
|
+
baseline policy (both load against the same env spaces).
|
|
21
|
+
- :mod:`.controller` - the agent process: the decide loop, the status file and
|
|
22
|
+
the episode logs. ``kestrel-agent.service`` runs it.
|
|
23
|
+
- :mod:`.metrics` - the agent-vs-sweep comparison, derived from the episode log.
|
|
24
|
+
- :mod:`.train` - offline training: replay the episode logs through SB3 into an
|
|
25
|
+
updated ``model.zip``.
|
|
26
|
+
- :mod:`.readout` - stdlib-only reader of the agent's ``status.json`` for the
|
|
27
|
+
daemon's dashboard card (never imports torch/gym, so the benign daemon can
|
|
28
|
+
import it with the heavy ML stack absent).
|
|
29
|
+
|
|
30
|
+
Only :mod:`.spec`, :mod:`.features`, :mod:`.metrics` and :mod:`.readout` are
|
|
31
|
+
safe to import without gymnasium/torch installed; :mod:`.env`, :mod:`.policies`
|
|
32
|
+
and :mod:`.controller` lazy-import the heavy stack. ``__init__`` imports none of
|
|
33
|
+
them, so importing :mod:`kestrel_pi.agent` costs nothing.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
# The agent's runtime directory (owned by pi, created by the build). All of the
|
|
39
|
+
# agent's persistent artifacts live here; the boot-partition importer and the
|
|
40
|
+
# backup plugin reference these names.
|
|
41
|
+
AGENT_DIR = "/var/lib/kestrel/agent"
|
|
42
|
+
# Each algorithm keeps its own model file (``model-<slug>.zip`` + a matching
|
|
43
|
+
# ``.meta.json`` sidecar), so switching algorithm and back reloads the saved
|
|
44
|
+
# model instead of retraining from scratch. ``MODEL_NAME``/``MODEL_META_NAME``
|
|
45
|
+
# are the pre-per-algorithm single-model names, kept only as a load-time
|
|
46
|
+
# migration fallback for a device that trained/imported one before this change.
|
|
47
|
+
MODEL_NAME = "model.zip"
|
|
48
|
+
MODEL_META_NAME = "model.meta.json"
|
|
49
|
+
STATUS_NAME = "status.json"
|
|
50
|
+
# Records the last on-device idle-training run (finished-at + duration) so the
|
|
51
|
+
# dashboard can show it and it survives an agent/daemon restart.
|
|
52
|
+
LAST_TRAIN_NAME = "last-train.json"
|
|
53
|
+
# A manual "train now" request from the dashboard: the config tool drops this
|
|
54
|
+
# file and the agent's training thread consumes it to start a run promptly.
|
|
55
|
+
TRAIN_REQUEST_NAME = "train-request"
|
|
56
|
+
# A running count of model adoptions by source over the device's lifetime: how
|
|
57
|
+
# many times a locally trained model vs a promoted remote model became the
|
|
58
|
+
# active model. Persisted so the dashboard can show which side is winning, and
|
|
59
|
+
# so it survives an agent/daemon restart.
|
|
60
|
+
PROMOTION_TALLY_NAME = "promotion-tally.json"
|
|
61
|
+
EPISODES_DIR = "episodes"
|
|
62
|
+
# Where the remote-train plugin drops a pulled remote model for the agent to
|
|
63
|
+
# evaluate: ``<AGENT_DIR>/.remote-staging/<slug>/model-<slug>.{zip,meta.json}``.
|
|
64
|
+
# The plugin only pulls-and-stages here (torch-free); the agent process scores
|
|
65
|
+
# the staged model against the active one and promotes the winner (quality-aware
|
|
66
|
+
# promotion, docs/design/agent-promotion-quality.md).
|
|
67
|
+
REMOTE_STAGING_DIR = ".remote-staging"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def model_slug(algorithm):
|
|
71
|
+
"""A filesystem-safe lowercase slug for an algorithm name, used to name its
|
|
72
|
+
per-algorithm model file (e.g. ``RecurrentPPO`` -> ``recurrentppo``,
|
|
73
|
+
``QR-DQN`` -> ``qr-dqn``). Pure stdlib so the daemon/CLI can derive the
|
|
74
|
+
filename without importing the heavy agent stack."""
|
|
75
|
+
text = (algorithm or "").strip().lower()
|
|
76
|
+
slug = "".join(ch if (ch.isalnum() or ch == "-") else "-" for ch in text)
|
|
77
|
+
slug = slug.strip("-")
|
|
78
|
+
return slug or "default"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def model_name_for(algorithm):
|
|
82
|
+
"""The per-algorithm model filename (e.g. ``model-dqn.zip``)."""
|
|
83
|
+
return "model-%s.zip" % model_slug(algorithm)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def model_meta_name_for(algorithm):
|
|
87
|
+
"""The per-algorithm model sidecar filename (e.g. ``model-dqn.meta.json``)."""
|
|
88
|
+
return "model-%s.meta.json" % model_slug(algorithm)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["AGENT_DIR", "MODEL_NAME", "MODEL_META_NAME", "STATUS_NAME",
|
|
92
|
+
"LAST_TRAIN_NAME", "TRAIN_REQUEST_NAME", "PROMOTION_TALLY_NAME",
|
|
93
|
+
"EPISODES_DIR", "REMOTE_STAGING_DIR", "model_slug", "model_name_for",
|
|
94
|
+
"model_meta_name_for"]
|