fix: fix all build issue of lcrconn-cli
This commit is contained in:
10
kernel/Cargo.lock
generated
10
kernel/Cargo.lock
generated
@@ -52,6 +52,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -146,9 +152,11 @@ dependencies = [
|
|||||||
name = "lcrconn-cli"
|
name = "lcrconn-cli"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"lcrconn",
|
"lcrconn",
|
||||||
"thiserror",
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ resolver = "3"
|
|||||||
members = ["lcrconn", "lcrconn-cli"]
|
members = ["lcrconn", "lcrconn-cli"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
thiserror = "2.0.12"
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ version = "1.0.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { workspace = true }
|
anyhow = "1.0.103"
|
||||||
lcrconn = { path="../lcrconn" }
|
lcrconn = { path="../lcrconn" }
|
||||||
clap = { version="4.5.48", features=["derive"]}
|
clap = { version="4.5.48", features=["derive"]}
|
||||||
|
strum = "=0.28.0"
|
||||||
|
strum_macros = "=0.28.0"
|
||||||
|
|||||||
541
kernel/lcrconn-cli/src/app.rs
Normal file
541
kernel/lcrconn-cli/src/app.rs
Normal file
@@ -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<String> {
|
||||||
|
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<dyn Resolver>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Create a new app with the given configuration.
|
||||||
|
pub fn new(config: AppConfig) -> Result<Self> {
|
||||||
|
let datasets = DatasetCollection::from_file(
|
||||||
|
config.get_resistor_dataset(),
|
||||||
|
config.get_capacitor_dataset(),
|
||||||
|
config.get_inductor_dataset(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let resolver: Box<dyn Resolver> = 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>()? {
|
||||||
|
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::<QueryDeviceChoice>()?.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::<QuerySortPriority>()?
|
||||||
|
.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>()? {
|
||||||
|
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<T>(&self) -> Result<T>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match words.parse::<T>() {
|
||||||
|
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<usize> {
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match words.parse::<usize>() {
|
||||||
|
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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: Option<f64> = 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<f64> {
|
||||||
|
// try parsing it first then check it by checker
|
||||||
|
let value = match user_value.parse::<f64>() {
|
||||||
|
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<f64> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
103
kernel/lcrconn-cli/src/cli.rs
Normal file
103
kernel/lcrconn-cli/src/cli.rs
Normal file
@@ -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<Cli> 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
|
||||||
|
}
|
||||||
@@ -1,641 +1,15 @@
|
|||||||
use std::io::{self, Write};
|
mod app;
|
||||||
use std::path::PathBuf;
|
mod cli;
|
||||||
|
|
||||||
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<Args> 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<MainCmd> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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<f64> {
|
|
||||||
// try parsing it first
|
|
||||||
let value = user_value.parse::<f64>().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<f64> {
|
|
||||||
// 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<dyn Resolver>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create a new app with the given configuration.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// See [`DatasetCollection::from_file`] and [`LutResolver::new`].
|
|
||||||
fn new(config: AppConfig) -> Result<Self, LcrConnError> {
|
|
||||||
let datasets = DatasetCollection::from_file(
|
|
||||||
&config.resistor_dataset,
|
|
||||||
&config.capacitor_dataset,
|
|
||||||
&config.inductor_dataset,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let resolver: Box<dyn Resolver> = 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<T>(&self, parser: impl Fn(&str) -> Option<T>) -> 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::<usize>() {
|
|
||||||
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<f64> = 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
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let config = cli::parse_args();
|
||||||
let config = AppConfig::from(args);
|
|
||||||
|
|
||||||
let app = match App::new(config) {
|
let app = app::App::new(config).unwrap_or_else(|err| {
|
||||||
Ok(app) => app,
|
eprintln!("Fail to initialize application: {}", err);
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error: {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = app.run() {
|
|
||||||
eprintln!("Error: {}", e);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
});
|
||||||
|
app.run().unwrap_or_else(|err| {
|
||||||
|
eprintln!("Runtime error: {}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ version = "1.0.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { workspace = true }
|
thiserror = "2.0.12"
|
||||||
ordered-float = "=5.3.0"
|
ordered-float = "=5.3.0"
|
||||||
itertools = "0.15.0"
|
itertools = "0.15.0"
|
||||||
strum = "=0.28.0"
|
strum = "=0.28.0"
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ pub mod common;
|
|||||||
pub mod dataset;
|
pub mod dataset;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod resolver;
|
pub mod resolver;
|
||||||
|
|
||||||
|
pub use common::DeviceKind;
|
||||||
|
pub use query::{Request, Response, ResponsePriority};
|
||||||
|
pub use resolver::{Resolver, BfsResolver, LutResolver};
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ class App:
|
|||||||
def __get_device_unit(self, device_kind: DeviceKind) -> str:
|
def __get_device_unit(self, device_kind: DeviceKind) -> str:
|
||||||
match device_kind:
|
match device_kind:
|
||||||
case DeviceKind.RESISTOR:
|
case DeviceKind.RESISTOR:
|
||||||
|
# YYC MARK: This is ohm char.
|
||||||
return "\u2126"
|
return "\u2126"
|
||||||
case DeviceKind.CAPACITOR:
|
case DeviceKind.CAPACITOR:
|
||||||
return "F"
|
return "F"
|
||||||
|
|||||||
Reference in New Issue
Block a user