| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| mod commands; |
| use { |
| crate::commands::{CmdHelper, Command, ReplControl}, |
| anyhow::{format_err, Context as _, Error}, |
| fidl_fuchsia_power as fpower, fidl_fuchsia_power_test as spower, |
| fidl_fuchsia_power_test::BatterySimulatorProxy, |
| fuchsia_async as fasync, |
| fuchsia_component::client::connect_to_service, |
| fuchsia_syslog as syslog, |
| futures::{ |
| channel::mpsc::{channel, SendError}, |
| Sink, SinkExt, Stream, StreamExt, TryFutureExt, |
| }, |
| pin_utils::pin_mut, |
| rustyline::{error::ReadlineError, CompletionType, Config, EditMode, Editor}, |
| std::{fmt, thread, time::Duration}, |
| }; |
| |
| static LOG_TAG: &str = "battery_simulator"; |
| static PROMPT: &str = "\x1b[34mbattman>\x1b[0m "; |
| |
| // ParseResult is used to convey the parsed commands input by user |
| // and display them either as an error or continue executing the commands |
| enum ParseResult<T> { |
| Valid(T), |
| Empty, |
| Error(String), |
| } |
| |
| // BatteryInfo is a wrapper for BatteryInfo so the print fmt could be |
| // implemented |
| struct BatteryInfo { |
| info: fpower::BatteryInfo, |
| } |
| |
| impl fmt::Display for BatteryInfo { |
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!( |
| formatter, |
| concat!( |
| "Battery Percentage: {:?}\n", |
| "Battery Status: {:?}\n", |
| "Charge Status: {:?}\n", |
| "Level Status: {:?}\n", |
| "Charge Source: {:?}\n", |
| "Time Remaining: {:?}\n", |
| ), |
| self.info.level_percent, |
| self.info.status, |
| self.info.charge_status, |
| self.info.level_status, |
| self.info.charge_source, |
| self.info.time_remaining.as_ref() |
| ) |
| } |
| } |
| |
| async fn print_battery_info(service: &BatterySimulatorProxy) -> Result<(), Error> { |
| if service.is_simulating().await? { |
| let bi = service.get_battery_info().await?; |
| println!("{}", BatteryInfo { info: bi }); |
| } else { |
| let battery_manager_server = connect_to_service::<fpower::BatteryManagerMarker>()?; |
| let bi = battery_manager_server.get_battery_info().await?; |
| println!("{}", BatteryInfo { info: bi }); |
| } |
| Ok(()) |
| } |
| |
| fn print_set_help_message() { |
| println!(concat!( |
| "'set' modifies the state of the simulated battery. ", |
| "Modifications only take place once disconnected.\n", |
| "The usage of set is as follows:\n\n", |
| "set <battery_attribute battery_attribute_value>\n\n", |
| "The corresponding field and field values is as follows:\n\n", |
| "\tBatteryPercentage\n", |
| "\t\tAny int value from [1..100]\n", |
| "\tChargeSource\n", |
| "\t\tUNKNOWN\n", |
| "\t\tNONE\n", |
| "\t\tAC_ADAPTER\n", |
| "\t\tUSB\n", |
| "\t\tWIRELESS\n", |
| "\tChargeStatus\n", |
| "\t\tUNKNOWN\n", |
| "\t\tNOT_CHARGING\n", |
| "\t\tCHARGING\n", |
| "\t\tDISCHARGING\n", |
| "\tLevelStatus\n", |
| "\t\tUNKNOWN\n", |
| "\t\tOK\n", |
| "\t\tWARNING\n", |
| "\t\tLOW\n", |
| "\t\tCRITICAL\n", |
| "\tBatteryStatus\n", |
| "\t\tUNKOWN\n", |
| "\t\tOK\n", |
| "\t\tNOT_AVAILABLE\n", |
| "\t\tNOT_PRESENT\n", |
| "\tTimeRemaining\n", |
| "\t\tInt value representing time in seconds\n\n", |
| "Examples:\n", |
| "set ChargeSource WIRELESS BatteryPercentage 12\n", |
| "set ChargeStatus DISCHARGING TimeRemaining 100 BatteryPercentage 2" |
| )) |
| } |
| |
| fn set_time_remaining(time_remaining: &str, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| let time = time_remaining.parse::<u64>()?; |
| let duration = Duration::new(time, 0); |
| service.set_time_remaining(duration.as_nanos() as i64)?; |
| Ok(()) |
| } |
| |
| fn set_charge_source(charge_source: &str, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| let res = match charge_source { |
| "UNKNOWN" => fpower::ChargeSource::Unknown, |
| "NONE" => fpower::ChargeSource::None, |
| "AC_ADAPTER" => fpower::ChargeSource::AcAdapter, |
| "USB" => fpower::ChargeSource::Usb, |
| "WIRELESS" => fpower::ChargeSource::Wireless, |
| _ => { |
| println!("{} does not exist as an option for ChargeSource. Type set --help for more information", charge_source); |
| return Ok(()); |
| } |
| }; |
| service.set_charge_source(res)?; |
| Ok(()) |
| } |
| |
| fn set_battery_percentage( |
| battery_percentage: &str, |
| service: &BatterySimulatorProxy, |
| ) -> Result<(), Error> { |
| let percent: f32 = battery_percentage.parse().unwrap(); |
| if percent < 0.0 || percent > 100.0 { |
| println!("Please enter battery percent from 0 to 100"); |
| return Ok(()); |
| } |
| service.set_battery_percentage(percent)?; |
| Ok(()) |
| } |
| |
| fn set_battery_status(battery_status: &str, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| let res = match battery_status { |
| "UNKNOWN" => fpower::BatteryStatus::Unknown, |
| "OK" => fpower::BatteryStatus::Ok, |
| "NOT_AVAILABLE" => fpower::BatteryStatus::NotAvailable, |
| "NOT_PRESENT" => fpower::BatteryStatus::NotPresent, |
| _ => { |
| println!("{} does not exist as an option for BatteryStatus. Type set --help for more information", battery_status); |
| return Ok(()); |
| } |
| }; |
| service.set_battery_status(res)?; |
| Ok(()) |
| } |
| |
| fn set_level_status(level_status: &str, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| let res = match level_status { |
| "UNKNOWN" => fpower::LevelStatus::Unknown, |
| "OK" => fpower::LevelStatus::Ok, |
| "WARNING" => fpower::LevelStatus::Warning, |
| "LOW" => fpower::LevelStatus::Low, |
| "CRITICAL" => fpower::LevelStatus::Critical, |
| _ => { |
| println!("{} does not exist as an option for LevelStatus. Type set --help for more information", level_status); |
| return Ok(()); |
| } |
| }; |
| service.set_level_status(res)?; |
| Ok(()) |
| } |
| |
| fn set_charge_status(charge_status: &str, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| let res = match charge_status { |
| "UNKNOWN" => fpower::ChargeStatus::Unknown, |
| "NOT_CHARGING" => fpower::ChargeStatus::NotCharging, |
| "CHARGING" => fpower::ChargeStatus::Charging, |
| "DISCHARGING" => fpower::ChargeStatus::Discharging, |
| "FULL" => fpower::ChargeStatus::Full, |
| _ => { |
| println!("{} does not exist as an option for ChargeStatus. Type set --help for more information", charge_status); |
| return Ok(()); |
| } |
| }; |
| service.set_charge_status(res)?; |
| Ok(()) |
| } |
| |
| // args is the string of arguments provided by the user. The format is as follows |
| // args: "<Field> <Value> ...". The <Field> is any field of the BatteryInfo and |
| // <Value> is the fields corresponding value. The "..." signifies the repetition of |
| // the previous pattern of field and value. |
| async fn set_all_information(args: String, service: &BatterySimulatorProxy) -> Result<(), Error> { |
| if !service.is_simulating().await? { |
| println!("Please disconnect before running set. Type <set --help> for more information"); |
| return Ok(()); |
| } |
| let commands: Vec<&str> = args.split(" ").collect(); |
| if commands.len() <= 1 || commands[0] == "--help" { |
| print_set_help_message(); |
| return Ok(()); |
| } else if commands.len() % 2 != 0 { |
| println!("Incorrect number of args. Type <set --help> for more information"); |
| return Ok(()); |
| } |
| for i in (0..).take(commands.len()).step_by(2) { |
| let field = commands[i]; |
| let arg = commands[i + 1]; |
| let _ = match field { |
| "BatteryPercentage" => set_battery_percentage(arg, service)?, |
| "BatteryStatus" => set_battery_status(arg, service)?, |
| "ChargeStatus" => set_charge_status(arg, service)?, |
| "ChargeSource" => set_charge_source(arg, service)?, |
| "LevelStatus" => set_level_status(arg, service)?, |
| "TimeRemaining" => set_time_remaining(arg, service)?, |
| _ => println!("Incorrect Battery Field {}", field), |
| }; |
| } |
| Ok(()) |
| } |
| |
| /// Handle a single raw input command from a user and indicate whether the command should |
| /// result in continuation or breaking of the read evaluate print loop. |
| async fn handle_command( |
| service: &BatterySimulatorProxy, |
| cmd: Command, |
| args: Vec<String>, |
| ) -> Result<ReplControl, Error> { |
| let args = args.join(" "); |
| match cmd { |
| Command::Get => print_battery_info(service).await, |
| Command::Set => set_all_information(args, service).await, |
| Command::Reconnect => service.reconnect_real_battery().map_err(|e| format_err!("{}", e)), |
| Command::Disconnect => service.disconnect_real_battery().map_err(|e| format_err!("{}", e)), |
| Command::Help => Ok(println!("{}", Command::help_msg().to_string())), |
| Command::Exit | Command::Quit => return Ok(ReplControl::Break), |
| }?; |
| |
| Ok(ReplControl::Continue) |
| } |
| |
| /// Parse a single raw input command from a user into the command type and argument lsit |
| fn parse_command(line: String) -> ParseResult<(Command, Vec<String>)> { |
| let components: Vec<_> = line.trim().split_whitespace().collect(); |
| match components.split_first() { |
| Some((raw_cmd, args)) => match raw_cmd.parse() { |
| Ok(cmd) => { |
| let args = args.into_iter().map(|s| s.to_string()).collect(); |
| ParseResult::Valid((cmd, args)) |
| } |
| Err(_) => ParseResult::Error(format!("\"{}\" is not a valid command", raw_cmd)), |
| }, |
| None => ParseResult::Empty, |
| } |
| } |
| |
| // Parses the command passed and if it's a valid command then it would handle what it should do |
| async fn parse_and_handle_cmd( |
| service: &BatterySimulatorProxy, |
| line: String, |
| ) -> Result<ReplControl, Error> { |
| match parse_command(line) { |
| ParseResult::Valid((cmd, args)) => handle_command(service, cmd, args).await, |
| ParseResult::Empty => Ok(ReplControl::Continue), |
| ParseResult::Error(err) => { |
| println!("{}", err); |
| Ok(ReplControl::Continue) |
| } |
| } |
| } |
| |
| /// Generates a rustyline `Editor` in a separate thread to manage user input. This input is returned |
| /// as a `Stream` of lines entered by the user. |
| /// |
| /// The thread exits and the `Stream` is exhausted when an error occurs on stdin or the user |
| /// sends a ctrl-c or ctrl-d sequence. |
| /// |
| /// Because rustyline shares control over output to the screen with other parts of the system, a |
| /// `Sink` is passed to the caller to send acknowledgements that a command has been processed and |
| /// that rustyline should handle the next line of input. |
| fn cmd_stream() -> (impl Stream<Item = String>, impl Sink<(), Error = SendError>) { |
| // Editor thread and command processing thread must be synchronized so that output |
| // is printed in the correct order. |
| let (mut cmd_sender, cmd_receiver) = channel(512); |
| let (ack_sender, mut ack_receiver) = channel(512); |
| thread::spawn(move || -> Result<(), Error> { |
| let mut exec = fasync::Executor::new().context("error creating the readline event loop")?; |
| let fut = async { |
| let config = Config::builder() |
| .auto_add_history(true) |
| .history_ignore_space(true) |
| .completion_type(CompletionType::List) |
| .edit_mode(EditMode::Vi) |
| .build(); |
| let mut rl: Editor<CmdHelper> = Editor::with_config(config); |
| // Add tab completion |
| let c = CmdHelper::new(); |
| rl.set_helper(Some(c)); |
| loop { |
| let readline = rl.readline(PROMPT); |
| match readline { |
| Ok(line) => { |
| cmd_sender.try_send(line)?; |
| } |
| Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => { |
| return Ok(()); |
| } |
| Err(e) => { |
| println!("Error: {:?}", e); |
| return Err(e.into()); |
| } |
| } |
| // Wait until processing thread is finished evaluating the last command |
| // before running the next loop in the repl |
| ack_receiver.next().await; |
| } |
| }; |
| exec.run_singlethreaded(fut) |
| }); |
| (cmd_receiver, ack_sender) |
| } |
| |
| async fn run_repl(service: BatterySimulatorProxy) -> Result<(), Error> { |
| // `cmd_stream` blocks on input in a separate thread and passes commands and acks back to |
| // the main thread via async channels. |
| let (mut commands, mut acks) = cmd_stream(); |
| while let Some(cmd) = commands.next().await { |
| match parse_and_handle_cmd(&service, cmd).await { |
| Ok(ReplControl::Continue) => {} |
| Ok(ReplControl::Break) => break, |
| Err(e) => println!("Error handling command: {}", e), |
| } |
| acks.send(()).await?; |
| } |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded] |
| async fn main() -> Result<(), Error> { |
| syslog::init_with_tags(&[LOG_TAG]).expect("Can't init logger - batsim main"); |
| println!("Welcome to the Battery Simulator. Type help and <Enter> to see all the options!"); |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>()?; |
| let repl = run_repl(battery_simulator) |
| .unwrap_or_else(|e| eprintln!("REPL failed unexpectedly {:?}", e)); |
| |
| // Pins repl value on the stack |
| pin_mut!(repl); |
| |
| repl.await; |
| |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_disconnect() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Test disconnect |
| let res = battery_simulator.is_simulating().await; |
| assert_eq!(res.unwrap(), true); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_set_battery_percentage() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Set Battery Percentage |
| let res = set_battery_percentage("12.0", &battery_simulator); |
| assert!(res.is_ok(), "Failed to set battery percentage"); |
| // Check Battery Percentage |
| let battery_info = battery_simulator.get_battery_info().await; |
| assert_eq!(battery_info.unwrap().level_percent.unwrap(), 12.0); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_set_battery_status() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Set Battery Status |
| let res = set_battery_status("OK", &battery_simulator); |
| assert!(res.is_ok(), "Failed to set battery status"); |
| // Check Battery Status |
| let battery_info = battery_simulator.get_battery_info().await; |
| assert_eq!(battery_info.unwrap().status.unwrap(), fpower::BatteryStatus::Ok); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_set_charge_status() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Set Charge Status |
| let res = set_charge_status("NOT_CHARGING", &battery_simulator); |
| assert!(res.is_ok(), "Failed to set charge status"); |
| // Check Charge Status |
| let battery_info = battery_simulator.get_battery_info().await; |
| assert_eq!(battery_info.unwrap().charge_status.unwrap(), fpower::ChargeStatus::NotCharging); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_set_charge_source() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Set Charge Status |
| let res = set_charge_source("WIRELESS", &battery_simulator); |
| assert!(res.is_ok(), "Failed to set charge source"); |
| // Check Charge Status |
| let battery_info = battery_simulator.get_battery_info().await; |
| assert_eq!(battery_info.unwrap().charge_source.unwrap(), fpower::ChargeSource::Wireless); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_set_level_status() { |
| // Connect to service |
| let battery_simulator = connect_to_service::<spower::BatterySimulatorMarker>().unwrap(); |
| // Disconnect battery |
| let res = battery_simulator.disconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to disconnect"); |
| // Set Level Status |
| let res = set_level_status("CRITICAL", &battery_simulator); |
| assert!(res.is_ok(), "Failed to set charge source"); |
| // Check Level Status |
| let battery_info = battery_simulator.get_battery_info().await; |
| assert_eq!(battery_info.unwrap().level_status.unwrap(), fpower::LevelStatus::Critical); |
| // Reconnect battery |
| let res = battery_simulator.reconnect_real_battery(); |
| assert!(res.is_ok(), "Failed to reconnect"); |
| } |
| } |