diff --git a/README.md b/README.md
index 3bcc6cd97..28aa59118 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# asusctl for ASUS ROG
+# asusctl for ASUS
@@ -7,9 +7,6 @@
-> [!WARNING]
-> **Kernel Patch Requirement:** Many features are developed alongside Linux kernel updates. If an expected feature is missing, ensure your system is running the latest stable kernel or a kernel containing the required patches.
-
`asusctl` is a system control utility for Linux designed primarily for ASUS laptops, with reduced functionality available for non-ASUS hardware.
The project consists of three core components:
@@ -44,15 +41,19 @@ Devices displaying these hardware IDs typically function without extra configura
Features such as battery charge thresholds use generic kernel interfaces and work on non-ASUS hardware, but platform and fan controls require ASUS-specific `asus-nb-wmi` or `asus-armoury` drivers.
-### Kernel requirements
+> [NOTE]
+> For models that does not expose N-Key on lsusb, that mostly means your device need lamparray support. You can follow this [issue](https://github.com/OpenGamingCollective/asusctl/issues/119) for more info.
-Due to ongoing development, the minimum suggested kernel version is always **the latest**, as improvements are merged upstream continuously.
+### Kernel requirements
-Support for Thermal Design Power (TDP) is tied to the new `asus-armoury` driver: available mainline since Linux 6.19: everything older is not supported.
+> [WARNING]
+> **Kernel Patch Requirement:** Many features are developed alongside Linux kernel updates. Due to ongoing upstream development, the recommended kernel version is always **the latest stable release**.
+- **General features:** Require a recent upstream kernel or distribution kernel containing the necessary ASUS WMI patches.
+- **TDP & modern platform controls:** Require the `asus-armoury` driver, available in mainline Linux **6.19 or greater**. Older kernels are not supported for these features.
### Display server support (X11)
-> [!NOTE]
+> [NOTE]
> X11 is officially unsupported. Technical assistance is not provided for X11 environments due to developer resource constraints and the unmaintained status of X11 itself.
>
> Users who require X11 integration may compile the GUI application with X11 support enabled using `cargo build --features "rog-control-center/x11"`. Operation on unmaintained display servers remains the responsibility of the user.
@@ -63,17 +64,20 @@ Feature availability depends on upstream Linux kernel support and specific hardw
### Power and performance
-- [x] **Battery charge thresholds:** Configure maximum charging limits (requires kernel support)
+- [x] **Battery charge thresholds:** Configure maximum charging limits (requires kernel and EC support)
- [x] **Custom fan curves:** Adjust fan profiles on supported hardware
- [x] **GPU MUX toggling:** Switch GPU operational modes (G-Sync / MUX) on 2022 and newer laptops
- [x] **Power profile management:** Control system performance profiles as detailed in [MANUAL.md](MANUAL.md)
+> [NOTE]
+> The battery charge threshold requires a supported EC. The EC may not expose all the charging limit: that means you may be able to only limit it to 80% and not every percentage. See this [issue](https://github.com/OpenGamingCollective/asusctl/issues/153) for more info.
+
### Lighting and displays
- [x] **Built-in LED controls:** Adjust integrated keyboard lighting modes
- [x] **Per-key RGB configuration:** Customize individual key backlight settings
- [x] **Advanced lighting effects:** Apply custom animation modes (currently undergoing revision)
-- [x] **AniMe Matrix displays:** Control panel rendering on equipped G14, M16, and Strix Scar 16/18 models
+- [x] **AniMe Matrix displays:** Control panel rendering on equipped G14, M16, and Strix Scar 16/18 models
### System integration
@@ -83,7 +87,7 @@ Feature availability depends on upstream Linux kernel support and specific hardw
### Additional hardware configuration notes
-Keyboard backlight support relies on hardware mappings defined in [`./rog-aura/data/aura_support.ron`](./rog-aura/data/aura_support.ron), installed to `/usr/share/asusd/aura_support.ron`. Because keyboard controller configurations vary across model generations and firmware revisions, explicit layout definitions prevent misconfigurations. Refer to the [rog-aura README](./rog-aura/README.md) for configuration details.
+Keyboard backlight support relies on hardware mappings defined in [`aura_support.ron`](./rog-aura/data/aura_support.ron), installed to `/usr/share/asusd/aura_support.ron`. Because keyboard controller configurations vary across model generations and firmware revisions, explicit layout definitions prevent misconfigurations. Refer to the [rog-aura README](./rog-aura/README.md) for configuration details.
## Installation and setup
@@ -229,4 +233,4 @@ References to ASUS products, services, or trademarks within this repository do n
## AI Disclaimer
-We do not accept code blindly written with just AI or "vibecoding". We encourage use of AI for finding bugs and as a tool used to assist development, but all of these must be verified by a human as AI makes mistakes and gives false bug reports as well. For further details, refer to [our contribution policy](./CONTRIBUTING.md)
+We do not accept code blindly written with just AI or "vibecoding". We encourage use of AI for finding bugs and as a tool used to assist development, but all of these must be verified by a human as AI makes mistakes and gives false bug reports as well. For further details, refer to our [contribution policy](CONTRIBUTING.md)
diff --git a/design-patterns.md b/design-patterns.md
deleted file mode 100644
index 1557b6de5..000000000
--- a/design-patterns.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Daemon
-
-## Controller pattern
-
-There are a series of traits in the daemon for use with controller objects. Not all traits are required:
-
-- `Reloadable`, for controllers that need the ability to reload (typically on start)
-- `ZbusAdd`, for controllers that have zbus derive. These need to run on the zbus server.
-- `CtrlTask`, for controllers that need to run tasks every loop.
-- `GetSupported`, see if the hardware/functions this controller requires are supported.
-
-The first 3 trait objects get owned by the daemon methods that required them, which is why an `Arc>` is required.
-
-Generally the actual controller object will need to live in its own world as its own struct.
-Then for each trait that is required a new struct is required that can have the trait implemented, and that struct would have a reference to the main controller via `Arc>`.
-
-### Example
-
-Main controller:
-
-For a very simple controller that doesn't need exclusive access you can clone across threads
-
-```rust
-#[derive(Clone)]
-pub struct CtrlAnime {
-
- config: Arc>,
-}
-
-// This is the task trait used for such things as file watches, or logind
-// notifications (boot/suspend/shutdown etc)
-impl crate::CtrlTask for CtrlAnime {}
-
-// The trait to easily add the controller to Zbus to enable the zbus derived functions
-// to be polled, run, react etc.
-impl crate::ZbusAdd for CtrlAnime {}
-
-impl CtrlAnime {}
-```
-
- Otherwise, you will need to share the controller via mutex
-
-```rust
-pub struct CtrlAnime {
-
-}
-// Like this
-#[derive(Clone)]
-pub struct CtrlAnimeTask(Arc>);
-
-#[derive(Clone)]
-pub struct CtrlAnimeZbus(Arc>);
-
-impl CtrlAnime {}
-```
-
-The task trait:
-
-```rust
-// Mutex should always be async mutex
-pub struct CtrlAnimeTask(Arc>);
-
-impl crate::CtrlTask for CtrlAnimeTask {
- // This will run once only
- async fn create_tasks(&self, signal_ctxt: SignalContext<'static>) -> Result<(), RogError> {
- let lock self.inner.lock().await;
-
- Ok(())
- }
-
- // This will run until the notification stream closes (which in most cases will be never)
- async fn create_tasks(&self, signal_ctxt: SignalContext<'static>) -> Result<(), RogError> {
- let inner1 = self.inner.clone();
- let inner2 = self.inner.clone();
- let inner3 = self.inner.clone();
- let inner4 = self.inner.clone();
- // This is a free method on CtrlTask trait
- self.create_sys_event_tasks(
- // Loop is required to try an attempt to get the mutex *without* blocking
- // other threads - it is possible to end up with deadlocks otherwise.
- move || loop {
- if let Some(lock) = inner1.try_lock() {
- run_action(true, lock, inner1.clone());
- break;
- }
- },
- move || loop {
- if let Some(lock) = inner2.try_lock() {
- run_action(false, lock, inner2.clone());
- break;
- }
- },
- move || loop {
- if let Some(lock) = inner3.try_lock() {
- run_action(true, lock, inner3.clone());
- break;
- }
- },
- move || loop {
- if let Some(lock) = inner4.try_lock() {
- run_action(false, lock, inner4.clone());
- break;
- }
- },
- )
- .await;
- }
-}
-```
-
-The reloader trait
-
-```rust
-pub struct CtrlAnimeReloader(Arc>);
-
-impl crate::Reloadable for CtrlAnimeReloader {
- async fn reload(&mut self) -> Result<(), RogError> {
- let lock = self.inner.lock().await;
-
- Ok(())
- }
-}
-```
-
-The Zbus requirements:
-
-```rust
-pub struct CtrlAnimeZbus(Arc>);
-
-#[async_trait]
-impl crate::ZbusAdd for CtrlAnimeZbus {
- fn add_to_server(self, server: &mut zbus::ObjectServer) {
- // This is a provided free helper trait with pre-set body. It will move self in-to.
- Self::add_to_server_helper(self, "/org/asuslinux/Anime", server).await;
- }
-}
-
-#[dbus_interface(name = "xyz.ljones.Asusd")]
-impl CtrlAnimeZbus {
- async fn () {
- let lock = self.inner.lock().await;
-
- }
-}
-```
-
-The controller can then be added to the daemon parts as required.
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 4340c9385..afa7dc23b 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -25,6 +25,13 @@
- [asusctl](usage/asusctl.md)
+# Developers' docs
+
+- [Design patterns](docs/developers/design_patterns.md)
+- [GPU Mode Switching summary](docs/developers/gpu_mode_switching_summary.md)
+- [Developers' manual](docs/developers/manual.md)
+- [Packaging](docs/developers/packaging.md)
+
# FAQ
- [General](faq/general.md)
diff --git a/docs/developers/design_patterns.md b/docs/developers/design_patterns.md
new file mode 100644
index 000000000..e23f05dc6
--- /dev/null
+++ b/docs/developers/design_patterns.md
@@ -0,0 +1,242 @@
+# Daemon
+
+## Synchronous Controller Architecture
+
+Controllers in the daemon manage hardware features, user configurations, and platform events using a **purely synchronous (`sync`) concurrency and state model**.
+
+In Linux, kernel interactions via sysfs (`/sys`), debugfs, ACPI WMI, and raw device nodes are inherently synchronous and blocking file operations. Managing controller state with standard library synchronous primitives (`std::sync`) ensures predictable execution, eliminates async runtime overhead, and keeps domain logic straightforward.
+
+### Controller Traits
+
+Controllers can implement the standard daemon lifecycle and capability traits:
+
+- `GetSupported`: Checks if the hardware or kernel features required by the controller are supported on the current machine.
+- `Reloadable`: For controllers that need the ability to reload state (typically on startup or upon receiving configuration reload signals).
+- `CtrlTask`: For background workers handling system lifecycle events (boot, suspend, resume, shutdown) or watching sysfs/udev nodes.
+- `ZbusRun`: For exposing controller interfaces to the D-Bus system bus via `zbus`.
+
+Depending on the controller's complexity and concurrency requirements, these traits can be implemented directly on the controller struct or via dedicated wrappers.
+
+### Synchronous Concurrency & State Ownership Models
+
+When sharing state across controller traits, background threads, or D-Bus handlers, choose the concurrency model based on ownership and access patterns:
+
+- **Thread Worker / Actor Pattern (`std::thread` + `std::sync::mpsc`)**: Preferred for controllers that manage sequential hardware I/O (such as Aura RGB USB HID, AniMe Matrix, or event queues). A dedicated OS worker thread retains single-ownership of the physical device handle and processes incoming commands from a `std::sync::mpsc::channel`. This naturally avoids concurrent hardware access, serializes I/O, and eliminates deadlocks by construction.
+- **Shared Memory with Standard Synchronous Locks (`Arc>` / `Arc>`)**: Suitable for lightweight in-memory state (such as configuration files or cached status). Prefer `std::sync::RwLock` for read-heavy state to allow concurrent readers without lock contention, and `std::sync::Mutex` when writes are frequent.
+- **Lock-Free Synchronization with Atomics (`std::sync::atomic`)**: For simple status flags, counters, or mode indicators (e.g. device connection status, active power mode, suspension state), prefer atomic types (`AtomicBool`, `AtomicU32`, `AtomicUsize`) with appropriate memory ordering (`Ordering::Relaxed`, `Ordering::Acquire`, `Ordering::Release`) to eliminate lock contention and mutex allocations entirely.
+- **Static & Write-Once Initialization (`std::sync::LazyLock` / `std::sync::OnceLock`)**: For immutable lookup tables, regexes, and device capability maps initialized lazily or once at startup, use `std::sync::LazyLock` or `std::sync::OnceLock` instead of dynamic locking primitives.
+- **Fast Critical Sections (Copy-on-Read)**: Keep locked critical sections minimal. Acquire the lock, copy or extract the required value into a local variable, and immediately drop the lock guard before performing hardware I/O or long-running operations.
+- **No Spin Locks or Busy-Waiting**: Never use spin locks or polling loops (`loop { try_lock() }`) for shared state or event dispatching. Use blocking synchronization (`std::sync::mpsc::Receiver::recv()`, condition variables, or OS event polling) to avoid wasting CPU cycles.
+
+### Examples
+
+#### 1. Controller with Shared In-Memory State (`Arc>`)
+
+For controllers that manage device settings and respond to system events (e.g. `CtrlPlatform`), keep the controller cheaply cloneable by wrapping read-heavy configuration in `std::sync::RwLock` and passing references to underlying platform handles:
+
+```rust
+use std::sync::{Arc, RwLock};
+use zbus::Connection;
+use zbus::object_server::SignalEmitter;
+use crate::error::RogError;
+use crate::{CtrlTask, Reloadable, ZbusRun};
+
+#[derive(Clone)]
+pub struct CtrlPlatform {
+ platform: RogPlatform,
+ power: AsusPower,
+ config: Arc>,
+}
+
+// Zbus interface registration
+impl ZbusRun for CtrlPlatform {
+ async fn add_to_server(self, server: &mut Connection) {
+ Self::add_to_server_helper(self, "/xyz/ljones/Platform", server).await;
+ }
+}
+
+// Synchronous configuration reload handler
+impl Reloadable for CtrlPlatform {
+ async fn reload(&mut self) -> Result<(), RogError> {
+ // Read configuration and extract value immediately to minimize critical section
+ let charge_limit = self
+ .config
+ .read()
+ .map_err(|e| RogError::LockError(e.to_string()))?
+ .charge_control_end_threshold;
+
+ // Perform hardware sysfs write without holding the lock
+ self.power.set_charge_control_end_threshold(charge_limit)?;
+ Ok(())
+ }
+}
+
+// Background task and system lifecycle event handling
+impl CtrlTask for CtrlPlatform {
+ fn zbus_path() -> &'static str {
+ "/xyz/ljones/Platform"
+ }
+
+ async fn create_tasks(&self, _signal_ctxt: SignalEmitter<'static>) -> Result<(), RogError> {
+ let ctrl = self.clone();
+
+ // Register event handlers for system lifecycle events
+ self.create_sys_event_tasks(
+ move |sleeping| {
+ let ctrl = ctrl.clone();
+ async move {
+ if !sleeping {
+ // Re-apply charge limit when resuming from sleep
+ if let Ok(config) = ctrl.config.read() {
+ let limit = config.charge_control_end_threshold;
+ drop(config);
+ ctrl.power.set_charge_control_end_threshold(limit).ok();
+ }
+ }
+ }
+ },
+ move |_shutdown| async move { /* handle shutdown */ },
+ move |_lid_closed| async move { /* handle lid switch event */ },
+ move |_on_ac| async move { /* handle AC/battery power transition */ },
+ )
+ .await;
+
+ Ok(())
+ }
+}
+```
+
+#### 2. Synchronous Worker Thread for Hardware I/O (`std::thread` + `std::sync::mpsc`)
+
+For hardware devices that require exclusive, serialized access (e.g. Aura RGB USB HID, AniMe Matrix animations), use a dedicated background OS worker thread with `std::sync::mpsc`:
+
+```rust
+use std::sync::mpsc::{self, Sender, SyncSender};
+use std::thread;
+use zbus::interface;
+use crate::error::RogError;
+
+enum DeviceMsg {
+ SetBrightness {
+ brightness: u8,
+ reply: Sender>,
+ },
+ SetMode {
+ mode: AuraMode,
+ reply: Sender>,
+ },
+}
+
+#[derive(Clone)]
+pub struct CtrlAura {
+ tx: SyncSender,
+}
+
+impl CtrlAura {
+ pub fn new(mut device: AuraDevice) -> Self {
+ let (tx, rx) = mpsc::sync_channel::(32);
+
+ // Dedicated OS worker thread retains single-ownership of the physical device
+ thread::Builder::new()
+ .name("aura-worker".into())
+ .spawn(move || {
+ // Blocking loop waits for incoming commands without busy-waiting
+ while let Ok(msg) = rx.recv() {
+ match msg {
+ DeviceMsg::SetBrightness { brightness, reply } => {
+ let res = device.write_brightness(brightness);
+ let _ = reply.send(res);
+ }
+ DeviceMsg::SetMode { mode, reply } => {
+ let res = device.write_mode(mode);
+ let _ = reply.send(res);
+ }
+ }
+ }
+ })
+ .expect("Failed to spawn aura-worker thread");
+
+ Self { tx }
+ }
+}
+
+#[interface(name = "xyz.ljones.Aura")]
+impl CtrlAura {
+ async fn set_brightness(&self, val: u8) -> zbus::fdo::Result<()> {
+ let (reply_tx, reply_rx) = mpsc::channel();
+
+ self.tx
+ .send(DeviceMsg::SetBrightness {
+ brightness: val,
+ reply: reply_tx,
+ })
+ .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
+
+ // Wait for synchronous worker response
+ reply_rx
+ .recv()
+ .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?
+ .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
+ }
+}
+```
+
+#### 3. Lock-Free Status & Direct Kernel Sysfs Management (`std::sync::atomic`)
+
+For controllers with simple state flags or high-frequency status queries, combine lock-free atomics with synchronous in-memory caching:
+
+```rust
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex};
+use zbus::interface;
+use crate::error::RogError;
+
+#[derive(Clone)]
+pub struct CtrlXgm {
+ // Lock-free atomic indicator for high-frequency status queries
+ is_active: Arc,
+ // Synchronous mutex for fast in-memory caching
+ cached_name: Arc>>,
+}
+
+impl CtrlXgm {
+ pub fn new() -> Self {
+ Self {
+ is_active: Arc::new(AtomicBool::new(false)),
+ cached_name: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ /// Fast lock-free query with zero lock contention
+ pub fn is_active(&self) -> bool {
+ self.is_active.load(Ordering::Acquire)
+ }
+
+ /// Fast synchronous cache update without async lock allocation
+ pub fn update_cached_name(&self, name: String) {
+ if let Ok(mut lock) = self.cached_name.lock() {
+ *lock = Some(name);
+ }
+ }
+
+ /// Synchronous sysfs kernel interaction
+ pub fn write_xgm_active(&self, active: bool) -> Result<(), RogError> {
+ std::fs::write(
+ "/sys/devices/platform/asus-nb-wmi/xgm_active",
+ if active { "1" } else { "0" },
+ )
+ .map_err(|e| RogError::SysfsWrite(e.to_string()))?;
+
+ self.is_active.store(active, Ordering::Release);
+ Ok(())
+ }
+}
+
+#[interface(name = "xyz.ljones.Xgm")]
+impl CtrlXgm {
+ async fn set_active(&self, active: bool) -> zbus::fdo::Result<()> {
+ self.write_xgm_active(active)
+ .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
+ }
+}
+```
diff --git a/GPU_MODE_SWITCHING_SUMMARY.md b/docs/developers/gpu_mode_switching_summary.md
similarity index 100%
rename from GPU_MODE_SWITCHING_SUMMARY.md
rename to docs/developers/gpu_mode_switching_summary.md
diff --git a/MANUAL.md b/docs/developers/manual.md
similarity index 100%
rename from MANUAL.md
rename to docs/developers/manual.md
diff --git a/PACKAGING.md b/docs/developers/packaging.md
similarity index 100%
rename from PACKAGING.md
rename to docs/developers/packaging.md