use std::path::PathBuf;
use std::fs;
use clap::{Parser, Subcommand, ValueEnum};
use plcviz::{L5xGraph, L5xNodeType, GraphType, PlcopenGraphBuilder, PlcopenGraphType};
#[derive(Parser)]
#[command(name = "plcviz")]
#[command(version, about = "Generate SVG diagrams from L5X and PLCopen XML files", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(value_name = "FILE")]
file: Option<PathBuf>,
#[arg(short = 't', long = "type", value_name = "TYPE", default_value = "structure")]
graph_type: GraphTypeArg,
#[arg(short = 'a', long = "aois")]
show_aois: bool,
}
#[derive(Subcommand)]
enum Commands {
Example,
}
#[derive(Clone, Copy, Default, ValueEnum)]
enum GraphTypeArg {
#[default]
Structure,
Call,
Dataflow,
Combined,
}
impl From<GraphTypeArg> for GraphType {
fn from(arg: GraphTypeArg) -> Self {
match arg {
GraphTypeArg::Structure => GraphType::Structure,
GraphTypeArg::Call => GraphType::CallGraph,
GraphTypeArg::Dataflow => GraphType::DataFlow,
GraphTypeArg::Combined => GraphType::Combined,
}
}
}
fn main() {
let cli = Cli::parse();
let graph_type: GraphType = cli.graph_type.into();
if let Some(Commands::Example) = cli.command {
run_example(graph_type);
return;
}
if let Some(path) = cli.file {
generate_from_l5x(path.to_str().unwrap(), graph_type, cli.show_aois);
} else {
eprintln!("Error: No input file specified");
eprintln!("Usage: plcviz <FILE> or plcviz example");
eprintln!("Try 'plcviz --help' for more information.");
std::process::exit(1);
}
}
fn run_example(graph_type: GraphType) {
eprintln!("Generating example: {}", graph_type.description());
let mut graph = L5xGraph::new();
match graph_type {
GraphType::Structure | GraphType::Combined => {
graph.add_program("MainProgram");
graph.add_program("CommProgram");
graph.add_routine("MainProgram", "MainRoutine");
graph.add_routine("MainProgram", "InitSequence");
graph.add_routine("MainProgram", "FaultHandler");
graph.add_routine("MainProgram", "MotorControl");
graph.add_routine("CommProgram", "EthernetComm");
graph.add_edge("MainProgram", "MainProgram.MainRoutine", None);
graph.add_edge("MainProgram", "MainProgram.InitSequence", None);
graph.add_edge("MainProgram", "MainProgram.FaultHandler", None);
graph.add_edge("MainProgram", "MainProgram.MotorControl", None);
graph.add_edge("CommProgram", "CommProgram.EthernetComm", None);
if graph_type == GraphType::Combined {
graph.add_call("MainProgram.MainRoutine", "MainProgram.InitSequence");
graph.add_call("MainProgram.MainRoutine", "MainProgram.MotorControl");
graph.add_call("MainProgram.MotorControl", "MainProgram.FaultHandler");
}
}
GraphType::CallGraph => {
graph.add_node("MainRoutine", "MainRoutine", L5xNodeType::Routine);
graph.add_node("InitSequence", "InitSequence", L5xNodeType::Routine);
graph.add_node("MotorControl", "MotorControl", L5xNodeType::Routine);
graph.add_node("FaultHandler", "FaultHandler", L5xNodeType::Routine);
graph.add_node("EthernetComm", "EthernetComm", L5xNodeType::Routine);
graph.add_node("Motor_AOI", "Motor_AOI", L5xNodeType::Aoi);
graph.add_call("MainRoutine", "InitSequence");
graph.add_call("MainRoutine", "MotorControl");
graph.add_call("MotorControl", "FaultHandler");
graph.add_call("MotorControl", "Motor_AOI");
}
GraphType::DataFlow => {
graph.add_node("MotorCmd", "MotorCmd", L5xNodeType::Tag);
graph.add_node("MotorFb", "MotorFb", L5xNodeType::Tag);
graph.add_node("Speed", "Speed", L5xNodeType::Tag);
graph.add_node("Fault", "Fault", L5xNodeType::Tag);
graph.add_node("MotorControl", "MotorControl", L5xNodeType::Routine);
graph.add_node("FaultHandler", "FaultHandler", L5xNodeType::Routine);
graph.add_edge("Speed", "MotorControl", Some("read"));
graph.add_edge("MotorControl", "MotorCmd", Some("write"));
graph.add_edge("MotorControl", "MotorFb", Some("write"));
graph.add_edge("MotorFb", "FaultHandler", Some("read"));
graph.add_edge("FaultHandler", "Fault", Some("write"));
}
}
let svg = graph.render_svg();
println!("{}", svg);
}
fn generate_from_l5x(path: &str, graph_type: GraphType, show_aois: bool) {
let path = PathBuf::from(path);
if !path.exists() {
eprintln!("Error: File not found: {}", path.display());
std::process::exit(1);
}
eprintln!("Generating {} from: {}", graph_type.description(), path.display());
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
if is_plcopen_format(&content) {
generate_from_plcopen(&content, graph_type);
} else {
generate_from_l5x_content(&content, graph_type, show_aois);
}
}
fn is_plcopen_format(content: &str) -> bool {
content.contains("<project") && content.contains("http://www.plcopen.org/xml/tc6")
}
fn generate_from_l5x_content(content: &str, graph_type: GraphType, show_aois: bool) {
let project: l5x::Project = match l5x::from_str(content) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing L5X: {}", e);
std::process::exit(1);
}
};
let target_type = project.target_type.as_deref().unwrap_or("Controller");
eprintln!("L5X export type: {}", target_type);
let svg = match target_type {
"Program" => generate_program_export(&project, graph_type, show_aois),
"AddOnInstructionDefinition" => generate_aoi_export(&project, graph_type),
_ => generate_controller_export(&project, graph_type, show_aois),
};
println!("{}", svg);
}
fn generate_from_plcopen(content: &str, graph_type: GraphType) {
let project: plcopen::Project = match plcopen::from_str(content) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing PLCopen XML: {}", e);
std::process::exit(1);
}
};
let project_name = project.content_header
.as_ref()
.map(|h| h.name.as_str())
.unwrap_or("unknown");
eprintln!("PLCopen project: {}", project_name);
let plcopen_graph_type = match graph_type {
GraphType::Structure | GraphType::Combined => PlcopenGraphType::Structure,
GraphType::CallGraph => PlcopenGraphType::CallGraph,
GraphType::DataFlow => PlcopenGraphType::DataTypeDeps,
};
let builder = PlcopenGraphBuilder::with_xml(project, plcopen_graph_type, content.to_string());
let graph = builder.build();
let svg = graph.render_svg();
println!("{}", svg);
}
fn generate_controller_export(project: &l5x::Project, graph_type: GraphType, show_aois: bool) -> String {
let mut graph = L5xGraph::new();
if let Some(ref controller) = project.controller {
if show_aois {
if let Some(ref aois) = controller.add_on_instruction_definitions {
for aoi in &aois.add_on_instruction_definition {
graph.add_node(&aoi.name, &aoi.name, L5xNodeType::Aoi);
}
}
}
if let Some(ref programs) = controller.programs {
for program in &programs.program {
add_program_to_graph(program, &mut graph, graph_type);
}
}
}
graph.render_svg()
}
fn generate_program_export(project: &l5x::Project, graph_type: GraphType, show_aois: bool) -> String {
let mut graph = L5xGraph::new();
if let Some(ref controller) = project.controller {
if show_aois {
if let Some(ref aois) = controller.add_on_instruction_definitions {
for aoi in &aois.add_on_instruction_definition {
if aoi.r#use.as_deref() == Some("Target") || aoi.r#use.is_none() {
graph.add_node(&aoi.name, &aoi.name, L5xNodeType::Aoi);
}
}
}
}
if let Some(ref programs) = controller.programs {
for program in &programs.program {
if program.r#use.as_deref() == Some("Target") {
add_program_to_graph(program, &mut graph, graph_type);
}
}
}
}
graph.render_svg()
}
fn generate_aoi_export(project: &l5x::Project, graph_type: GraphType) -> String {
let mut graph = L5xGraph::new();
if let Some(ref controller) = project.controller {
if let Some(ref aois) = controller.add_on_instruction_definitions {
for aoi in &aois.add_on_instruction_definition {
if aoi.r#use.as_deref() == Some("Target") {
let aoi_name = &aoi.name;
graph.add_node(aoi_name, aoi_name, L5xNodeType::Aoi);
for item in &aoi.content {
if let l5x::UDIDefinitionContent::Routines(ref routines) = item {
for routine in &routines.routine {
let routine_id = format!("{}.{}", aoi_name, routine.name);
if graph_type == GraphType::Structure || graph_type == GraphType::Combined {
graph.add_node(&routine_id, &routine.name, L5xNodeType::Routine);
graph.add_edge(aoi_name, &routine_id, None);
} else {
graph.add_node(&routine_id, &routine.name, L5xNodeType::Routine);
}
if graph_type == GraphType::CallGraph || graph_type == GraphType::Combined {
extract_jsr_calls(&routine_id, aoi_name, routine, &mut graph);
}
}
}
}
}
}
}
}
graph.render_svg()
}
fn add_program_to_graph(program: &l5x::AProgram, graph: &mut L5xGraph, graph_type: GraphType) {
let prog_name = &program.name;
if graph_type == GraphType::Structure || graph_type == GraphType::Combined {
graph.add_program(prog_name);
}
if let Some(ref routines) = program.routines {
for routine in &routines.routine {
let routine_id = format!("{}.{}", prog_name, routine.name);
if graph_type == GraphType::Structure || graph_type == GraphType::Combined {
graph.add_routine(prog_name, &routine.name);
graph.add_edge(prog_name, &routine_id, None);
} else {
graph.add_node(&routine_id, &routine.name, L5xNodeType::Routine);
}
if graph_type == GraphType::CallGraph || graph_type == GraphType::Combined {
extract_jsr_calls(&routine_id, prog_name, routine, graph);
}
}
}
}
fn extract_jsr_calls(
routine_id: &str,
program_name: &str,
routine: &l5x::Routine,
graph: &mut L5xGraph,
) {
for item in &routine.content {
match item {
l5x::RoutineContent::RLLContent(rll_content) => {
for rung in &rll_content.rung {
for rung_item in &rung.content {
if let l5x::RungContent::Text(text_wide) = rung_item {
let text = extract_text_from_textwide(text_wide);
extract_jsr_from_text(routine_id, program_name, &text, graph);
}
}
}
}
l5x::RoutineContent::STContent(st_content) => {
for item in &st_content.content {
if let l5x::STContentContent::Line(line) = item {
if let Some(ref text) = line.text {
extract_jsr_from_text(routine_id, program_name, text, graph);
}
}
}
}
_ => {}
}
}
}
fn extract_text_from_textwide(text_wide: &l5x::TextWide) -> String {
let mut result = String::new();
for item in &text_wide.content {
if let l5x::TextWideContent::TextContent(s) = item {
result.push_str(s);
}
}
result
}
fn extract_jsr_from_text(
from_routine: &str,
program_name: &str,
text: &str,
graph: &mut L5xGraph,
) {
let jsr_pattern = regex::Regex::new(r"JSR\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*[,)]").unwrap();
for cap in jsr_pattern.captures_iter(text) {
if let Some(routine_name) = cap.get(1) {
let target_name = routine_name.as_str();
let target_id = format!("{}.{}", program_name, target_name);
graph.add_call(from_routine, &target_id);
}
}
}