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..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, 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/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()) 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..03493ee 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,66 @@ 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)] + 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); 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: 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. + /// + /// # 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 +413,58 @@ 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; + } + + 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(()) + }) + } + /// 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};