diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1f39fdab..f14a88b4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: - uses: rust3ds/test-runner/setup@v1 with: - toolchain: nightly-2024-02-18 + toolchain: nightly-2024-03-10 - name: Build workspace docs run: cargo 3ds --verbose doc --verbose --no-deps --workspace diff --git a/ctru-rs/examples/ptm-user.rs b/ctru-rs/examples/ptm-user.rs new file mode 100644 index 00000000..00c2443a --- /dev/null +++ b/ctru-rs/examples/ptm-user.rs @@ -0,0 +1,46 @@ +//! Power-Time Services example. +//! +//! This example shows off common functionality found in the PTM family of system services, like pedometer steps count, battery state reading +//! and some light shows with the notification LED. + +use ctru::prelude::*; +use ctru::services::ptm::user::{BatteryLevel, PTMUser}; + +fn main() { + let apt = Apt::new().unwrap(); + let mut hid = Hid::new().unwrap(); + let gfx = Gfx::new().unwrap(); + let _top_screen = Console::new(gfx.top_screen.borrow_mut()); + + let ptm_user = PTMUser::new().unwrap(); + + // Let's gather some simple data with PTM:User + let battery_level = ptm_user.battery_level().unwrap(); + let charging = ptm_user.is_charging().unwrap(); + let steps = ptm_user.step_count().unwrap(); + + if battery_level >= BatteryLevel::Low { + println!("The battery level is sufficient to play a while.") + } else { + println!("The battery level is low.") + } + + if charging { + println!("The battery is currently charging.") + } else { + println!("The battery is discharging.") + } + + println!("You accumulated a total of {steps} steps."); + + println!("\x1b[29;16HPress Start to exit"); + + while apt.main_loop() { + gfx.wait_for_vblank(); + + hid.scan_input(); + if hid.keys_down().contains(KeyPad::START) { + break; + } + } +} diff --git a/ctru-rs/src/services/mod.rs b/ctru-rs/src/services/mod.rs index e4ddb449..5638256c 100644 --- a/ctru-rs/src/services/mod.rs +++ b/ctru-rs/src/services/mod.rs @@ -22,6 +22,7 @@ pub mod hid; pub mod ir_user; pub mod ndsp; pub mod ps; +pub mod ptm; mod reference; pub mod soc; pub mod sslc; diff --git a/ctru-rs/src/services/ptm/mod.rs b/ctru-rs/src/services/ptm/mod.rs new file mode 100644 index 00000000..6d561055 --- /dev/null +++ b/ctru-rs/src/services/ptm/mod.rs @@ -0,0 +1,11 @@ +//! Power-Time service. +//! +//! This service manages user information such as registered playtime, step count (using the pedometer) and control to various +//! hardware and features to notify the user during play (such as the Notification/Info LED). +#![doc(alias = "led")] +#![doc(alias = "playtime")] +#![doc(alias = "step")] +#![doc(alias = "power")] + +pub mod sysm; +pub mod user; diff --git a/ctru-rs/src/services/ptm/sysm.rs b/ctru-rs/src/services/ptm/sysm.rs new file mode 100644 index 00000000..e437c763 --- /dev/null +++ b/ctru-rs/src/services/ptm/sysm.rs @@ -0,0 +1,150 @@ +//! PTM SystemMenu service. +//! +//! This sub-service of the Power-Time service family is able to control shutdown/sleep functionality and how those states are +//! communicated to the user (such as via the notification/battery LED). +#[doc(alias = "sleep")] +#[doc(alias = "shutdown")] +#[doc(alias = "led")] +use std::sync::Mutex; +use std::time::Duration; + +use crate::error::{Result, ResultCode}; +use crate::services::ServiceReference; + +static PTMSYSM_ACTIVE: Mutex<()> = Mutex::new(()); + +/// Handle to the PTM:SysM service. +pub struct PTMSysM { + _service_handler: ServiceReference, +} + +impl PTMSysM { + /// Initialize a new service handle. + /// + /// # Errors + /// + /// This function will return an error if the service was unable to be initialized. + /// Since this service requires no special or elevated permissions, errors are rare in practice. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::sysm::PTMSysM; + /// + /// let ptm_sysm = PTMSysM::new()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "ptmSysmInit")] + pub fn new() -> Result { + let handler = ServiceReference::new( + &PTMSYSM_ACTIVE, + || { + ResultCode(unsafe { ctru_sys::ptmSysmInit() })?; + + Ok(()) + }, + || unsafe { + ctru_sys::ptmSysmExit(); + }, + )?; + + Ok(Self { + _service_handler: handler, + }) + } + + /// Try putting the console in sleep mode. + /// + /// # Notes + /// + /// This request can be denied for various reasons. This does not "force" the console to sleep. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::sysm::PTMSysM; + /// use std::time::Duration; + /// + /// let ptm_sysm = PTMSysM::new()?; + /// + /// // Request the activation of sleep mode. + /// ptm_sysm.request_sleep().unwrap(); + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMSYSM_RequestSleep")] + pub fn request_sleep(&self) -> Result<()> { + ResultCode(unsafe { ctru_sys::PTMSYSM_RequestSleep() })?; + + Ok(()) + } + + /// Request a system shutdown within the given timeout. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::sysm::PTMSysM; + /// use std::time::Duration; + /// + /// let ptm_sysm = PTMSysM::new()?; + /// + /// // Shutdown the system (usually the request succeeds immediately). + /// ptm_sysm.request_shutdown(Duration::from_nanos(0)).unwrap(); + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMSYSM_ShutdownAsync")] + pub fn request_shutdown(&self, timeout: Duration) -> Result<()> { + let timeout = timeout.as_nanos() as u64; + + ResultCode(unsafe { ctru_sys::PTMSYSM_ShutdownAsync(timeout) })?; + + Ok(()) + } + + /// Request a system reboot within the given timeout. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::sysm::PTMSysM; + /// use std::time::Duration; + /// + /// let ptm_sysm = PTMSysM::new()?; + /// + /// // Reboot the system. + /// ptm_sysm.request_reboot(Duration::from_nanos(0)).unwrap(); + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMSYSM_RebootAsync")] + pub fn request_reboot(&self, timeout: Duration) -> Result<()> { + let timeout = timeout.as_nanos() as u64; + + ResultCode(unsafe { ctru_sys::PTMSYSM_RebootAsync(timeout) })?; + + Ok(()) + } +} diff --git a/ctru-rs/src/services/ptm/user.rs b/ctru-rs/src/services/ptm/user.rs new file mode 100644 index 00000000..890b5fa3 --- /dev/null +++ b/ctru-rs/src/services/ptm/user.rs @@ -0,0 +1,251 @@ +//! PTM User service. +//! +//! This sub-service of the Power-Time service family includes getters for various hardware/console states, such as whether +//! the console [shell is open](PTMUser::shell_state) or the current [battery level](PTMUser::battery_level). +#[doc(alias = "battery")] +#[doc(alias = "shell")] +use std::sync::Mutex; + +use crate::error::{Error, Result, ResultCode}; +use crate::services::ServiceReference; + +static PTMU_ACTIVE: Mutex<()> = Mutex::new(()); + +/// Whether the console's shell is open or closed. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ShellState { + /// Clam shell is currently closed. + Closed = 0, + /// Clam shell is currently open. + Open = 1, +} + +/// Representation of the console's battery charge level. +/// +/// These values correspond to the various states the battery is shown to be in the Home Menu UI. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum BatteryLevel { + /// Battery charge at 0%. System shutdown is imminent. + Drained = 0, + /// Battery charge between 5-1%. + Critical = 1, + /// Battery charge between 10-6%. + VeryLow = 2, + /// Battery charge between 30-11%. + Low = 3, + /// Battery charge between 60-31%. + Medium = 4, + /// Battery charge between 100-61%. + High = 5, +} + +/// Handle to the PTM:User service. +pub struct PTMUser { + _service_handler: ServiceReference, +} + +impl PTMUser { + /// Initialize a new service handle. + /// + /// # Errors + /// + /// This function will return an error if the service was unable to be initialized. + /// Since this service requires no special or elevated permissions, errors are rare in practice. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::user::PTMUser; + /// + /// let ptmu = PTMUser::new()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "ptmuInit")] + pub fn new() -> Result { + let handler = ServiceReference::new( + &PTMU_ACTIVE, + || { + ResultCode(unsafe { ctru_sys::ptmuInit() })?; + + Ok(()) + }, + || unsafe { + ctru_sys::ptmuExit(); + }, + )?; + + Ok(Self { + _service_handler: handler, + }) + } + + /// Returns whether the console's clamshell is closed or open. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::user::{PTMUser, ShellState}; + /// + /// let ptmu = PTMUser::new()?; + /// + /// let state = ptmu.shell_state()?; + /// + /// match state { + /// ShellState::Closed => println!("The shell is closed! How are you able to read this?"), + /// ShellState::Open => println!("The shell is open! That might seem obvious to you."), + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMU_GetShellState")] + pub fn shell_state(&self) -> Result { + let mut state: u8 = 0; + + ResultCode(unsafe { ctru_sys::PTMU_GetShellState(&mut state) })?; + + state.try_into() + } + + /// Returns the console's current battery charge level. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::user::{PTMUser, BatteryLevel}; + /// + /// let ptmu = PTMUser::new()?; + /// + /// let charge = ptmu.battery_level()?; + /// + /// if charge <= BatteryLevel::Low { + /// println!("You should put the console to charge!"); + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMU_GetBatteryLevel")] + pub fn battery_level(&self) -> Result { + let mut level: u8 = 0; + + ResultCode(unsafe { ctru_sys::PTMU_GetBatteryLevel(&mut level) })?; + + level.try_into() + } + + /// Returns whether the console is currently charging its battery. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::user::PTMUser; + /// + /// let ptmu = PTMUser::new()?; + /// + /// let is_charging = ptmu.is_charging()?; + /// + /// if is_charging { + /// println!("That is one juicy power line."); + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMU_GetBatteryChargeState")] + pub fn is_charging(&self) -> Result { + let mut charging: u8 = 0; + + ResultCode(unsafe { ctru_sys::PTMU_GetBatteryChargeState(&mut charging) })?; + + match charging { + 0 => Ok(false), + 1 => Ok(true), + v => Err(Error::Other(format!( + "unexpected charging state value: {v}", + ))), + } + } + + /// Returns the console's total step count. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ptm::user::PTMUser; + /// + /// let ptmu = PTMUser::new()?; + /// + /// let steps = ptmu.step_count()?; + /// + /// println!("You accumulated {steps} steps. Don't stop moving!"); + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "PTMU_GetTotalStepCount")] + pub fn step_count(&self) -> Result { + let mut steps: u32 = 0; + + ResultCode(unsafe { ctru_sys::PTMU_GetTotalStepCount(&mut steps) })?; + + Ok(steps) + } +} + +impl TryFrom for ShellState { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Closed), + 1 => Ok(Self::Open), + v => Err(Error::Other(format!("unexpected shell state value: {v}",))), + } + } +} + +impl TryFrom for BatteryLevel { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Drained), + 1 => Ok(Self::Critical), + 2 => Ok(Self::VeryLow), + 3 => Ok(Self::Low), + 4 => Ok(Self::Medium), + 5 => Ok(Self::High), + v => Err(Error::Other( + format!("unexpected battery level value: {v}",), + )), + } + } +} + +from_impl!(ShellState, u8); +from_impl!(BatteryLevel, u8);