diff --git a/cflib2/__init__.py b/cflib2/__init__.py index 5fa176a..dac0343 100644 --- a/cflib2/__init__.py +++ b/cflib2/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/_rust.pyi b/cflib2/_rust.pyi index b55d7cf..341b808 100644 --- a/cflib2/_rust.pyi +++ b/cflib2/_rust.pyi @@ -411,6 +411,10 @@ class Crazyflie: r""" Get the platform subsystem """ + def supervisor(self) -> Supervisor: + r""" + Get the supervisor subsystem + """ def __str__(self) -> builtins.str: ... def __repr__(self) -> builtins.str: ... @@ -428,30 +432,6 @@ class DisconnectedError(CrazyflieError): ... -@typing.final -class EmergencyControl: - r""" - Emergency control interface - - Provides emergency stop functionality that immediately stops all motors. - """ - async def send_emergency_stop(self) -> None: - r""" - Send emergency stop command - - Immediately stops all motors and puts the Crazyflie into a locked state. - The drone will require a reboot before it can fly again. - """ - async def send_emergency_stop_watchdog(self) -> None: - r""" - Send emergency stop watchdog - - Activates/resets a watchdog failsafe that will automatically emergency stop - the drone if this message isn't sent every 1000ms. Once activated by the first - call, you must continue sending this periodically forever or the drone will - automatically emergency stop. Use only if you need automatic failsafe behavior. - """ - @typing.final class ExternalPose: r""" @@ -797,10 +777,7 @@ class LedRingColor: 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 intensity(self, value: builtins.int) -> None: ... def __new__( cls, r: builtins.int = 0, @@ -815,7 +792,7 @@ class 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 """ def set( self, @@ -970,10 +947,6 @@ class Localization: r""" Localization subsystem wrapper """ - def emergency(self) -> EmergencyControl: - r""" - Get the emergency control interface - """ def external_pose(self) -> ExternalPose: r""" Get the external pose interface @@ -1480,22 +1453,6 @@ class Platform: As such, this shall only be used for test purpose in a controlled environment. """ - async def send_arming_request(self, do_arm: builtins.bool) -> None: - r""" - Send system arm/disarm request - - Arms or disarms the Crazyflie's safety systems. When disarmed, the motors - will not spin even if thrust commands are sent. - - # Arguments - * `do_arm` - true to arm, false to disarm - """ - async def send_crash_recovery_request(self) -> None: - r""" - Send crash recovery request - - Requests recovery from a crash state detected by the Crazyflie. - """ async def get_app_channel(self) -> typing.Optional[AppChannel]: r""" Get the bidirectional app channel for custom communication @@ -1578,6 +1535,143 @@ class ProtocolVersionNotSupportedError(CrazyflieError): ... +@typing.final +class Supervisor: + r""" + Supervisor subsystem + + Monitors the Crazyflie state and exposes arming, crash recovery, + and emergency stop controls. Obtain via `crazyflie.supervisor()`. + """ + async def read(self) -> SupervisorState: + r""" + Read a consistent snapshot of the supervisor state + + All flags on the returned snapshot are decoded from a single bitfield + read, so they are from the same moment and mutually consistent. Uses + time-based caching to avoid flooding the link. + + The snapshot does not update itself: re-read to get fresh state, for + example on every iteration when polling. + + Example: + state = await cf.supervisor().read() + if state.can_be_armed and not state.is_armed: + await cf.supervisor().send_arming_request(True) + + # When polling, read inside the loop: + while not (await cf.supervisor().read()).is_armed: + await asyncio.sleep(0.5) + """ + async def send_arming_request(self, do_arm: builtins.bool) -> None: + r""" + Send arm/disarm request + + Arms or disarms the Crazyflie's motors. When disarmed, the motors + will not spin even if thrust commands are sent. + + Args: + do_arm: True to arm, False to disarm + """ + async def send_crash_recovery_request(self) -> None: + r""" + Send crash recovery request + + Requests recovery from a crash state detected by the Crazyflie. + """ + async def send_emergency_stop(self) -> None: + r""" + Send emergency stop + + Immediately stops all motors and puts the Crazyflie into a locked state. + The drone will require a reboot before it can fly again. + """ + async def send_emergency_stop_watchdog(self) -> None: + r""" + Send emergency stop watchdog + + Activates/resets a watchdog failsafe that will automatically emergency + stop the drone if this message is not sent every 1000 ms. Once + activated, you must keep sending this periodically or the drone will + stop. Use only when you need automatic failsafe behaviour on + communication loss. + """ + +@typing.final +class SupervisorState: + r""" + A snapshot of the supervisor state + + Decoded from a single supervisor bitfield read: all flags on one snapshot + are from the same moment and mutually consistent. The snapshot does not + update itself - call `Supervisor.read()` again for fresh state. + """ + @property + def raw(self) -> builtins.int: + r""" + Raw bitfield value + """ + @property + def can_be_armed(self) -> builtins.bool: + r""" + The Crazyflie can be armed - will accept an arming command + """ + @property + def is_armed(self) -> builtins.bool: + r""" + The Crazyflie is armed + """ + @property + def is_auto_armed(self) -> builtins.bool: + r""" + The Crazyflie is configured to automatically arm + """ + @property + def can_fly(self) -> builtins.bool: + r""" + The Crazyflie is ready to fly + """ + @property + def is_flying(self) -> builtins.bool: + r""" + The Crazyflie is flying + """ + @property + def is_tumbled(self) -> builtins.bool: + r""" + The Crazyflie is tumbled (upside down) + """ + @property + def is_locked(self) -> builtins.bool: + r""" + The Crazyflie is in the locked state and must be restarted + """ + @property + def is_crashed(self) -> builtins.bool: + r""" + The Crazyflie has crashed + """ + @property + def hl_control_active(self) -> builtins.bool: + r""" + High level commander is actively flying the drone + """ + @property + def hl_traj_finished(self) -> builtins.bool: + r""" + High level commander trajectory has finished + """ + @property + def hl_control_disabled(self) -> builtins.bool: + r""" + High level commander is disabled and not producing setpoints + """ + def active_states(self) -> builtins.list[builtins.str]: + r""" + Names of all active states in this snapshot + """ + def __repr__(self) -> builtins.str: ... + class SystemError(CrazyflieError): r""" Async executor error. diff --git a/cflib2/commander.py b/cflib2/commander.py index 03d06b2..bf9b636 100644 --- a/cflib2/commander.py +++ b/cflib2/commander.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/console.py b/cflib2/console.py index 59f7781..f392c41 100644 --- a/cflib2/console.py +++ b/cflib2/console.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/error.py b/cflib2/error.py index 0706496..ae90760 100644 --- a/cflib2/error.py +++ b/cflib2/error.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/high_level_commander.py b/cflib2/high_level_commander.py index 25462e7..4a6bff1 100644 --- a/cflib2/high_level_commander.py +++ b/cflib2/high_level_commander.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/localization.py b/cflib2/localization.py index a8a41d3..2027145 100644 --- a/cflib2/localization.py +++ b/cflib2/localization.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ @@ -23,7 +21,6 @@ """Localization subsystem types""" from cflib2._rust import ( - EmergencyControl, ExternalPose, Lighthouse, LighthouseAngleData, @@ -33,7 +30,6 @@ ) __all__ = [ - "EmergencyControl", "ExternalPose", "Lighthouse", "LighthouseAngleData", diff --git a/cflib2/log.py b/cflib2/log.py index c30100f..9a86491 100644 --- a/cflib2/log.py +++ b/cflib2/log.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/memory.py b/cflib2/memory.py index df9e175..8d68865 100644 --- a/cflib2/memory.py +++ b/cflib2/memory.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/param.py b/cflib2/param.py index e7f0874..38b754f 100644 --- a/cflib2/param.py +++ b/cflib2/param.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/platform.py b/cflib2/platform.py index 6e1fa33..b643278 100644 --- a/cflib2/platform.py +++ b/cflib2/platform.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/cflib2/supervisor.py b/cflib2/supervisor.py new file mode 100644 index 0000000..10e385a --- /dev/null +++ b/cflib2/supervisor.py @@ -0,0 +1,25 @@ +# ,---------, ____ _ __ +# | ,-^-, | / __ )(_) /_______________ _____ ___ +# | ( 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 . +"""Supervisor subsystem types""" + +from cflib2._rust import Supervisor, SupervisorState + +__all__ = ["Supervisor", "SupervisorState"] diff --git a/cflib2/toc_cache.py b/cflib2/toc_cache.py index 88b0585..ee6016d 100644 --- a/cflib2/toc_cache.py +++ b/cflib2/toc_cache.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # ,---------, ____ _ __ # | ,-^-, | / __ )(_) /_______________ _____ ___ # | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ diff --git a/examples/arming.py b/examples/arming.py index 3f83551..a9868c8 100644 --- a/examples/arming.py +++ b/examples/arming.py @@ -54,7 +54,7 @@ async def main() -> None: cf = await Crazyflie.connect_from_uri(context, args.uri) print("Connected!") - platform = cf.platform() + supervisor = cf.supervisor() try: print("\n⚠️ WARNING: This will ARM the Crazyflie!") @@ -64,7 +64,7 @@ async def main() -> None: # Arm the Crazyflie print("\n1. Arming the Crazyflie...") - await platform.send_arming_request(do_arm=True) + await supervisor.send_arming_request(do_arm=True) print(" ✓ Armed! Motors can now spin.") # Wait a few seconds @@ -76,14 +76,14 @@ async def main() -> None: # Disarm the Crazyflie print("\n3. Disarming the Crazyflie...") - await platform.send_arming_request(do_arm=False) + await supervisor.send_arming_request(do_arm=False) print(" ✓ Disarmed! Motors are now disabled.") print("\n✓ Arming cycle complete!") except KeyboardInterrupt: print("\n\n⚠️ Interrupted! Disarming for safety...") - await platform.send_arming_request(do_arm=False) + await supervisor.send_arming_request(do_arm=False) print(" ✓ Disarmed!") finally: diff --git a/examples/emergency_stop.py b/examples/emergency_stop.py index 4152c3e..5c65249 100644 --- a/examples/emergency_stop.py +++ b/examples/emergency_stop.py @@ -55,10 +55,8 @@ async def main() -> None: cf = await Crazyflie.connect_from_uri(context, args.uri) print("Connected!") - platform = cf.platform() + supervisor = cf.supervisor() commander = cf.commander() - localization = cf.localization() - emergency = localization.emergency() try: print("\n⚠️ WARNING: This will ARM and SPIN the motors!") @@ -69,7 +67,7 @@ async def main() -> None: # Arm the Crazyflie print("\n1. Arming the Crazyflie...") - await platform.send_arming_request(do_arm=True) + await supervisor.send_arming_request(do_arm=True) await asyncio.sleep(0.3) print(" ✓ Armed!") @@ -91,7 +89,7 @@ async def main() -> None: # Send emergency stop print("\n4. Sending emergency stop command...") - await emergency.send_emergency_stop() + await supervisor.send_emergency_stop() await asyncio.sleep(0.5) print(" ✓ Emergency stop sent!") @@ -101,7 +99,7 @@ async def main() -> None: except KeyboardInterrupt: print("\n\n⚠️ Interrupted! Attempting to disarm for safety...") try: - await platform.send_arming_request(do_arm=False) + await supervisor.send_arming_request(do_arm=False) print(" ✓ Disarmed!") except Exception: print(" ⚠️ Could not disarm (may already be in emergency state)") diff --git a/examples/emergency_watchdog.py b/examples/emergency_watchdog.py index 9b6b13e..ecbd9fa 100644 --- a/examples/emergency_watchdog.py +++ b/examples/emergency_watchdog.py @@ -61,10 +61,8 @@ async def main() -> None: cf = await Crazyflie.connect_from_uri(context, args.uri) print("Connected!") - platform = cf.platform() + supervisor = cf.supervisor() commander = cf.commander() - localization = cf.localization() - emergency = localization.emergency() try: print("\n⚠️ WARNING: This will ARM and SPIN the motors!") @@ -75,7 +73,7 @@ async def main() -> None: # Arm the Crazyflie print("\n1. Arming the Crazyflie...") - await platform.send_arming_request(do_arm=True) + await supervisor.send_arming_request(do_arm=True) await asyncio.sleep(0.3) print(" ✓ Armed!") @@ -96,7 +94,7 @@ async def main() -> None: # Activate watchdog print("\n4. Activating watchdog (1000ms timeout)...") - await emergency.send_emergency_stop_watchdog() + await supervisor.send_emergency_stop_watchdog() print(" ✓ Watchdog activated!") print( @@ -111,7 +109,7 @@ async def main() -> None: sys.stdout.flush() # Send watchdog message - await emergency.send_emergency_stop_watchdog() + await supervisor.send_emergency_stop_watchdog() # Keep motors spinning and wait 800ms for _ in range(8): @@ -143,7 +141,7 @@ async def main() -> None: except KeyboardInterrupt: print("\n\n⚠️ Interrupted! Attempting to disarm for safety...") try: - await platform.send_arming_request(do_arm=False) + await supervisor.send_arming_request(do_arm=False) print(" ✓ Disarmed!") except Exception: print(" ⚠️ Could not disarm (may already be in emergency state)") diff --git a/examples/reading_supervisor.py b/examples/reading_supervisor.py new file mode 100644 index 0000000..4c5354a --- /dev/null +++ b/examples/reading_supervisor.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 . +""" +Reads the Crazyflie's supervisor state 20 times at 0.5 s intervals. + +Hold the Crazyflie in your hand and tilt it upside down to observe state +changes. Once the tilt exceeds ~90°, can_fly becomes False and is_tumbled +becomes True. + +Change the URI variable to your Crazyflie configuration. +""" + +import asyncio +from dataclasses import dataclass + +import tyro + +from cflib2 import Crazyflie, 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: + supervisor = cf.supervisor() + print("Reading supervisor state:") + for _ in range(20): + print("=" * 78) + # One read gives a consistent snapshot of all state flags + state = await supervisor.read() + print(f"Can fly: {state.can_fly}") + print(f"Is tumbled: {state.is_tumbled}") + print(f"Bitfield: 0x{state.raw:04x}") + print(f"Active states: {state.active_states()}") + print("=" * 78) + await asyncio.sleep(0.5) + finally: + print("Disconnecting...") + await cf.disconnect() + print("Done!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/trajectory.py b/examples/trajectory.py index 5c4b18d..ee7a29a 100644 --- a/examples/trajectory.py +++ b/examples/trajectory.py @@ -454,7 +454,7 @@ async def main() -> None: # Arm the Crazyflie print("Arming...") - await cf.platform().send_arming_request(True) + await cf.supervisor().send_arming_request(True) await asyncio.sleep(1.0) takeoff_yaw = 3.14 / 2 if args.relative_yaw else 0.0 diff --git a/rust/src/crazyflie.rs b/rust/src/crazyflie.rs index 1b2b4f9..67cddd3 100644 --- a/rust/src/crazyflie.rs +++ b/rust/src/crazyflie.rs @@ -28,7 +28,7 @@ use std::sync::Arc; use crate::error::to_pyerr; use crate::link_context::LinkContext; -use crate::subsystems::{Commander, Console, HighLevelCommander, Localization, Memory, Param, Platform, Log}; +use crate::subsystems::{Commander, Console, HighLevelCommander, Localization, Memory, Param, Platform, Log, Supervisor}; use crate::toc_cache::{NoTocCache, InMemoryTocCache, FileTocCache}; @@ -245,6 +245,13 @@ impl Crazyflie { } } + /// Get the supervisor subsystem + fn supervisor(&self) -> Supervisor { + Supervisor { + cf: self.inner.clone(), + } + } + /// The URI used to connect to this Crazyflie #[getter] fn uri(&self) -> &str { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3a23fa8..6c152f2 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -36,9 +36,10 @@ use crazyflie::Crazyflie; use link_context::LinkContext; use subsystems::{ Commander, Console, Log, LogBlock, LogData, LogStream, Param, PersistentParamState, Platform, AppChannel, - Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning, + Localization, ExternalPose, Lighthouse, LocoPositioning, LighthouseAngleData, LighthouseAngles, Memory, Poly, Poly4D, CompressedStart, CompressedSegment, LedRingColor, + Supervisor, SupervisorState, }; use toc_cache::{NoTocCache, InMemoryTocCache, FileTocCache}; @@ -58,7 +59,6 @@ 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::()?; @@ -70,6 +70,8 @@ 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::()?; m.add_class::()?; diff --git a/rust/src/subsystems/localization.rs b/rust/src/subsystems/localization.rs index 2be68ac..717e4ed 100644 --- a/rust/src/subsystems/localization.rs +++ b/rust/src/subsystems/localization.rs @@ -19,7 +19,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Localization subsystem - emergency stop, external pose, lighthouse, and loco positioning +//! Localization subsystem - external pose, lighthouse, and loco positioning use pyo3::prelude::*; use pyo3_stub_gen::derive::*; @@ -39,13 +39,6 @@ pub struct Localization { #[gen_stub_pymethods] #[pymethods] impl Localization { - /// Get the emergency control interface - fn emergency(&self) -> EmergencyControl { - EmergencyControl { - cf: self.cf.clone(), - } - } - /// Get the external pose interface fn external_pose(&self) -> ExternalPose { ExternalPose { @@ -66,49 +59,6 @@ impl Localization { } } -/// Emergency control interface -/// -/// Provides emergency stop functionality that immediately stops all motors. -#[gen_stub_pyclass] -#[pyclass] -pub struct EmergencyControl { - cf: Arc, -} - -#[gen_stub_pymethods] -#[pymethods] -impl EmergencyControl { - /// Send emergency stop command - /// - /// Immediately stops all motors and puts the Crazyflie into a locked state. - /// The drone will require a reboot before it can fly again. - #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] - fn send_emergency_stop<'py>(&self, py: Python<'py>) -> PyResult> { - let cf = self.cf.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - cf.supervisor.send_emergency_stop().await - .map_err(crate::error::to_pyerr)?; - Ok(()) - }) - } - - /// Send emergency stop watchdog - /// - /// Activates/resets a watchdog failsafe that will automatically emergency stop - /// the drone if this message isn't sent every 1000ms. Once activated by the first - /// call, you must continue sending this periodically forever or the drone will - /// automatically emergency stop. Use only if you need automatic failsafe behavior. - #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] - fn send_emergency_stop_watchdog<'py>(&self, py: Python<'py>) -> PyResult> { - let cf = self.cf.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - cf.supervisor.send_emergency_stop_watchdog().await - .map_err(crate::error::to_pyerr)?; - Ok(()) - }) - } -} - /// External pose interface /// /// Provides functionality to send external position and pose data from motion diff --git a/rust/src/subsystems/mod.rs b/rust/src/subsystems/mod.rs index 4f2a722..d0579b0 100644 --- a/rust/src/subsystems/mod.rs +++ b/rust/src/subsystems/mod.rs @@ -29,12 +29,14 @@ mod log; pub mod memory; mod param; mod platform; +mod supervisor; pub use commander::Commander; pub use console::Console; pub use high_level_commander::HighLevelCommander; -pub use localization::{Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning, LighthouseAngleData, LighthouseAngles}; +pub use localization::{Localization, ExternalPose, Lighthouse, LocoPositioning, LighthouseAngleData, LighthouseAngles}; pub use log::{Log, LogBlock, LogData, LogStream}; pub use memory::{Memory, Poly, Poly4D, CompressedStart, CompressedSegment, LedRingColor}; pub use param::{Param, PersistentParamState}; pub use platform::{Platform, AppChannel}; +pub use supervisor::{Supervisor, SupervisorState}; diff --git a/rust/src/subsystems/platform.rs b/rust/src/subsystems/platform.rs index 3cdc80b..f70f52b 100644 --- a/rust/src/subsystems/platform.rs +++ b/rust/src/subsystems/platform.rs @@ -102,34 +102,6 @@ impl Platform { }) } - /// Send system arm/disarm request - /// - /// Arms or disarms the Crazyflie's safety systems. When disarmed, the motors - /// will not spin even if thrust commands are sent. - /// - /// # Arguments - /// * `do_arm` - true to arm, false to disarm - #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] - fn send_arming_request<'py>(&self, py: Python<'py>, do_arm: bool) -> PyResult> { - let cf = self.cf.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - cf.supervisor.send_arming_request(do_arm).await.map_err(to_pyerr)?; - Ok(()) - }) - } - - /// Send crash recovery request - /// - /// Requests recovery from a crash state detected by the Crazyflie. - #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] - fn send_crash_recovery_request<'py>(&self, py: Python<'py>) -> PyResult> { - let cf = self.cf.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - cf.supervisor.send_crash_recovery_request().await.map_err(to_pyerr)?; - Ok(()) - }) - } - /// Get the bidirectional app channel for custom communication /// /// The app channel allows bidirectional communication between ground software diff --git a/rust/src/subsystems/supervisor.rs b/rust/src/subsystems/supervisor.rs new file mode 100644 index 0000000..79ef36f --- /dev/null +++ b/rust/src/subsystems/supervisor.rs @@ -0,0 +1,229 @@ +// ,---------, ____ _ __ +// | ,-^-, | / __ )(_) /_______________ _____ ___ +// | ( 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 . + +//! Supervisor subsystem - Crazyflie state, arming, crash recovery, and emergency stop + +use pyo3::prelude::*; +use pyo3_stub_gen::derive::*; +use std::sync::Arc; + +use crate::error::to_pyerr; + +/// Supervisor subsystem +/// +/// Monitors the Crazyflie state and exposes arming, crash recovery, +/// and emergency stop controls. Obtain via `crazyflie.supervisor()`. +#[gen_stub_pyclass] +#[pyclass] +pub struct Supervisor { + pub(crate) cf: Arc, +} + +#[gen_stub_pymethods] +#[pymethods] +impl Supervisor { + /// Read a consistent snapshot of the supervisor state + /// + /// All flags on the returned snapshot are decoded from a single bitfield + /// read, so they are from the same moment and mutually consistent. Uses + /// time-based caching to avoid flooding the link. + /// + /// The snapshot does not update itself: re-read to get fresh state, for + /// example on every iteration when polling. + /// + /// Example: + /// state = await cf.supervisor().read() + /// if state.can_be_armed and not state.is_armed: + /// await cf.supervisor().send_arming_request(True) + /// + /// # When polling, read inside the loop: + /// while not (await cf.supervisor().read()).is_armed: + /// await asyncio.sleep(0.5) + #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, SupervisorState]"))] + fn read<'py>(&self, py: Python<'py>) -> PyResult> { + let cf = self.cf.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let info = cf.supervisor.read_bitfield().await.map_err(to_pyerr)?; + Ok(SupervisorState { info }) + }) + } + + /// Send arm/disarm request + /// + /// Arms or disarms the Crazyflie's motors. When disarmed, the motors + /// will not spin even if thrust commands are sent. + /// + /// Args: + /// do_arm: True to arm, False to disarm + #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] + fn send_arming_request<'py>(&self, py: Python<'py>, do_arm: bool) -> PyResult> { + let cf = self.cf.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + cf.supervisor.send_arming_request(do_arm).await.map_err(to_pyerr)?; + Ok(()) + }) + } + + /// Send crash recovery request + /// + /// Requests recovery from a crash state detected by the Crazyflie. + #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] + fn send_crash_recovery_request<'py>(&self, py: Python<'py>) -> PyResult> { + let cf = self.cf.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + cf.supervisor.send_crash_recovery_request().await.map_err(to_pyerr)?; + Ok(()) + }) + } + + /// Send emergency stop + /// + /// Immediately stops all motors and puts the Crazyflie into a locked state. + /// The drone will require a reboot before it can fly again. + #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] + fn send_emergency_stop<'py>(&self, py: Python<'py>) -> PyResult> { + let cf = self.cf.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + cf.supervisor.send_emergency_stop().await.map_err(to_pyerr)?; + Ok(()) + }) + } + + /// Send emergency stop watchdog + /// + /// Activates/resets a watchdog failsafe that will automatically emergency + /// stop the drone if this message is not sent every 1000 ms. Once + /// activated, you must keep sending this periodically or the drone will + /// stop. Use only when you need automatic failsafe behaviour on + /// communication loss. + #[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))] + fn send_emergency_stop_watchdog<'py>(&self, py: Python<'py>) -> PyResult> { + let cf = self.cf.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + cf.supervisor.send_emergency_stop_watchdog().await.map_err(to_pyerr)?; + Ok(()) + }) + } +} + +/// A snapshot of the supervisor state +/// +/// Decoded from a single supervisor bitfield read: all flags on one snapshot +/// are from the same moment and mutually consistent. The snapshot does not +/// update itself - call `Supervisor.read()` again for fresh state. +#[gen_stub_pyclass] +#[pyclass(frozen)] +pub struct SupervisorState { + info: crazyflie_lib::subsystems::supervisor::SupervisorInfo, +} + +#[gen_stub_pymethods] +#[pymethods] +impl SupervisorState { + /// Raw bitfield value + #[getter] + fn raw(&self) -> u16 { + self.info.raw + } + + /// The Crazyflie can be armed - will accept an arming command + #[getter] + fn can_be_armed(&self) -> bool { + self.info.can_be_armed() + } + + /// The Crazyflie is armed + #[getter] + fn is_armed(&self) -> bool { + self.info.is_armed() + } + + /// The Crazyflie is configured to automatically arm + #[getter] + fn is_auto_armed(&self) -> bool { + self.info.is_auto_armed() + } + + /// The Crazyflie is ready to fly + #[getter] + fn can_fly(&self) -> bool { + self.info.can_fly() + } + + /// The Crazyflie is flying + #[getter] + fn is_flying(&self) -> bool { + self.info.is_flying() + } + + /// The Crazyflie is tumbled (upside down) + #[getter] + fn is_tumbled(&self) -> bool { + self.info.is_tumbled() + } + + /// The Crazyflie is in the locked state and must be restarted + #[getter] + fn is_locked(&self) -> bool { + self.info.is_locked() + } + + /// The Crazyflie has crashed + #[getter] + fn is_crashed(&self) -> bool { + self.info.is_crashed() + } + + /// High level commander is actively flying the drone + #[getter] + fn hl_control_active(&self) -> bool { + self.info.hl_control_active() + } + + /// High level commander trajectory has finished + #[getter] + fn hl_traj_finished(&self) -> bool { + self.info.hl_traj_finished() + } + + /// High level commander is disabled and not producing setpoints + #[getter] + fn hl_control_disabled(&self) -> bool { + self.info.hl_control_disabled() + } + + /// Names of all active states in this snapshot + fn active_states(&self) -> Vec { + self.info + .active_states() + .into_iter() + .map(|s| s.to_string()) + .collect() + } + + fn __repr__(&self) -> String { + format!( + "SupervisorState(raw=0x{:04x}, active=[{}])", + self.info.raw, + self.active_states().join(", ") + ) + } +}