628 lines
21 KiB
Rust
628 lines
21 KiB
Rust
// use std::time::Duration;
|
|
// use std::{thread, time};
|
|
// use serialport::{SerialPort, StopBits, Parity, FlowControl, DataBits};
|
|
// use std::io::{self, Read, Write, BufWriter};
|
|
use regex::Regex;
|
|
use rustyline::Editor;
|
|
use rustyline::error::ReadlineError;
|
|
// use std::fs::OpenOptions;
|
|
use std::io::{BufReader, BufRead, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
// use std::collections::HashMap;
|
|
use clap::{Parser};
|
|
|
|
pub mod perf {
|
|
use chrono::{DateTime, Utc, Local};
|
|
|
|
pub struct Perf {
|
|
start: DateTime<Utc>,
|
|
end: DateTime<Utc>,
|
|
delta_ms: i64,
|
|
delta_mks: i64,
|
|
delta_ns: i64,
|
|
}
|
|
|
|
impl Perf {
|
|
pub fn new() -> Perf {
|
|
Perf {
|
|
start: Utc::now(),
|
|
end: Utc::now(),
|
|
delta_ms: 0,
|
|
delta_mks: 0,
|
|
delta_ns: 0,
|
|
}
|
|
}
|
|
|
|
pub fn start(&mut self) {
|
|
self.start = Utc::now();
|
|
}
|
|
|
|
// measure: "ms" or "mks"
|
|
pub fn stop(&mut self, out_msg: &str, measure: &str) {
|
|
self.end = Utc::now();
|
|
self.delta_ms = (self.end - self.start).num_microseconds().unwrap()/1000;
|
|
self.delta_mks = (self.end - self.start).num_microseconds().unwrap();
|
|
self.delta_ns = (self.end - self.start).num_nanoseconds().unwrap();
|
|
|
|
if measure == "mks" { println!("{} (mks): {:?}", out_msg, self.delta_mks); }
|
|
else if measure == "ns" { println!("{} (ns): {:?}", out_msg, self.delta_ns); }
|
|
else { println!("{} (ms): {:?}", out_msg, self.delta_ms); }
|
|
}
|
|
|
|
pub fn stop_restart(&mut self, out_msg: &str, measure: &str) {
|
|
self.stop(out_msg, measure);
|
|
self.start = Utc::now();
|
|
}
|
|
|
|
pub fn cur_time(format: &str) -> String {
|
|
let now = Local::now();
|
|
let fmt = now.format(format).to_string();
|
|
return fmt;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
use crate::perf::Perf;
|
|
|
|
|
|
pub mod uart_transact {
|
|
const MAX_BUF: usize = 4096;
|
|
use serialport::{SerialPort, StopBits, Parity, FlowControl, DataBits};
|
|
use std::time::Duration;
|
|
use crate::perf::Perf;
|
|
use std::io::{self, Write, BufWriter, BufReader, BufRead};
|
|
use std::fs::{File, OpenOptions};
|
|
use std::path::Path;
|
|
|
|
#[derive(PartialEq)]
|
|
pub enum TypeEn {
|
|
Rx = 0,
|
|
Tx,
|
|
RxTx,
|
|
TxRx,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct PortSettingsT {
|
|
baud_rate: u32,
|
|
data_bits: DataBits,
|
|
parity: Parity,
|
|
stop_bits: StopBits,
|
|
flow_ctrl: FlowControl,
|
|
timeout_msec: u64,
|
|
}
|
|
|
|
impl PortSettingsT {
|
|
pub fn new(baud_rate: u32, data_bits_: u8, parity_: &str, stop_bits_: u8,
|
|
flow_ctrl_: &str, timeout_msec: u64) -> Result<Self, String> {
|
|
|
|
let data_bits = match data_bits_ {
|
|
5 => DataBits::Five,
|
|
6 => DataBits::Six,
|
|
7 => DataBits::Seven,
|
|
8 => DataBits::Eight,
|
|
_ => { return Err(format!(
|
|
"error: the number of data bits specified is incorrect
|
|
(type from 5 to 8)"));
|
|
}
|
|
};
|
|
|
|
let parity = match parity_ {
|
|
"Even" => Parity::Even,
|
|
"Odd" => Parity::Odd,
|
|
"None" => Parity::None,
|
|
_ => { return Err(format!(
|
|
"error: the parity of data specified is incorrect
|
|
(type \"Even\", \"Odd\" or \"None\")"));
|
|
}
|
|
};
|
|
|
|
let stop_bits = match stop_bits_ {
|
|
1 => StopBits::One,
|
|
2 => StopBits::Two,
|
|
_ => { return Err(format!(
|
|
"error: the stop bits of data specified is incorrect
|
|
(type 1 or 2)"));
|
|
}
|
|
};
|
|
|
|
let flow_ctrl = match flow_ctrl_ {
|
|
"None" => FlowControl::None,
|
|
"Software" => FlowControl::Software,
|
|
"Hardware" => FlowControl::Hardware,
|
|
_ => { return Err(format!(
|
|
"error: the parity of data specified is incorrect
|
|
(type \"None\", \"Software\" or \"Hardware\")"));
|
|
}
|
|
};
|
|
|
|
Ok(Self { baud_rate, data_bits, parity, stop_bits, flow_ctrl, timeout_msec })
|
|
}
|
|
}
|
|
|
|
struct Logger {
|
|
out: BufWriter<File>,
|
|
log_flag: bool,
|
|
}
|
|
|
|
impl Logger {
|
|
fn new<P: AsRef<Path>>(filename: P, log_flag: bool) -> Result<Self, io::Error> {
|
|
let path = Path::new(filename.as_ref());
|
|
|
|
let file = match path.exists() {
|
|
true => OpenOptions::new().write(true).append(true).open(filename.as_ref())?,
|
|
false => File::create(filename.as_ref())?
|
|
};
|
|
|
|
let out = BufWriter::new(file);
|
|
|
|
Ok(Self { out, log_flag })
|
|
}
|
|
|
|
fn log(&mut self, msg: &str) -> Result<(), io::Error> {
|
|
if self.log_flag == true {
|
|
writeln!(&mut self.out, "{} {};{:?}",
|
|
Perf::cur_time("%Y.%m.%d"),
|
|
Perf::cur_time("%H:%M:%S.%3f"),
|
|
msg.trim())?;
|
|
self.out.flush()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct UartTransact {
|
|
type_: TypeEn,
|
|
port_name: String,
|
|
port_settings: PortSettingsT,
|
|
tx_packet: String,
|
|
rx_packet: String,
|
|
|
|
port: Box<dyn SerialPort>,
|
|
save_data_flag: bool,
|
|
save_cmd_flag: bool,
|
|
data_writer: Logger,
|
|
cmd_writer: Logger,
|
|
}
|
|
|
|
impl UartTransact {
|
|
pub fn new(trans_type: &str,
|
|
port_name: &str,
|
|
port_settings: PortSettingsT,
|
|
save_data_flag: bool,
|
|
save_cmd_flag: bool) -> Result<Self, io::Error> {
|
|
|
|
let mut type_ = TypeEn::TxRx;
|
|
if trans_type == "TxRx" { type_ = TypeEn::TxRx; }
|
|
else if trans_type == "Tx" { type_ = TypeEn::Rx; }
|
|
else if trans_type == "RxTx" { type_ = TypeEn::RxTx; }
|
|
else if trans_type == "Rx" { type_ = TypeEn::Rx; }
|
|
|
|
let mut filename = format!("../log/data_{}.log", Perf::cur_time("%Y%m%d"));
|
|
let data_writer = Logger::new(filename.clone(), save_data_flag)?;
|
|
filename = format!("../log/commands_{}.log", Perf::cur_time("%Y%m%d"));
|
|
let cmd_writer = Logger::new(filename.clone(), save_data_flag)?;
|
|
|
|
let port = serialport::new(port_name, port_settings.baud_rate)
|
|
.timeout(Duration::from_millis(port_settings.timeout_msec)).open().unwrap();
|
|
|
|
let tx_packet = String::new();
|
|
let rx_packet = String::new();
|
|
|
|
Ok( Self { type_, port_name: port_name.to_string(), port_settings, save_data_flag, save_cmd_flag,
|
|
data_writer, cmd_writer,
|
|
port, tx_packet, rx_packet,
|
|
})
|
|
}
|
|
|
|
|
|
pub fn add_packet_for_tx(&mut self, buf: String, size: u32) -> Result<(), String> {
|
|
if size as usize >= MAX_BUF {
|
|
return Err(format!("too much data for transmit! Data size must be < {} bytes", MAX_BUF));
|
|
}
|
|
else { self.tx_packet = buf; }
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
pub fn start_transact(&mut self) -> Result<(), String> {
|
|
|
|
if self.type_ == TypeEn::TxRx {
|
|
self.cmd_writer.log(&self.tx_packet).unwrap();
|
|
|
|
// println!("write to port: ({:?})", self.tx_packet.as_bytes());
|
|
|
|
match self.port.write(&self.tx_packet.as_bytes()) {
|
|
Ok(_) => {},
|
|
Err(e) => eprintln!("Error sending data: {}", e),
|
|
}
|
|
|
|
let mut reader = BufReader::new(&mut self.port);
|
|
|
|
let mut s = String::new();
|
|
if let Err(err) = reader.read_line(&mut s) {
|
|
println!("error: {}", err);
|
|
return Ok(());
|
|
}
|
|
if s.len() > 0 {
|
|
self.rx_packet = s.clone();
|
|
self.data_writer.log(&s).unwrap();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_received_packet(&mut self) -> String {
|
|
self.rx_packet.clone()
|
|
}
|
|
|
|
fn set_port_settings(port_name: &str,
|
|
set: PortSettingsT) -> Result<Box<dyn SerialPort>, String> {
|
|
|
|
let mut port = match serialport::new(port_name, set.baud_rate)
|
|
.timeout(Duration::from_millis(set.timeout_msec)).open() {
|
|
Ok(p) => p,
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
};
|
|
|
|
match port.set_baud_rate(set.baud_rate) {
|
|
Ok(_) => {},
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
}
|
|
match port.set_data_bits(set.data_bits) {
|
|
Ok(_) => {},
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
}
|
|
match port.set_parity(set.parity) {
|
|
Ok(_) => {},
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
}
|
|
match port.set_stop_bits(set.stop_bits) {
|
|
Ok(_) => {},
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
}
|
|
match port.set_flow_control(set.flow_ctrl) {
|
|
Ok(_) => {},
|
|
Err(msg) => return Err(format!("error: failed to open serial port {} ({})", port_name, msg)),
|
|
}
|
|
|
|
return Ok(port);
|
|
}
|
|
}
|
|
}
|
|
|
|
use crate::uart_transact::*;
|
|
|
|
fn read_str(s: &str) -> Result<Vec<String>, String> {
|
|
let re = match Regex::new(r#""([^"]+)"|(\S+)"#) {
|
|
Ok(val) => val,
|
|
Err(_) => { return Err(format!("Error in regex!")); }
|
|
};
|
|
|
|
let res: Vec<String> = re.find_iter(&s)
|
|
.map(|match_| match_.as_str().to_owned().replace("\"", ""))
|
|
.collect();
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
fn uart_cmd_interp(cmd: &str) -> String {
|
|
let mut out = String::new();
|
|
|
|
let args = match read_str(&cmd) {
|
|
Ok(arg) => arg,
|
|
Err(msg) => {
|
|
return format!("error: failed read input string: {}", msg);
|
|
}
|
|
};
|
|
|
|
let args_cnt = args.len();
|
|
if args_cnt == 0 { return format!("{}", out); }
|
|
let cmd_head = &args[0].clone();
|
|
|
|
let mut timeout: u64 = 200;
|
|
let mut port_set = PortSettingsT::new(9600, 8, "None", 1, "None", timeout).unwrap();
|
|
let mut save_data_flag = true;
|
|
let mut save_cmd_flag = true;
|
|
let mut trans_type = "TxRx";
|
|
|
|
if cmd_head == "exit" { return format!("exit"); }
|
|
else if cmd_head.contains("help") {
|
|
out.push_str(&format!("Serial port terminal supports the following commands:"));
|
|
out.push_str(&format!("\n1. exit - \t\texcecute quit from this program"));
|
|
out.push_str(&format!("\n2. help - \t\texplain how to use this program"));
|
|
out.push_str(&format!("\n3. show_ports - \tshows all available serial ports"));
|
|
out.push_str(&format!("\n4. send - \t\tsends specified <data>\n\tUse: send <port_name> <data>\n\tExample: send /dev/ttyUSB0 \"command data1 data2\""));
|
|
out.push_str(&format!("\n5. set_port - set port with specified settings\n \tUse: set_port <port_name> <speed> <data_bits> <parity> <stop_bits> <flow_ctrl> <timeout> <transact_type> <save_data_flags>\n \tWhere: \n\t\t<port name> - port name \n\t\t<speed> - speed (9600 for example) \n\t\t<data bits>: 5, 6, 7, 8 \n\t\t<parity>: None, Even, Odd \n\t\t<stop bits>: 1, 2 \n\t\t<flow ctrl>: None, Software, Hardware \n\t\t<timeout> - timeout for receive and transmit in msec \n\t\t<transaction type>: tx, rx, TxRx, RxTx (tx - transmit, rx - receive) \n\t\t<save_data_flags>: true, false \n\tExample: set_port /dev/ttyUSB0 9600 8 Even 1 None 1000 TxRx true\n"));
|
|
return out;
|
|
}
|
|
else if cmd_head.contains("show_ports") {
|
|
match serialport::available_ports() {
|
|
Ok(ports) => {
|
|
for port in ports {
|
|
out.push_str(&format!("{}: {:?}\n", port.port_name, port.port_type));
|
|
}
|
|
}
|
|
Err(msg) => { return format!("error while get all available serial port list: {}", msg); }
|
|
}
|
|
|
|
return out;
|
|
}
|
|
else if cmd_head == "send" {
|
|
if args_cnt != 3 { return format!("Error while parse command! {}", "arguments count must be 2"); }
|
|
else {
|
|
let port_name = &args[1];
|
|
|
|
let mut uart_trans = UartTransact::new(trans_type, port_name,
|
|
port_set, save_data_flag, save_cmd_flag).unwrap();
|
|
|
|
let mut data_out = args[2].clone();
|
|
data_out.push_str(std::str::from_utf8(b"\x0D\x0A").unwrap()); // CR + LF
|
|
|
|
uart_trans.add_packet_for_tx(data_out.clone(), data_out.len() as u32).unwrap();
|
|
uart_trans.start_transact().unwrap();
|
|
|
|
let s = uart_trans.get_received_packet();
|
|
let data = format!("{}: {}", Perf::cur_time("%H:%M:%S.%3f"), s.trim());
|
|
return data;
|
|
}
|
|
}
|
|
else if cmd_head.contains("set_port") {
|
|
|
|
if args_cnt != 10 { return format!("Error while parse command! {}", "arguments count must be 9"); }
|
|
else {
|
|
let port_name = &args[1];
|
|
let speed = args[2].parse::<u32>().unwrap();
|
|
let data_bits = args[3].parse::<u8>().unwrap();
|
|
let parity = &args[4];
|
|
let stop_bits = args[5].parse::<u8>().unwrap();
|
|
let flow_ctrl = &args[6];
|
|
timeout = args[7].parse::<u64>().unwrap();
|
|
trans_type = &args[8];
|
|
save_data_flag = args[9].parse::<bool>().unwrap();
|
|
save_cmd_flag = save_data_flag;
|
|
|
|
port_set = PortSettingsT::new(speed, data_bits, parity, stop_bits,
|
|
flow_ctrl, timeout).unwrap();
|
|
|
|
let uart_trans = UartTransact::new(trans_type, &port_name,
|
|
port_set.clone(), save_data_flag, save_cmd_flag).unwrap();
|
|
|
|
let data = format!("{}: port {} was set with the following settings: {:?}",
|
|
Perf::cur_time("%H:%M:%S.%3f"),
|
|
port_name,
|
|
port_set);
|
|
return data;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
fn cli_mode() {
|
|
let fname_hist = "../settings/cmd_history.log".to_string();
|
|
let mut editor = Editor::<()>::new();
|
|
if editor.load_history(&fname_hist).is_err() {
|
|
println!("error while reading cmd_history.log file!", );
|
|
return;
|
|
}
|
|
|
|
loop {
|
|
let s: String = match editor.readline(">> ") {
|
|
Ok(s_) => s_.to_string(),
|
|
Err(ReadlineError::Interrupted) => {
|
|
editor.save_history(&fname_hist)
|
|
.expect("error while save the command to the history file (cmd_history.log)");
|
|
return;
|
|
},
|
|
Err(ReadlineError::Eof) => {
|
|
editor.save_history(&fname_hist)
|
|
.expect("error while save the command to the history file (cmd_history.log)");
|
|
return;
|
|
},
|
|
Err(msg) => {
|
|
println!("{}{:?}",
|
|
"unknown command",
|
|
msg);
|
|
return;
|
|
}
|
|
};
|
|
|
|
editor.add_history_entry(&s);
|
|
|
|
let reply = uart_cmd_interp(&s);
|
|
|
|
if reply == "exit" { break; }
|
|
else { println!("{}", reply); }
|
|
|
|
}
|
|
|
|
editor.save_history(&fname_hist).unwrap();
|
|
}
|
|
|
|
fn tcp_server() {
|
|
let listener = TcpListener::bind("127.0.0.1:10000").unwrap();
|
|
println!("TCP-server is running");
|
|
|
|
for stream in listener.incoming() {
|
|
match stream {
|
|
Ok(stream) => {
|
|
println!("connect to client: {:?}", stream);
|
|
handle_client(stream);
|
|
|
|
},
|
|
Err(ref msg) => {
|
|
println!("error: failed attempt to set connection with client: ({:?}): {}",
|
|
stream, msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_client(mut stream: TcpStream) {
|
|
loop {
|
|
let mut perf = Perf::new();
|
|
let mut reader = BufReader::new(&stream);
|
|
let mut rx_data = String::new();
|
|
if let Err(msg) = reader.read_line(&mut rx_data) {
|
|
println!("error: failed attempt to read data from client: {}", msg);
|
|
return;
|
|
}
|
|
|
|
// println!("rx_data: ({})", rx_data.trim());
|
|
|
|
perf.start();
|
|
|
|
if rx_data.starts_with("send") ||
|
|
rx_data.starts_with("help") ||
|
|
rx_data.starts_with("set_port") ||
|
|
rx_data.starts_with("show_ports") {
|
|
let reply = uart_cmd_interp(&rx_data.trim());
|
|
|
|
// reply.push('\n');
|
|
if let Err(msg) = stream.write_all(reply.as_bytes()) {
|
|
println!("error: failed attempt to send data to client: {}", msg);
|
|
return;
|
|
}
|
|
}
|
|
else if rx_data.starts_with("exit") {
|
|
println!("The client requested to close the connection");
|
|
return;
|
|
|
|
}
|
|
else if rx_data == "" {
|
|
let data = format!("empty string! ");
|
|
|
|
if let Err(msg) = stream.write_all(data.as_bytes()) {
|
|
println!("error: failed attempt to send data to client: {}", msg);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else {
|
|
let data = format!("unsuppoted command: ({})\n", rx_data.trim());
|
|
|
|
if let Err(msg) = stream.write_all(data.as_bytes()) {
|
|
println!("error: failed attempt to send data to client: {}", msg);
|
|
return;
|
|
|
|
}
|
|
}
|
|
|
|
perf.stop("speed: ", "mks");
|
|
}
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, author, about, long_about = None)]
|
|
struct Cli {
|
|
/// working mode: tcp - remote control with TCP/IP, cli - command line interface
|
|
#[arg(long, short = 'm', value_name = "MODE")]
|
|
mode: Option<String>,
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let cli = Cli::parse();
|
|
// println!("{:#?}", cli);
|
|
|
|
if let Some(mode) = &cli.mode {
|
|
if mode == "cli" { cli_mode(); }
|
|
else if mode == "tcp" { tcp_server(); }
|
|
else { println!("error: incorrect parameter. type --help"); }
|
|
}
|
|
|
|
}
|
|
|
|
// fn print_type_of<T>(_: &T) {
|
|
// println!("{}", std::any::type_name::<T>());
|
|
// }
|
|
|
|
|
|
// perf.stop("time for transaction", "mks");
|
|
|
|
// let mut buffer = Vec::new();
|
|
// match port.read_to_end(&mut buffer) {
|
|
// Ok(_) => {
|
|
// if buffer.len() > 0 {
|
|
// let data = format!("{}: {:?}", cur_time("%H:%M:%S"), buffer);
|
|
// println!("{}", data);
|
|
// if save_data_flag == true {
|
|
// writeln!(data_writer, "{}",
|
|
// format!("{};{};{}",
|
|
// cur_time("%Y.%m.%d"),
|
|
// cur_time("%H:%M:%S"),
|
|
// String::from_utf8(buffer).expect("msg")))
|
|
// .expect("err");
|
|
// }
|
|
// }
|
|
// },
|
|
// Err(msg) => eprintln!("{}", msg),
|
|
// }
|
|
|
|
// let mut cnt: u32 = 0;
|
|
// let mut i = 0;
|
|
// let mut buf: String = String::new();
|
|
|
|
// let start = Utc::now();
|
|
// let mut timeout_flag = false;
|
|
|
|
// let mut delta1 = 0;
|
|
// while cnt == 0 {
|
|
// cnt = match port.bytes_to_read() {
|
|
// Ok(c) => c,
|
|
// Err(msg) => { println!("{}", msg); return; }
|
|
// };
|
|
// let delta = Utc::now() - start;
|
|
// delta1 = delta.num_microseconds().unwrap()/1000;
|
|
// if delta1 > timeout_ms {
|
|
// println!("timeout: {}", delta1);
|
|
// timeout_flag = true;
|
|
// break;
|
|
// };
|
|
// }
|
|
|
|
// println!("time (ms): {}", delta1);
|
|
|
|
// // println!("time (ms): {:?}", delta.num_microseconds().unwrap()/1000);
|
|
|
|
// if timeout_flag == false {
|
|
// while i < 500000 {
|
|
// // thread::sleep(time::Duration::from_millis(10));
|
|
// i += 1;
|
|
// let mut buffer = [0u8; 4096];
|
|
|
|
// cnt = match port.bytes_to_read() {
|
|
// Ok(c) => c,
|
|
// Err(msg) => { println!("{}", msg); return; }
|
|
// };
|
|
|
|
// if cnt > 0 {
|
|
// match port.read(&mut buffer) {
|
|
// Ok(count) => {
|
|
// let s = std::str::from_utf8(&buffer[..count]).unwrap();
|
|
// buf.push_str(s);
|
|
// println!("{}", s);
|
|
// },
|
|
|
|
// Err(ref e) if e.kind() == io::ErrorKind::TimedOut => { println!("timeout!"); },
|
|
// Err(e) => eprintln!("error reading from port ({}): {}", port_name, e),
|
|
// }
|
|
// }
|
|
// else {}
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
// if buf.len() > 0 {
|
|
// let data = format!("{}: {:?}", cur_time("%H:%M:%S"), buf);
|
|
// println!("{}", data);
|
|
// if save_data_flag == true {
|
|
// writeln!(data_writer, "{};{};{}", cur_time("%Y.%m.%d"), cur_time("%H:%M:%S"), buf.trim()).expect("err");
|
|
// }
|
|
// }
|
|
|