From b7714744568bc46e3eb72ae3b278985bc5220524 Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Thu, 5 Mar 2026 17:01:13 +0100 Subject: [PATCH 1/2] Added bindings for the led_driver memory --- cflib2/__init__.py | 2 + cflib2/_rust.pyi | 90 ++++++++++++++++++++++++++++ rust/src/lib.rs | 3 +- rust/src/subsystems/memory.rs | 109 +++++++++++++++++++++++++++++++++- rust/src/subsystems/mod.rs | 2 +- 5 files changed, 202 insertions(+), 4 deletions(-) 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/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}; From d4c9cc17b358d2dccc4ae95e4b7563c3b7a35ffe Mon Sep 17 00:00:00 2001 From: ArisMorgens Date: Thu, 5 Mar 2026 17:01:35 +0100 Subject: [PATCH 2/2] Added led_ring example --- examples/led_ring.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 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..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())