From 5bfdfb71c2056d1cdd96466bc0ea68aa79be650f Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 29 Jun 2026 21:52:39 +0800 Subject: [PATCH] fix: fix all build issue of lcrconn-cli --- kernel/Cargo.lock | 10 +- kernel/Cargo.toml | 2 +- kernel/lcrconn-cli/Cargo.toml | 4 +- kernel/lcrconn-cli/src/app.rs | 541 ++++++++++++++++++++++ kernel/lcrconn-cli/src/cli.rs | 103 +++++ kernel/lcrconn-cli/src/main.rs | 646 +-------------------------- kernel/lcrconn/Cargo.toml | 2 +- kernel/lcrconn/src/lib.rs | 4 + legacy/src/lcr_connector/__init__.py | 1 + 9 files changed, 673 insertions(+), 640 deletions(-) create mode 100644 kernel/lcrconn-cli/src/app.rs create mode 100644 kernel/lcrconn-cli/src/cli.rs diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 4619af4..c77742b 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + [[package]] name = "autocfg" version = "1.5.1" @@ -146,9 +152,11 @@ dependencies = [ name = "lcrconn-cli" version = "1.0.0" dependencies = [ + "anyhow", "clap", "lcrconn", - "thiserror", + "strum", + "strum_macros", ] [[package]] diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index a37b6ba..35d8209 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -3,4 +3,4 @@ resolver = "3" members = ["lcrconn", "lcrconn-cli"] [workspace.dependencies] -thiserror = "2.0.12" + diff --git a/kernel/lcrconn-cli/Cargo.toml b/kernel/lcrconn-cli/Cargo.toml index 6d875cf..6ef4b34 100644 --- a/kernel/lcrconn-cli/Cargo.toml +++ b/kernel/lcrconn-cli/Cargo.toml @@ -4,6 +4,8 @@ version = "1.0.0" edition = "2024" [dependencies] -thiserror = { workspace = true } +anyhow = "1.0.103" lcrconn = { path="../lcrconn" } clap = { version="4.5.48", features=["derive"]} +strum = "=0.28.0" +strum_macros = "=0.28.0" diff --git a/kernel/lcrconn-cli/src/app.rs b/kernel/lcrconn-cli/src/app.rs new file mode 100644 index 0000000..74881d6 --- /dev/null +++ b/kernel/lcrconn-cli/src/app.rs @@ -0,0 +1,541 @@ +use crate::cli::{AppConfig, AppResolver}; +use anyhow::Result; +use lcrconn::{ + BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority, + common::{Circuit, CircuitDeviceScale, JointKind, sanitize_device_value, sanitize_floating_point}, + dataset::{DatasetCollection, from_human_readable_value, to_human_readable_value}, + query::MAX_RESPONSE_CNT, +}; +use std::io::Write; +use std::str::FromStr; +use strum_macros::EnumString; + +// region: App Utility Enums + +/// The command for the main menu. +#[derive(Debug, Clone, Copy, EnumString)] +enum MainCmd { + #[strum(serialize = "query")] + Query, + #[strum(serialize = "help")] + Help, + #[strum(serialize = "exit")] + Exit, +} + +/// The device choice for query. +#[derive(Debug, Clone, Copy, EnumString)] +enum QueryDeviceChoice { + #[strum(serialize = "r")] + Resistor, + #[strum(serialize = "c")] + Capacitor, + #[strum(serialize = "l")] + Inductor, +} + +impl QueryDeviceChoice { + fn to_device_kind(self) -> DeviceKind { + match self { + Self::Resistor => DeviceKind::Resistor, + Self::Capacitor => DeviceKind::Capacitor, + Self::Inductor => DeviceKind::Inductor, + } + } +} + +/// The sort priority for query results. +#[derive(Debug, Clone, Copy, EnumString)] +enum QuerySortPriority { + #[strum(serialize = "l")] + LessDevices, + #[strum(serialize = "a")] + MoreAccuracy, +} + +impl QuerySortPriority { + fn to_response_priority(self) -> ResponsePriority { + match self { + Self::LessDevices => ResponsePriority::LessDevices, + Self::MoreAccuracy => ResponsePriority::MoreAccuracy, + } + } +} + +/// The command for the page viewer. +#[derive(Debug, Clone, Copy, EnumString)] +enum PageViewerCmd { + #[strum(serialize = "f")] + PreviousPage, + #[strum(serialize = "b")] + NextPage, + #[strum(serialize = "q")] + Quit, +} + +// endregion + +// region: App Utility Functions + +/// Read a single line from stdin, trimmed of surrounding whitespace. +fn read_line() -> Result { + let mut line = String::new(); + std::io::stdin().read_line(&mut line)?; + Ok(line.trim().to_string()) +} + +/// Get the unit string for a device kind. +fn get_device_unit(device_kind: DeviceKind) -> &'static str { + match device_kind { + // YYC MARK: This is ohm char. + DeviceKind::Resistor => "\u{2126}", + DeviceKind::Capacitor => "F", + DeviceKind::Inductor => "H", + } +} + +// endregion + +/// The app. +pub struct App { + /// The resolver for the app. + resolver: Box, +} + +impl App { + /// Create a new app with the given configuration. + pub fn new(config: AppConfig) -> Result { + let datasets = DatasetCollection::from_file( + config.get_resistor_dataset(), + config.get_capacitor_dataset(), + config.get_inductor_dataset(), + )?; + + let resolver: Box = match config.get_resolver() { + AppResolver::Lut => Box::new(LutResolver::new(&datasets)?), + AppResolver::Bfs => Box::new(BfsResolver::new(datasets)), + }; + + Ok(Self { resolver }) + } + + /// Run the app. + pub fn run(&self) -> Result<()> { + println!("LCR Connector"); + println!(r#"Type "help" for more info. Type "exit" to quit."#); + self.op_main()?; + Ok(()) + } + + // region: Subcommand Processors + + fn op_main(&self) -> Result<()> { + loop { + match self.accept_command::()? { + MainCmd::Query => self.op_query()?, + MainCmd::Help => { + println!("LCR Connector Help:"); + println!(); + println!("query: do a query."); + println!("help: show all command."); + println!("exit: exit this app."); + } + MainCmd::Exit => break, + } + } + Ok(()) + } + + fn op_query(&self) -> Result<()> { + // collecting request infos + println!("What are you connecting?"); + println!("r: resistor"); + println!("l: inductor"); + println!("c: capacitor"); + let device_kind = self.accept_command::()?.to_device_kind(); + + println!("Your target value?"); + println!(r#"Example: "2.1k", "0.75m", "3.2M" and etc."#); + let target_value = self.accept_device_value()?; + + println!("Your tolerance?"); + println!(r#"It can be absolute value like "2.1k"."#); + println!(r#"Or relative value to your target value like "19.5%"."#); + let tolerance = self.accept_device_value_tolerance(target_value)?; + + println!("How to sort result?"); + println!("a: more accuracy"); + println!("l: less component"); + let response_priority = self + .accept_command::()? + .to_response_priority(); + + println!("How may result are you expected?"); + let count_limit = self.accept_count_value()?; + + // build request and ask resolver + let request = Request::new( + device_kind, + target_value, + tolerance, + response_priority, + count_limit, + )?; + let response = self.resolver.resolve(&request)?; + + // use page viewer to show result + self.op_page_viewer(&response)?; + + Ok(()) + } + + fn op_page_viewer(&self, response: &Response) -> Result<()> { + let cnt = response.len(); + if cnt == 0 { + println!("Sorry, no result!"); + println!("Please consider adjusting your requirements and try again."); + return Ok(()); + } + + const ITEMS_PER_PAGE: usize = 10; + let all_page = cnt / ITEMS_PER_PAGE; + let mut current_page = 0usize; + + loop { + // print list + for i in 0..ITEMS_PER_PAGE - 1 { + // build index and check it + let index = current_page * (ITEMS_PER_PAGE - 1) + i; + if index >= cnt { + continue; + } + // and print it + self.illustrate_response(response, index)?; + } + + // print page footer + println!(); + println!("Page {} of {}.", current_page + 1, all_page + 1); + println!("f: previous page. b: next page. q: quit this viewer."); + // check command + match self.accept_command::()? { + PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1), + PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1), + PageViewerCmd::Quit => break, + } + } + + Ok(()) + } + + // endregion + + // region: Command Utilities + + /// Accept a command from the user. + /// + /// Loops until a valid command is entered. + fn accept_command(&self) -> Result + where + T: FromStr, + { + loop { + self.show_prompt_arrow()?; + let words = read_line()?; + if words.is_empty() { + continue; + } + + match words.parse::() { + Ok(cmd) => return Ok(cmd), + Err(_) => println!("Unknown command, please try again."), + } + } + } + + /// Accept a count value from the user. + fn accept_count_value(&self) -> Result { + loop { + self.show_prompt_arrow()?; + let words = read_line()?; + if words.is_empty() { + continue; + } + + match words.parse::() { + Ok(value) => { + if value > MAX_RESPONSE_CNT || value == 0 { + println!("Wrong value, please try again."); + } else { + return Ok(value); + } + } + Err(_) => { + println!("Wrong value, please try again."); + } + } + } + } + + /// Accept a device value from the user. + fn accept_device_value(&self) -> Result { + loop { + self.show_prompt_arrow()?; + let words = read_line()?; + if words.is_empty() { + continue; + } + + let value = self.parse_human_readable_value(&words); + match value { + Some(v) => return Ok(v), + None => println!("Wrong value, please try again."), + } + } + } + + /// Accept a tolerance value from the user. + /// + /// The tolerance can be an absolute value (like "2.1k") or a percentage + /// relative to the target value (like "19.5%"). + fn accept_device_value_tolerance(&self, target_value: f64) -> Result { + loop { + self.show_prompt_arrow()?; + let words = read_line()?; + if words.is_empty() { + continue; + } + + let value: Option = if let Some(pct_str) = words.strip_suffix('%') { + let value = self.parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0); + value + .map(|v| v / 100.0 * target_value) + .map(|v| sanitize_device_value(v)) + .transpose() + .ok() + .flatten() + } else { + self.parse_human_readable_value(&words) + }; + + match value { + Some(v) => return Ok(v), + None => println!("Wrong value, please try again."), + } + } + } + + fn show_prompt_arrow(&self) -> Result<()> { + print!("> "); + std::io::stdout().flush()?; + Ok(()) + } + + /// Parse a plain float value. + /// + /// # Arguments + /// + /// * `user_value` - The value to parse. + /// * `checker` - A function that checks if the input is valid. + /// It takes a float as input and returns a bool. True means the input is valid, + /// otherwise False. + /// + /// # Returns + /// + /// The parsed value if it is valid, otherwise `None`. + fn parse_plain_float(&self, user_value: &str, checker: impl Fn(&f64) -> bool) -> Option { + // try parsing it first then check it by checker + let value = match user_value.parse::() { + Ok(value) => value, + Err(_) => return None, + }; + let value = sanitize_floating_point(value).ok()?; + if checker(&value) { Some(value) } else { None } + } + + /// Parse a human-readable device value. + /// + /// # Arguments + /// + /// * `user_value` - The value to parse. + /// + /// # Returns + /// + /// The parsed value if it is valid and positive, otherwise `None`. + fn parse_human_readable_value(&self, user_value: &str) -> Option { + // parse it + let value = from_human_readable_value(user_value).ok()?; + // then check its range + if value > 0.0 { Some(value) } else { None } + } + + // endregion + + // region: Response Display Utilities + + /// Format a device value for display in the circuit graph. + fn to_circuit_graph_value(&self, value: f64, device_kind: DeviceKind) -> String { + // Remove sign and append device unit + let hr = to_human_readable_value(value); + let without_sign = &hr[1..]; + format!("{}{}", without_sign, get_device_unit(device_kind)) + } + + /// Format a device value for the plan header. + fn to_plan_head_value(&self, value: f64, device_kind: DeviceKind) -> String { + // Remove sign and append device unit + let hr = to_human_readable_value(value); + let without_sign = &hr[1..]; + format!("{}{}", without_sign, get_device_unit(device_kind)) + } + + /// Format a difference value for the plan header. + fn to_plan_head_diff(&self, value: f64, device_kind: DeviceKind) -> String { + // Keep the sign and append device unit + format!( + "{}{}", + to_human_readable_value(value), + get_device_unit(device_kind) + ) + } + + /// Format a relative difference as percentage. + fn to_plan_head_diff_pct(&self, value: f64) -> String { + // Keep the sign and format it as percentage style without trailing device unit + format!("{:.2}%", value * 100.0) + } + + // YYC MARK: + // The function showing circuit graph should be maintained carefully. + // First, we want they are show in console properly, + // And we also want they have good code view. + // + // I notices that the number part of the output of `to_human_readable_value` will only be + // "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility, + // that the absolute value of exponential part is larger than 99, is close to zero. + // After putting the scale unit and device unit together like " nF", + // the whole maximum size of the built string is 14. + // + // So we need pick a larger number and odd number for the space for showing device value, + // because odd value can be divided by two so it can be split as two parts equally + // for the convenient alignment of some circuit graphs. + // My picked value is 16. + // So you will see that I use `:^16` for a center alignment to given string. + // + // After this, we also need set the padding value carefully. + // This value should consider the length of f-string syntax, pre-defined chars and required chars. + // To make sure a pretty showcase both in code and display. + + /// Illustrate a response item. + fn illustrate_response(&self, response: &Response, index: usize) -> Result<()> { + let item = response.get(index).expect("unexpected invalid index"); + let device_kind = response.device_kind(); + // print header + println!( + "Plan {:<4} Value: {:<16} Diff: {} ({})", + index + 1, + self.to_plan_head_value(item.value(), device_kind), + self.to_plan_head_diff(item.difference(), device_kind), + self.to_plan_head_diff_pct(item.relative_difference()), + ); + // print circuit graph + self.illustrate_circuit(item.circuit(), device_kind)?; + Ok(()) + } + + /// Illustrate a circuit based on its device scale. + fn illustrate_circuit( + &self, + circuit: &Circuit, + device_kind: DeviceKind, + ) -> Result<()> { + match circuit.device_scale() { + CircuitDeviceScale::One => { + self.illustrate_one_device_circuit(circuit, device_kind); + } + CircuitDeviceScale::Two => { + self.illustrate_two_device_circuit(circuit, device_kind)?; + } + CircuitDeviceScale::Three => { + self.illustrate_three_device_circuit(circuit, device_kind)?; + } + } + Ok(()) + } + + /// Illustrate a one-device circuit. + fn illustrate_one_device_circuit(&self, circuit: &Circuit, device_kind: DeviceKind) { + let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind); + println!("──[{:^16}]──", dev1); + } + + /// Illustrate a two-device circuit. + fn illustrate_two_device_circuit( + &self, + circuit: &Circuit, + device_kind: DeviceKind, + ) -> Result<()> { + let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind); + let j2 = circuit.second_device_joint()?; + let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind); + match j2 { + JointKind::Series => { + println!("──[{:^16}]──[{:^16}]──", dev1, dev2); + } + JointKind::Parallel => { + let sep0 = " ".repeat(6 + (16 - 10)); + println!(" ┌──[{:^16}]──┐ ", dev1); + println!("──┤ {} ├──", sep0); + println!(" └──[{:^16}]──┘ ", dev2); + } + } + Ok(()) + } + + /// Illustrate a three-device circuit. + fn illustrate_three_device_circuit( + &self, + circuit: &Circuit, + device_kind: DeviceKind, + ) -> Result<()> { + let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind); + let j2 = circuit.second_device_joint()?; + let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind); + let j3 = circuit.third_device_joint()?; + let dev3 = self.to_circuit_graph_value(circuit.third_device_value()?, device_kind); + match j2 { + JointKind::Series => match j3 { + JointKind::Series => { + // All in series + println!("──[{dev1:^16}]──[{dev2:^16}]──[{dev3:^16}]──"); + } + JointKind::Parallel => { + // First series then parallel + let sep0 = "─".repeat(6 + ((16 - 10) / 2)); + let sep1 = " ".repeat(6 + 2 * (16 - 10)); + println!(" ┌──[{dev1:^16}]──[{dev2:^16}]──┐ "); + println!("──┤ {sep1} ├──"); + println!(" └───{sep0}[{dev3:^16}]{sep0}───┘ "); + } + }, + JointKind::Parallel => match j3 { + JointKind::Series => { + // First parallel then series + let sep0 = " ".repeat(6 + (16 - 10)); + println!(" {sep0} ┌──[{dev1:^16}]──┐ "); + println!("──[{dev3:^16}]──┤ {sep0} ├──"); + println!(" {sep0} └──[{dev2:^16}]──┘ "); + } + JointKind::Parallel => { + // All in parallel + println!(" ┌──[{dev1:^16}]──┐ "); + println!("──┼──[{dev2:^16}]──┼──"); + println!(" └──[{dev3:^16}]──┘ "); + } + }, + } + Ok(()) + } + + // endregion +} diff --git a/kernel/lcrconn-cli/src/cli.rs b/kernel/lcrconn-cli/src/cli.rs new file mode 100644 index 0000000..d5a2a80 --- /dev/null +++ b/kernel/lcrconn-cli/src/cli.rs @@ -0,0 +1,103 @@ +use std::path::{Path, PathBuf}; + +use clap::{Parser, ValueEnum}; + +/// The configuration for the app. +pub struct AppConfig { + /// The resolver for the app. + resolver: AppResolver, + /// The path to the resistor dataset file. + resistor_dataset: PathBuf, + /// The path to the capacitor dataset file. + capacitor_dataset: PathBuf, + /// The path to the inductor dataset file. + inductor_dataset: PathBuf, +} + +impl AppConfig { + /// Get the resolver. + pub fn get_resolver(&self) -> &AppResolver { + &self.resolver + } + /// Get the path to the resistor dataset file. + pub fn get_resistor_dataset(&self) -> &Path { + &self.resistor_dataset + } + /// Get the path to the capacitor dataset file. + pub fn get_capacitor_dataset(&self) -> &Path { + &self.capacitor_dataset + } + /// Get the path to the inductor dataset file. + pub fn get_inductor_dataset(&self) -> &Path { + &self.inductor_dataset + } +} + +/// The resolver for the app. +#[derive(Debug, Clone, ValueEnum)] +pub enum AppResolver { + /// The look-up table resolver. + #[value(name = "lut")] + Lut, + /// The BFS resolver. + #[value(name = "bfs")] + Bfs, +} + +/// Get the resistor, capacitor, or inductor circuit which has the closest value +/// for your given value within at most 3 devices. +#[derive(Parser)] +#[command( + name = "LCR Connector", + version, + about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices." +)] +struct Cli { + /// The resolver you want to use. + #[arg(short = 's', long = "resolver", required = true, value_enum)] + resolver: AppResolver, + + /// The path to the resistor dataset file. + #[arg( + short = 'r', + long = "resistor", + required = true, + value_name = "RESISTOR.TXT" + )] + resistor_dataset: PathBuf, + + /// The path to the inductor dataset file. + #[arg( + short = 'l', + long = "inductor", + required = true, + value_name = "INDUCTOR.TXT" + )] + inductor_dataset: PathBuf, + + /// The path to the capacitor dataset file. + #[arg( + short = 'c', + long = "capacitor", + required = true, + value_name = "CAPACITOR.TXT" + )] + capacitor_dataset: PathBuf, +} + +impl From for AppConfig { + fn from(args: Cli) -> Self { + Self { + resolver: args.resolver, + resistor_dataset: args.resistor_dataset, + capacitor_dataset: args.capacitor_dataset, + inductor_dataset: args.inductor_dataset, + } + } +} + +pub fn parse_args() -> AppConfig { + let args = Cli::parse(); + let config = AppConfig::from(args); + config +} diff --git a/kernel/lcrconn-cli/src/main.rs b/kernel/lcrconn-cli/src/main.rs index e080906..8c6972d 100644 --- a/kernel/lcrconn-cli/src/main.rs +++ b/kernel/lcrconn-cli/src/main.rs @@ -1,641 +1,15 @@ -use std::io::{self, Write}; -use std::path::PathBuf; - -use clap::Parser; -use lcrconn::{ - from_human_readable_value, to_human_readable_value, BfsResolver, Circuit, CircuitDeviceScale, - DatasetCollection, DeviceKind, JointKind, LcrConnError, LutResolver, Request, Resolver, - Response, ResponsePriority, MAX_RESPONSE_CNT, -}; - -// ============================================================================ -// Command-line arguments -// ============================================================================ - -/// The resolver for the app. -#[derive(Clone, Debug, clap::ValueEnum)] -pub enum AppResolver { - /// The look-up table resolver. - #[value(name = "lut")] - Lut, - /// The BFS resolver. - #[value(name = "bfs")] - Bfs, -} - -/// The configuration for the app. -struct AppConfig { - /// The resolver for the app. - resolver: AppResolver, - /// The path to the resistor dataset file. - resistor_dataset: PathBuf, - /// The path to the capacitor dataset file. - capacitor_dataset: PathBuf, - /// The path to the inductor dataset file. - inductor_dataset: PathBuf, -} - -/// Get the resistor, capacitor, or inductor circuit which has the closest value -/// for your given value within at most 3 devices. -#[derive(Parser)] -#[command( - name = "LCR Connector", - about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices." -)] -struct Args { - /// The resolver you want to use. - #[arg(short = 's', long)] - resolver: AppResolver, - - /// The path to the resistor dataset file. - #[arg(short = 'r', long, value_name = "RESISTOR.TXT")] - resistor_dataset: PathBuf, - - /// The path to the inductor dataset file. - #[arg(short = 'l', long, value_name = "INDUCTOR.TXT")] - inductor_dataset: PathBuf, - - /// The path to the capacitor dataset file. - #[arg(short = 'c', long, value_name = "CAPACITOR.TXT")] - capacitor_dataset: PathBuf, -} - -impl From for AppConfig { - fn from(args: Args) -> Self { - Self { - resolver: args.resolver, - resistor_dataset: args.resistor_dataset, - capacitor_dataset: args.capacitor_dataset, - inductor_dataset: args.inductor_dataset, - } - } -} - -// ============================================================================ -// Interactive command enums -// ============================================================================ - -/// The command for the main menu. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum MainCmd { - Query, - Help, - Exit, -} - -fn parse_main_cmd(s: &str) -> Option { - match s { - "query" => Some(MainCmd::Query), - "help" => Some(MainCmd::Help), - "exit" => Some(MainCmd::Exit), - _ => None, - } -} - -/// The device choice for query. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum QueryDeviceChoice { - Resistor, - Capacitor, - Inductor, -} - -impl QueryDeviceChoice { - fn parse(s: &str) -> Option { - match s { - "r" => Some(Self::Resistor), - "c" => Some(Self::Capacitor), - "l" => Some(Self::Inductor), - _ => None, - } - } - - fn to_device_kind(self) -> DeviceKind { - match self { - Self::Resistor => DeviceKind::Resistor, - Self::Capacitor => DeviceKind::Capacitor, - Self::Inductor => DeviceKind::Inductor, - } - } -} - -/// The sort priority for query results. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum QuerySortPriority { - LessDevices, - MoreAccuracy, -} - -impl QuerySortPriority { - fn parse(s: &str) -> Option { - match s { - "l" => Some(Self::LessDevices), - "a" => Some(Self::MoreAccuracy), - _ => None, - } - } - - fn to_response_priority(self) -> ResponsePriority { - match self { - Self::LessDevices => ResponsePriority::LessDevices, - Self::MoreAccuracy => ResponsePriority::MoreAccuracy, - } - } -} - -/// The command for the page viewer. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum PageViewerCmd { - PreviousPage, - NextPage, - Quit, -} - -impl PageViewerCmd { - fn parse(s: &str) -> Option { - match s { - "f" => Some(Self::PreviousPage), - "b" => Some(Self::NextPage), - "q" => Some(Self::Quit), - _ => None, - } - } -} - -// ============================================================================ -// Input utilities -// ============================================================================ - -/// Read a single line from stdin, trimmed of surrounding whitespace. -fn read_line() -> String { - let mut line = String::new(); - io::stdin() - .read_line(&mut line) - .expect("Failed to read from stdin"); - line.trim().to_string() -} - -/// Parse a plain float value. -/// -/// # Arguments -/// -/// * `user_value` - The value to parse. -/// * `checker` - A function that checks if the input is valid. -/// It takes a float as input and returns a bool. True means the input is valid, -/// otherwise False. -/// -/// # Returns -/// -/// The parsed value if it is valid, otherwise `None`. -fn parse_plain_float(user_value: &str, checker: impl Fn(&f64) -> bool) -> Option { - // try parsing it first - let value = user_value.parse::().ok()?; - // then check it by checker - if checker(&value) { - Some(value) - } else { - None - } -} - -/// Parse a human-readable device value. -/// -/// # Arguments -/// -/// * `user_value` - The value to parse. -/// -/// # Returns -/// -/// The parsed value if it is valid and positive, otherwise `None`. -fn parse_human_readable_value(user_value: &str) -> Option { - // parse it - let value = from_human_readable_value(user_value).ok()?; - // then check its range - if value > 0.0 { - Some(value) - } else { - None - } -} - -// ============================================================================ -// Response display utilities -// ============================================================================ - -/// Get the unit string for a device kind. -fn get_device_unit(device_kind: DeviceKind) -> &'static str { - match device_kind { - DeviceKind::Resistor => "\u{2126}", - DeviceKind::Capacitor => "F", - DeviceKind::Inductor => "H", - } -} - -/// Format a device value for display in the circuit graph. -fn to_circuit_graph_value(value: f64, device_kind: DeviceKind) -> String { - // Remove sign and append device unit - let hr = to_human_readable_value(value); - let without_sign = &hr[1..]; - format!("{}{}", without_sign, get_device_unit(device_kind)) -} - -/// Format a device value for the plan header. -fn to_plan_head_value(value: f64, device_kind: DeviceKind) -> String { - // Remove sign and append device unit - let hr = to_human_readable_value(value); - let without_sign = &hr[1..]; - format!("{}{}", without_sign, get_device_unit(device_kind)) -} - -/// Format a difference value for the plan header. -fn to_plan_head_diff(value: f64, device_kind: DeviceKind) -> String { - // Keep the sign and append device unit - format!("{}{}", to_human_readable_value(value), get_device_unit(device_kind)) -} - -/// Format a relative difference as percentage. -fn to_plan_head_diff_pct(value: f64) -> String { - // Keep the sign and format it as percentage style without trailing device unit - format!("{:.2}%", value * 100.0) -} - -// YYC MARK: -// The function showing circuit graph should be maintained carefully. -// First, we want they are show in console properly, -// And we also want they have good code view. -// -// I notices that the number part of the output of `to_human_readable_value` will only be -// "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility, -// that the absolute value of exponential part is larger than 99, is close to zero. -// After putting the scale unit and device unit together like " nF", -// the whole maximum size of the built string is 14. -// -// So we need pick a larger number and odd number for the space for showing device value, -// because odd value can be divided by two so it can be split as two parts equally -// for the convenient alignment of some circuit graphs. -// My picked value is 16. -// So you will see that I use `:^16` for a center alignment to given string. -// -// After this, we also need set the padding value carefully. -// This value should consider the length of f-string syntax, pre-defined chars and required chars. -// To make sure a pretty showcase both in code and display. - -/// Illustrate a one-device circuit. -fn illustrate_one_device_circuit(circuit: &Circuit, device_kind: DeviceKind) { - let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); - println!("──[{:^16}]──", dev1); -} - -/// Illustrate a two-device circuit. -fn illustrate_two_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { - let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); - let j2 = circuit.second_device_joint()?; - let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind); - match j2 { - JointKind::Series => { - println!("──[{:^16}]──[{:^16}]──", dev1, dev2); - } - JointKind::Parallel => { - let sep0 = " ".repeat(6 + (16 - 10)); - println!(" ┌──[{:^16}]──┐ ", dev1); - println!("──┤ {} ├──", sep0); - println!(" └──[{:^16}]──┘ ", dev2); - } - } - Ok(()) -} - -/// Illustrate a three-device circuit. -fn illustrate_three_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { - let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); - let j2 = circuit.second_device_joint()?; - let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind); - let j3 = circuit.third_device_joint()?; - let dev3 = to_circuit_graph_value(circuit.third_device_value()?, device_kind); - match j2 { - JointKind::Series => match j3 { - JointKind::Series => { - // All in series - println!("──[{:^16}]──[{:^16}]──[{:^16}]──", dev1, dev2, dev3); - } - JointKind::Parallel => { - // First series then parallel - let sep0 = "\u{2500}".repeat(6 + ((16 - 10) / 2)); - let sep1 = " ".repeat(6 + 2 * (16 - 10)); - println!(" ┌──[{:^16}]──[{:^16}]──┐ ", dev1, dev2); - println!("──┤ {} ├──", sep1); - println!(" └───{}[{:^16}]{}───┘ ", sep0, dev3, sep0); - } - }, - JointKind::Parallel => match j3 { - JointKind::Series => { - // First parallel then series - let sep0 = " ".repeat(6 + (16 - 10)); - println!(" {} ┌──[{:^16}]──┐ ", sep0, dev1); - println!("──[{:^16}]──┤ {} ├──", dev3, sep0); - println!(" {} └──[{:^16}]──┘ ", sep0, dev2); - } - JointKind::Parallel => { - // All in parallel - println!(" ┌──[{:^16}]──┐ ", dev1); - println!("──┼──[{:^16}]──┼──", dev2); - println!(" └──[{:^16}]──┘ ", dev3); - } - }, - } - Ok(()) -} - -/// Illustrate a circuit based on its device scale. -fn illustrate_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { - match circuit.device_scale() { - CircuitDeviceScale::One => { - illustrate_one_device_circuit(circuit, device_kind); - } - CircuitDeviceScale::Two => { - illustrate_two_device_circuit(circuit, device_kind)?; - } - CircuitDeviceScale::Three => { - illustrate_three_device_circuit(circuit, device_kind)?; - } - } - Ok(()) -} - -/// Illustrate a response item. -fn illustrate_response(response: &Response, index: usize) -> Result<(), LcrConnError> { - let item = &response[index]; - let device_kind = response.device_kind(); - // print header - println!( - "Plan {:<4} Value: {:<16} Diff: {} ({})", - index + 1, - to_plan_head_value(item.value(), device_kind), - to_plan_head_diff(item.difference(), device_kind), - to_plan_head_diff_pct(item.relative_difference()), - ); - // print circuit graph - illustrate_circuit(item.circuit(), device_kind)?; - Ok(()) -} - -// ============================================================================ -// App -// ============================================================================ - -/// The app. -struct App { - /// The resolver for the app. - resolver: Box, -} - -impl App { - /// Create a new app with the given configuration. - /// - /// # Errors - /// - /// See [`DatasetCollection::from_file`] and [`LutResolver::new`]. - fn new(config: AppConfig) -> Result { - let datasets = DatasetCollection::from_file( - &config.resistor_dataset, - &config.capacitor_dataset, - &config.inductor_dataset, - )?; - - let resolver: Box = match config.resolver { - AppResolver::Lut => Box::new(LutResolver::new(&datasets)?), - AppResolver::Bfs => Box::new(BfsResolver::new(datasets)), - }; - - Ok(Self { resolver }) - } - - /// Run the app. - fn run(&self) -> Result<(), LcrConnError> { - println!("LCR Connector"); - println!("Type \"help\" for more info. Type \"exit\" to quit."); - self.op_main()?; - Ok(()) - } - - // ======================================================================== - // Subcommand Processors - // ======================================================================== - - fn op_main(&self) -> Result<(), LcrConnError> { - loop { - match self.accept_command(parse_main_cmd) { - MainCmd::Query => self.op_query()?, - MainCmd::Help => { - println!("LCR Connector Help:"); - println!(); - println!("query: do a query."); - println!("help: show all command."); - println!("exit: exit this app."); - } - MainCmd::Exit => break, - } - } - Ok(()) - } - - fn op_query(&self) -> Result<(), LcrConnError> { - // collecting request infos - println!("What are you connecting?"); - println!("r: resistor"); - println!("l: inductor"); - println!("c: capacitor"); - let device_kind = self - .accept_command(QueryDeviceChoice::parse) - .to_device_kind(); - - println!("Your target value?"); - println!("Example: \"2.1k\", \"0.75m\", \"3.2M\" and etc."); - let target_value = self.accept_device_value(); - - println!("Your tolerance?"); - println!("It can be absolute value like \"2.1k\"."); - println!("Or relative value to your target value like \"19.5%\"."); - let tolerance = self.accept_device_value_tolerance(target_value); - - println!("How to sort result?"); - println!("a: more accuracy"); - println!("l: less component"); - let response_priority = self - .accept_command(QuerySortPriority::parse) - .to_response_priority(); - - println!("How may result are you expected?"); - let count_limit = self.accept_count_value(); - - // build request and ask resolver - let request = Request::new( - device_kind, - target_value, - tolerance, - response_priority, - count_limit, - )?; - let response = self.resolver.resolve(&request)?; - - // use page viewer to show result - self.op_page_viewer(&response)?; - - Ok(()) - } - - fn op_page_viewer(&self, response: &Response) -> Result<(), LcrConnError> { - let cnt = response.len(); - if cnt == 0 { - println!("Sorry, no result!"); - println!("Please consider adjusting your requirements and try again."); - return Ok(()); - } - - const ITEMS_PER_PAGE: usize = 10; - let all_page = cnt / ITEMS_PER_PAGE; - let mut current_page = 0usize; - - loop { - // print list - for i in 0..ITEMS_PER_PAGE - 1 { - // build index and check it - let index = current_page * (ITEMS_PER_PAGE - 1) + i; - if index >= cnt { - continue; - } - // and print it - illustrate_response(response, index)?; - } - - // print page footer - println!(); - println!("Page {} of {}.", current_page + 1, all_page + 1); - println!("f: previous page. b: next page. q: quit this viewer."); - // check command - match self.accept_command(PageViewerCmd::parse) { - PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1), - PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1), - PageViewerCmd::Quit => break, - } - } - - Ok(()) - } - - // ======================================================================== - // Command Utilities - // ======================================================================== - - /// Accept a command from the user. - /// - /// Loops until a valid command is entered. - fn accept_command(&self, parser: impl Fn(&str) -> Option) -> T { - loop { - self.show_prompt_arrow(); - let words = read_line(); - if words.is_empty() { - continue; - } - - match parser(&words) { - Some(cmd) => return cmd, - None => println!("Unknown command, please try again."), - } - } - } - - /// Accept a count value from the user. - fn accept_count_value(&self) -> usize { - loop { - self.show_prompt_arrow(); - let words = read_line(); - if words.is_empty() { - continue; - } - - match words.parse::() { - Ok(value) => { - if value > MAX_RESPONSE_CNT || value == 0 { - println!("Wrong value, please try again."); - } else { - return value; - } - } - Err(_) => { - println!("Wrong value, please try again."); - } - } - } - } - - /// Accept a device value from the user. - fn accept_device_value(&self) -> f64 { - loop { - self.show_prompt_arrow(); - let words = read_line(); - if words.is_empty() { - continue; - } - - let value = parse_human_readable_value(&words); - match value { - Some(v) => return v, - None => println!("Wrong value, please try again."), - } - } - } - - /// Accept a tolerance value from the user. - /// - /// The tolerance can be an absolute value (like "2.1k") or a percentage - /// relative to the target value (like "19.5%"). - fn accept_device_value_tolerance(&self, target_value: f64) -> f64 { - loop { - self.show_prompt_arrow(); - let words = read_line(); - if words.is_empty() { - continue; - } - - let value: Option = if let Some(pct_str) = words.strip_suffix('%') { - let value = parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0); - value.map(|v| v / 100.0 * target_value) - } else { - parse_human_readable_value(&words) - }; - - match value { - Some(v) => return v, - None => println!("Wrong value, please try again."), - } - } - } - - fn show_prompt_arrow(&self) { - print!("> "); - io::stdout().flush().expect("Failed to flush stdout"); - } -} - -// ============================================================================ -// Entry point -// ============================================================================ +mod app; +mod cli; fn main() { - let args = Args::parse(); - let config = AppConfig::from(args); + let config = cli::parse_args(); - let app = match App::new(config) { - Ok(app) => app, - Err(e) => { - eprintln!("Error: {}", e); - std::process::exit(1); - } - }; - - if let Err(e) = app.run() { - eprintln!("Error: {}", e); + let app = app::App::new(config).unwrap_or_else(|err| { + eprintln!("Fail to initialize application: {}", err); std::process::exit(1); - } + }); + app.run().unwrap_or_else(|err| { + eprintln!("Runtime error: {}", err); + std::process::exit(1); + }); } diff --git a/kernel/lcrconn/Cargo.toml b/kernel/lcrconn/Cargo.toml index a204f42..9be7cdd 100644 --- a/kernel/lcrconn/Cargo.toml +++ b/kernel/lcrconn/Cargo.toml @@ -4,7 +4,7 @@ version = "1.0.0" edition = "2024" [dependencies] -thiserror = { workspace = true } +thiserror = "2.0.12" ordered-float = "=5.3.0" itertools = "0.15.0" strum = "=0.28.0" diff --git a/kernel/lcrconn/src/lib.rs b/kernel/lcrconn/src/lib.rs index d6e1170..2a5dc56 100644 --- a/kernel/lcrconn/src/lib.rs +++ b/kernel/lcrconn/src/lib.rs @@ -2,3 +2,7 @@ pub mod common; pub mod dataset; pub mod query; pub mod resolver; + +pub use common::DeviceKind; +pub use query::{Request, Response, ResponsePriority}; +pub use resolver::{Resolver, BfsResolver, LutResolver}; diff --git a/legacy/src/lcr_connector/__init__.py b/legacy/src/lcr_connector/__init__.py index 7bd6b8f..759a9d3 100644 --- a/legacy/src/lcr_connector/__init__.py +++ b/legacy/src/lcr_connector/__init__.py @@ -321,6 +321,7 @@ class App: def __get_device_unit(self, device_kind: DeviceKind) -> str: match device_kind: case DeviceKind.RESISTOR: + # YYC MARK: This is ohm char. return "\u2126" case DeviceKind.CAPACITOR: return "F"