diff --git a/cflib2/__init__.py b/cflib2/__init__.py
index 322777f..e14afc1 100644
--- a/cflib2/__init__.py
+++ b/cflib2/__init__.py
@@ -25,6 +25,7 @@
from cflib2._rust import (
Crazyflie,
LinkContext,
+ LedRingColor,
# TOC cache classes (passed to Crazyflie.connect_from_uri)
NoTocCache,
InMemoryTocCache,
@@ -34,6 +35,7 @@
__all__ = [
"Crazyflie",
"LinkContext",
+ "LedRingColor",
"NoTocCache",
"InMemoryTocCache",
"FileTocCache",
diff --git a/cflib2/_rust.pyi b/cflib2/_rust.pyi
index e39c969..252eb89 100644
--- a/cflib2/_rust.pyi
+++ b/cflib2/_rust.pyi
@@ -657,6 +657,86 @@ class InMemoryTocCache:
Get the number of cached TOCs
"""
+@typing.final
+class LedRingColor:
+ r"""
+ A single LED color and intensity for the Crazyflie LED ring.
+
+ Used to build the list of 12 LED values passed to `Memory.write_led_ring()`.
+ """
+ @property
+ def r(self) -> builtins.int:
+ r"""
+ Red component (0-255)
+ """
+ @r.setter
+ def r(self, value: builtins.int) -> None:
+ r"""
+ Red component (0-255)
+ """
+ @property
+ def g(self) -> builtins.int:
+ r"""
+ Green component (0-255)
+ """
+ @g.setter
+ def g(self, value: builtins.int) -> None:
+ r"""
+ Green component (0-255)
+ """
+ @property
+ def b(self) -> builtins.int:
+ r"""
+ Blue component (0-255)
+ """
+ @b.setter
+ def b(self, value: builtins.int) -> None:
+ r"""
+ Blue component (0-255)
+ """
+ @property
+ def intensity(self) -> builtins.int:
+ r"""
+ Intensity percentage (0-100); values above 100 are clamped to 100
+ """
+ @intensity.setter
+ def intensity(self, value: builtins.int) -> None:
+ r"""
+ Intensity percentage (0-100); values above 100 are clamped to 100
+ """
+ def __new__(
+ cls,
+ r: builtins.int = 0,
+ g: builtins.int = 0,
+ b: builtins.int = 0,
+ intensity: builtins.int = 100,
+ ) -> LedRingColor:
+ r"""
+ Create a new LedRingColor.
+
+ # Arguments
+ * `r` - Red component (0-255, default 0)
+ * `g` - Green component (0-255, default 0)
+ * `b` - Blue component (0-255, default 0)
+ * `intensity` - Intensity percentage (0-100, default 100); clamped to 100 if higher
+ """
+ def set(
+ self,
+ r: builtins.int,
+ g: builtins.int,
+ b: builtins.int,
+ intensity: typing.Optional[builtins.int] = None,
+ ) -> None:
+ r"""
+ Set R/G/B and optionally intensity in one call.
+
+ # Arguments
+ * `r` - Red component (0-255)
+ * `g` - Green component (0-255)
+ * `b` - Blue component (0-255)
+ * `intensity` - Intensity percentage (0-100); if None, keeps current value; clamped to 100 if higher
+ """
+
@typing.final
class Lighthouse:
r"""
@@ -985,6 +1065,16 @@ class Memory:
* `segments` - List of CompressedSegment instances
* `start_addr` - Address in trajectory memory (default 0)
"""
+ async def write_led_ring(self, leds: typing.Sequence[LedRingColor]) -> None:
+ r"""
+ Write LED colors to the Crazyflie LED ring.
+
+ Opens the LED driver memory, sets all 12 LED values, writes them to
+ the ring, and closes the memory.
+
+ # Arguments
+ * `leds` - List of exactly 12 LedRingColor instances
+ """
def get_memories(
self, memory_type: typing.Optional[builtins.int] = None
) -> builtins.list[tuple[builtins.int, builtins.int, builtins.int]]:
diff --git a/examples/led_ring.py b/examples/led_ring.py
new file mode 100644
index 0000000..bd42e6c
--- /dev/null
+++ b/examples/led_ring.py
@@ -0,0 +1,72 @@
+# ,---------, ____ _ __
+# | ,-^-, | / __ )(_) /_______________ _____ ___
+# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
+# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
+# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
+#
+# Copyright (C) 2026 Bitcraze AB
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+"""
+Simple example that connects to the crazyflie at `URI` and writes to
+the LED memory so that individual leds in the LED-ring can be set,
+it has been tested with (and designed for) the LED-ring deck.
+
+Change the URI variable to your Crazyflie configuration.
+"""
+
+import asyncio
+from dataclasses import dataclass
+
+import tyro
+
+from cflib2 import Crazyflie, LedRingColor, LinkContext
+
+
+@dataclass
+class Args:
+ uri: str = "radio://0/80/2M/E7E7E7E7E7"
+ """Crazyflie URI"""
+
+
+async def main() -> None:
+ args = tyro.cli(Args)
+
+ print(f"Connecting to {args.uri}...")
+ ctx = LinkContext()
+ cf = await Crazyflie.connect_from_uri(ctx, args.uri)
+ print("Connected!")
+
+ try:
+ # Set virtual mem effect
+ await cf.param().set("ring.effect", 13)
+
+ # Build LED list and set individual LEDs
+ leds = [LedRingColor() for _ in range(12)]
+ leds[0].set(r=0, g=100, b=0)
+ leds[3].set(r=0, g=0, b=100)
+ leds[6].set(r=100, g=0, b=0)
+ leds[9].set(r=100, g=100, b=100)
+ await cf.memory().write_led_ring(leds)
+
+ await asyncio.sleep(2)
+
+ finally:
+ print("Disconnecting...")
+ await cf.disconnect()
+ print("Done!")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index f8287eb..7ae6fee 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -37,7 +37,7 @@ use subsystems::{
Commander, Console, Log, LogBlock, LogData, LogStream, Param, Platform, AppChannel,
Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning,
LighthouseAngleData, LighthouseAngles,
- Memory, Poly, Poly4D, CompressedStart, CompressedSegment,
+ Memory, Poly, Poly4D, CompressedStart, CompressedSegment, LedRingColor,
};
use toc_cache::{NoTocCache, InMemoryTocCache, FileTocCache};
@@ -67,6 +67,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
diff --git a/rust/src/subsystems/memory.rs b/rust/src/subsystems/memory.rs
index e366966..ad556e8 100644
--- a/rust/src/subsystems/memory.rs
+++ b/rust/src/subsystems/memory.rs
@@ -21,10 +21,11 @@
//! # Memory subsystem bindings
//!
-//! Provides Python bindings for trajectory memory operations.
+//! Provides Python bindings for memory operations.
//! Trajectory data is built in Python using [`Poly`], [`Poly4D`],
//! [`CompressedStart`], and [`CompressedSegment`], then uploaded
-//! via the [`Memory`] subsystem.
+//! via the [`Memory`] subsystem. LED ring colors are set using
+//! [`LedRingColor`] and written via [`Memory::write_led_ring`].
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
@@ -34,6 +35,61 @@ use std::sync::Arc;
use crate::error::to_pyerr;
use crazyflie_lib::subsystems::memory::MemoryType;
+/// A single LED color and intensity for the Crazyflie LED ring.
+///
+/// Used to build the list of 12 LED values passed to `Memory.write_led_ring()`.
+#[gen_stub_pyclass]
+#[pyclass]
+#[derive(Clone, Debug)]
+pub struct LedRingColor {
+ /// Red component (0-255)
+ #[pyo3(get, set)]
+ r: u8,
+ /// Green component (0-255)
+ #[pyo3(get, set)]
+ g: u8,
+ /// Blue component (0-255)
+ #[pyo3(get, set)]
+ b: u8,
+ /// Intensity percentage (0-100); values above 100 are clamped to 100
+ #[pyo3(get, set)]
+ intensity: u8,
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl LedRingColor {
+ /// Create a new LedRingColor.
+ ///
+ /// # Arguments
+ /// * `r` - Red component (0-255, default 0)
+ /// * `g` - Green component (0-255, default 0)
+ /// * `b` - Blue component (0-255, default 0)
+ /// * `intensity` - Intensity percentage (0-100, default 100); clamped to 100 if higher
+ #[new]
+ #[pyo3(signature = (r=0, g=0, b=0, intensity=100))]
+ fn new(r: u8, g: u8, b: u8, intensity: u8) -> Self {
+ Self { r, g, b, intensity: intensity.min(100) }
+ }
+
+ /// Set R/G/B and optionally intensity in one call.
+ ///
+ /// # Arguments
+ /// * `r` - Red component (0-255)
+ /// * `g` - Green component (0-255)
+ /// * `b` - Blue component (0-255)
+ /// * `intensity` - Intensity percentage (0-100); if None, keeps current value; clamped to 100 if higher
+ #[pyo3(signature = (r, g, b, intensity=None))]
+ fn set(&mut self, r: u8, g: u8, b: u8, intensity: Option) {
+ self.r = r;
+ self.g = g;
+ self.b = b;
+ if let Some(i) = intensity {
+ self.intensity = i.min(100);
+ }
+ }
+}
+
/// A polynomial with up to 8 coefficients.
///
/// Coefficients beyond the provided values are zero-filled.
@@ -352,6 +408,55 @@ impl Memory {
})
}
+ /// Write LED colors to the Crazyflie LED ring.
+ ///
+ /// Opens the LED driver memory, sets all 12 LED values, writes them to
+ /// the ring, and closes the memory.
+ ///
+ /// # Arguments
+ /// * `leds` - List of exactly 12 LedRingColor instances
+ #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))]
+ fn write_led_ring<'py>(
+ &self,
+ py: Python<'py>,
+ leds: Vec,
+ ) -> PyResult> {
+ let cf = self.cf.clone();
+ pyo3_async_runtimes::tokio::future_into_py(py, async move {
+ if leds.len() != 12 {
+ return Err(PyValueError::new_err(
+ format!("Expected 12 LEDs, got {}", leds.len())
+ ));
+ }
+
+ let memories = cf.memory.get_memories(Some(MemoryType::DriverLed));
+ let mem_device = (*memories.first()
+ .ok_or_else(|| to_pyerr(crazyflie_lib::Error::MemoryError(
+ "No LED driver memory found on Crazyflie".to_owned()
+ )))?)
+ .clone();
+
+ let mut led_mem: crazyflie_lib::subsystems::memory::LedDriverMemory =
+ cf.memory.initialize_memory(mem_device).await
+ .ok_or_else(|| to_pyerr(crazyflie_lib::Error::MemoryError(
+ "Failed to open LED driver memory".to_owned()
+ )))?
+ .map_err(to_pyerr)?;
+
+ for (i, led) in leds.iter().enumerate() {
+ led_mem.leds[i].r = led.r;
+ led_mem.leds[i].g = led.g;
+ led_mem.leds[i].b = led.b;
+ led_mem.leds[i].intensity = led.intensity;
+ }
+
+ led_mem.write_leds().await.map_err(to_pyerr)?;
+ cf.memory.close_memory(led_mem).await.map_err(to_pyerr)?;
+
+ Ok(())
+ })
+ }
+
/// List all memories available on the Crazyflie.
///
/// Returns a list of tuples `(id, type, size)`:
diff --git a/rust/src/subsystems/mod.rs b/rust/src/subsystems/mod.rs
index e9d8be6..7eb6c6c 100644
--- a/rust/src/subsystems/mod.rs
+++ b/rust/src/subsystems/mod.rs
@@ -35,6 +35,6 @@ pub use console::Console;
pub use high_level_commander::HighLevelCommander;
pub use localization::{Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning, LighthouseAngleData, LighthouseAngles};
pub use log::{Log, LogBlock, LogData, LogStream};
-pub use memory::{Memory, Poly, Poly4D, CompressedStart, CompressedSegment};
+pub use memory::{Memory, Poly, Poly4D, CompressedStart, CompressedSegment, LedRingColor};
pub use param::Param;
pub use platform::{Platform, AppChannel};