From 016ebed02d1a3f5d4eb7ec87f8a1ee4fc6a2fe69 Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Thu, 5 Mar 2026 17:01:13 +0100 Subject: [PATCH 1/6] Added bindings for the led_driver memory --- cflib2/_rust.pyi | 90 ++++++++++++++++++++++++++++ cflib2/memory.py | 4 +- rust/src/lib.rs | 3 +- rust/src/subsystems/memory.rs | 109 +++++++++++++++++++++++++++++++++- rust/src/subsystems/mod.rs | 2 +- 5 files changed, 202 insertions(+), 6 deletions(-) diff --git a/cflib2/_rust.pyi b/cflib2/_rust.pyi index fb1ef42..a7ad243 100644 --- a/cflib2/_rust.pyi +++ b/cflib2/_rust.pyi @@ -736,6 +736,86 @@ class InvalidParameterError(CrazyflieError): ... +@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""" @@ -1096,6 +1176,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/cflib2/memory.py b/cflib2/memory.py index 5d4b1a4..73c3ab5 100644 --- a/cflib2/memory.py +++ b/cflib2/memory.py @@ -22,6 +22,6 @@ # along with this program. If not, see . """Memory subsystem types""" -from cflib2._rust import CompressedSegment, CompressedStart, Memory, Poly, Poly4D +from cflib2._rust import CompressedSegment, CompressedStart, LedRingColor, Memory, Poly, Poly4D -__all__ = ["CompressedSegment", "CompressedStart", "Memory", "Poly", "Poly4D"] +__all__ = ["CompressedSegment", "CompressedStart", "LedRingColor", "Memory", "Poly", "Poly4D"] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 604e7d6..3a23fa8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -38,7 +38,7 @@ use subsystems::{ Commander, Console, Log, LogBlock, LogData, LogStream, Param, PersistentParamState, 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}; @@ -69,6 +69,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 c72d664..4f2a722 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, PersistentParamState}; pub use platform::{Platform, AppChannel}; From d00cb9e8604202d3909c450a1a6facc3cbb940ec Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Thu, 5 Mar 2026 17:01:35 +0100 Subject: [PATCH 2/6] Added led_ring example --- examples/led_ring.py | 73 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 examples/led_ring.py diff --git a/examples/led_ring.py b/examples/led_ring.py new file mode 100644 index 0000000..30ceb46 --- /dev/null +++ b/examples/led_ring.py @@ -0,0 +1,73 @@ +# ,---------, ____ _ __ +# | ,-^-, | / __ )(_) /_______________ _____ ___ +# | ( 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, LinkContext +from cflib2.memory import LedRingColor + + +@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()) From 5d4df636b60e2ac6dedd36b20772372c436da893 Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Mon, 23 Mar 2026 14:48:37 +0100 Subject: [PATCH 3/6] Re-formated memory.py respecting the import order and the line length limit --- cflib2/memory.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cflib2/memory.py b/cflib2/memory.py index 73c3ab5..df9e175 100644 --- a/cflib2/memory.py +++ b/cflib2/memory.py @@ -22,6 +22,20 @@ # along with this program. If not, see . """Memory subsystem types""" -from cflib2._rust import CompressedSegment, CompressedStart, LedRingColor, Memory, Poly, Poly4D +from cflib2._rust import ( + CompressedSegment, + CompressedStart, + LedRingColor, + Memory, + Poly, + Poly4D, +) -__all__ = ["CompressedSegment", "CompressedStart", "LedRingColor", "Memory", "Poly", "Poly4D"] +__all__ = [ + "CompressedSegment", + "CompressedStart", + "LedRingColor", + "Memory", + "Poly", + "Poly4D", +] From cd0941181f329bccecf1c9aad6274bee7fb2649f Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Tue, 24 Mar 2026 11:18:03 +0100 Subject: [PATCH 4/6] Use rust lib's write time intensity clamping --- rust/src/subsystems/memory.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/src/subsystems/memory.rs b/rust/src/subsystems/memory.rs index ad556e8..88b83ce 100644 --- a/rust/src/subsystems/memory.rs +++ b/rust/src/subsystems/memory.rs @@ -51,7 +51,7 @@ pub struct LedRingColor { /// Blue component (0-255) #[pyo3(get, set)] b: u8, - /// Intensity percentage (0-100); values above 100 are clamped to 100 + /// Intensity percentage (0-100); values above 100 are clamped to 100 at write time #[pyo3(get, set)] intensity: u8, } @@ -65,11 +65,11 @@ impl LedRingColor { /// * `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 + /// * `intensity` - Intensity percentage (0-100, default 100); values above 100 are clamped to 100 at write time #[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) } + Self { r, g, b, intensity } } /// Set R/G/B and optionally intensity in one call. @@ -78,14 +78,14 @@ impl LedRingColor { /// * `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 + /// * `intensity` - Intensity percentage (0-100); if None, keeps current value; values above 100 are clamped to 100 at write time #[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); + self.intensity = i; } } } From 9b1d083f619249a09bb96a5ef35ea53386044a03 Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Tue, 24 Mar 2026 11:25:03 +0100 Subject: [PATCH 5/6] Split read and close memory into two separate awaited calls --- rust/src/subsystems/memory.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rust/src/subsystems/memory.rs b/rust/src/subsystems/memory.rs index 88b83ce..f55e105 100644 --- a/rust/src/subsystems/memory.rs +++ b/rust/src/subsystems/memory.rs @@ -450,8 +450,11 @@ impl Memory { 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)?; + let write_result = led_mem.write_leds().await.map_err(to_pyerr); + let close_result = cf.memory.close_memory(led_mem).await.map_err(to_pyerr); + + write_result?; + close_result?; Ok(()) }) From c1f16a513545244d2e7ddddb439804f96e2cd751 Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Wed, 25 Mar 2026 12:10:46 +0100 Subject: [PATCH 6/6] Clamp LED intensity at assignment --- rust/src/subsystems/memory.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rust/src/subsystems/memory.rs b/rust/src/subsystems/memory.rs index f55e105..03493ee 100644 --- a/rust/src/subsystems/memory.rs +++ b/rust/src/subsystems/memory.rs @@ -51,8 +51,8 @@ pub struct LedRingColor { /// Blue component (0-255) #[pyo3(get, set)] b: u8, - /// Intensity percentage (0-100); values above 100 are clamped to 100 at write time - #[pyo3(get, set)] + /// Intensity percentage (0-100); values above 100 are clamped to 100 + #[pyo3(get)] intensity: u8, } @@ -65,11 +65,16 @@ impl LedRingColor { /// * `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); values above 100 are clamped to 100 at write time + /// * `intensity` - Intensity percentage (0-100, default 100); values above 100 are clamped to 100 #[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 } + Self { r, g, b, intensity: intensity.min(100) } + } + + #[setter] + fn set_intensity(&mut self, value: u8) { + self.intensity = value.min(100); } /// Set R/G/B and optionally intensity in one call. @@ -78,14 +83,14 @@ impl LedRingColor { /// * `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; values above 100 are clamped to 100 at write time + /// * `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; + self.intensity = i.min(100); } } }