提交 95b2b236 authored 作者: Serhij S's avatar Serhij S

CLI

上级 9717562e
差异被折叠。
[package]
name = "roboplc-cli"
version = "0.1.0"
edition = "2021"
authors = ["Serhij S. <div@altertech.com>"]
license = "Apache-2.0"
description = "RoboPLC command-line interface"
repository = "https://github.com/eva-ics/roboplc"
keywords = ["realtime", "robots", "plc", "industrial"]
readme = "README.md"
[[bin]]
name = "robo"
path = "src/main.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive", "env"] }
colored = "2.1.0"
prettytable-rs = "0.10.0"
roboplc = "0.1.27"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
toml = "0.8.12"
ureq = { version = "2.9.6", features = ["json", "native-certs", "native-tls"] }
ureq_multipart = "1.1.1"
which = "6.0.1"
# RoboPLC CLI
CLI for [RoboPLC](https://crates.io/crates/roboplc) project.
use core::fmt;
use std::{
env,
path::{Path, PathBuf},
time::Duration,
};
use clap::Parser;
use colored::Colorize as _;
use roboplc::thread_rt::Scheduling;
use serde::{Deserialize, Serialize};
use serde_json::json;
use ureq::Agent;
use ureq_multipart::MultipartBuilder;
use which::which;
const API_PREFIX: &str = "/roboplc/api";
#[derive(Debug, Serialize, Deserialize)]
pub struct State {
pid: Option<u32>,
mode: Mode,
memory_used: Option<u64>,
run_time: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Run,
Config,
Unknown,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Mode::Run => write!(f, "RUN"),
Mode::Config => write!(f, "CONFIG"),
Mode::Unknown => write!(f, "UNKNOWN"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Task {
name: String,
pid: u32,
cpu: i32,
cpu_usage: f32,
sched: Scheduling,
priority: i32,
}
#[derive(Parser)]
struct Args {
#[clap(short = 'T', long, default_value = "30", help = "Manager API timeout")]
timeout: f64,
#[clap(short = 'U', long, env = "ROBOPLC_URL", help = "Manager URL")]
url: String,
#[clap(short = 'k', long, env = "ROBOPLC_KEY", help = "Management key")]
key: String,
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Parser)]
enum SubCommand {
#[clap(name = "stat", about = "Get program status")]
Stat(StatCommand),
#[clap(name = "config", about = "Put remote in CONFIG mode")]
Config,
#[clap(name = "run", about = "Put remote in RUN mode")]
Run,
#[clap(name = "flash", about = "Flash program")]
Flash(FlashCommand),
}
#[derive(Parser)]
struct FlashCommand {
#[clap(long, env = "CARGO", help = "cargo/cross binary path")]
cargo: Option<PathBuf>,
#[clap(long, help = "Override remote cargo target")]
cargo_target: Option<String>,
#[clap(long, help = "Do not compile a Rust project, use a file instead")]
file: Option<PathBuf>,
#[clap(long, help = "Force flash (automatically put remote in CONFIG mode)")]
force: bool,
#[clap(long, help = "Put remote in RUN mode after flashing")]
run: bool,
}
fn stat_command(
url: &str,
key: &str,
agent: Agent,
_opts: StatCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let resp = agent
.post(&format!("{}{}/query.stats.program", url, API_PREFIX))
.set("x-auth-key", key)
.call()
.process_error()?;
let stats: State = resp.into_json()?;
let mode_colored = match stats.mode {
Mode::Run => format!("{}", stats.mode).green(),
Mode::Config => format!("{}", stats.mode).yellow(),
Mode::Unknown => format!("{}", stats.mode).red(),
};
println!("Mode {}", mode_colored);
if let Some(pid) = stats.pid {
println!("PID {}", pid);
}
if let Some(memory) = stats.memory_used {
println!("Mem {}", memory);
}
if let Some(run_time) = stats.run_time {
println!("Up {}", run_time);
}
Ok(())
}
macro_rules! ok {
() => {
println!("{}", "OK".green());
};
}
fn set_mode_command(
url: &str,
key: &str,
agent: Agent,
mode: Mode,
) -> Result<(), Box<dyn std::error::Error>> {
agent
.post(&format!("{}{}/set.program.mode", url, API_PREFIX))
.set("x-auth-key", key)
.send_json(ureq::json!({
"mode": mode,
}))
.process_error()?;
ok!();
Ok(())
}
#[derive(Deserialize)]
struct KernelInfo {
machine: String,
}
trait PrintErr<T> {
fn process_error(self) -> Result<T, Box<dyn std::error::Error>>;
}
impl<T> PrintErr<T> for Result<T, ureq::Error> {
fn process_error(self) -> Result<T, Box<dyn std::error::Error>> {
match self {
Ok(v) => Ok(v),
Err(e) => match e.kind() {
ureq::ErrorKind::HTTP => {
let response = e.into_response().unwrap();
let status = response.status();
let msg = format!(
"{} ({})",
response.into_string().unwrap_or_default(),
status
);
eprintln!("{}: {}", "Error".red(), msg);
Err("Remote".into())
}
_ => Err(e.into()),
},
}
}
}
fn flash_file(
url: &str,
key: &str,
agent: Agent,
file: PathBuf,
force: bool,
run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let (content_type, data) = MultipartBuilder::new()
.add_file("file", file)?
.add_text(
"params",
&serde_json::to_string(&json! {
{
"force": force,
"run": run,
}
})?,
)?
.finish()?;
agent
.post(&format!("{}{}/flash", url, API_PREFIX))
.set("x-auth-key", key)
.set("content-type", &content_type)
.send_bytes(&data)
.process_error()?;
Ok(())
}
fn flash(
url: &str,
key: &str,
agent: Agent,
opts: FlashCommand,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = opts.file {
flash_file(url, key, agent, file, opts.force, opts.run)?;
} else {
let cargo_target = if let Some(cargo_target) = opts.cargo_target {
cargo_target
} else {
let resp = agent
.post(&format!("{}{}/query.info.kernel", url, API_PREFIX))
.set("x-auth-key", key)
.call()?;
let info: KernelInfo = resp.into_json()?;
format!("{}-unknown-linux-gnu", info.machine)
};
let cargo = opts
.cargo
.unwrap_or_else(|| which("cross").unwrap_or_else(|_| Path::new("cargo").to_owned()));
let Some(name) = find_name_and_chdir() else {
return Err("Could not find Cross.toml/binary name".into());
};
let binary_name = Path::new("target")
.join(&cargo_target)
.join("release")
.join(name);
println!("Machine: {}", url.yellow());
println!("Cargo: {}", cargo.display().to_string().yellow());
println!("Cargo target: {}", cargo_target.yellow());
println!("Binary: {}", binary_name.display().to_string().yellow());
println!("Compiling...");
let result = std::process::Command::new(cargo)
.arg("build")
.arg("--release")
.arg("--target")
.arg(cargo_target)
.status()?;
if !result.success() {
return Err("Compilation failed".into());
}
println!("Flashing...");
flash_file(url, key, agent, binary_name, opts.force, opts.run)?;
}
ok!();
Ok(())
}
fn find_name_and_chdir() -> Option<String> {
let mut current_dir = env::current_dir().ok()?;
loop {
let mut cargo_toml_path = current_dir.clone();
cargo_toml_path.push("Cargo.toml");
if cargo_toml_path.exists() {
let contents = std::fs::read_to_string(cargo_toml_path).ok()?;
let value = contents.parse::<toml::Value>().ok()?;
env::set_current_dir(current_dir).ok()?;
return value["package"]["name"].as_str().map(String::from);
} else if !current_dir.pop() {
break;
}
}
None
}
#[derive(Parser)]
struct StatCommand {}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let agent: Agent = ureq::AgentBuilder::new()
.timeout_read(Duration::from_secs_f64(args.timeout))
.timeout_write(Duration::from_secs_f64(args.timeout))
.build();
match args.subcmd {
SubCommand::Stat(opts) => {
stat_command(&args.url, &args.key, agent, opts)?;
}
SubCommand::Config => {
set_mode_command(&args.url, &args.key, agent, Mode::Config)?;
}
SubCommand::Run => {
set_mode_command(&args.url, &args.key, agent, Mode::Run)?;
}
SubCommand::Flash(opts) => {
flash(&args.url, &args.key, agent, opts)?;
}
}
Ok(())
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论