redeploy 0.2.36__tar.gz → 0.2.38__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.
- {redeploy-0.2.36 → redeploy-0.2.38}/PKG-INFO +6 -6
- {redeploy-0.2.36 → redeploy-0.2.38}/README.md +5 -5
- {redeploy-0.2.36 → redeploy-0.2.38}/pyproject.toml +1 -1
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/__init__.py +1 -1
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/executor.py +3 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/handlers.py +60 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/hardware.py +64 -22
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/hardware.py +9 -5
- redeploy-0.2.38/redeploy/hardware/__init__.py +8 -0
- redeploy-0.2.38/redeploy/hardware/data/__init__.py +4 -0
- redeploy-0.2.38/redeploy/hardware/data/hyperpixel.py +41 -0
- redeploy-0.2.38/redeploy/hardware/data/official.py +18 -0
- redeploy-0.2.38/redeploy/hardware/fixes.py +203 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/models.py +21 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/SOURCES.txt +4 -0
- redeploy-0.2.38/tests/test_hardware_config.py +162 -0
- redeploy-0.2.36/redeploy/hardware/__init__.py +0 -2
- redeploy-0.2.36/redeploy/hardware/data/__init__.py +0 -2
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/audit.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/cli.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/discovery.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/fleet.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/observe.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/parse.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/patterns.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/ssh.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/steps.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/verify.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy/version.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/setup.cfg +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_iac.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_parse.py +0 -0
- {redeploy-0.2.36 → redeploy-0.2.38}/tests/test_redeploy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.38
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
27
27
|
|
|
28
28
|
## AI Cost Tracking
|
|
29
29
|
|
|
30
|
-
     
|
|
31
|
+
  
|
|
32
32
|
|
|
33
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
34
|
-
- 👤 **Human dev:** ~$
|
|
33
|
+
- 🤖 **LLM usage:** $7.5000 (59 commits)
|
|
34
|
+
- 👤 **Human dev:** ~$1944 (19.4h @ $100/h, 30min dedup)
|
|
35
35
|
|
|
36
36
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
    
|
|
41
41
|
|
|
42
42
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
43
43
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (59 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1944 (19.4h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.38"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -15,6 +15,7 @@ from .handlers import (
|
|
|
15
15
|
run_ssh, run_scp, run_rsync, run_docker_build, run_podman_build,
|
|
16
16
|
run_docker_health_wait, run_container_log_tail, run_http_check,
|
|
17
17
|
run_version_check, run_plugin, run_wait, run_inline_script,
|
|
18
|
+
run_ensure_config_line, run_raspi_config,
|
|
18
19
|
)
|
|
19
20
|
from .progress import ProgressEmitter
|
|
20
21
|
from .rollback import rollback_steps
|
|
@@ -227,6 +228,8 @@ class Executor:
|
|
|
227
228
|
StepAction.WAIT: lambda s: run_wait(s),
|
|
228
229
|
StepAction.PLUGIN: lambda s: run_plugin(s, self.probe, self.plan, self._emitter, self.dry_run),
|
|
229
230
|
StepAction.INLINE_SCRIPT: lambda s: run_inline_script(s, self.probe, self.plan),
|
|
231
|
+
StepAction.ENSURE_CONFIG_LINE: lambda s: run_ensure_config_line(s, self.probe),
|
|
232
|
+
StepAction.RASPI_CONFIG: lambda s: run_raspi_config(s, self.probe),
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
handler = dispatch.get(step.action)
|
|
@@ -338,3 +338,63 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
|
|
|
338
338
|
raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
|
|
339
339
|
|
|
340
340
|
return script
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ── hardware-specific handlers ────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def run_ensure_config_line(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
346
|
+
"""Idempotent add/replace a line in a remote config.txt."""
|
|
347
|
+
from ..hardware.config_txt import ensure_line
|
|
348
|
+
|
|
349
|
+
if not step.config_file or not step.config_line:
|
|
350
|
+
raise StepError(step, "ensure_config_line requires config_file and config_line")
|
|
351
|
+
|
|
352
|
+
config_path = step.config_file
|
|
353
|
+
r = probe.run(f"sudo cat {config_path}", timeout=10)
|
|
354
|
+
if not r.ok:
|
|
355
|
+
raise StepError(step, f"Cannot read {config_path}: {r.stderr[:200]}")
|
|
356
|
+
|
|
357
|
+
edit = ensure_line(
|
|
358
|
+
r.out,
|
|
359
|
+
step.config_line,
|
|
360
|
+
section=step.config_section or "all",
|
|
361
|
+
replaces_pattern=step.config_replaces_pattern,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if not edit.changed:
|
|
365
|
+
step.status = StepStatus.DONE
|
|
366
|
+
step.result = f"no-op: {edit.diff_summary}"
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
# Write atomically: base64-encode to avoid shell quoting issues
|
|
370
|
+
encoded = base64.b64encode(edit.new_content.encode()).decode()
|
|
371
|
+
tmp = f"/tmp/redeploy-cfg-{step.id}.txt"
|
|
372
|
+
write_r = probe.run(
|
|
373
|
+
f"echo '{encoded}' | base64 -d | sudo tee {tmp} > /dev/null && sudo mv {tmp} {config_path}",
|
|
374
|
+
timeout=15,
|
|
375
|
+
)
|
|
376
|
+
if not write_r.ok:
|
|
377
|
+
raise StepError(step, f"Cannot write {config_path}: {write_r.stderr[:200]}")
|
|
378
|
+
|
|
379
|
+
step.status = StepStatus.DONE
|
|
380
|
+
step.result = edit.diff_summary
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def run_raspi_config(step: MigrationStep, probe: "RemoteProbe") -> None:
|
|
384
|
+
"""Run raspi-config nonint to enable/disable an interface."""
|
|
385
|
+
from ..hardware.raspi_config import build_raspi_config_command
|
|
386
|
+
|
|
387
|
+
if not step.raspi_interface or not step.raspi_state:
|
|
388
|
+
raise StepError(step, "raspi_config requires raspi_interface and raspi_state")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
cmd = build_raspi_config_command(step.raspi_interface, step.raspi_state)
|
|
392
|
+
except ValueError as exc:
|
|
393
|
+
raise StepError(step, str(exc))
|
|
394
|
+
|
|
395
|
+
r = probe.run(cmd, timeout=30)
|
|
396
|
+
if not r.ok:
|
|
397
|
+
raise StepError(step, f"raspi-config failed: {r.stderr[:200]}")
|
|
398
|
+
|
|
399
|
+
step.status = StepStatus.DONE
|
|
400
|
+
step.result = f"applied: {cmd}"
|
|
@@ -177,32 +177,42 @@ def _print_diagnostics(console, hw, show_fix):
|
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
def _apply_fix(console, p, hw, apply_fix_component):
|
|
181
|
-
"""Apply fix for a specific component."""
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
def _apply_fix(console, p, hw, apply_fix_component, panel_id=None):
|
|
181
|
+
"""Apply fix for a specific component using fix plan generator."""
|
|
182
|
+
from ...hardware.fixes import generate_fix_plan
|
|
183
|
+
from ...hardware.panels import get
|
|
184
|
+
|
|
185
|
+
# Get panel if specified
|
|
186
|
+
panel = get(panel_id) if panel_id else None
|
|
187
|
+
|
|
188
|
+
# Generate fix plan
|
|
189
|
+
steps = generate_fix_plan(hw, apply_fix_component, panel)
|
|
190
|
+
if not steps:
|
|
184
191
|
console.print(f"[yellow]No fix available for component: {apply_fix_component}[/yellow]")
|
|
192
|
+
if panel_id:
|
|
193
|
+
console.print(f"[dim]Panel: {panel_id}[/dim]")
|
|
185
194
|
sys.exit(1)
|
|
186
195
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
console.print(f"[bold]Fix plan for {apply_fix_component}[/bold]")
|
|
197
|
+
if panel:
|
|
198
|
+
console.print(f" Panel: {panel.name}")
|
|
199
|
+
|
|
200
|
+
# Execute steps via Executor (simplified version)
|
|
201
|
+
from ...apply.executor import Executor
|
|
202
|
+
|
|
203
|
+
executor = Executor(probe=p, console=console, dry_run=False)
|
|
204
|
+
for step in steps:
|
|
205
|
+
console.print(f"[cyan]→ {step.description}[/cyan]")
|
|
206
|
+
try:
|
|
207
|
+
executor._run_single_step(step)
|
|
208
|
+
console.print(f"[green] ✓ done[/green]")
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
console.print(f"[red] ✗ failed: {exc}[/red]")
|
|
211
|
+
sys.exit(1)
|
|
202
212
|
|
|
203
213
|
|
|
204
214
|
@click.command()
|
|
205
|
-
@click.argument("host")
|
|
215
|
+
@click.argument("host", required=False, default=None)
|
|
206
216
|
@click.option(
|
|
207
217
|
"--format", "output_fmt",
|
|
208
218
|
default="rich",
|
|
@@ -223,8 +233,19 @@ def _apply_fix(console, p, hw, apply_fix_component):
|
|
|
223
233
|
"(e.g. 'backlight', 'overlay'). Requires sudo on target."
|
|
224
234
|
),
|
|
225
235
|
)
|
|
236
|
+
@click.option(
|
|
237
|
+
"--panel", "panel_id",
|
|
238
|
+
default=None,
|
|
239
|
+
metavar="PANEL_ID",
|
|
240
|
+
help="Specify panel ID explicitly (use --list-panels to see available)",
|
|
241
|
+
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--list-panels",
|
|
244
|
+
is_flag=True,
|
|
245
|
+
help="List available panel definitions and exit",
|
|
246
|
+
)
|
|
226
247
|
@click.option("--ssh-key", default=None, type=click.Path(), help="SSH private key path")
|
|
227
|
-
def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
|
|
248
|
+
def hardware(host, output_fmt, show_fix, apply_fix_component, panel_id, list_panels, ssh_key):
|
|
228
249
|
"""Probe and diagnose hardware on a remote host.
|
|
229
250
|
|
|
230
251
|
Checks DSI display, DRM connectors, backlight controller, I2C buses,
|
|
@@ -236,8 +257,29 @@ def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
|
|
|
236
257
|
redeploy hardware pi@192.168.188.109
|
|
237
258
|
redeploy hardware pi@192.168.188.109 --fix
|
|
238
259
|
redeploy hardware pi@192.168.188.109 --format yaml
|
|
260
|
+
redeploy hardware --list-panels
|
|
239
261
|
"""
|
|
240
262
|
console = Console()
|
|
263
|
+
|
|
264
|
+
# Handle --list-panels
|
|
265
|
+
if list_panels:
|
|
266
|
+
from ...hardware.panels import all_panels
|
|
267
|
+
panels = all_panels()
|
|
268
|
+
t = Table(show_header=True, box=box.ROUNDED)
|
|
269
|
+
t.add_column("ID", style="cyan")
|
|
270
|
+
t.add_column("Name", style="bold")
|
|
271
|
+
t.add_column("Vendor", style="dim")
|
|
272
|
+
t.add_column("Resolution", style="dim")
|
|
273
|
+
t.add_column("Overlay", style="dim")
|
|
274
|
+
for p in panels:
|
|
275
|
+
res = f"{p.resolution[0]}×{p.resolution[1]}" if p.resolution else "—"
|
|
276
|
+
t.add_row(p.id, p.name, p.vendor, res, p.overlay)
|
|
277
|
+
console.print(t)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if not host:
|
|
281
|
+
raise click.UsageError("HOST argument is required unless --list-panels is used.")
|
|
282
|
+
|
|
241
283
|
hw, p = _probe_hardware(host, ssh_key, console)
|
|
242
284
|
|
|
243
285
|
if _format_output(hw, output_fmt):
|
|
@@ -264,7 +306,7 @@ def hardware(host, output_fmt, show_fix, apply_fix_component, ssh_key):
|
|
|
264
306
|
_print_diagnostics(console, hw, show_fix)
|
|
265
307
|
|
|
266
308
|
if apply_fix_component:
|
|
267
|
-
_apply_fix(console, p, hw, apply_fix_component)
|
|
309
|
+
_apply_fix(console, p, hw, apply_fix_component, panel_id)
|
|
268
310
|
|
|
269
311
|
# Exit code based on errors
|
|
270
312
|
errors = hw.errors
|
|
@@ -53,13 +53,16 @@ def probe_board(p: RemoteProbe) -> tuple[Optional[str], Optional[str]]:
|
|
|
53
53
|
return board, kernel
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def probe_config_txt(p: RemoteProbe) -> str:
|
|
57
|
-
"""Read /boot/firmware/config.txt (RPi5) or /boot/config.txt.
|
|
56
|
+
def probe_config_txt(p: RemoteProbe) -> tuple[str, str]:
|
|
57
|
+
"""Read /boot/firmware/config.txt (RPi5) or /boot/config.txt.
|
|
58
|
+
|
|
59
|
+
Returns (content, path) — path is needed for idempotent edits later.
|
|
60
|
+
"""
|
|
58
61
|
for path in ("/boot/firmware/config.txt", "/boot/config.txt"):
|
|
59
62
|
r = p.run(f"cat {path} 2>/dev/null")
|
|
60
63
|
if r.ok and r.out.strip():
|
|
61
|
-
return r.out
|
|
62
|
-
return ""
|
|
64
|
+
return r.out, path
|
|
65
|
+
return "", "/boot/firmware/config.txt"
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
def probe_drm_outputs(p: RemoteProbe) -> list[DrmOutput]:
|
|
@@ -232,7 +235,7 @@ def _analyze(hw: HardwareInfo) -> list[HardwareDiagnostic]:
|
|
|
232
235
|
def probe_hardware(p: RemoteProbe) -> HardwareInfo:
|
|
233
236
|
"""Probe hardware state of the remote host and return HardwareInfo with diagnostics."""
|
|
234
237
|
board, kernel = probe_board(p)
|
|
235
|
-
config_txt = probe_config_txt(p)
|
|
238
|
+
config_txt, config_txt_path = probe_config_txt(p)
|
|
236
239
|
|
|
237
240
|
# Parse active DSI overlays from config.txt
|
|
238
241
|
dsi_overlays = [
|
|
@@ -266,6 +269,7 @@ def probe_hardware(p: RemoteProbe) -> HardwareInfo:
|
|
|
266
269
|
board=board,
|
|
267
270
|
kernel=kernel,
|
|
268
271
|
config_txt=config_txt,
|
|
272
|
+
config_txt_path=config_txt_path,
|
|
269
273
|
dsi_overlays=dsi_overlays,
|
|
270
274
|
drm_outputs=drm_outputs,
|
|
271
275
|
wlr_outputs=wlr_outputs,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Hardware configuration support for Raspberry Pi."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .panels import PanelDefinition, get, all_panels, infer_from_hardware, register
|
|
5
|
+
from . import data as _data # noqa: F401 — populates panel registry
|
|
6
|
+
|
|
7
|
+
__all__ = ["PanelDefinition", "get", "all_panels", "infer_from_hardware", "register"]
|
|
8
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""HyperPixel panel definitions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from redeploy.hardware.panels import PanelDefinition, register
|
|
5
|
+
|
|
6
|
+
register(PanelDefinition(
|
|
7
|
+
id="hyperpixel4-square",
|
|
8
|
+
name="HyperPixel 4.0 Square 720×720",
|
|
9
|
+
vendor="hyperpixel",
|
|
10
|
+
overlay="vc4-kms-dpi-hyperpixel4sq",
|
|
11
|
+
overlay_params=(),
|
|
12
|
+
resolution=(720, 720),
|
|
13
|
+
requires_spi_touch=True,
|
|
14
|
+
notes=[
|
|
15
|
+
"Dotyk przez SPI — raspi-config → SPI → enable",
|
|
16
|
+
"Nie wymaga I2C",
|
|
17
|
+
],
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
register(PanelDefinition(
|
|
21
|
+
id="hyperpixel4",
|
|
22
|
+
name="HyperPixel 4.0 800×480",
|
|
23
|
+
vendor="hyperpixel",
|
|
24
|
+
overlay="vc4-kms-dpi-hyperpixel4",
|
|
25
|
+
overlay_params=(),
|
|
26
|
+
resolution=(800, 480),
|
|
27
|
+
requires_spi_touch=True,
|
|
28
|
+
notes=[
|
|
29
|
+
"Dotyk przez SPI — raspi-config → SPI → enable",
|
|
30
|
+
],
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
register(PanelDefinition(
|
|
34
|
+
id="hyperpixel2r",
|
|
35
|
+
name="HyperPixel 2.1 Round 480×480",
|
|
36
|
+
vendor="hyperpixel",
|
|
37
|
+
overlay="vc4-kms-dpi-hyperpixel2r",
|
|
38
|
+
overlay_params=(),
|
|
39
|
+
resolution=(480, 480),
|
|
40
|
+
requires_spi_touch=True,
|
|
41
|
+
))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Official Raspberry Pi display panel definitions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from redeploy.hardware.panels import PanelDefinition, register
|
|
5
|
+
|
|
6
|
+
register(PanelDefinition(
|
|
7
|
+
id="rpi-dsi-7-inch",
|
|
8
|
+
name='Official Raspberry Pi 7" DSI 800×480',
|
|
9
|
+
vendor="official",
|
|
10
|
+
overlay="vc4-kms-dsi-rpi-touchscreen",
|
|
11
|
+
overlay_params=(),
|
|
12
|
+
resolution=(800, 480),
|
|
13
|
+
requires_i2c_touch=True,
|
|
14
|
+
notes=[
|
|
15
|
+
"Oryginalny ekran RPi — sterownik vc4-kms-dsi-rpi-touchscreen",
|
|
16
|
+
"Podłączenie zasilania przez 4-pin JST na module ekranu",
|
|
17
|
+
],
|
|
18
|
+
))
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Fix-plan generators: HardwareInfo + PanelDefinition → list[MigrationStep]."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from redeploy.models import ConflictSeverity, MigrationStep, StepAction, HardwareInfo
|
|
7
|
+
from redeploy.hardware.panels import PanelDefinition, infer_from_hardware
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _step(
|
|
11
|
+
id: str,
|
|
12
|
+
action: StepAction,
|
|
13
|
+
description: str,
|
|
14
|
+
risk: ConflictSeverity = ConflictSeverity.LOW,
|
|
15
|
+
**kw,
|
|
16
|
+
) -> MigrationStep:
|
|
17
|
+
return MigrationStep(id=id, action=action, description=description, risk=risk, **kw)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _detect_port(hw: HardwareInfo) -> str:
|
|
21
|
+
"""Detect which DSI port has a connected display."""
|
|
22
|
+
for output in hw.drm_outputs:
|
|
23
|
+
if "DSI" in output.name and output.status == "connected":
|
|
24
|
+
if "DSI-1" in output.name:
|
|
25
|
+
return "dsi1"
|
|
26
|
+
if "DSI-0" in output.name:
|
|
27
|
+
return "dsi0"
|
|
28
|
+
return "dsi1" # safe default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── fix generators ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def fix_dsi_not_enabled(
|
|
34
|
+
hw: HardwareInfo,
|
|
35
|
+
panel: PanelDefinition | None = None,
|
|
36
|
+
) -> list[MigrationStep]:
|
|
37
|
+
"""Generate steps to configure DSI panel + reboot + verify."""
|
|
38
|
+
panel = panel or infer_from_hardware(hw)
|
|
39
|
+
if panel is None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Cannot infer panel from hardware — specify --panel <id> explicitly. "
|
|
42
|
+
"Run 'redeploy hardware --list-panels' for available IDs."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
port = _detect_port(hw)
|
|
46
|
+
config_path = hw.config_txt_path
|
|
47
|
+
steps: list[MigrationStep] = []
|
|
48
|
+
|
|
49
|
+
# 1. Backup
|
|
50
|
+
steps.append(_step(
|
|
51
|
+
id="backup_config_txt",
|
|
52
|
+
action=StepAction.SSH_CMD,
|
|
53
|
+
description="Backup config.txt before editing",
|
|
54
|
+
command=(
|
|
55
|
+
f"sudo cp {config_path} {config_path}.bak.$(date +%Y%m%d_%H%M%S)"
|
|
56
|
+
),
|
|
57
|
+
risk=ConflictSeverity.LOW,
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
# 2. KMS overlay (if not already present)
|
|
61
|
+
if panel.requires_kms:
|
|
62
|
+
steps.append(_step(
|
|
63
|
+
id="ensure_kms",
|
|
64
|
+
action=StepAction.ENSURE_CONFIG_LINE,
|
|
65
|
+
description="Enable KMS driver (vc4-kms-v3d)",
|
|
66
|
+
config_file=config_path,
|
|
67
|
+
config_line="dtoverlay=vc4-kms-v3d",
|
|
68
|
+
config_replaces_pattern=r"^#?\s*dtoverlay=vc4-kms-v3d",
|
|
69
|
+
config_section="all",
|
|
70
|
+
risk=ConflictSeverity.MEDIUM,
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# 3. Panel overlay
|
|
74
|
+
steps.append(_step(
|
|
75
|
+
id="set_panel_overlay",
|
|
76
|
+
action=StepAction.ENSURE_CONFIG_LINE,
|
|
77
|
+
description=f"Set overlay for {panel.name}",
|
|
78
|
+
config_file=config_path,
|
|
79
|
+
config_line=panel.overlay_line(port=port),
|
|
80
|
+
config_replaces_pattern=r"^dtoverlay=vc4-kms-dsi-.*",
|
|
81
|
+
config_section="all",
|
|
82
|
+
risk=ConflictSeverity.MEDIUM,
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# 4. I2C for touch (if needed)
|
|
86
|
+
if panel.requires_i2c_touch:
|
|
87
|
+
steps.append(_step(
|
|
88
|
+
id="enable_i2c",
|
|
89
|
+
action=StepAction.RASPI_CONFIG,
|
|
90
|
+
description="Enable I2C for touch controller",
|
|
91
|
+
raspi_interface="i2c",
|
|
92
|
+
raspi_state="enable",
|
|
93
|
+
risk=ConflictSeverity.LOW,
|
|
94
|
+
))
|
|
95
|
+
|
|
96
|
+
# 5. SPI for touch (if needed)
|
|
97
|
+
if panel.requires_spi_touch:
|
|
98
|
+
steps.append(_step(
|
|
99
|
+
id="enable_spi",
|
|
100
|
+
action=StepAction.RASPI_CONFIG,
|
|
101
|
+
description="Enable SPI for touch controller",
|
|
102
|
+
raspi_interface="spi",
|
|
103
|
+
raspi_state="enable",
|
|
104
|
+
risk=ConflictSeverity.LOW,
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
# 6. Reboot
|
|
108
|
+
steps.append(_step(
|
|
109
|
+
id="reboot_rpi",
|
|
110
|
+
action=StepAction.SSH_CMD,
|
|
111
|
+
description="Reboot — required by overlay change",
|
|
112
|
+
command="sudo reboot",
|
|
113
|
+
risk=ConflictSeverity.HIGH,
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
# 7. Wait
|
|
117
|
+
steps.append(_step(
|
|
118
|
+
id="wait_for_rpi",
|
|
119
|
+
action=StepAction.WAIT,
|
|
120
|
+
description="Wait for RPi to come back online",
|
|
121
|
+
seconds=90,
|
|
122
|
+
risk=ConflictSeverity.MEDIUM,
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
# 8. Verify
|
|
126
|
+
steps.append(_step(
|
|
127
|
+
id="verify_dsi_initialized",
|
|
128
|
+
action=StepAction.SSH_CMD,
|
|
129
|
+
description="Verify DSI initialized in dmesg",
|
|
130
|
+
command="dmesg | grep -iE 'dsi|vc4|panel' | tail -20",
|
|
131
|
+
risk=ConflictSeverity.LOW,
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
return steps
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def fix_enable_i2c(
|
|
138
|
+
hw: HardwareInfo,
|
|
139
|
+
panel: PanelDefinition | None = None,
|
|
140
|
+
) -> list[MigrationStep]:
|
|
141
|
+
"""Enable I2C interface via raspi-config."""
|
|
142
|
+
return [_step(
|
|
143
|
+
id="enable_i2c",
|
|
144
|
+
action=StepAction.RASPI_CONFIG,
|
|
145
|
+
description="Enable I2C",
|
|
146
|
+
raspi_interface="i2c",
|
|
147
|
+
raspi_state="enable",
|
|
148
|
+
risk=ConflictSeverity.LOW,
|
|
149
|
+
)]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def fix_enable_spi(
|
|
153
|
+
hw: HardwareInfo,
|
|
154
|
+
panel: PanelDefinition | None = None,
|
|
155
|
+
) -> list[MigrationStep]:
|
|
156
|
+
"""Enable SPI interface via raspi-config."""
|
|
157
|
+
return [_step(
|
|
158
|
+
id="enable_spi",
|
|
159
|
+
action=StepAction.RASPI_CONFIG,
|
|
160
|
+
description="Enable SPI",
|
|
161
|
+
raspi_interface="spi",
|
|
162
|
+
raspi_state="enable",
|
|
163
|
+
risk=ConflictSeverity.LOW,
|
|
164
|
+
)]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── fix registry ──────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
# Maps hardware_rules rule name / diagnostic component → fix generator
|
|
170
|
+
FixGenerator = Callable[[HardwareInfo, PanelDefinition | None], list[MigrationStep]]
|
|
171
|
+
|
|
172
|
+
FIX_REGISTRY: dict[str, FixGenerator] = {
|
|
173
|
+
"no_dsi_overlay": fix_dsi_not_enabled,
|
|
174
|
+
"dsi_overlay_no_drm_connector": fix_dsi_not_enabled,
|
|
175
|
+
"dsi_connector_not_connected": fix_dsi_not_enabled,
|
|
176
|
+
"dsi_connected_no_backlight": fix_dsi_not_enabled,
|
|
177
|
+
"i2c": fix_enable_i2c,
|
|
178
|
+
"spi": fix_enable_spi,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def generate_fix_plan(
|
|
183
|
+
hw: HardwareInfo,
|
|
184
|
+
component: str,
|
|
185
|
+
panel: PanelDefinition | None = None,
|
|
186
|
+
) -> list[MigrationStep]:
|
|
187
|
+
"""From a component name or rule name, return fix steps."""
|
|
188
|
+
# Direct rule name match first
|
|
189
|
+
if component in FIX_REGISTRY:
|
|
190
|
+
return FIX_REGISTRY[component](hw, panel)
|
|
191
|
+
|
|
192
|
+
# Fall back: find diagnostics for this component and use their rule_name
|
|
193
|
+
for diag in hw.diagnostics:
|
|
194
|
+
if diag.component == component:
|
|
195
|
+
# Try to find a matching rule by message keywords
|
|
196
|
+
for key in FIX_REGISTRY:
|
|
197
|
+
if key in diag.message.lower().replace(" ", "_"):
|
|
198
|
+
return FIX_REGISTRY[key](hw, panel)
|
|
199
|
+
# The DSI-related diagnostics all resolve to dsi_not_enabled
|
|
200
|
+
if component in ("dsi", "overlay", "backlight"):
|
|
201
|
+
return fix_dsi_not_enabled(hw, panel)
|
|
202
|
+
|
|
203
|
+
return []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Shared data models for redeploy: InfraState, MigrationPlan, Target."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from pathlib import Path
|
|
@@ -185,6 +186,7 @@ class HardwareInfo(BaseModel):
|
|
|
185
186
|
board: Optional[str] = None # e.g. "Raspberry Pi 5 Model B Rev 1.0"
|
|
186
187
|
kernel: Optional[str] = None # uname -r
|
|
187
188
|
config_txt: str = "" # full /boot/firmware/config.txt
|
|
189
|
+
config_txt_path: str = "/boot/firmware/config.txt" # probed path, varies by OS
|
|
188
190
|
|
|
189
191
|
# Display
|
|
190
192
|
drm_outputs: list[DrmOutput] = Field(default_factory=list)
|
|
@@ -209,6 +211,15 @@ class HardwareInfo(BaseModel):
|
|
|
209
211
|
def has_dsi(self) -> bool:
|
|
210
212
|
return any("DSI" in o.name for o in self.drm_outputs)
|
|
211
213
|
|
|
214
|
+
@property
|
|
215
|
+
def kms_enabled(self) -> bool:
|
|
216
|
+
"""True if vc4-kms-v3d overlay is present in config.txt."""
|
|
217
|
+
return any(
|
|
218
|
+
re.match(r'\s*dtoverlay=vc4-kms-v3d', line)
|
|
219
|
+
for line in self.config_txt.splitlines()
|
|
220
|
+
if not line.strip().startswith("#")
|
|
221
|
+
)
|
|
222
|
+
|
|
212
223
|
@property
|
|
213
224
|
def dsi_connected(self) -> bool:
|
|
214
225
|
return any("DSI" in o.name and o.status == "connected" for o in self.drm_outputs)
|
|
@@ -316,6 +327,16 @@ class MigrationStep(BaseModel):
|
|
|
316
327
|
plugin_type: Optional[str] = None # e.g. "browser_reload"
|
|
317
328
|
plugin_params: dict = Field(default_factory=dict) # passed to plugin
|
|
318
329
|
|
|
330
|
+
# ensure_config_line params
|
|
331
|
+
config_file: Optional[str] = None # remote path, e.g. /boot/firmware/config.txt
|
|
332
|
+
config_line: Optional[str] = None # line to ensure presence of
|
|
333
|
+
config_section: str = "all" # [all], [pi5], etc.
|
|
334
|
+
config_replaces_pattern: Optional[str] = None # regex — replace matching line
|
|
335
|
+
|
|
336
|
+
# raspi_config params
|
|
337
|
+
raspi_interface: Optional[str] = None # i2c, spi, camera, onewire, ssh, vnc, serial
|
|
338
|
+
raspi_state: Optional[str] = None # enable, disable
|
|
339
|
+
|
|
319
340
|
result: Optional[str] = None # filled after apply
|
|
320
341
|
error: Optional[str] = None
|
|
321
342
|
|