rndrsbc 0.1.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.
- rndrsbc-0.1.0/.gitignore +27 -0
- rndrsbc-0.1.0/PKG-INFO +113 -0
- rndrsbc-0.1.0/README.md +93 -0
- rndrsbc-0.1.0/assets/fonts/Roboto-Bold.ttf +0 -0
- rndrsbc-0.1.0/assets/fonts/Roboto-Light.ttf +0 -0
- rndrsbc-0.1.0/assets/fonts/Roboto-Regular.ttf +0 -0
- rndrsbc-0.1.0/assets/icons/weather/01d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/01n.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/022d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/022n.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/02d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/02n.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/03d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/04d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/09d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/10d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/10n.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/11d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/13d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/48d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/50d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/51d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/53d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/56d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/57d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/71d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/73d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/77d.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/aqi.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/firstquarter.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/fullmoon.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/humidity.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/lastquarter.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/newmoon.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/pressure.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/sunrise.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/sunset.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/uvi.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/visibility.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/waningcrescent.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/waninggibbous.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/waxingcrescent.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/waxinggibbous.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather/wind.png +0 -0
- rndrsbc-0.1.0/assets/icons/weather-snow.png +0 -0
- rndrsbc-0.1.0/config.template.json +17 -0
- rndrsbc-0.1.0/core/__init__.py +1 -0
- rndrsbc-0.1.0/core/buttons.py +126 -0
- rndrsbc-0.1.0/core/canvas.py +394 -0
- rndrsbc-0.1.0/core/color.py +69 -0
- rndrsbc-0.1.0/core/config_schema.py +166 -0
- rndrsbc-0.1.0/core/i18n.py +131 -0
- rndrsbc-0.1.0/core/layout.py +208 -0
- rndrsbc-0.1.0/core/manifest.py +129 -0
- rndrsbc-0.1.0/core/migrations.py +78 -0
- rndrsbc-0.1.0/core/oauth.py +464 -0
- rndrsbc-0.1.0/core/panel_health.py +259 -0
- rndrsbc-0.1.0/core/paths.py +213 -0
- rndrsbc-0.1.0/core/pipeline.py +191 -0
- rndrsbc-0.1.0/core/providers.py +416 -0
- rndrsbc-0.1.0/core/registry.py +208 -0
- rndrsbc-0.1.0/core/security.py +122 -0
- rndrsbc-0.1.0/core/telemetry.py +97 -0
- rndrsbc-0.1.0/core/transitions.py +59 -0
- rndrsbc-0.1.0/core/updates.py +173 -0
- rndrsbc-0.1.0/displays/__init__.py +1 -0
- rndrsbc-0.1.0/displays/base.py +81 -0
- rndrsbc-0.1.0/displays/framebuffer.py +99 -0
- rndrsbc-0.1.0/displays/inky.py +74 -0
- rndrsbc-0.1.0/displays/virtual.py +48 -0
- rndrsbc-0.1.0/displays/waveshare.py +156 -0
- rndrsbc-0.1.0/main.py +222 -0
- rndrsbc-0.1.0/pyproject.toml +47 -0
- rndrsbc-0.1.0/requirements.txt +27 -0
- rndrsbc-0.1.0/rndrsbc/__init__.py +7 -0
- rndrsbc-0.1.0/rndrsbc/__main__.py +83 -0
- rndrsbc-0.1.0/rndrsbc/_registry_cli.py +103 -0
- rndrsbc-0.1.0/rndrsbc/_update.py +99 -0
- rndrsbc-0.1.0/rndrsbc/doctor.py +282 -0
- rndrsbc-0.1.0/server/__init__.py +1 -0
- rndrsbc-0.1.0/server/app.py +1766 -0
- rndrsbc-0.1.0/server/dev_studio.py +416 -0
- rndrsbc-0.1.0/server/onboarding.py +374 -0
- rndrsbc-0.1.0/server/scheduler.py +334 -0
- rndrsbc-0.1.0/widgets/__init__.py +1 -0
- rndrsbc-0.1.0/widgets/base.py +302 -0
- rndrsbc-0.1.0/widgets/calendar/widget.py +281 -0
- rndrsbc-0.1.0/widgets/clock/widget.py +137 -0
- rndrsbc-0.1.0/widgets/network/widget.py +116 -0
- rndrsbc-0.1.0/widgets/onboarding/widget.py +117 -0
- rndrsbc-0.1.0/widgets/photo_frame/widget.py +245 -0
- rndrsbc-0.1.0/widgets/system_stats/widget.py +91 -0
- rndrsbc-0.1.0/widgets/weather/widget.py +716 -0
rndrsbc-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Runtime state (self-contained mode) — gitignored, survives only locally
|
|
2
|
+
/data/
|
|
3
|
+
/config.json
|
|
4
|
+
/.claim_token
|
|
5
|
+
/live_*.png
|
|
6
|
+
/test_*.png
|
|
7
|
+
/build/
|
|
8
|
+
/*.egg-info/
|
|
9
|
+
/dist/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
|
|
13
|
+
# Secrets — must NEVER be committed (OAuth token store, mode 600)
|
|
14
|
+
/secrets/
|
|
15
|
+
|
|
16
|
+
# Python test runner cache
|
|
17
|
+
/.pytest_cache/
|
|
18
|
+
|
|
19
|
+
# Benchmark output artifacts (CI perf reports)
|
|
20
|
+
/bench/reports/
|
|
21
|
+
|
|
22
|
+
# Vendored wheels are reproducible outputs of build_vendor_deps.py from the
|
|
23
|
+
# pinned requirements.txt — keep the manifest, not the 32MB binaries.
|
|
24
|
+
/vendor/deps/_extracted/
|
|
25
|
+
/vendor/deps/*.whl
|
|
26
|
+
/vendor/deps/*.tar.gz
|
|
27
|
+
/vendor/deps/*.zip
|
rndrsbc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rndrsbc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native E-Paper Operating Platform for Raspberry Pi color e-paper panels.
|
|
5
|
+
Project-URL: Homepage, https://github.com/rndrsbc/rndrsbc
|
|
6
|
+
Author: rndrSBC
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: pillow>=10.0.0
|
|
10
|
+
Requires-Dist: qrcode[pil]>=7.4.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
12
|
+
Requires-Dist: werkzeug>=3.0.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: build; extra == 'dev'
|
|
15
|
+
Requires-Dist: twine; extra == 'dev'
|
|
16
|
+
Provides-Extra: pi
|
|
17
|
+
Requires-Dist: rpi-gpio; extra == 'pi'
|
|
18
|
+
Requires-Dist: spidev; extra == 'pi'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# rndrSBC
|
|
22
|
+
|
|
23
|
+
> **Ultra-lightweight, high-performance native E-Paper operating platform designed for Single-Board Computers.**
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Why rndrSBC?
|
|
28
|
+
|
|
29
|
+
* **Chromium-Free**: 0MB browser bloat. No headless Chrome, no WebKit, no X11.
|
|
30
|
+
* **Insanely Fast**: Renders vector dashboards in **35ms–60ms** (vs. 10–15 seconds on Chrome).
|
|
31
|
+
* **Zero Swap & Memory Thrashing**: Runs in **< 15MB RAM** (ideal for Pi Zero W / Pi Zero 2W / Pi 3).
|
|
32
|
+
* **Responsive Canvas Engine**: Resolution-independent vector layout with fractional split boxes and auto-scaling typography.
|
|
33
|
+
* **100% Clean-Room Architecture**: Fully standalone with modular display drivers and widget system.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### Quick Start (Development / PC / Mac)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. (Optional) Vendor the runtime deps for a fully-offline copy
|
|
41
|
+
python3 build_vendor_deps.py
|
|
42
|
+
|
|
43
|
+
# 2. Run single-pass render test
|
|
44
|
+
python3 main.py --once
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Output is rendered directly to `live_screen.png`.
|
|
48
|
+
|
|
49
|
+
### Run the web dashboard (any platform)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 main.py 8080 # foreground
|
|
53
|
+
python3 main.py --daemon 8080 # background, self-detaches (no sudo)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Raspberry Pi — no `install.sh` required
|
|
59
|
+
|
|
60
|
+
rndrSBC is **relocatable**: it anchors every path (config, previews, photo
|
|
61
|
+
library) to its own folder, and it can load its Python deps straight from a
|
|
62
|
+
vendored wheel bundle. So deployment is simply:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Copy the project folder onto the Pi, then:
|
|
66
|
+
python3 main.py 8080
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Optional, per-target dependency bundle (needs internet once, on the Pi):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python3 build_vendor_deps.py # detects this machine's arch
|
|
73
|
+
# or for a Raspberry Pi explicitly:
|
|
74
|
+
python3 build_vendor_deps.py --platform aarch64 # 64-bit OS / Pi 4+ / Pi 5
|
|
75
|
+
python3 build_vendor_deps.py --platform armv7l # 32-bit OS
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The resulting `vendor/deps/*.whl` files let the platform run **fully offline
|
|
79
|
+
with zero pip installs**. Skip this and rndrSBC will fall back to your
|
|
80
|
+
system's packages (or create a private `.venv` on first run).
|
|
81
|
+
|
|
82
|
+
If you want rndrSBC to start at boot (optional), use the relocatable unit:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# edit the two placeholders in rndrSBC.service, then:
|
|
86
|
+
sudo cp rndrSBC.service /etc/systemd/system/
|
|
87
|
+
sudo systemctl daemon-reload
|
|
88
|
+
sudo systemctl enable --now rndrSBC
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> A legacy `install.sh` is still included for compatibility, but it is no
|
|
92
|
+
> longer required.
|
|
93
|
+
|
|
94
|
+
### Recent Additions (this build)
|
|
95
|
+
|
|
96
|
+
**Physical Buttons (GPIO)** — `core/buttons.py`
|
|
97
|
+
- Wire three buttons to GPIO 17 / 27 / 22 (to GND), enabled in the dashboard.
|
|
98
|
+
- NEXT cycles widgets, PREV goes back, TOGGLE-QUIET pauses/restarts refreshes.
|
|
99
|
+
- Falls back gracefully when RPi.GPIO is absent (simulator / laptop / CI) — buttons are auto-disabled.
|
|
100
|
+
|
|
101
|
+
**CalDAV + iCal Calendar** — `widgets/calendar/widget.py`
|
|
102
|
+
- Calendar widget now accepts a `caldav` config (URL + basic-auth user/password) in addition to public `iCal` feed URLs.
|
|
103
|
+
- Fetches via HTTPS `REPORT` query, parses with the native ICS parser (no extra deps); per-date event-dot markers on the month grid.
|
|
104
|
+
|
|
105
|
+
**Localization (i18n)** — `core/i18n.py`
|
|
106
|
+
- New `language` global setting (English, Español, Français, Deutsch, Italiano, Português, Nederlands, Türkçe).
|
|
107
|
+
- Localizes the calendar weekday headers + agenda labels and the weather "Updated / Feels Like / Wind / Humidity" labels.
|
|
108
|
+
- Add languages in `core/i18n.py` with three dicts (`MONTHS`, `WEEKDAYS`, `LABELS`).
|
|
109
|
+
|
|
110
|
+
**Reflect-mode refresh** — `core/transitions.py` + scheduler
|
|
111
|
+
- `refresh_mode: "auto"` (default) uses dirty-rect partial refresh when the panel supports it, with a full-refresh recharge every 20 partials.
|
|
112
|
+
- `refresh_mode: "full"` opt-out forces full-frame always (dashboard Display Transitions card).
|
|
113
|
+
|
rndrsbc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# rndrSBC
|
|
2
|
+
|
|
3
|
+
> **Ultra-lightweight, high-performance native E-Paper operating platform designed for Single-Board Computers.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### Why rndrSBC?
|
|
8
|
+
|
|
9
|
+
* **Chromium-Free**: 0MB browser bloat. No headless Chrome, no WebKit, no X11.
|
|
10
|
+
* **Insanely Fast**: Renders vector dashboards in **35ms–60ms** (vs. 10–15 seconds on Chrome).
|
|
11
|
+
* **Zero Swap & Memory Thrashing**: Runs in **< 15MB RAM** (ideal for Pi Zero W / Pi Zero 2W / Pi 3).
|
|
12
|
+
* **Responsive Canvas Engine**: Resolution-independent vector layout with fractional split boxes and auto-scaling typography.
|
|
13
|
+
* **100% Clean-Room Architecture**: Fully standalone with modular display drivers and widget system.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Quick Start (Development / PC / Mac)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. (Optional) Vendor the runtime deps for a fully-offline copy
|
|
21
|
+
python3 build_vendor_deps.py
|
|
22
|
+
|
|
23
|
+
# 2. Run single-pass render test
|
|
24
|
+
python3 main.py --once
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Output is rendered directly to `live_screen.png`.
|
|
28
|
+
|
|
29
|
+
### Run the web dashboard (any platform)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 main.py 8080 # foreground
|
|
33
|
+
python3 main.py --daemon 8080 # background, self-detaches (no sudo)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Raspberry Pi — no `install.sh` required
|
|
39
|
+
|
|
40
|
+
rndrSBC is **relocatable**: it anchors every path (config, previews, photo
|
|
41
|
+
library) to its own folder, and it can load its Python deps straight from a
|
|
42
|
+
vendored wheel bundle. So deployment is simply:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Copy the project folder onto the Pi, then:
|
|
46
|
+
python3 main.py 8080
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Optional, per-target dependency bundle (needs internet once, on the Pi):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 build_vendor_deps.py # detects this machine's arch
|
|
53
|
+
# or for a Raspberry Pi explicitly:
|
|
54
|
+
python3 build_vendor_deps.py --platform aarch64 # 64-bit OS / Pi 4+ / Pi 5
|
|
55
|
+
python3 build_vendor_deps.py --platform armv7l # 32-bit OS
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The resulting `vendor/deps/*.whl` files let the platform run **fully offline
|
|
59
|
+
with zero pip installs**. Skip this and rndrSBC will fall back to your
|
|
60
|
+
system's packages (or create a private `.venv` on first run).
|
|
61
|
+
|
|
62
|
+
If you want rndrSBC to start at boot (optional), use the relocatable unit:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# edit the two placeholders in rndrSBC.service, then:
|
|
66
|
+
sudo cp rndrSBC.service /etc/systemd/system/
|
|
67
|
+
sudo systemctl daemon-reload
|
|
68
|
+
sudo systemctl enable --now rndrSBC
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> A legacy `install.sh` is still included for compatibility, but it is no
|
|
72
|
+
> longer required.
|
|
73
|
+
|
|
74
|
+
### Recent Additions (this build)
|
|
75
|
+
|
|
76
|
+
**Physical Buttons (GPIO)** — `core/buttons.py`
|
|
77
|
+
- Wire three buttons to GPIO 17 / 27 / 22 (to GND), enabled in the dashboard.
|
|
78
|
+
- NEXT cycles widgets, PREV goes back, TOGGLE-QUIET pauses/restarts refreshes.
|
|
79
|
+
- Falls back gracefully when RPi.GPIO is absent (simulator / laptop / CI) — buttons are auto-disabled.
|
|
80
|
+
|
|
81
|
+
**CalDAV + iCal Calendar** — `widgets/calendar/widget.py`
|
|
82
|
+
- Calendar widget now accepts a `caldav` config (URL + basic-auth user/password) in addition to public `iCal` feed URLs.
|
|
83
|
+
- Fetches via HTTPS `REPORT` query, parses with the native ICS parser (no extra deps); per-date event-dot markers on the month grid.
|
|
84
|
+
|
|
85
|
+
**Localization (i18n)** — `core/i18n.py`
|
|
86
|
+
- New `language` global setting (English, Español, Français, Deutsch, Italiano, Português, Nederlands, Türkçe).
|
|
87
|
+
- Localizes the calendar weekday headers + agenda labels and the weather "Updated / Feels Like / Wind / Humidity" labels.
|
|
88
|
+
- Add languages in `core/i18n.py` with three dicts (`MONTHS`, `WEEKDAYS`, `LABELS`).
|
|
89
|
+
|
|
90
|
+
**Reflect-mode refresh** — `core/transitions.py` + scheduler
|
|
91
|
+
- `refresh_mode: "auto"` (default) uses dirty-rect partial refresh when the panel supports it, with a full-refresh recharge every 20 partials.
|
|
92
|
+
- `refresh_mode: "full"` opt-out forces full-frame always (dashboard Display Transitions card).
|
|
93
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "rndrSBC default configuration. Copy to config.json (or let the deployment scaffold create it) then adjust.",
|
|
3
|
+
"display": {
|
|
4
|
+
"driver": "virtual",
|
|
5
|
+
"model": "epd7in3f",
|
|
6
|
+
"orientation": 0
|
|
7
|
+
},
|
|
8
|
+
"device": {
|
|
9
|
+
"name": "rndrSBC Frame",
|
|
10
|
+
"language": "en",
|
|
11
|
+
"timezone": "UTC"
|
|
12
|
+
},
|
|
13
|
+
"active_playlist": "default",
|
|
14
|
+
"playlists": {
|
|
15
|
+
"default": ["weather", "network", "calendar", "photo_frame", "system_stats"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
rndrSBC - Physical Button Controller
|
|
3
|
+
Wires up to 3 GPIO push-buttons to cycles through playlist actions.
|
|
4
|
+
|
|
5
|
+
Buttons (BCM pin numbering by default, configurable in config):
|
|
6
|
+
BUTTON_NEXT -> advance to next widget in active playlist
|
|
7
|
+
BUTTON_PREV -> go back to previous widget
|
|
8
|
+
BUTTON_TOGGLE-> toggle between quiet-hours suspend and active refresh
|
|
9
|
+
|
|
10
|
+
The controller degrades gracefully: on platforms without RPi.GPIO
|
|
11
|
+
(a laptop, CI, the VirtualDisplay), it logs and no-ops so the rest
|
|
12
|
+
of the system runs normally.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("rndrSBC.buttons")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import RPi.GPIO as GPIO
|
|
23
|
+
_HAS_GPIO = True
|
|
24
|
+
except Exception:
|
|
25
|
+
GPIO = None
|
|
26
|
+
_HAS_GPIO = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ButtonController:
|
|
30
|
+
"""Debounced physical button listener mapped to scheduler actions."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_PINS = {
|
|
33
|
+
"next": 17,
|
|
34
|
+
"prev": 27,
|
|
35
|
+
"toggle": 22,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(self, scheduler, config: dict, pins: dict = None):
|
|
39
|
+
self.scheduler = scheduler
|
|
40
|
+
self.config = config
|
|
41
|
+
self.pins = dict(self.DEFAULT_PINS)
|
|
42
|
+
if pins:
|
|
43
|
+
self.pins.update({k: v for k, v in pins.items() if v is not None})
|
|
44
|
+
self._thread = None
|
|
45
|
+
self._running = False
|
|
46
|
+
self._last_state = {}
|
|
47
|
+
self._last_trigger = {}
|
|
48
|
+
self._debounce_ms = int(config.get("buttons", {}).get("debounce_ms", 200))
|
|
49
|
+
self._enabled = bool(config.get("buttons", {}).get("enabled", True))
|
|
50
|
+
|
|
51
|
+
def _on_next(self):
|
|
52
|
+
items = self.scheduler.active_playlist_items
|
|
53
|
+
if items:
|
|
54
|
+
nxt = (self.scheduler.current_index + 1) % len(items)
|
|
55
|
+
self.scheduler.trigger_render_now(nxt, force_hardware=True)
|
|
56
|
+
logger.info("[BUTTON] Next widget -> index %d", nxt)
|
|
57
|
+
|
|
58
|
+
def _on_prev(self):
|
|
59
|
+
items = self.scheduler.active_playlist_items
|
|
60
|
+
if items:
|
|
61
|
+
prv = (self.scheduler.current_index - 1) % len(items)
|
|
62
|
+
self.scheduler.trigger_render_now(prv, force_hardware=True)
|
|
63
|
+
logger.info("[BUTTON] Previous widget -> index %d", prv)
|
|
64
|
+
|
|
65
|
+
def _on_toggle(self):
|
|
66
|
+
current = bool(self.config.get("quiet_hours", {}).get("enabled", False))
|
|
67
|
+
self.config.setdefault("quiet_hours", {})["enabled"] = not current
|
|
68
|
+
state = "enabled" if not current else "disabled"
|
|
69
|
+
logger.info("[BUTTON] Quiet-hours %s", state)
|
|
70
|
+
if not current:
|
|
71
|
+
self.scheduler.trigger_render_now(self.scheduler.current_index, force_hardware=True)
|
|
72
|
+
|
|
73
|
+
def _dispatch(self, action):
|
|
74
|
+
try:
|
|
75
|
+
getattr(self, f"_on_{action}")()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Button action '{action}' failed: {e}")
|
|
78
|
+
|
|
79
|
+
def _poll(self):
|
|
80
|
+
"""Non-blocking polling loop with software debounce (works on any GPIO lib)."""
|
|
81
|
+
while self._running:
|
|
82
|
+
for action, pin in self.pins.items():
|
|
83
|
+
try:
|
|
84
|
+
state = GPIO.input(pin)
|
|
85
|
+
except Exception:
|
|
86
|
+
continue
|
|
87
|
+
now = time.time()
|
|
88
|
+
prev = self._last_state.get(action)
|
|
89
|
+
if prev == 1 and state == 0: # falling edge (pressed to GND)
|
|
90
|
+
if now - self._last_trigger.get(action, 0) >= self._debounce_ms / 1000.0:
|
|
91
|
+
self._last_trigger[action] = now
|
|
92
|
+
self._dispatch(action)
|
|
93
|
+
self._last_state[action] = state
|
|
94
|
+
time.sleep(0.02)
|
|
95
|
+
|
|
96
|
+
def start(self):
|
|
97
|
+
if not self._enabled:
|
|
98
|
+
logger.info("Buttons disabled in config.")
|
|
99
|
+
return
|
|
100
|
+
if not _HAS_GPIO:
|
|
101
|
+
logger.info("RPi.GPIO not available - physical buttons skipped (virtual/simulator mode).")
|
|
102
|
+
return
|
|
103
|
+
try:
|
|
104
|
+
GPIO.setmode(GPIO.BCM)
|
|
105
|
+
for action, pin in self.pins.items():
|
|
106
|
+
logger.info("Configuring %s button on GPIO %d", action, pin)
|
|
107
|
+
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
|
108
|
+
self._last_state[action] = GPIO.input(pin)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to configure GPIO buttons: {e}")
|
|
111
|
+
return
|
|
112
|
+
self._running = True
|
|
113
|
+
self._thread = threading.Thread(target=self._poll, daemon=True, name="rndrSBC-Buttons")
|
|
114
|
+
self._thread.start()
|
|
115
|
+
logger.info("Physical button controller started.")
|
|
116
|
+
|
|
117
|
+
def stop(self):
|
|
118
|
+
self._running = False
|
|
119
|
+
if self._thread:
|
|
120
|
+
self._thread.join(timeout=3)
|
|
121
|
+
if _HAS_GPIO and self._enabled:
|
|
122
|
+
try:
|
|
123
|
+
GPIO.cleanup()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
logger.info("Button controller stopped.")
|