First commit with entire first rendition of the project
This commit is contained in:
103
docs/display-orientation.md
Normal file
103
docs/display-orientation.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Display Orientation
|
||||
|
||||
## Purpose
|
||||
|
||||
Allow the user to configure the physical orientation of the Inky Impression
|
||||
display so that images are displayed upright regardless of how the panel is
|
||||
mounted. The orientation setting also drives the Unsplash orientation filter so
|
||||
fetched photos match the display's effective aspect ratio (portrait vs.
|
||||
landscape).
|
||||
|
||||
## Configuration
|
||||
|
||||
A new `display.orientation` key (integer, degrees clockwise) in
|
||||
`config.yaml` / `DEFAULT_CONFIG`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
orientation: 0 # 0, 90, 180, 270
|
||||
```
|
||||
|
||||
| Value | Unsplash filter | Effective aspect |
|
||||
|-------|----------------|------------------|
|
||||
| 0 | landscape | width > height |
|
||||
| 90 | portrait | height > width |
|
||||
| 180 | landscape | width > height |
|
||||
| 270 | portrait | height > width |
|
||||
|
||||
The old `unsplash.orientation` config key is **no longer used** by
|
||||
`refresh()` — orientation is now derived from `display.orientation` +
|
||||
`display.resolution`.
|
||||
|
||||
## Image Processing Pipeline
|
||||
|
||||
1. **Fetch** raw photo from Unsplash (with orientation filter already matching
|
||||
the effective aspect ratio — see below).
|
||||
2. **`process_image()`** in `InkyDisplay`:
|
||||
- Compute effective width/height by swapping physical dimensions when
|
||||
orientation is 90° or 270°.
|
||||
- Crop to effective aspect ratio.
|
||||
- Resize to effective dimensions.
|
||||
- Rotate by `-orientation` degrees (so the result matches the physical
|
||||
resolution of the display panel).
|
||||
3. **`show()`** sends the processed (and rotated) image to `inky.set_image()`.
|
||||
No additional transformation is needed — the panel hardware always expects
|
||||
its native `width × height` pixel grid.
|
||||
|
||||
## Unsplash Filter Derivation
|
||||
|
||||
In `cli.py:refresh()`, after reading `display.orientation` and
|
||||
`display.resolution`:
|
||||
|
||||
1. If orientation is 90 or 270, swap width ↔ height to get effective
|
||||
dimensions.
|
||||
2. If effective height > effective width → request `"portrait"`.
|
||||
3. If effective width > effective height → request `"landscape"`.
|
||||
4. If roughly equal → request `"squarish"`.
|
||||
|
||||
This means the Unsplash API is sent `orientation=portrait` when the display is
|
||||
mounted vertically, which returns taller photos that need less cropping.
|
||||
|
||||
## Hot-Reload
|
||||
|
||||
`_reload_config()` in `cli.py` detects changes to
|
||||
`display.orientation` and updates `display.orientation` live. The next
|
||||
`process_image()` call uses the new orientation. No display loop restart is
|
||||
needed.
|
||||
|
||||
## Library Caching
|
||||
|
||||
`_save_to_library()` passes the **effective** width/height to
|
||||
`save_unsplash_image()` (swapped when orientation is 90/270). The stored image
|
||||
is processed to the effective dimensions at orientation=0 (no rotation). This
|
||||
keeps library images in their "natural" viewing orientation.
|
||||
|
||||
The preview endpoint (`/api/preview/<id>`) creates an `InkyDisplay` with the
|
||||
current orientation, so the preview matches what the physical display shows.
|
||||
|
||||
## UI
|
||||
|
||||
Settings page (Display article) shows a `<select>` with four options:
|
||||
|
||||
- 0° (Landscape)
|
||||
- 90° (Portrait)
|
||||
- 180° (Landscape)
|
||||
- 270° (Portrait)
|
||||
|
||||
Saved via the existing `PUT /api/config/display/orientation` route.
|
||||
|
||||
The old `unsplash.orientation` dropdown has been **removed** from the settings
|
||||
page — it is now auto-derived from the display orientation.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **0° (default):** No rotation applied. Preserves existing behavior.
|
||||
- **180°:** Image is cropped/resized at physical dimensions, then rotated
|
||||
upside-down. No dimension swap. Useful for ceiling-mounted displays.
|
||||
- **90° / 270°:** Width and height are swapped for cropping/resizing, then
|
||||
rotated back to physical dimensions. Images fetched from Unsplash with the
|
||||
matching portrait/landscape filter.
|
||||
- **Preview:** Shows the rotated image so the user sees exactly what the
|
||||
display will show.
|
||||
- **rotate(expand=True):** Only used with 90° multiples so no fractional
|
||||
pixels appear.
|
||||
146
docs/mqtt.md
Normal file
146
docs/mqtt.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# MQTT Integration
|
||||
|
||||
Spectra can be controlled and monitored over MQTT. The MQTT client runs as a background thread in the display loop process (`spectra` service) and connects to any standard MQTT broker.
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable MQTT in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
enabled: true
|
||||
broker: 192.168.1.100
|
||||
port: 1883
|
||||
topic_prefix: spectra
|
||||
client_id: spectra-display
|
||||
username: ""
|
||||
password: ""
|
||||
```
|
||||
|
||||
- **enabled** — set to `true` to start the MQTT client
|
||||
- **broker** — hostname or IP of your MQTT broker
|
||||
- **port** — broker port (default: 1883)
|
||||
- **topic_prefix** — prefix for all MQTT topics (default: `spectra`)
|
||||
- **client_id** — unique client identifier for the MQTT connection
|
||||
- **username / password** — optional credentials for authenticated brokers
|
||||
|
||||
Changes to MQTT settings are hot-reloaded by the display loop — no service restart needed. If you disable MQTT, the client disconnects cleanly.
|
||||
|
||||
## Topics
|
||||
|
||||
All topics use the configured `topic_prefix`. The examples below assume the default prefix `spectra`.
|
||||
|
||||
### Commands (subscribe)
|
||||
|
||||
Spectra subscribes to `spectra/command/#` and dispatches based on the command suffix.
|
||||
|
||||
| Topic | Payload | Action |
|
||||
|-------|---------|--------|
|
||||
| `spectra/command/refresh` | *(ignored)* | Triggers an Unsplash refresh on the next display cycle |
|
||||
| `spectra/command/clear` | *(ignored)* | Clears the display to white on the next cycle |
|
||||
| `spectra/command/show/<id>` | *(ignored)* | Shows a library image by its database ID |
|
||||
| `spectra/command/status` | *(ignored)* | Publishes a fresh status message |
|
||||
|
||||
Example commands:
|
||||
|
||||
```bash
|
||||
# Refresh from Unsplash
|
||||
mosquitto_pub -h 192.168.1.100 -t "spectra/command/refresh" -n
|
||||
|
||||
# Clear the display
|
||||
mosquitto_pub -h 192.168.1.100 -t "spectra/command/clear" -n
|
||||
|
||||
# Show a specific image from the library (ID 5)
|
||||
mosquitto_pub -h 192.168.1.100 -t "spectra/command/show/5" -n
|
||||
|
||||
# Request a status update
|
||||
mosquitto_pub -h 192.168.1.100 -t "spectra/command/status" -n
|
||||
```
|
||||
|
||||
### Status (publish)
|
||||
|
||||
Spectra publishes JSON status messages to `spectra/status`.
|
||||
|
||||
**Connection event** (published on successful connect):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "connected",
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
**Command acknowledgements** (published after processing each command):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "refresh",
|
||||
"status": "triggered"
|
||||
}
|
||||
```
|
||||
|
||||
On error (e.g. invalid image ID):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "show",
|
||||
"status": "error",
|
||||
"error": "image_not_found"
|
||||
}
|
||||
```
|
||||
|
||||
**Heartbeat** (published every 5 minutes):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "heartbeat",
|
||||
"status": "running",
|
||||
"timestamp": 1719334800.0
|
||||
}
|
||||
```
|
||||
|
||||
## Home Assistant integration
|
||||
|
||||
You can integrate Spectra into Home Assistant using MQTT. Add the following to your `configuration.yaml`:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
sensor:
|
||||
- name: "Spectra Status"
|
||||
state_topic: "spectra/status"
|
||||
value_template: "{{ value_json.action }}"
|
||||
|
||||
button:
|
||||
- name: "Spectra Refresh"
|
||||
command_topic: "spectra/command/refresh"
|
||||
payload_press: ""
|
||||
- name: "Spectra Clear"
|
||||
command_topic: "spectra/command/clear"
|
||||
payload_press: ""
|
||||
```
|
||||
|
||||
## Example: Node-RED flow
|
||||
|
||||
A Node-RED flow can listen for commands via HTTP (from a web dashboard) and forward them to MQTT, or forward MQTT status events to a database.
|
||||
|
||||
```json
|
||||
[{"id":"spectra-mqtt","type":"mqtt in","topic":"spectra/command/#","name":"Spectra Commands"}]
|
||||
```
|
||||
|
||||
Subscribe to `spectra/status` to build a real-time dashboard with display state, last action, and connectivity status.
|
||||
|
||||
## Debugging
|
||||
|
||||
Check the display service logs for MQTT connection events:
|
||||
|
||||
```bash
|
||||
journalctl -u spectra -f | grep MQTT
|
||||
```
|
||||
|
||||
A successful connection logs:
|
||||
```
|
||||
MQTT connected to 192.168.1.100:1883
|
||||
MQTT subscribed to spectra/command/#
|
||||
```
|
||||
|
||||
A failed connection logs the return code (see [MQTT spec](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718035) for rc meanings).
|
||||
314
docs/web-interface.md
Normal file
314
docs/web-interface.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Spectra Web Interface
|
||||
|
||||
## Status
|
||||
|
||||
| Step | Description | Status | Date |
|
||||
|------|-------------|--------|------|
|
||||
| 1 | Scaffold: web module, Flask app factory, models, templates, CLI integration | ✅ Done | 2026-06-25 |
|
||||
| 2 | Config API: read/write config.yaml from web UI | ✅ Done | 2026-06-25 |
|
||||
| 3 | Image upload: endpoint, validation, gallery | ✅ Done | 2026-06-25 |
|
||||
| 4 | Display trigger: trigger file in cli.py, Show Now | ✅ Done | 2026-06-25 |
|
||||
| 5 | Rotation queue: select images, weight system | ✅ Done | 2026-06-25 |
|
||||
| 6 | Schedule editor: full schedule in UI | ✅ Done | 2026-06-25 |
|
||||
| 7 | systemd service + installer updates | ✅ Done | 2026-06-25 |
|
||||
| 8 | Polish: preview, responsiveness, error handling | ✅ Done | 2026-06-25 |
|
||||
| 9 | MQTT integration: commands, status, heartbeat | ✅ Done | 2026-06-25 |
|
||||
| 10 | Drag-and-drop multi-image upload | ✅ Done | 2026-06-25 |
|
||||
| 11 | Config hot-reload in display loop | ✅ Done | 2026-06-25 |
|
||||
| 12 | Unsplash image caching to library DB | ✅ Done | 2026-06-25 |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Raspberry Pi
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Browser │◄──►│ Flask App │───►│ Display Loop │ │
|
||||
│ │ (GUI) │ │ (port 5000) │ │ (cli.py) │ │
|
||||
│ └──────────┘ │ │ │ │ │
|
||||
│ │ /api/* │ │ ┌───────────┐ │ │
|
||||
│ │ /gallery │ │ │ fetcher │ │ │
|
||||
│ │ /settings │ │ │ display │ │ │
|
||||
│ │ /upload │ │ └───────────┘ │ │
|
||||
│ └──────┬───────┘ └────────┬────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌────▼─────┐ ┌─────▼──────┐ │
|
||||
│ │ SQLite │ │ trigger │ │
|
||||
│ │ web.db │ │ .json │ │
|
||||
│ └──────────┘ └────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────▼─────┐ │
|
||||
│ │ uploads/ │ │
|
||||
│ │ (images) │ │
|
||||
│ └──────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Web framework | Flask | Lightweight, zero-config, well-suited for embedded, huge ecosystem |
|
||||
| Frontend | Server-rendered HTML + htmx | No build step, minimal CPU/RAM on Pi, dynamic UI with minimal JS |
|
||||
| Styling | Pico CSS (CDN) | Minimal, responsive, classless CSS framework — no custom CSS needed for basics |
|
||||
| Database | SQLite via Flask-SQLAlchemy | No daemon needed, single file, built into Python. ORM keeps code clean |
|
||||
| Image validation | Pillow `verify()` + extension whitelist | Double-check: extension filter prevents upload of non-image files, Pillow verify catches corrupted data |
|
||||
| Config persistence | `ConfigManager` class wraps `config.py` | Single point for read/write, falls back to `~/.config/spectra/config.yaml` when `/etc/` is not writable |
|
||||
| Cache directory | `SPECTRA_CACHE_DIR` env var, fallback `~/.cache/spectra/` | Allows override in constrained environments, default works without root |
|
||||
| Display loop integration | File-based trigger (`trigger.json`) | Minimal changes to existing loop, no DB dependency in the display loop, survives restarts |
|
||||
| CLI integration | `spectra web` subcommand via `sys.argv` check | Preserves full backward compatibility — `spectra --once` still works unchanged |
|
||||
| Config API PUT for individual keys | `PUT /api/config/<section>/<key>` with `{"value": ...}` body | Simple, type-safe, avoids parsing complex nested patches on the frontend |
|
||||
|
||||
---
|
||||
|
||||
## Module structure
|
||||
|
||||
```
|
||||
spectra/
|
||||
├── __init__.py
|
||||
├── __main__.py
|
||||
├── cli.py # CLI entry point, display loop, trigger handling
|
||||
├── config.py # Config loader (read-only, shared)
|
||||
├── config_manager.py # NEW — read/write config.yaml (write support)
|
||||
├── display.py # Display abstraction (+ show_file, clear)
|
||||
├── fetcher.py # Unsplash API client
|
||||
├── trigger.py # Shared trigger file read/write (thread-safe)
|
||||
├── mqtt.py # MQTT client: commands + status publishing
|
||||
├── library.py # Unsplash image caching to SQLite library
|
||||
├── web/
|
||||
│ ├── __init__.py
|
||||
│ ├── server.py # Flask app factory + all routes
|
||||
│ ├── models.py # SQLAlchemy models (Image, Rotation, Setting)
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html # Layout with Pico CSS nav
|
||||
│ │ ├── index.html # Dashboard
|
||||
│ │ ├── gallery.html # Image gallery
|
||||
│ │ ├── upload.html # Upload form
|
||||
│ │ ├── settings.html# Settings editor
|
||||
│ │ └── preview.html # Image preview
|
||||
│ └── static/
|
||||
│ ├── css/app.css
|
||||
│ └── js/app.js
|
||||
└── systemd/
|
||||
├── spectra.service # Existing — display loop
|
||||
└── spectra-web.service # NEW — web server
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
Managed by Flask-SQLAlchemy. Auto-creates on first `create_app()`. Three tables:
|
||||
|
||||
### `image`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | INTEGER PK | autoincrement |
|
||||
| filename | TEXT | stored filename on disk |
|
||||
| source | TEXT | `'upload'` or `'unsplash'` |
|
||||
| unsplash_id | TEXT | nullable, Unsplash photo ID |
|
||||
| title | TEXT | user-friendly title |
|
||||
| author | TEXT | credit line |
|
||||
| width | INTEGER | image pixel width |
|
||||
| height | INTEGER | image pixel height |
|
||||
| filepath | TEXT | absolute path to cached file |
|
||||
| created_at | DATETIME | auto-set on creation |
|
||||
| seen_count | INTEGER | times shown on display |
|
||||
| last_shown | DATETIME | nullable, last display time |
|
||||
|
||||
### `rotation`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | INTEGER PK | autoincrement |
|
||||
| image_id | INTEGER FK → image.id | |
|
||||
| weight | INTEGER | higher = shown more often |
|
||||
| active | BOOLEAN | toggle on/off without removing |
|
||||
| created_at | DATETIME | auto-set |
|
||||
|
||||
### `setting`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| key | TEXT PK | setting name |
|
||||
| value | TEXT | setting value (stringified) |
|
||||
|
||||
---
|
||||
|
||||
## API endpoints
|
||||
|
||||
### Config
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/config` | Full config dict |
|
||||
| `PATCH` | `/api/config` | Deep-merge changes into config and save |
|
||||
| `GET` | `/api/config/<section>` | Single section (e.g. `display`, `schedule`) |
|
||||
| `PUT` | `/api/config/<section>/<key>` | Set a single value (`{"value": ...}`) |
|
||||
|
||||
### Images
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/images` | Paginated list (`?page=&per_page=&source=`) |
|
||||
| `POST` | `/api/images` | Upload (multipart: file + title + author + show_now) |
|
||||
| `GET` | `/api/images/<id>` | Image metadata |
|
||||
| `DELETE` | `/api/images/<id>` | Delete (removes DB record + file + rotation entries) |
|
||||
| `GET` | `/api/images/<id>/file` | Serve the image file |
|
||||
| `GET` | `/api/images/<id>/thumbnail` | Auto-generated 320×240 thumbnail |
|
||||
| `POST` | `/api/images/<id>/show` | Trigger display of this image |
|
||||
|
||||
### Display
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/preview/<id>` | E-paper processed preview (crop + resize applied) |
|
||||
| `POST` | `/api/display/refresh` | Trigger Unsplash refresh |
|
||||
| `POST` | `/api/display/clear` | Clear display (white) |
|
||||
| `GET` | `/api/display/status` | Current state (simulation, resolution, pending trigger, last shown) |
|
||||
|
||||
### Rotation
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/rotation` | All rotation entries with nested image data |
|
||||
| `POST` | `/api/rotation` | Add images (`{"image_ids": [1,2,3]}`) |
|
||||
| `DELETE` | `/api/rotation/<id>` | Remove entry |
|
||||
| `PATCH` | `/api/rotation/<id>` | Update `weight` or `active` |
|
||||
|
||||
---
|
||||
|
||||
## Trigger mechanism
|
||||
|
||||
The display loop (`cli.py`) and web server (`web/server.py`) coordinate via a JSON trigger file at `SPECTRA_CACHE_DIR/trigger.json`.
|
||||
|
||||
### Trigger actions
|
||||
|
||||
| Action | Payload | Effect |
|
||||
|--------|---------|--------|
|
||||
| `refresh` | `{"action": "refresh"}` | Next loop iteration fetches from Unsplash |
|
||||
| `show_upload` | `{"action": "show_upload", "path": "...", "image_id": 1}` | Loads the given image file and displays it |
|
||||
| `clear` | `{"action": "clear"}` | Sets display to blank white |
|
||||
|
||||
### Flow
|
||||
|
||||
1. Web server writes trigger file via `write_trigger()` (thread-safe with lock)
|
||||
2. Display loop checks trigger at top of each iteration via `handle_trigger()`
|
||||
3. If trigger exists: execute action, then `clear_trigger()` (delete file)
|
||||
4. If no trigger: default Unsplash refresh
|
||||
|
||||
Trigger file path resolution (in order of priority):
|
||||
1. `SPECTRA_CACHE_DIR` environment variable
|
||||
2. `/var/cache/spectra/` (fallback if writable)
|
||||
3. `~/.cache/spectra/` (fallback for non-root/dev environments)
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
|
||||
### `spectra` (display — unchanged)
|
||||
|
||||
```
|
||||
spectra [-h] [-c CONFIG] [--once] [--simulate] [--width WIDTH] [--height HEIGHT] [-v]
|
||||
```
|
||||
|
||||
Full backward compatibility preserved. The display loop now additionally checks for trigger files before each refresh.
|
||||
|
||||
### `spectra web` (web server — new)
|
||||
|
||||
```
|
||||
spectra web [-h] [-c CONFIG] [--host HOST] [--port PORT] [-v]
|
||||
```
|
||||
|
||||
Starts the Flask development server. Routes are registered inline in `server.py` via `_register_routes()`.
|
||||
|
||||
---
|
||||
|
||||
## systemd
|
||||
|
||||
### `spectra-web.service`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Spectra Web Interface
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/spectra web
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### `spectra.service` (unchanged)
|
||||
|
||||
Display loop — runs `spectra` without flags (continuous mode).
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
# requirements.txt (additions)
|
||||
Flask>=3.0.0
|
||||
Flask-SQLAlchemy>=3.1.0
|
||||
```
|
||||
|
||||
Flask and Flask-SQLAlchemy are the only new dependencies — Pico CSS and htmx are loaded from CDN (no npm/build step needed).
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
`install.sh` updated:
|
||||
|
||||
1. Inky library (Raspberry Pi only)
|
||||
2. Python dependencies (`pip install -r requirements.txt`)
|
||||
3. Package install (`pip install -e .`)
|
||||
4. Display service (copies unit, enables)
|
||||
5. **Web service** (copies `spectra-web.service`, enables)
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- Web server binds to `0.0.0.0:5000` by default (Pi's LAN)
|
||||
- No built-in authentication (recommend nginx reverse proxy + HTTP basic auth for production)
|
||||
- File uploads: extension whitelist + Pillow `verify()` — no arbitrary code execution
|
||||
- Uploaded images stored outside web root (`/var/cache/spectra/uploads/` or `~/.cache/spectra/uploads/`)
|
||||
- Unsplash API key stored in config (permissions on `/etc/spectra/config.yaml` or `~/.config/spectra/config.yaml`)
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **No multi-user auth**: The web server has no authentication. Recommended for LAN use only, or behind an nginx reverse proxy with HTTP basic auth.
|
||||
- **Resolution changes need hardware re-init**: If display resolution changes in the config while using real hardware (not simulation), the Inky display must be re-initialized. The display service picks up the new values but the hardware may not resize until next boot.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- **Cache directory fallback**: The project uses `/var/cache/spectra/` when run as root (Raspberry Pi), and `~/.cache/spectra/` when run as a regular user (dev). Override with `SPECTRA_CACHE_DIR` env var.
|
||||
- **Config write safety**: `ConfigManager` resolves the config path by checking search paths in order, falling back to `~/.config/spectra/config.yaml` for writes when none exist. Always uses `os.path.abspath()` to avoid directory resolution issues.
|
||||
- **Image upload validation**: Two-stage — extension check against whitelist (`{png, jpg, jpeg, gif, bmp, webp}`) then Pillow `verify()` to catch corrupted files. Max file size: 20MB.
|
||||
- **Thumbnail generation**: 320×240 PNG thumbnails generated on first request and cached alongside the source file as `filename.thumb`. Uses `Image.thumbnail()` for aspect-ratio preservation.
|
||||
- **No CSRF protection**: Current implementation has no CSRF tokens since htmx sends `POST`/`DELETE` requests. Add Flask-WTF or a simple token check before production deployment on a public network.
|
||||
- **`display.clear()`**: Creates a blank palette-mode (`"P"`) white image and pushes it to the display. Avoids relying on internal `_buf` attributes that differ across Inky board revisions.
|
||||
|
||||
---
|
||||
|
||||
## Future work (post-v1 — not implemented)
|
||||
|
||||
- [ ] WebSocket push for live display status updates
|
||||
- [ ] S3/Nextcloud import sources
|
||||
- [ ] Multi-user auth via nginx reverse proxy
|
||||
- [ ] OTA software updates
|
||||
- [ ] Touchscreen kiosk mode (mirrors the display locally)
|
||||
- [ ] zrok/Cloudflare Tunnel for secure remote access
|
||||
Reference in New Issue
Block a user