184 lines
7.8 KiB
Markdown
184 lines
7.8 KiB
Markdown
# Ambiligth
|
|
|
|
Ambiligth is a highly optimized, cross-platform Rust application that captures your screen's dominant color in real-time and synchronizes it with your smart lights via Home Assistant. It acts as a software-based "Ambilight" for your monitor, creating an immersive viewing experience.
|
|
|
|
## Features
|
|
|
|
- **Cross-Platform Screen Capture:**
|
|
- Linux (via `grim-rs`)
|
|
- macOS (via `CoreGraphics`)
|
|
- Windows (via `GDI` / `BitBlt`)
|
|
- **Advanced Color Processing:** Uses `okmain` (Oklab color space) to accurately and efficiently extract the perceptual dominant color of the screen.
|
|
- **Smart Image Diffing:** Uses `image-compare` (MSSIM simple algorithm) to perceptually compare screen frames. It only sends updates to Home Assistant if the screen content has changed significantly, preventing API spam and light flickering.
|
|
- **RGBW/RGBWW Support:** Intelligently preserves brightness and white channels for advanced smart lights, updating only the color values.
|
|
- **Smooth Transitions & Filtering:** Built-in exponential smoothing and configurable Home Assistant transition times.
|
|
- **Auto-Restore:** Optionally takes a snapshot of your lights' state on startup and restores them to their original colors when you exit the program.
|
|
|
|
## Prerequisites
|
|
|
|
- Rust (cargo)
|
|
- A running Home Assistant instance with a Long-Lived Access Token.
|
|
- Color-capable smart lights integrated into Home Assistant.
|
|
- *Optional:* `nix` (if you are using the provided Nix flake environment).
|
|
|
|
## Configuration
|
|
|
|
Ambiligth is configured via a `config.toml` file in the root of the project, or via environment variables (e.g., `HA_URL`, `HA_TOKEN`).
|
|
|
|
Create a `config.toml` file with the following options:
|
|
|
|
```toml
|
|
# Your Home Assistant URL (default: "http://localhost:8123")
|
|
ha_url = "http://192.168.1.100:8123"
|
|
|
|
# Your Long-Lived Access Token (Required)
|
|
ha_token = "ey..."
|
|
|
|
# Target Frames Per Second for light updates (default: 15)
|
|
# This is the MAXIMUM rate - lights only update when color actually changes
|
|
target_fps = 15
|
|
|
|
# Color smoothing factor from 0.0 to 1.0 (default: 0.0)
|
|
# Higher values mean slower, smoother color transitions.
|
|
smoothing = 0.5
|
|
|
|
# Time in seconds for the lights to transition to the new color (default: 0.5)
|
|
transition = 0.5
|
|
|
|
# Minimum perceptual difference percentage to trigger an update (default: 1.0)
|
|
# Increase this if lights are flickering on mostly static screens.
|
|
min_diff_percent = 2.0
|
|
|
|
# Restore lights to their original state on exit (default: true)
|
|
restore_on_exit = true
|
|
|
|
# List of specific light entity IDs to control.
|
|
# If left empty, it will auto-detect all color-capable lights.
|
|
lights = [
|
|
"light.monitor_backlight",
|
|
"light.desk_strip"
|
|
]
|
|
```
|
|
|
|
## Running the Application
|
|
|
|
Because screen capture and image processing are highly CPU-intensive, **you must run this application in release mode** to achieve real-time performance and avoid high CPU usage.
|
|
|
|
```bash
|
|
# Standard cargo run
|
|
cargo run --release
|
|
|
|
# If you are using the Nix dev shell
|
|
nix develop -c cargo run --release
|
|
```
|
|
|
|
## How It Works
|
|
|
|
1. **Capture:** Takes a fast, SIMD-optimized screenshot using platform-native APIs.
|
|
2. **Diff:** Compares the new screenshot against the previous one using Structural Similarity (MSSIM). If the difference is below `min_diff_percent`, it skips the update.
|
|
3. **Analyze:** Calculates the dominant color of the current frame using the Oklab color space.
|
|
4. **Smooth:** Blends the new dominant color with the previous one based on the `smoothing` factor.
|
|
5. **Update:** Constructs a highly specific JSON payload for Home Assistant, ensuring white channels and brightness are preserved, and sends it asynchronously via `reqwest`.
|
|
|
|
|
|
|
|
# TODO: Parallel Event Loop Restructuring (COMPLETED)
|
|
|
|
This architecture has been implemented. See `src/eventloop.rs` for details.
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Shared State │
|
|
│ │
|
|
│ current_screenshot ← Latest captured (screenshot loop) │
|
|
│ active_screenshot ← Screenshot that produced target_color│
|
|
│ target_color ← Color calculated from active_screenshot│
|
|
│ active_color ← Smoothed color sent to lights │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
Data Flow:
|
|
|
|
Screenshot Loop (screenshot_fps) Color Calc (on-demand)
|
|
│ │
|
|
▼ ▼
|
|
[current_screenshot] [target_color]
|
|
│ │
|
|
│ diff(current, active) │
|
|
│ > min_diff_percent? │
|
|
▼ │
|
|
[active_screenshot] ◄───────────────────────┘
|
|
│ │
|
|
│ Smoothing Loop (target_fps)│
|
|
│ │ │
|
|
│ ▼ │
|
|
│ [active_color] ◄─────────┘
|
|
│ │
|
|
▼ ▼
|
|
Light Update (parallel to all lights)
|
|
```
|
|
|
|
## Implementation Tasks
|
|
|
|
### Phase 1: Settings Update (`settings.rs`)
|
|
- Add `screenshot_fps` config option (default: 3)
|
|
- Remove `transition` option (smoothing handles transitions)
|
|
|
|
### Phase 2: Create Shared State (`state.rs`)
|
|
```rust
|
|
pub struct AppState {
|
|
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
|
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
|
pub target_color: RwLock<Option<Rgb<f32>>>,
|
|
pub active_color: RwLock<Rgb<f32>>,
|
|
}
|
|
```
|
|
|
|
### Phase 3: Parallel Event Loop (`eventloop.rs`)
|
|
|
|
**Thread 1 - Screenshot Loop** (std::thread, runs at `screenshot_fps`)
|
|
- Capture screenshot → store in `current_screenshot`
|
|
- Compare `current_screenshot` vs `active_screenshot`
|
|
- If diff > `min_diff_percent`:
|
|
- Copy `current_screenshot` → `active_screenshot`
|
|
- Spawn color calc task
|
|
|
|
**On-demand - Color Calculation** (tokio::spawn)
|
|
- Calculate dominant color from `active_screenshot`
|
|
- Update `target_color`
|
|
|
|
**Thread 2 - Smoothing & Light Update** (tokio, runs at `target_fps`)
|
|
- Lerp `active_color` toward `target_color` using `smoothing` factor
|
|
- Send `active_color` to all lights **in parallel**
|
|
|
|
### Phase 4: Parallel Light Updates (`homeassistant.rs`)
|
|
- Refactor `set_lights_color` to update single light, return future
|
|
- Create `set_lights_color_parallel` using `futures::future::join_all`
|
|
|
|
## Concurrency Strategy
|
|
|
|
| Resource | Lock Type | Writer | Readers |
|
|
|---------------------|-----------|---------------------|----------------------------|
|
|
| current_screenshot | RwLock | Screenshot loop | Diff check |
|
|
| active_screenshot | RwLock | Screenshot loop | Color calc |
|
|
| target_color | RwLock | Color calc task | Smoothing loop |
|
|
| active_color | RwLock | Smoothing loop | Light update |
|
|
|
|
Each field has a single writer - no complex synchronization needed.
|
|
|
|
## File Changes Summary
|
|
|
|
| File | Changes |
|
|
|-------------------|----------------------------------------------|
|
|
| `settings.rs` | Add `screenshot_fps`, remove `transition` |
|
|
| `state.rs` | **New** - Shared state struct |
|
|
| `eventloop.rs` | **Rewrite** - Multi-threaded architecture |
|
|
| `homeassistant.rs`| Add parallel light update method |
|
|
| `main.rs` | Wire up new state, pass to eventloop |
|
|
|
|
|
|
|
|
|
|
|