#![recursion_limit = "512"]
use clap::Parser;
mod app_new;
use app_new::generate_new_app;
mod app_config;
use app_config::get_user_input;
mod serial_monitor;
mod native_terminal;
mod line_editor;
mod app_build;
use app_build::build_raft_app;
mod app_flash;
use app_flash::flash_raft_app;
mod app_ota;
use app_ota::ota_raft_app;
mod app_debug_remote;
mod terminal_io;
mod raft_cli_utils;
mod console_log;
use raft_cli_utils::is_wsl;
use raft_cli_utils::check_target_folder_valid;
use raft_cli_utils::get_flash_tool_cmd;
use raft_cli_utils::{read_build_info, write_build_info, BuildInfo};
mod app_ports;
use app_ports::{PortsCmd, manage_ports};
mod app_libs;
use app_libs::{fetch_raft_libs, LibsCmd};
mod cmd_history;
const HISTORY_FILE_NAME: &str = ".raftcli_history";
#[derive(Clone, Parser, Debug)]
enum Action {
#[clap(name = "new", about = "Create a new raft app", alias = "n")]
New(NewCmd),
#[clap(name = "build", about = "Build a raft app", alias = "b")]
Build(BuildCmd),
#[clap(name = "monitor", about = "Monitor a serial port", alias = "m")]
Monitor(MonitorCmd),
#[clap(name = "run", about = "Build, flash and monitor a raft app", alias = "r")]
Run(RunCmd),
#[clap(name = "flash", about = "Flash firmware to the device", alias = "f")]
Flash(FlashCmd),
#[clap(name = "ota", about = "Over-the-air update", alias = "o")]
Ota(OtaCmd),
#[clap(name = "ports", about = "Manage serial ports", alias = "p")]
Ports(PortsCmd),
#[clap(name = "libs", about = "Fetch local Raft development libraries", alias = "l")]
Libs(LibsCmd),
#[clap(name = "debug", about = "Start remote debug console", alias = "d")]
DebugRemote(DebugRemoteCmd),
#[clap(name = "esptool", about = "Run esptool directly with arguments", alias = "e")]
Esptool(EsptoolCmd),
}
#[derive(Clone, Parser, Debug)]
struct NewCmd {
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
base_folder: Option<String>,
#[clap(short = 'c', long, help = "Clean the target folder")]
clean: bool,
}
#[derive(Clone, Parser, Debug)]
struct BuildCmd {
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
app_folder: Option<String>,
#[clap(short = 's', long, help = "System type to build")]
sys_type: Option<String>,
#[clap(short = 'c', long, help = "Clean the target folder")]
clean: bool,
#[clap(short = 'n', long, help = "Clean only")]
clean_only: bool,
#[clap(long, help = "Use docker for build")]
docker: bool,
#[clap(long, help = "Do not use docker for build")]
no_docker: bool,
#[clap(short = 'i', long, help = "Find and use local ESP IDF matching Dockerfile version")]
idf_local_build: bool,
#[clap(short = 'e', long, help = "Full path to ESP IDF folder for local build (when not using docker)")]
esp_idf_path: Option<String>,
}
#[derive(Clone, Parser, Debug)]
struct MonitorCmd {
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
app_folder: Option<String>,
#[clap(short = 'p', long, help = "Serial port")]
port: Option<String>,
#[clap(short = 'b', long, help = "Baud rate")]
monitor_baud: Option<u32>,
#[clap(short = 'r', long, help = "Disable serial port reconnection when monitoring")]
no_reconnect: bool,
#[clap(short = 'n', long, help = "Native serial port when in WSL")]
native_serial_port: bool,
#[arg(short = 'l', long, help = "Log serial data to file")]
log: bool,
#[arg(short = 'g', long, default_value = "./logs", help = "Folder for log files")]
log_folder: Option<String>,
#[clap(short = 'v', long, help = "Vendor ID")]
vid: Option<String>,
#[clap(long, value_name = "MODE", help = "Prefix received lines with wall-clock time: 'first' (on first byte) or 'eol' (on newline)")]
rx_timestamps: Option<String>,
#[clap(long, help = "Stream serial data to stdout without terminal UI for automation")]
agent_stream: bool,
}
#[derive(Clone, Parser, Debug)]
struct RunCmd {
app_folder: Option<String>,
#[clap(short = 's', long, help = "System type to build")]
sys_type: Option<String>,
#[clap(short = 'c', long, help = "Clean the target folder")]
clean: bool,
#[clap(long, help = "Use docker for build")]
docker: bool,
#[clap(long, help = "Do not use docker for build")]
no_docker: bool,
#[clap(short = 'i', long, help = "Find and use local ESP IDF matching Dockerfile version")]
idf_local_build: bool,
#[clap(short = 'e', long, help = "Full path to ESP IDF folder for local build (when not using docker)")]
esp_idf_path: Option<String>,
#[clap(short = 'p', long, help = "Serial port")]
port: Option<String>,
#[clap(short = 'o', long, help = "IP address or hostname for OTA flashing")]
ip_addr: Option<String>,
#[clap(short = 'b', long, help = "Monitor baud rate")]
monitor_baud: Option<u32>,
#[clap(short = 'r', long, help = "Disable serial port reconnection when monitoring")]
no_reconnect: bool,
#[clap(short = 'n', long, help = "Native serial port when in WSL")]
native_serial_port: bool,
#[clap(short = 'f', long, help = "Flash baud rate")]
flash_baud: Option<u32>,
#[clap(short = 't', long, help = "Flash tool (e.g. esptool)")]
flash_tool: Option<String>,
#[arg(short = 'l', long, help = "Log serial data to file")]
log: bool,
#[arg(short = 'g', long, default_value = "./logs", help = "Folder for log files")]
log_folder: Option<String>,
#[clap(short = 'v', long, help = "Vendor ID")]
vid: Option<String>,
#[clap(long, help = "Skip flashing the file system image", conflicts_with = "fs")]
no_fs: bool,
#[clap(long, help = "Flash the file system image (overrides saved --no-fs)", conflicts_with = "no_fs")]
fs: bool,
#[clap(long, value_name = "MODE", help = "Prefix received lines with wall-clock time: 'first' (on first byte) or 'eol' (on newline)")]
rx_timestamps: Option<String>,
#[clap(long, help = "Stream serial data to stdout without terminal UI for automation")]
agent_stream: bool,
}
#[derive(Clone, Parser, Debug)]
struct FlashCmd {
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
app_folder: Option<String>,
#[clap(short = 's', long, help = "System type to flash")]
sys_type: Option<String>,
#[clap(short = 'p', long, help = "Serial port")]
port: Option<String>,
#[clap(short = 'n', long, help = "Native serial port when in WSL")]
native_serial_port: bool,
#[clap(short = 'f', long, help = "Flash baud rate")]
flash_baud: Option<u32>,
#[clap(short = 't', long, help = "Flash tool (e.g. esptool)")]
flash_tool: Option<String>,
#[clap(short = 'v', long, help = "Vendor ID")]
vid: Option<String>,
#[clap(long, help = "Skip flashing the file system image", conflicts_with = "fs")]
no_fs: bool,
#[clap(long, help = "Flash the file system image (overrides saved --no-fs)", conflicts_with = "no_fs")]
fs: bool,
}
#[derive(Clone, Parser, Debug)]
struct OtaCmd {
#[clap(help = "IP address or hostname for OTA", value_name = "IP_ADDRESS_OR_HOSTNAME")]
ip_addr: String,
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
app_folder: Option<String>,
#[clap(short = 'p', long, help = "IP Port")]
ip_port: Option<u16>,
#[clap(short = 's', long, help = "System type to ota update")]
sys_type: Option<String>,
#[clap(short = 'c', long, help = "Use curl for OTA")]
use_curl: bool,
}
#[derive(Clone, Parser, Debug)]
struct DebugRemoteCmd {
#[clap(help = "Device address for debugging (hostname or IP)", value_name = "IP_ADDRESS_OR_HOSTNAME")]
device_address: String,
#[clap(help = "Path to the application folder", value_name = "APPLICATION_FOLDER")]
app_folder: Option<String>,
#[clap(short = 'p', long, help = "Port for debugging", default_value = "8080")]
port: u16,
#[clap(short = 'l', long, help = "Log debug console data to file")]
log: bool,
#[clap(short = 'g', long, default_value = "./logs", help = "Folder for log files")]
log_folder: Option<String>,
}
#[derive(Clone, Parser, Debug)]
struct EsptoolCmd {
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
#[clap(short = 'n', long, help = "Native serial port when in WSL")]
native_serial_port: bool,
}
#[derive(Parser, Debug)]
#[clap(version, author, about)]
struct Cli {
#[clap(subcommand)]
action: Action,
}
fn main() {
let args = Cli::parse();
match args.action {
Action::New(cmd) => {
let base_folder = cmd.base_folder.unwrap_or(".".to_string());
let folder_valid = check_target_folder_valid(&base_folder, cmd.clean);
if !folder_valid {
println!("Error: target folder is not valid");
std::process::exit(1);
}
let json_config_str = get_user_input(&base_folder);
let json_config = serde_json::from_str(&json_config_str.unwrap()).unwrap();
let _result = generate_new_app(&base_folder, json_config).unwrap();
}
Action::Build(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let result = build_raft_app(&cmd.sys_type, cmd.clean,
cmd.clean_only, app_folder, cmd.docker, cmd.no_docker,
cmd.idf_local_build, cmd.esp_idf_path);
if result.is_err() {
println!("Build failed {:?}", result);
std::process::exit(1);
}
}
Action::Monitor(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let saved = read_build_info(&app_folder);
let port = cmd.port.clone().or(saved.last_port.clone());
let vid = cmd.vid.clone().or(saved.last_vid.clone());
let monitor_baud = cmd.monitor_baud.or(saved.last_monitor_baud).unwrap_or(115200);
let mut updates = BuildInfo::default();
if cmd.port.is_some() { updates.last_port = cmd.port.clone(); }
if cmd.vid.is_some() { updates.last_vid = cmd.vid.clone(); }
if cmd.monitor_baud.is_some() { updates.last_monitor_baud = cmd.monitor_baud; }
if updates.last_port.is_some() || updates.last_vid.is_some() || updates.last_monitor_baud.is_some() {
if let Err(e) = write_build_info(&app_folder, &updates) {
println!("Warning: Failed to save settings to raft.info: {}", e);
}
}
let log = cmd.log;
let mut log_folder = cmd.log_folder.unwrap_or("./logs".to_string());
if !log_folder.starts_with("/") {
let mut log_folder_path = std::path::PathBuf::from(&app_folder);
log_folder_path.push(log_folder);
log_folder = log_folder_path.to_str().unwrap().to_string();
}
if !cmd.native_serial_port && is_wsl() {
let result = serial_monitor::start_non_native(app_folder,
port.clone(), monitor_baud, cmd.no_reconnect, log, log_folder, vid.clone(),
cmd.rx_timestamps.clone(), cmd.agent_stream);
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
println!("Serial monitor error: {}", e);
std::process::exit(1);
}
}
}
let result = serial_monitor::start_native(app_folder,
port, monitor_baud, cmd.no_reconnect, log, log_folder, vid,
cmd.rx_timestamps,
HISTORY_FILE_NAME.to_string(), cmd.agent_stream);
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
println!("Serial monitor error: {}", e);
std::process::exit(1);
}
}
}
Action::Run(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let saved = read_build_info(&app_folder);
let port = cmd.port.clone().or(saved.last_port.clone());
let vid = cmd.vid.clone().or(saved.last_vid.clone());
let flash_baud = cmd.flash_baud.or(saved.last_flash_baud).unwrap_or(1000000);
let monitor_baud = cmd.monitor_baud.or(saved.last_monitor_baud).unwrap_or(115200);
let no_fs = if cmd.no_fs { true } else if cmd.fs { false } else { saved.last_no_fs.unwrap_or(false) };
let mut updates = BuildInfo::default();
if cmd.port.is_some() { updates.last_port = cmd.port.clone(); }
if cmd.vid.is_some() { updates.last_vid = cmd.vid.clone(); }
if cmd.flash_baud.is_some() { updates.last_flash_baud = cmd.flash_baud; }
if cmd.monitor_baud.is_some() { updates.last_monitor_baud = cmd.monitor_baud; }
if cmd.no_fs || cmd.fs { updates.last_no_fs = Some(no_fs); }
if updates.last_port.is_some() || updates.last_vid.is_some() || updates.last_flash_baud.is_some()
|| updates.last_monitor_baud.is_some() || updates.last_no_fs.is_some() {
if let Err(e) = write_build_info(&app_folder, &updates) {
println!("Warning: Failed to save settings to raft.info: {}", e);
}
}
let result = build_raft_app(&cmd.sys_type, cmd.clean, false,
app_folder.clone(), cmd.docker, cmd.no_docker,
cmd.idf_local_build,
cmd.esp_idf_path);
if result.is_err() {
println!("Build failed {:?}", result);
std::process::exit(1);
}
let result = flash_raft_app(&cmd.sys_type,
app_folder.clone(),
port.clone(),
cmd.native_serial_port,
vid.clone(),
flash_baud,
cmd.flash_tool,
no_fs);
if result.is_err() {
println!("Flash operation failed {:?}", result);
std::process::exit(1);
}
let log = cmd.log;
let log_folder = cmd.log_folder.unwrap_or("./logs".to_string());
if !cmd.native_serial_port && is_wsl() {
let result = serial_monitor::start_non_native(app_folder,
port.clone(), monitor_baud, cmd.no_reconnect, log, log_folder, vid.clone(),
cmd.rx_timestamps.clone(), cmd.agent_stream);
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
println!("Serial monitor error: {}", e);
std::process::exit(1);
}
}
}
let result = serial_monitor::start_native(app_folder,
port, monitor_baud, cmd.no_reconnect, log, log_folder, vid,
cmd.rx_timestamps,
HISTORY_FILE_NAME.to_string(), cmd.agent_stream);
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
println!("Serial monitor error: {}", e);
std::process::exit(1);
}
}
}
Action::Flash(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let saved = read_build_info(&app_folder);
let port = cmd.port.clone().or(saved.last_port.clone());
let vid = cmd.vid.clone().or(saved.last_vid.clone());
let flash_baud = cmd.flash_baud.or(saved.last_flash_baud).unwrap_or(1000000);
let no_fs = if cmd.no_fs { true } else if cmd.fs { false } else { saved.last_no_fs.unwrap_or(false) };
let mut updates = BuildInfo::default();
if cmd.port.is_some() { updates.last_port = cmd.port.clone(); }
if cmd.vid.is_some() { updates.last_vid = cmd.vid.clone(); }
if cmd.flash_baud.is_some() { updates.last_flash_baud = cmd.flash_baud; }
if cmd.no_fs || cmd.fs { updates.last_no_fs = Some(no_fs); }
if updates.last_port.is_some() || updates.last_vid.is_some() || updates.last_flash_baud.is_some()
|| updates.last_no_fs.is_some() {
if let Err(e) = write_build_info(&app_folder, &updates) {
println!("Warning: Failed to save settings to raft.info: {}", e);
}
}
let result = flash_raft_app(&cmd.sys_type,
app_folder.clone(),
port,
cmd.native_serial_port,
vid,
flash_baud,
cmd.flash_tool,
no_fs);
if result.is_err() {
println!("Flash operation failed {:?}", result);
std::process::exit(1);
}
}
Action::Ota(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let result = ota_raft_app(&cmd.sys_type,
app_folder.clone(),
cmd.ip_addr.clone(),
cmd.ip_port.clone(),
cmd.use_curl);
if result.is_err() {
println!("OTA operation failed {:?}", result);
std::process::exit(1);
}
}
Action::Ports(cmd) => {
manage_ports(&cmd);
}
Action::Libs(cmd) => {
if let Err(e) = fetch_raft_libs(&cmd) {
eprintln!("Libs operation failed: {}", e);
std::process::exit(1);
}
}
Action::DebugRemote(cmd) => {
let app_folder = cmd.app_folder.unwrap_or(".".to_string());
let log = cmd.log;
let mut log_folder = cmd.log_folder.unwrap_or("./logs".to_string());
if !log_folder.starts_with("/") {
let mut log_folder_path = std::path::PathBuf::from(&app_folder);
log_folder_path.push(log_folder);
log_folder = log_folder_path.to_str().unwrap().to_string();
}
let server_address = format!("{}:{}", cmd.device_address, cmd.port);
if let Err(e) = app_debug_remote::start_debug_console(
app_folder,
server_address,
log,
log_folder,
HISTORY_FILE_NAME.to_string(),
) {
eprintln!("Error starting debug console: {}", e);
}
}
Action::Esptool(cmd) => {
let esptool_cmd = get_flash_tool_cmd(None, cmd.native_serial_port);
if is_wsl() && !cmd.native_serial_port {
println!("WSL detected: Delegating esptool to Windows raft.exe");
let mut args = vec!["esptool".to_string()];
args.extend(cmd.args);
args.push("-n".to_string());
let output = std::process::Command::new("raft.exe")
.args(&args)
.status();
match output {
Ok(status) => {
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
}
Err(e) => {
eprintln!("Error executing raft.exe: {}", e);
std::process::exit(1);
}
}
} else {
println!("Executing: {} {:?}", esptool_cmd, cmd.args);
let child = if esptool_cmd.starts_with("python -m ") {
let module = esptool_cmd.strip_prefix("python -m ").unwrap();
let mut args = vec!["-m".to_string(), module.to_string()];
args.extend(cmd.args.clone());
std::process::Command::new("python")
.args(&args)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
} else {
std::process::Command::new(&esptool_cmd)
.args(&cmd.args)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
};
match child {
Ok(mut child) => {
match child.wait() {
Ok(status) => {
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
}
Err(e) => {
eprintln!("Error waiting for process: {}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("Error executing {}: {}", esptool_cmd, e);
eprintln!("\nMake sure esptool is installed. You can install it with:");
eprintln!(" pip install esptool");
std::process::exit(1);
}
}
}
}
}
std::process::exit(0);
}