Compare commits

...

3 Commits

Author SHA1 Message Date
Miguel 376f7f6cf6 Primera version usando la libreria DotNetSiemensPLCToolBoxLibrary 2025-03-25 14:49:54 +01:00
Miguel da9af34ded Funciones basicas 2025-03-25 08:52:35 +01:00
Miguel 38c54ab40b Agregado de progress dialog 2025-03-24 20:14:15 +01:00
41 changed files with 4345 additions and 2066 deletions

View File

@ -1,11 +1,17 @@
<Application x:Class="S7Explorer.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:S7Explorer"
StartupUri="Views/MainWindow.xaml">
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Aquí puedes incluir estilos globales -->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<!-- Define the application resources -->
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="8,3" />
<Setter Property="MinWidth" Value="80" />
</Style>
<Style x:Key="StatusTextStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="5" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</Application.Resources>
</Application>

View File

@ -1,88 +1,8 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows;
namespace S7Explorer
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Asegurarnos de que existe la carpeta Resources para los íconos
EnsureResourcesExist();
// Configurar manejo de excepciones no controladas
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
Current.DispatcherUnhandledException += Current_DispatcherUnhandledException;
}
private void EnsureResourcesExist()
{
try
{
// Directorio de recursos
string resourcesDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources");
if (!Directory.Exists(resourcesDir))
{
Directory.CreateDirectory(resourcesDir);
}
// Crear íconos básicos si no existen
CreateDefaultIconIfNotExists(resourcesDir, "project.png");
CreateDefaultIconIfNotExists(resourcesDir, "device.png");
CreateDefaultIconIfNotExists(resourcesDir, "folder.png");
CreateDefaultIconIfNotExists(resourcesDir, "db.png");
CreateDefaultIconIfNotExists(resourcesDir, "fb.png");
CreateDefaultIconIfNotExists(resourcesDir, "fc.png");
CreateDefaultIconIfNotExists(resourcesDir, "ob.png");
CreateDefaultIconIfNotExists(resourcesDir, "symbol.png");
CreateDefaultIconIfNotExists(resourcesDir, "default.png");
}
catch (Exception)
{
// Ignorar errores en la creación de recursos - no son críticos
}
}
private void CreateDefaultIconIfNotExists(string directory, string filename)
{
string filePath = Path.Combine(directory, filename);
if (!File.Exists(filePath))
{
// Crear un ícono simple - en una aplicación real, incluirías recursos reales
BitmapSource bmp = BitmapSource.Create(16, 16, 96, 96, System.Windows.Media.PixelFormats.Bgr32, null, new byte[16 * 16 * 4], 16 * 4);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
encoder.Save(fileStream);
}
}
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
HandleException(e.ExceptionObject as Exception);
}
private void Current_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
HandleException(e.Exception);
e.Handled = true;
}
private void HandleException(Exception ex)
{
if (ex == null) return;
MessageBox.Show($"Ha ocurrido un error inesperado: {ex.Message}\n\nDetalles: {ex.StackTrace}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
// Aquí podrías añadir registro de errores
}
}
}

File diff suppressed because it is too large Load Diff

143
Helpers/SearchHelper.cs Normal file
View File

@ -0,0 +1,143 @@
using S7Explorer.Models;
using S7Explorer.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace S7Explorer.Helpers
{
public class SearchHelper
{
private readonly LogService _logService;
public SearchHelper()
{
_logService = LogService.Instance;
}
public List<SearchResult> Search(ProjectStructure project, string searchText, bool caseSensitive, bool useRegex)
{
_logService.LogInfo($"Searching for '{searchText}' (Case sensitive: {caseSensitive}, Regex: {useRegex})");
List<SearchResult> results = new List<SearchResult>();
try
{
if (string.IsNullOrWhiteSpace(searchText))
return results;
// Prepare regex if needed
Regex? regex = null;
if (useRegex)
{
try
{
RegexOptions options = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase;
regex = new Regex(searchText, options);
}
catch (Exception ex)
{
_logService.LogError($"Invalid regex pattern: {ex.Message}");
return results;
}
}
// Search blocks
SearchProjectItem(project.BlocksFolder, results, searchText, caseSensitive, regex);
// Search symbols
SearchProjectItem(project.SymbolsFolder, results, searchText, caseSensitive, regex);
// Search hardware
SearchProjectItem(project.HardwareFolder, results, searchText, caseSensitive, regex);
_logService.LogInfo($"Found {results.Count} results");
return results;
}
catch (Exception ex)
{
_logService.LogError($"Error during search: {ex.Message}");
return results;
}
}
private void SearchProjectItem(ProjectItem item, List<SearchResult> results, string searchText,
bool caseSensitive, Regex? regex)
{
// Check name
bool nameMatch = MatchesSearch(item.Name, searchText, caseSensitive, regex);
// For blocks, check content and comments
bool contentMatch = false;
if (item is BlockItem blockItem)
{
contentMatch = MatchesSearch(blockItem.BlockContent, searchText, caseSensitive, regex) ||
MatchesSearch(blockItem.BlockComment, searchText, caseSensitive, regex);
}
// For symbols, check address, type, and comment
else if (item is SymbolItem symbolItem)
{
contentMatch = MatchesSearch(symbolItem.SymbolAddress, searchText, caseSensitive, regex) ||
MatchesSearch(symbolItem.SymbolDataType, searchText, caseSensitive, regex) ||
MatchesSearch(symbolItem.SymbolComment, searchText, caseSensitive, regex);
}
// For hardware, check module info
else if (item is HardwareItem hardwareItem)
{
contentMatch = MatchesSearch(hardwareItem.ModuleType, searchText, caseSensitive, regex) ||
MatchesSearch(hardwareItem.OrderNumber, searchText, caseSensitive, regex) ||
MatchesSearch(hardwareItem.Position, searchText, caseSensitive, regex) ||
MatchesSearch(hardwareItem.Address, searchText, caseSensitive, regex);
}
if (nameMatch || contentMatch)
{
results.Add(new SearchResult
{
Item = item,
MatchType = nameMatch ? (contentMatch ? MatchType.Both : MatchType.Name) : MatchType.Content
});
}
// Search children recursively
foreach (var child in item.Children)
{
SearchProjectItem(child, results, searchText, caseSensitive, regex);
}
}
private bool MatchesSearch(string text, string searchText, bool caseSensitive, Regex? regex)
{
if (string.IsNullOrEmpty(text))
return false;
if (regex != null)
return regex.IsMatch(text);
return caseSensitive
? text.Contains(searchText)
: text.ToLower().Contains(searchText.ToLower());
}
}
public class SearchResult
{
public ProjectItem Item { get; set; } = null!;
public MatchType MatchType { get; set; }
public override string ToString()
{
return $"{Item.Name} - {MatchType}";
}
}
public enum MatchType
{
Name,
Content,
Both
}
}

98
Helpers/TreeViewHelper.cs Normal file
View File

@ -0,0 +1,98 @@
using S7Explorer.Models;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace S7Explorer.Helpers
{
public static class TreeViewHelper
{
public static ProjectItem? FindItemByPath(ObservableCollection<ProjectItem> items, string path)
{
if (string.IsNullOrEmpty(path))
return null;
string[] parts = path.Split('/');
return FindItemByParts(items, parts, 0);
}
private static ProjectItem? FindItemByParts(IEnumerable<ProjectItem> items, string[] parts, int level)
{
if (level >= parts.Length)
return null;
var matchingItem = items.FirstOrDefault(i => i.Name == parts[level]);
if (matchingItem == null)
return null;
if (level == parts.Length - 1)
return matchingItem;
return FindItemByParts(matchingItem.Children, parts, level + 1);
}
public static ProjectItem? FindItemByName(ObservableCollection<ProjectItem> items, string name)
{
foreach (var item in items)
{
if (item.Name == name)
return item;
var found = FindItemByName(item.Children, name);
if (found != null)
return found;
}
return null;
}
public static List<ProjectItem> FindItemsByText(ObservableCollection<ProjectItem> items, string text)
{
List<ProjectItem> results = new List<ProjectItem>();
FindItemsByTextRecursive(items, text.ToLower(), results);
return results;
}
private static void FindItemsByTextRecursive(IEnumerable<ProjectItem> items, string text, List<ProjectItem> results)
{
foreach (var item in items)
{
if (item.Name.ToLower().Contains(text))
results.Add(item);
FindItemsByTextRecursive(item.Children, text, results);
}
}
public static void ExpandToItem(ProjectItem item)
{
// Expand all parent items to make the item visible
ProjectItem? current = item.Parent;
while (current != null)
{
current.IsExpanded = true;
current = current.Parent;
}
}
public static void ExpandAll(ProjectItem item)
{
item.IsExpanded = true;
foreach (var child in item.Children)
{
ExpandAll(child);
}
}
public static void CollapseAll(ProjectItem item)
{
item.IsExpanded = false;
foreach (var child in item.Children)
{
CollapseAll(child);
}
}
}
}

View File

@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace S7Explorer
{
/// <summary>
/// Extensiones LINQ para funcionalidades específicas de la aplicación
/// </summary>
public static class LinqExtensions
{
/// <summary>
/// Cuenta elementos en una colección que cumplen un predicado
/// </summary>
public static int Count<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
if (source == null)
return 0;
return source.Where(predicate).Count();
}
/// <summary>
/// Verifica si una colección contiene al menos un elemento
/// </summary>
public static bool Any<T>(this IEnumerable<T> source)
{
if (source == null)
return false;
return source.Any(item => true);
}
/// <summary>
/// Verifica si una colección contiene al menos un elemento que cumpla un predicado
/// </summary>
public static bool Any<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
if (source == null)
return false;
using (var enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
if (predicate(enumerator.Current))
return true;
}
}
return false;
}
/// <summary>
/// Selecciona elementos de una colección que cumplen un predicado
/// </summary>
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
if (source == null)
yield break;
foreach (var item in source)
{
if (predicate(item))
yield return item;
}
}
}
}

126
MainWindow.xaml Normal file
View File

@ -0,0 +1,126 @@
<Window x:Class="S7Explorer.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:S7Explorer"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" mc:Ignorable="d" Title="S7 Project Explorer" Height="700"
Width="1000" WindowStartupLocation="CenterScreen">
<Window.Resources>
<!-- Icon Converter -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<!-- HierarchicalDataTemplate for TreeView Items -->
<HierarchicalDataTemplate x:Key="ProjectItemTemplate" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="5,0,0,0" />
</StackPanel>
</HierarchicalDataTemplate>
<!-- Log Entry Template -->
<DataTemplate x:Key="LogEntryTemplate">
<TextBlock Text="{Binding}" />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="120" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<ToolBar Grid.Row="0">
<Button Content="Load Project" Command="{Binding LoadProjectCommand}"
ToolTip="Load a Siemens S7 project file (.s7p)" Style="{StaticResource ToolbarButtonStyle}" />
<Button Content="Reload" Command="{Binding ReloadProjectCommand}" ToolTip="Reload the current project"
Style="{StaticResource ToolbarButtonStyle}" IsEnabled="{Binding IsProjectLoaded}" />
<Separator />
<Button Content="Export" Command="{Binding ExportDocumentationCommand}"
ToolTip="Export project documentation to a file" Style="{StaticResource ToolbarButtonStyle}"
IsEnabled="{Binding IsProjectLoaded}" />
<Separator />
<Label Content="Search:" VerticalAlignment="Center" />
<TextBox Width="200" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
VerticalAlignment="Center" Margin="5" />
<Button Content="Find" Command="{Binding SearchCommand}" ToolTip="Search in project"
Style="{StaticResource ToolbarButtonStyle}" IsEnabled="{Binding IsProjectLoaded}" />
<CheckBox Content="Case Sensitive" VerticalAlignment="Center" Margin="5"
IsChecked="{Binding IsCaseSensitive}" />
<CheckBox Content="Regex" VerticalAlignment="Center" Margin="5" IsChecked="{Binding UseRegex}" />
</ToolBar>
<!-- Main Content -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Project Explorer Tree -->
<DockPanel Grid.Column="0">
<ToolBar DockPanel.Dock="Top">
<Button Content="Expand All" Command="{Binding ExpandAllCommand}" ToolTip="Expand all tree nodes"
Style="{StaticResource ToolbarButtonStyle}" IsEnabled="{Binding IsProjectLoaded}" />
<Button Content="Collapse All" Command="{Binding CollapseAllCommand}"
ToolTip="Collapse all tree nodes" Style="{StaticResource ToolbarButtonStyle}"
IsEnabled="{Binding IsProjectLoaded}" />
</ToolBar>
<TreeView ItemsSource="{Binding ProjectStructure}" ItemTemplate="{StaticResource ProjectItemTemplate}"
SelectedItemChanged="TreeView_SelectedItemChanged" VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
</TreeView>
</DockPanel>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<!-- Property Grid -->
<xctk:PropertyGrid Grid.Column="2" SelectedObject="{Binding SelectedItemDetails}"
AutoGenerateProperties="True" IsReadOnly="True" ShowSearchBox="True" ShowSortOptions="True"
ShowTitle="True" ShowAdvancedOptions="False" />
</Grid>
<!-- Status Bar -->
<StatusBar Grid.Row="2">
<StatusBarItem>
<TextBlock Text="{Binding ProjectPath}" Style="{StaticResource StatusTextStyle}" />
</StatusBarItem>
<Separator />
<StatusBarItem>
<TextBlock Text="{Binding SelectedItem.Name, StringFormat=Selected: {0}}"
Style="{StaticResource StatusTextStyle}" />
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<ProgressBar Width="100" Height="15" IsIndeterminate="{Binding IsLoading}"
Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}" />
</StatusBarItem>
</StatusBar>
<!-- Log View -->
<DockPanel Grid.Row="3">
<ToolBar DockPanel.Dock="Top">
<Label Content="Log:" VerticalAlignment="Center" />
<Button Content="Clear" Command="{Binding ClearLogCommand}" ToolTip="Clear log messages"
Style="{StaticResource ToolbarButtonStyle}" />
</ToolBar>
<ListView ItemsSource="{Binding LogEntries}" ItemTemplate="{StaticResource LogEntryTemplate}"
ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling">
</ListView>
</DockPanel>
<!-- Loading Overlay -->
<Grid Grid.RowSpan="4" Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}">
<Rectangle Fill="Black" Opacity="0.3" />
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Loading..." FontSize="20" Foreground="White" />
<ProgressBar Width="200" Height="20" IsIndeterminate="True" Margin="0,10,0,0" />
</StackPanel>
</Grid>
</Grid>
</Window>

96
MainWindow.xaml.cs Normal file
View File

@ -0,0 +1,96 @@
using S7Explorer.Models;
using S7Explorer.Services;
using S7Explorer.ViewModels;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace S7Explorer
{
public enum LogLevel
{
Debug,
Info,
Warning,
Error
}
public class LogEntry
{
public DateTime Timestamp { get; set; }
public LogLevel Level { get; set; }
public string Message { get; set; } = string.Empty;
public override string ToString()
{
return $"[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level}] {Message}";
}
}
public partial class MainWindow : Window
{
private MainViewModel ViewModel;
public MainWindow()
{
InitializeComponent();
ViewModel = new MainViewModel();
DataContext = ViewModel;
}
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue is ProjectItem item)
{
ViewModel.SelectedItem = item;
}
}
}
public class IconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is string iconName)
{
// In a real application, return an actual image based on the icon name
// Here we're just returning a placeholder
return null;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class LogLevelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is LogLevel level)
{
return level switch
{
LogLevel.Debug => Brushes.Gray,
LogLevel.Info => Brushes.Black,
LogLevel.Warning => Brushes.Orange,
LogLevel.Error => Brushes.Red,
_ => Brushes.Black
};
}
return Brushes.Black;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

64
Models/BlockItem.cs Normal file
View File

@ -0,0 +1,64 @@
using System.Text;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5;
namespace S7Explorer.Models
{
public class BlockItem : ProjectItem
{
public ProjectBlockInfo? BlockInfo { get; set; }
public S7Block? BlockData { get; set; }
public string BlockType { get; set; } = string.Empty;
public int BlockNumber { get; set; }
public string BlockComment { get; set; } = string.Empty;
public string BlockContent { get; set; } = string.Empty;
// Override to provide block-specific object for property grid
public override object? GetDetailsObject()
{
return BlockData;
}
// Override to provide the appropriate icon
public override string GetIcon()
{
return BlockType switch
{
"OB" => "OB",
"FC" => "FC",
"FB" => "FB",
"DB" => "DB",
"UDT" => "UDT",
"SFC" => "SFC",
"SFB" => "SFB",
_ => "Block"
};
}
// Generate export text for documentation
public override string GetExportText()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Block: {Name}");
sb.AppendLine($"Type: {BlockType}{BlockNumber}");
if (!string.IsNullOrEmpty(BlockComment))
sb.AppendLine($"Comment: {BlockComment}");
if (BlockData != null)
{
sb.AppendLine($"Author: {BlockData.Author}");
sb.AppendLine($"Family: {BlockData.Family}");
sb.AppendLine($"Version: {BlockData.Version}");
sb.AppendLine($"Last Modified: {BlockData.LastCodeChange}");
}
sb.AppendLine();
sb.AppendLine("// Block Content");
sb.AppendLine(BlockContent);
sb.AppendLine();
return sb.ToString();
}
}
}

0
Models/CrossReference.cs Normal file
View File

65
Models/ExportSettings.cs Normal file
View File

@ -0,0 +1,65 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace S7Explorer.Models
{
public class ExportSettings : ObservableObject
{
private bool _exportBlocks = true;
private bool _exportSymbols = true;
private bool _exportHardware = true;
private bool _includeBlockCode = true;
private bool _includeComments = true;
private string _exportPath = string.Empty;
private ExportFormat _exportFormat = ExportFormat.PlainText;
public bool ExportBlocks
{
get => _exportBlocks;
set => SetProperty(ref _exportBlocks, value);
}
public bool ExportSymbols
{
get => _exportSymbols;
set => SetProperty(ref _exportSymbols, value);
}
public bool ExportHardware
{
get => _exportHardware;
set => SetProperty(ref _exportHardware, value);
}
public bool IncludeBlockCode
{
get => _includeBlockCode;
set => SetProperty(ref _includeBlockCode, value);
}
public bool IncludeComments
{
get => _includeComments;
set => SetProperty(ref _includeComments, value);
}
public string ExportPath
{
get => _exportPath;
set => SetProperty(ref _exportPath, value);
}
public ExportFormat ExportFormat
{
get => _exportFormat;
set => SetProperty(ref _exportFormat, value);
}
}
public enum ExportFormat
{
PlainText,
MarkDown,
HTML,
JSON
}
}

40
Models/HardwareItem.cs Normal file
View File

@ -0,0 +1,40 @@
using System.Text;
namespace S7Explorer.Models
{
public class HardwareItem : ProjectItem
{
public string ModuleType { get; set; } = string.Empty;
public string OrderNumber { get; set; } = string.Empty;
public string HardwareVersion { get; set; } = string.Empty;
public string FirmwareVersion { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public object? HardwareData { get; set; }
public override object? GetDetailsObject()
{
return HardwareData;
}
public override string GetIcon()
{
return "Hardware";
}
public override string GetExportText()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Hardware: {Name}");
sb.AppendLine($"Type: {ModuleType}");
sb.AppendLine($"Order Number: {OrderNumber}");
sb.AppendLine($"Hardware Version: {HardwareVersion}");
sb.AppendLine($"Firmware Version: {FirmwareVersion}");
sb.AppendLine($"Position: {Position}");
sb.AppendLine($"Address: {Address}");
sb.AppendLine();
return sb.ToString();
}
}
}

72
Models/ProjectItem.cs Normal file
View File

@ -0,0 +1,72 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace S7Explorer.Models
{
// Base class for all project items
public class ProjectItem : ObservableObject
{
private string _name = string.Empty;
private string _path = string.Empty;
private ProjectItem? _parent;
private ObservableCollection<ProjectItem> _children = new();
private bool _isExpanded;
private bool _isSelected;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string Path
{
get => _path;
set => SetProperty(ref _path, value);
}
public ProjectItem? Parent
{
get => _parent;
set => SetProperty(ref _parent, value);
}
public ObservableCollection<ProjectItem> Children
{
get => _children;
set => SetProperty(ref _children, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
public virtual object? GetDetailsObject()
{
return null;
}
public virtual string GetIcon()
{
return "FolderClosed";
}
public virtual string GetExportText()
{
return $"Item: {Name}\r\n";
}
public override string ToString()
{
return Name;
}
}
}

View File

@ -0,0 +1,56 @@
using DotNetSiemensPLCToolBoxLibrary.Projectfiles;
using System.Collections.ObjectModel;
using System.Text;
namespace S7Explorer.Models
{
public class ProjectStructure : ProjectItem
{
public Project? ProjectData { get; set; }
public string ProjectPath { get; set; } = string.Empty;
public string ProjectVersion { get; set; } = string.Empty;
public DateTime CreationDate { get; set; }
public DateTime LastModifiedDate { get; set; }
public ProjectItem BlocksFolder { get; } = new ProjectItem { Name = "Blocks" };
public ProjectItem SymbolsFolder { get; } = new ProjectItem { Name = "Symbols" };
public ProjectItem HardwareFolder { get; } = new ProjectItem { Name = "Hardware" };
public ProjectStructure()
{
Name = "Project";
Children.Add(BlocksFolder);
Children.Add(SymbolsFolder);
Children.Add(HardwareFolder);
}
public override object? GetDetailsObject()
{
return new
{
ProjectPath,
ProjectVersion,
CreationDate,
LastModifiedDate
};
}
public override string GetIcon()
{
return "Project";
}
public override string GetExportText()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Project: {Name}");
sb.AppendLine($"Path: {ProjectPath}");
sb.AppendLine($"Version: {ProjectVersion}");
sb.AppendLine($"Created: {CreationDate}");
sb.AppendLine($"Last Modified: {LastModifiedDate}");
sb.AppendLine();
return sb.ToString();
}
}
}

View File

@ -1,150 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Newtonsoft.Json;
namespace S7Explorer.Models
{
public class S7Block : S7Object
{
private string _authorName;
private string _family;
private string _version;
private DateTime? _modified;
private int _size;
private string _language;
[DisplayName("Autor")]
public string AuthorName
{
get => _authorName;
set
{
if (_authorName != value)
{
_authorName = value;
OnPropertyChanged(nameof(AuthorName));
}
}
}
[DisplayName("Familia")]
public string Family
{
get => _family;
set
{
if (_family != value)
{
_family = value;
OnPropertyChanged(nameof(Family));
}
}
}
[DisplayName("Versión")]
public string Version
{
get => _version;
set
{
if (_version != value)
{
_version = value;
OnPropertyChanged(nameof(Version));
}
}
}
[DisplayName("Modificado")]
public DateTime? Modified
{
get => _modified;
set
{
if (_modified != value)
{
_modified = value;
OnPropertyChanged(nameof(Modified));
}
}
}
[DisplayName("Tamaño (bytes)")]
public int Size
{
get => _size;
set
{
if (_size != value)
{
_size = value;
OnPropertyChanged(nameof(Size));
}
}
}
[DisplayName("Lenguaje")]
public string Language
{
get => _language;
set
{
if (_language != value)
{
_language = value;
OnPropertyChanged(nameof(Language));
}
}
}
public S7Block()
{
// Establece valores predeterminados específicos para bloques
}
}
public class S7DataBlock : S7Block
{
private bool _instanceDb;
[DisplayName("Es DB de Instancia")]
public bool IsInstanceDb
{
get => _instanceDb;
set
{
if (_instanceDb != value)
{
_instanceDb = value;
OnPropertyChanged(nameof(IsInstanceDb));
}
}
}
public S7DataBlock()
{
ObjectType = S7ObjectType.DataBlock;
}
}
public class S7FunctionBlock : S7Block
{
public S7FunctionBlock()
{
ObjectType = S7ObjectType.FunctionBlock;
}
}
/// <summary>
/// Clase para representar un parámetro de función
/// </summary>
public class FunctionParameter
{
public string Name { get; set; }
public string DataType { get; set; }
public string Direction { get; set; } // IN, OUT, IN/OUT
public string Description { get; set; }
}
}

View File

@ -1,169 +1,49 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Newtonsoft.Json;
using S7Explorer.Parsers;
using System.Text;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5;
namespace S7Explorer.Models
{
public class S7Function : S7Block
public class S7Function
{
private string _returnType;
private List<FunctionParameter> _parameters;
private string _interface;
[DisplayName("Tipo de Retorno")]
[Description("Tipo de dato que retorna la función")]
[Category("Interfaz")]
public string ReturnType
{
get => _returnType;
set
{
if (_returnType != value)
{
_returnType = value;
OnPropertyChanged(nameof(ReturnType));
}
}
}
[Browsable(false)]
[JsonIgnore]
public List<FunctionParameter> Parameters
{
get => _parameters;
set
{
if (_parameters != value)
{
_parameters = value;
OnPropertyChanged(nameof(Parameters));
// Actualizar la interfaz formateada cuando se cambian los parámetros
UpdateFormattedInterface();
}
}
}
[DisplayName("Interfaz")]
[Description("Interfaz de la función con sus parámetros")]
[Category("Interfaz")]
[EditorAttribute(typeof(System.ComponentModel.Design.MultilineStringEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public string Interface
{
get => _interface;
set
{
if (_interface != value)
{
_interface = value;
OnPropertyChanged(nameof(Interface));
}
}
}
// Propiedades adicionales específicas de FC
[DisplayName("Parámetros de Entrada")]
[Description("Número de parámetros de entrada")]
[Category("Estadísticas")]
public int InputParameterCount =>
Parameters?.Count(p => p.Direction == "IN") ?? 0;
[DisplayName("Parámetros de Salida")]
[Description("Número de parámetros de salida")]
[Category("Estadísticas")]
public int OutputParameterCount =>
Parameters?.Count(p => p.Direction == "OUT") ?? 0;
[DisplayName("Parámetros IN/OUT")]
[Description("Número de parámetros de entrada/salida")]
[Category("Estadísticas")]
public int InOutParameterCount =>
Parameters?.Count(p => p.Direction == "IN/OUT") ?? 0;
[DisplayName("Total Parámetros")]
[Description("Número total de parámetros")]
[Category("Estadísticas")]
public int TotalParameterCount =>
Parameters?.Count ?? 0;
public S7Block Block { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<S7FunctionParameter> Parameters { get; set; } = new List<S7FunctionParameter>();
public string Code { get; set; }
public S7Function()
{
ObjectType = S7ObjectType.Function;
Parameters = new List<FunctionParameter>();
}
/// <summary>
/// Actualiza la representación formateada de la interfaz basada en los parámetros
/// </summary>
private void UpdateFormattedInterface()
public S7Function(S7Block block)
{
if (Parameters == null || Parameters.Count == 0)
{
Interface = "// No hay parámetros definidos";
return;
}
Block = block;
Name = block?.Name ?? string.Empty;
Description = block?.Title ?? string.Empty;
var builder = new System.Text.StringBuilder();
// Code would need to be extracted from the block
// This is just a placeholder
Code = "// Function code would be extracted here";
}
// Agrupar por dirección
var inputParams = Parameters.Where(p => p.Direction == "IN").ToList();
var outputParams = Parameters.Where(p => p.Direction == "OUT").ToList();
var inOutParams = Parameters.Where(p => p.Direction == "IN/OUT").ToList();
public override string ToString()
{
return Name;
}
}
// Añadir tipo de retorno
builder.AppendLine($"FUNCTION {Name} : {ReturnType ?? "VOID"}");
builder.AppendLine();
public class S7FunctionParameter
{
public string Name { get; set; }
public string DataType { get; set; }
public string Comment { get; set; }
public string Direction { get; set; } // IN, OUT, IN_OUT
// Añadir parámetros de entrada
if (inputParams.Any())
{
builder.AppendLine("VAR_INPUT");
foreach (var param in inputParams)
{
builder.Append($" {param.Name} : {param.DataType}");
if (!string.IsNullOrEmpty(param.Description))
builder.Append($"; // {param.Description}");
builder.AppendLine();
}
builder.AppendLine("END_VAR");
builder.AppendLine();
}
// Añadir parámetros de salida
if (outputParams.Any())
{
builder.AppendLine("VAR_OUTPUT");
foreach (var param in outputParams)
{
builder.Append($" {param.Name} : {param.DataType}");
if (!string.IsNullOrEmpty(param.Description))
builder.Append($"; // {param.Description}");
builder.AppendLine();
}
builder.AppendLine("END_VAR");
builder.AppendLine();
}
// Añadir parámetros de entrada/salida
if (inOutParams.Any())
{
builder.AppendLine("VAR_IN_OUT");
foreach (var param in inOutParams)
{
builder.Append($" {param.Name} : {param.DataType}");
if (!string.IsNullOrEmpty(param.Description))
builder.Append($"; // {param.Description}");
builder.AppendLine();
}
builder.AppendLine("END_VAR");
}
Interface = builder.ToString();
public override string ToString()
{
return $"{Direction} {Name} : {DataType}";
}
}
}

View File

@ -1,120 +0,0 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using Newtonsoft.Json;
namespace S7Explorer.Models
{
public enum S7ObjectType
{
Project,
Device,
Folder,
DataBlock,
FunctionBlock,
Function,
Organization,
Symbol
}
public class S7Object : INotifyPropertyChanged
{
private string _name;
private string _description;
private bool _isExpanded;
public string Id { get; set; }
public string Number { get; set; }
[DisplayName("Nombre")]
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
[DisplayName("Descripción")]
public string Description
{
get => _description;
set
{
if (_description != value)
{
_description = value;
OnPropertyChanged(nameof(Description));
}
}
}
[Browsable(false)]
public S7ObjectType ObjectType { get; set; }
[Browsable(false)]
public string IconSource => GetIconPath();
[Browsable(false)]
[JsonIgnore]
public S7Object Parent { get; set; }
[Browsable(false)]
public ObservableCollection<S7Object> Children { get; set; } = new ObservableCollection<S7Object>();
[Browsable(false)]
public bool IsExpanded
{
get => _isExpanded;
set
{
if (_isExpanded != value)
{
_isExpanded = value;
OnPropertyChanged(nameof(IsExpanded));
}
}
}
// Para búsquedas de texto
[Browsable(false)]
public bool ContainsText(string searchText)
{
if (string.IsNullOrWhiteSpace(searchText))
return false;
searchText = searchText.ToLowerInvariant();
return Name?.ToLowerInvariant().Contains(searchText) == true ||
Description?.ToLowerInvariant().Contains(searchText) == true ||
Number?.ToLowerInvariant().Contains(searchText) == true;
}
private string GetIconPath()
{
return ObjectType switch
{
S7ObjectType.Project => "/Resources/project.png",
S7ObjectType.Device => "/Resources/device.png",
S7ObjectType.Folder => "/Resources/folder.png",
S7ObjectType.DataBlock => "/Resources/db.png",
S7ObjectType.FunctionBlock => "/Resources/fb.png",
S7ObjectType.Function => "/Resources/fc.png",
S7ObjectType.Organization => "/Resources/ob.png",
S7ObjectType.Symbol => "/Resources/symbol.png",
_ => "/Resources/default.png"
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -1,96 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
namespace S7Explorer.Models
{
public class S7Project : S7Object
{
private string _filePath;
private string _version;
private DateTime _created;
private DateTime _lastModified;
[DisplayName("Ruta de Archivo")]
public string FilePath
{
get => _filePath;
set
{
if (_filePath != value)
{
_filePath = value;
OnPropertyChanged(nameof(FilePath));
}
}
}
[DisplayName("Versión STEP7")]
public string Version
{
get => _version;
set
{
if (_version != value)
{
_version = value;
OnPropertyChanged(nameof(Version));
}
}
}
[DisplayName("Creado")]
public DateTime Created
{
get => _created;
set
{
if (_created != value)
{
_created = value;
OnPropertyChanged(nameof(Created));
}
}
}
[DisplayName("Última Modificación")]
public DateTime LastModified
{
get => _lastModified;
set
{
if (_lastModified != value)
{
_lastModified = value;
OnPropertyChanged(nameof(LastModified));
}
}
}
// Directorio base del proyecto (donde están las carpetas ombstx, YDBs, etc.)
[Browsable(false)]
public string ProjectDirectory => Path.GetDirectoryName(FilePath);
public S7Project()
{
ObjectType = S7ObjectType.Project;
Created = DateTime.Now;
LastModified = DateTime.Now;
}
// Inicializa un proyecto a partir de un archivo .s7p
public S7Project(string filePath) : this()
{
FilePath = filePath;
Name = Path.GetFileNameWithoutExtension(filePath);
// Trata de obtener fechas del archivo
if (File.Exists(filePath))
{
var fileInfo = new FileInfo(filePath);
Created = fileInfo.CreationTime;
LastModified = fileInfo.LastWriteTime;
}
}
}
}

View File

@ -1,74 +0,0 @@
using System.ComponentModel;
namespace S7Explorer.Models
{
public class S7Symbol : S7Object
{
private string _address;
private string _dataType;
private string _comment;
[DisplayName("Dirección")]
public string Address
{
get => _address;
set
{
if (_address != value)
{
_address = value;
OnPropertyChanged(nameof(Address));
}
}
}
[DisplayName("Tipo de Datos")]
public string DataType
{
get => _dataType;
set
{
if (_dataType != value)
{
_dataType = value;
OnPropertyChanged(nameof(DataType));
}
}
}
[DisplayName("Comentario")]
public string Comment
{
get => _comment;
set
{
if (_comment != value)
{
_comment = value;
OnPropertyChanged(nameof(Comment));
}
}
}
public S7Symbol()
{
ObjectType = S7ObjectType.Symbol;
}
// Sobrescribe el método de búsqueda de texto para incluir dirección y tipo de datos
public new bool ContainsText(string searchText)
{
if (base.ContainsText(searchText))
return true;
if (string.IsNullOrWhiteSpace(searchText))
return false;
searchText = searchText.ToLowerInvariant();
return Address?.ToLowerInvariant().Contains(searchText) == true ||
DataType?.ToLowerInvariant().Contains(searchText) == true ||
Comment?.ToLowerInvariant().Contains(searchText) == true;
}
}
}

37
Models/SymbolItem.cs Normal file
View File

@ -0,0 +1,37 @@
using System.Text;
namespace S7Explorer.Models
{
public class SymbolItem : ProjectItem
{
public object? SymbolData { get; set; }
public string SymbolAddress { get; set; } = string.Empty;
public string SymbolDataType { get; set; } = string.Empty;
public string SymbolComment { get; set; } = string.Empty;
public override object? GetDetailsObject()
{
return SymbolData;
}
public override string GetIcon()
{
return "Symbol";
}
public override string GetExportText()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Symbol: {Name}");
sb.AppendLine($"Address: {SymbolAddress}");
sb.AppendLine($"Data Type: {SymbolDataType}");
if (!string.IsNullOrEmpty(SymbolComment))
sb.AppendLine($"Comment: {SymbolComment}");
sb.AppendLine();
return sb.ToString();
}
}
}

View File

@ -1,139 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NDbfReader;
namespace S7Explorer.Parsers
{
public class DbfParser
{
// Lee campos específicos de un archivo DBF
public static List<Dictionary<string, string>> ReadDbfFile(string filePath, IEnumerable<string> fieldNames)
{
var result = new List<Dictionary<string, string>>();
if (!File.Exists(filePath))
throw new FileNotFoundException($"No se encontró el archivo DBF: {filePath}");
try
{
// Abrir tabla DBF con codificación específica (importante para caracteres especiales en STEP7)
using var stream = File.OpenRead(filePath);
using var table = Table.Open(stream);
// Crear lector usando la API correcta
var reader = table.OpenReader();
while (reader.Read())
{
var record = new Dictionary<string, string>();
foreach (var fieldName in fieldNames)
{
// Obtener valor y convertir a string si no es null
var value = reader.GetValue(fieldName);
// Manejar los diferentes tipos de datos
if (value is byte[] byteValue)
{
// Para campos de tipo binario como MC5CODE
record[fieldName] = Encoding.GetEncoding(1252).GetString(byteValue);
}
else if (value is DateTime dateValue)
{
// Mantener formato consistente para fechas
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
}
else
{
// Para todos los demás tipos
record[fieldName] = value?.ToString() ?? string.Empty;
}
}
result.Add(record);
}
}
catch (Exception ex)
{
throw new Exception($"Error al leer el archivo DBF {filePath}: {ex.Message}", ex);
}
return result;
}
// Convierte un string que representa un número a un entero opcional
public static int? StringToInt(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
// Eliminar espacios y caracteres no numéricos iniciales
string trimmedValue = value.Trim();
int startIndex = 0;
while (startIndex < trimmedValue.Length && !char.IsDigit(trimmedValue[startIndex]))
startIndex++;
if (startIndex >= trimmedValue.Length)
return null;
// Extraer solo los dígitos
string numericPart = new string(trimmedValue.Substring(startIndex)
.TakeWhile(char.IsDigit)
.ToArray());
if (int.TryParse(numericPart, out int result))
return result;
return null;
}
// Convierte códigos Windows-1252 a UTF-8 para manejar caracteres especiales en STEP7
public static string ConvertCP1252ToUtf8(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
try
{
// Primero decodificar como Windows-1252 y luego encodear como UTF-8
byte[] bytes = Encoding.GetEncoding(1252).GetBytes(input);
return Encoding.UTF8.GetString(bytes);
}
catch
{
// En caso de error, devolver el string original
return input;
}
}
// Busca un archivo DBF en varias ubicaciones posibles basadas en el patrón de archivos STEP7
public static string FindDbfFile(string basePath, string relativePath)
{
string path = Path.Combine(basePath, relativePath);
if (File.Exists(path))
return path;
// Comprobar si se puede encontrar en un directorio padre
string parentPath = Directory.GetParent(basePath)?.FullName;
if (!string.IsNullOrEmpty(parentPath))
{
path = Path.Combine(parentPath, relativePath);
if (File.Exists(path))
return path;
}
// Intentar buscar por nombre de archivo en subdirectorios
string fileName = Path.GetFileName(relativePath);
foreach (var subdir in Directory.GetDirectories(basePath, "*", SearchOption.AllDirectories))
{
path = Path.Combine(subdir, fileName);
if (File.Exists(path))
return path;
}
return null;
}
}
}

View File

@ -1,439 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using S7Explorer.Models;
namespace S7Explorer.Parsers
{
/// <summary>
/// Parser específico para los bloques de función (FC) de STEP7
/// </summary>
public class FCParser
{
private readonly string _projectDirectory;
public FCParser(string projectDirectory)
{
_projectDirectory = projectDirectory;
}
/// <summary>
/// Parsea los bloques FC desde los archivos SUBBLK.DBF del proyecto
/// </summary>
/// <param name="subblockListId">ID de la lista de subbloques del dispositivo</param>
/// <returns>Lista de objetos S7Function representando los bloques FC</returns>
public List<S7Function> ParseFunctionBlocks(int subblockListId)
{
var functions = new List<S7Function>();
try
{
// Construir la ruta al archivo SUBBLK.DBF que contiene información de los bloques
string subblockFolder = $"{_projectDirectory}\\ombstx\\offline\\{subblockListId:X8}";
string subblkPath = Path.Combine(subblockFolder, "SUBBLK.DBF");
if (!File.Exists(subblkPath))
{
return CreateSampleFunctions(); // En caso de que no exista, usar datos de muestra
}
// Leer datos del archivo DBF
var records = DbfParser.ReadDbfFile(subblkPath, new[]
{
"SUBBLKTYP", "BLKNUMBER", "BLKNAME", "AUTHOR", "FAMILY",
"VERSION", "CREATEDATE", "MODDATE", "INTERFLEN", "MC5LEN", "MC5CODE"
});
// Filtrar solo los registros que correspondan a FCs (tipo 00003)
var fcRecords = records.Where(r => r["SUBBLKTYP"] == "00003").ToList();
foreach (var record in fcRecords)
{
// Convertir número de bloque a entero
if (!int.TryParse(record["BLKNUMBER"], out int blockNumber))
continue;
// Crear objeto FC con los datos del registro
var fc = new S7Function
{
Number = $"FC{blockNumber}",
Name = DbfParser.ConvertCP1252ToUtf8(record["BLKNAME"]).Trim(),
AuthorName = DbfParser.ConvertCP1252ToUtf8(record["AUTHOR"]).Trim(),
Family = DbfParser.ConvertCP1252ToUtf8(record["FAMILY"]).Trim(),
Version = record["VERSION"].Trim(),
Size = ParseBlockSize(record["MC5LEN"]),
ObjectType = S7ObjectType.Function,
Language = DetermineLanguageFromMC5Code(record["MC5CODE"]),
ReturnType = DetermineReturnTypeFromMC5Code(record["MC5CODE"])
};
// Intentar extraer fecha de modificación
if (DateTime.TryParse(record["MODDATE"], out DateTime modDate))
{
fc.Modified = modDate;
}
// Extraer más información del código MC5
ExtractAdditionalInfoFromMC5Code(fc, record["MC5CODE"]);
// Analizar variables de entrada/salida del interfaz
AnalyzeInterfaceData(fc, record["MC5CODE"]);
functions.Add(fc);
}
// Ordenar los bloques por número
functions = functions.OrderBy(f => ExtractNumber(f.Number)).ToList();
}
catch (Exception ex)
{
// En caso de error, añadir un FC de error para informar al usuario
var errorFc = new S7Function
{
Name = "Error_Parsing_FC",
Number = "FC999",
Description = $"Error al parsear FCs: {ex.Message}",
ObjectType = S7ObjectType.Function
};
functions.Add(errorFc);
}
// Si no se encontraron FCs, usar datos de muestra
if (functions.Count == 0)
{
return CreateSampleFunctions();
}
return functions;
}
/// <summary>
/// Extrae el número de un bloque a partir de su identificador (ej: "FC5" -> 5)
/// </summary>
private int ExtractNumber(string blockId)
{
if (string.IsNullOrEmpty(blockId) || blockId.Length < 3)
return 9999; // Valor alto para ordenar al final
if (int.TryParse(blockId.Substring(2), out int number))
return number;
return 9999;
}
/// <summary>
/// Parsea el tamaño del bloque a partir del valor MC5LEN
/// </summary>
private int ParseBlockSize(string mc5Len)
{
if (int.TryParse(mc5Len, out int size))
return size;
return 0;
}
/// <summary>
/// Determina el lenguaje de programación a partir del código MC5
/// </summary>
private string DetermineLanguageFromMC5Code(string mc5Code)
{
if (string.IsNullOrEmpty(mc5Code))
return "Desconocido";
// Esta es una lógica simplificada. En una implementación real,
// necesitarías analizar patrones específicos en el código MC5
if (mc5Code.Contains("STL") || mc5Code.Contains("AWL"))
return "AWL";
else if (mc5Code.Contains("SCL"))
return "SCL";
else if (mc5Code.Contains("GRAPH"))
return "GRAPH";
else if (mc5Code.Contains("KOP") || mc5Code.Contains("LAD"))
return "KOP";
else if (mc5Code.Contains("FUP") || mc5Code.Contains("FBD"))
return "FUP";
// Por defecto asumimos AWL (lenguaje más común)
return "AWL";
}
/// <summary>
/// Determina el tipo de retorno a partir del código MC5
/// </summary>
private string DetermineReturnTypeFromMC5Code(string mc5Code)
{
if (string.IsNullOrEmpty(mc5Code))
return "VOID";
// Esta es una lógica simplificada. En una implementación real,
// necesitarías analizar patrones específicos en el código MC5
if (mc5Code.Contains("BOOL") && mc5Code.Contains("RET_VAL"))
return "BOOL";
else if (mc5Code.Contains("INT") && mc5Code.Contains("RET_VAL"))
return "INT";
else if (mc5Code.Contains("REAL") && mc5Code.Contains("RET_VAL"))
return "REAL";
else if (mc5Code.Contains("WORD") && mc5Code.Contains("RET_VAL"))
return "WORD";
else if (mc5Code.Contains("DWORD") && mc5Code.Contains("RET_VAL"))
return "DWORD";
else if (mc5Code.Contains("TIME") && mc5Code.Contains("RET_VAL"))
return "TIME";
// Buscar cualquier tipo de dato asociado a RET_VAL
int retValPos = mc5Code.IndexOf("RET_VAL");
if (retValPos > 0)
{
string[] dataTypes = { "BYTE", "DINT", "CHAR", "STRING", "DATE", "TIME_OF_DAY", "DATE_AND_TIME" };
foreach (string type in dataTypes)
{
if (mc5Code.IndexOf(type, retValPos - 50, 100) > 0)
return type;
}
}
return "VOID";
}
/// <summary>
/// Extrae información adicional del código MC5
/// </summary>
private void ExtractAdditionalInfoFromMC5Code(S7Function fc, string mc5Code)
{
if (string.IsNullOrEmpty(mc5Code))
return;
try
{
// Extraer descripción (comentario del bloque)
int descStart = mc5Code.IndexOf("//");
if (descStart >= 0)
{
int descEnd = mc5Code.IndexOf('\n', descStart);
if (descEnd > descStart)
{
string comment = mc5Code.Substring(descStart + 2, descEnd - descStart - 2).Trim();
if (!string.IsNullOrEmpty(comment))
fc.Description = DbfParser.ConvertCP1252ToUtf8(comment);
}
}
// Añadir otros metadatos que se puedan extraer
}
catch
{
// Ignorar errores en la extracción para no detener el proceso
}
}
/// <summary>
/// Analiza los datos de interfaz para extraer parámetros de entrada/salida
/// </summary>
private void AnalyzeInterfaceData(S7Function fc, string mc5Code)
{
if (string.IsNullOrEmpty(mc5Code))
return;
try
{
// Crear lista de parámetros
fc.Parameters = new List<FunctionParameter>();
// Buscar sección de interfaz
int varInputPos = mc5Code.IndexOf("VAR_INPUT");
int varOutputPos = mc5Code.IndexOf("VAR_OUTPUT");
int varInOutPos = mc5Code.IndexOf("VAR_IN_OUT");
// Procesar parámetros de entrada
if (varInputPos >= 0)
{
int endPos = DetermineNextSectionPos(mc5Code, varInputPos);
string inputSection = mc5Code.Substring(varInputPos, endPos - varInputPos);
ExtractParameters(inputSection, "IN", fc.Parameters);
}
// Procesar parámetros de salida
if (varOutputPos >= 0)
{
int endPos = DetermineNextSectionPos(mc5Code, varOutputPos);
string outputSection = mc5Code.Substring(varOutputPos, endPos - varOutputPos);
ExtractParameters(outputSection, "OUT", fc.Parameters);
}
// Procesar parámetros de entrada/salida
if (varInOutPos >= 0)
{
int endPos = DetermineNextSectionPos(mc5Code, varInOutPos);
string inOutSection = mc5Code.Substring(varInOutPos, endPos - varInOutPos);
ExtractParameters(inOutSection, "IN/OUT", fc.Parameters);
}
}
catch
{
// Ignorar errores en la extracción para no detener el proceso
}
}
/// <summary>
/// Determina la posición de la siguiente sección en el código
/// </summary>
private int DetermineNextSectionPos(string code, int startPos)
{
string[] sections = { "VAR_INPUT", "VAR_OUTPUT", "VAR_IN_OUT", "VAR_TEMP", "VAR", "BEGIN" };
int minPos = code.Length;
foreach (string section in sections)
{
int pos = code.IndexOf(section, startPos + 1);
if (pos > 0 && pos < minPos)
minPos = pos;
}
return minPos;
}
/// <summary>
/// Extrae parámetros individuales de una sección de interfaz
/// </summary>
private void ExtractParameters(string section, string direction, List<FunctionParameter> parameters)
{
// Dividir por líneas
string[] lines = section.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string trimmedLine = line.Trim();
// Ignorar líneas de declaración de sección, comentarios o END_VAR
if (trimmedLine.StartsWith("VAR_") || trimmedLine.StartsWith("//") ||
trimmedLine.StartsWith("END_VAR") || string.IsNullOrWhiteSpace(trimmedLine))
continue;
// Procesar línea con declaración de parámetro
int colonPos = trimmedLine.IndexOf(':');
if (colonPos > 0)
{
string paramName = trimmedLine.Substring(0, colonPos).Trim();
// Extraer tipo y comentario
string remainder = trimmedLine.Substring(colonPos + 1).Trim();
string paramType = remainder;
string comment = string.Empty;
// Verificar si hay comentario en la línea
int commentPos = remainder.IndexOf("//");
if (commentPos > 0)
{
paramType = remainder.Substring(0, commentPos).Trim();
comment = remainder.Substring(commentPos + 2).Trim();
}
// Limpiar cualquier punto y coma al final del tipo
if (paramType.EndsWith(";"))
paramType = paramType.Substring(0, paramType.Length - 1).Trim();
// Añadir parámetro a la lista
parameters.Add(new FunctionParameter
{
Name = paramName,
DataType = paramType,
Direction = direction,
Description = comment
});
}
}
}
/// <summary>
/// Crea bloques FC de ejemplo cuando no se pueden parsear los reales
/// </summary>
private List<S7Function> CreateSampleFunctions()
{
return new List<S7Function>
{
new S7Function {
Name = "Calc_Setpoint",
Number = "FC1",
Size = 124,
ReturnType = "REAL",
Description = "Cálculo de punto de consigna",
Modified = DateTime.Now.AddDays(-8),
Language = "SCL",
Parameters = new List<FunctionParameter>
{
new FunctionParameter { Name = "Target", DataType = "REAL", Direction = "IN", Description = "Valor objetivo" },
new FunctionParameter { Name = "Actual", DataType = "REAL", Direction = "IN", Description = "Valor actual" },
new FunctionParameter { Name = "Gain", DataType = "REAL", Direction = "IN", Description = "Ganancia" }
}
},
new S7Function {
Name = "Scale_Analog",
Number = "FC2",
Size = 68,
ReturnType = "REAL",
Description = "Escalado de valor analógico",
Modified = DateTime.Now.AddDays(-12),
Language = "AWL",
Parameters = new List<FunctionParameter>
{
new FunctionParameter { Name = "Raw", DataType = "INT", Direction = "IN", Description = "Valor bruto" },
new FunctionParameter { Name = "RawLow", DataType = "INT", Direction = "IN", Description = "Valor mínimo bruto" },
new FunctionParameter { Name = "RawHigh", DataType = "INT", Direction = "IN", Description = "Valor máximo bruto" },
new FunctionParameter { Name = "ScaleLow", DataType = "REAL", Direction = "IN", Description = "Valor mínimo escalado" },
new FunctionParameter { Name = "ScaleHigh", DataType = "REAL", Direction = "IN", Description = "Valor máximo escalado" }
}
},
new S7Function {
Name = "CheckAlarms",
Number = "FC3",
Size = 154,
ReturnType = "BOOL",
Description = "Verificación de alarmas",
Modified = DateTime.Now.AddDays(-5),
Language = "KOP",
Parameters = new List<FunctionParameter>
{
new FunctionParameter { Name = "Value", DataType = "REAL", Direction = "IN", Description = "Valor a comprobar" },
new FunctionParameter { Name = "LowLimit", DataType = "REAL", Direction = "IN", Description = "Límite inferior" },
new FunctionParameter { Name = "HighLimit", DataType = "REAL", Direction = "IN", Description = "Límite superior" },
new FunctionParameter { Name = "AlarmStatus", DataType = "WORD", Direction = "OUT", Description = "Estado de alarmas" }
}
},
new S7Function {
Name = "Timer_Control",
Number = "FC4",
Size = 86,
ReturnType = "VOID",
Description = "Control de temporizadores",
Modified = DateTime.Now.AddDays(-3),
Language = "FUP",
Parameters = new List<FunctionParameter>
{
new FunctionParameter { Name = "Start", DataType = "BOOL", Direction = "IN", Description = "Iniciar temporizador" },
new FunctionParameter { Name = "Duration", DataType = "TIME", Direction = "IN", Description = "Duración" },
new FunctionParameter { Name = "TimerNo", DataType = "INT", Direction = "IN", Description = "Número de temporizador" },
new FunctionParameter { Name = "Status", DataType = "BOOL", Direction = "OUT", Description = "Estado (activo/inactivo)" },
new FunctionParameter { Name = "ElapsedTime", DataType = "TIME", Direction = "OUT", Description = "Tiempo transcurrido" }
}
},
new S7Function {
Name = "String_Process",
Number = "FC5",
Size = 210,
ReturnType = "INT",
Description = "Procesamiento de cadenas",
Modified = DateTime.Now.AddDays(-1),
Language = "SCL",
Parameters = new List<FunctionParameter>
{
new FunctionParameter { Name = "InStr", DataType = "STRING", Direction = "IN", Description = "Cadena de entrada" },
new FunctionParameter { Name = "Operation", DataType = "INT", Direction = "IN", Description = "Operación a realizar" },
new FunctionParameter { Name = "OutStr", DataType = "STRING", Direction = "OUT", Description = "Cadena de salida" }
}
}
};
}
}
}

View File

@ -1,412 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using S7Explorer.Models;
namespace S7Explorer.Parsers
{
public class S7ProjectParser
{
private readonly string _projectFilePath;
private readonly string _projectDirectory;
public S7ProjectParser(string projectFilePath)
{
_projectFilePath = projectFilePath;
_projectDirectory = Path.GetDirectoryName(projectFilePath);
if (string.IsNullOrEmpty(_projectDirectory))
throw new ArgumentException("No se pudo determinar el directorio del proyecto");
}
public async Task<S7Project> ParseProjectAsync()
{
return await Task.Run(() =>
{
// Crear objeto de proyecto
var project = new S7Project(_projectFilePath);
try
{
// Estructura básica del proyecto
var devicesFolder = new S7Object
{
Name = "Dispositivos",
ObjectType = S7ObjectType.Folder,
Parent = project
};
project.Children.Add(devicesFolder);
// Parsear dispositivos
ParseDevices(devicesFolder);
// Para uso en el futuro: más carpetas de alto nivel
var sharedFolder = new S7Object
{
Name = "Datos Globales",
ObjectType = S7ObjectType.Folder,
Parent = project
};
project.Children.Add(sharedFolder);
return project;
}
catch (Exception ex)
{
// En caso de error, al menos retornamos un proyecto básico con el error
var errorObject = new S7Object
{
Name = "Error al parsear proyecto",
Description = ex.Message,
ObjectType = S7ObjectType.Folder,
Parent = project
};
project.Children.Add(errorObject);
return project;
}
});
}
private void ParseDevices(S7Object devicesFolder)
{
try
{
// Obtener lista de dispositivos a partir del archivo de información
var deviceIdInfos = ParseDeviceIdInfos();
foreach (var deviceInfo in deviceIdInfos)
{
var device = new S7Object
{
Name = deviceInfo.Name,
ObjectType = S7ObjectType.Device,
Parent = devicesFolder
};
devicesFolder.Children.Add(device);
// Crear carpetas para cada tipo de bloque
var dbFolder = CreateBlockFolder(device, "Bloques de datos (DB)");
var fbFolder = CreateBlockFolder(device, "Bloques de función (FB)");
var fcFolder = CreateBlockFolder(device, "Funciones (FC)");
var obFolder = CreateBlockFolder(device, "Bloques de organización (OB)");
var symbolsFolder = CreateBlockFolder(device, "Tabla de símbolos");
// Parsear bloques y símbolos si se dispone de IDs
if (deviceInfo.SymbolListId.HasValue)
{
ParseSymbols(symbolsFolder, deviceInfo.SymbolListId.Value);
}
if (deviceInfo.SubblockListId.HasValue)
{
ParseBlocks(dbFolder, fbFolder, fcFolder, obFolder, deviceInfo.SubblockListId.Value);
}
}
}
catch (Exception ex)
{
// Si falla el parseo, añadimos un nodo de error pero seguimos con el resto
var errorNode = new S7Object
{
Name = "Error al parsear dispositivos",
Description = ex.Message,
ObjectType = S7ObjectType.Folder,
Parent = devicesFolder
};
devicesFolder.Children.Add(errorNode);
}
}
private S7Object CreateBlockFolder(S7Object parent, string name)
{
var folder = new S7Object
{
Name = name,
ObjectType = S7ObjectType.Folder,
Parent = parent
};
parent.Children.Add(folder);
return folder;
}
private List<DeviceIdInfo> ParseDeviceIdInfos()
{
var result = new List<DeviceIdInfo>();
// Esto es una simplificación - en una implementación real
// necesitarías parsear los archivos reales como en el proyecto C++
var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF");
if (File.Exists(s7resoffPath))
{
try
{
// Leer la tabla de dispositivos
var records = DbfParser.ReadDbfFile(s7resoffPath, new[] { "ID", "NAME", "RSRVD4_L" });
foreach (var record in records)
{
var device = new DeviceIdInfo
{
Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]),
};
// Procesamiento simplificado para IDs de Subblock y Symbol List
device.SubblockListId = DbfParser.StringToInt(record["RSRVD4_L"]);
// NOTA: En una implementación completa, tendrías que leer linkhrs.lnk
// para obtener SubblockListId y SymbolListId reales
result.Add(device);
}
}
catch (Exception)
{
// Error de parseo - ignorar y continuar
}
}
// Si no se encuentran dispositivos, crear uno de ejemplo
if (result.Count == 0)
{
result.Add(new DeviceIdInfo
{
Name = "Dispositivo de ejemplo"
});
}
return result;
}
private void ParseSymbols(S7Object symbolsFolder, int symbolListId)
{
// Esta es una implementación de muestra
// En una versión completa, leerías los archivos YDBs reales
// Crear algunos símbolos de ejemplo
var symbols = new List<S7Symbol>
{
new S7Symbol {
Name = "Motor_Start",
Address = "I0.0",
DataType = "BOOL",
Comment = "Pulsador de inicio del motor"
},
new S7Symbol {
Name = "Motor_Stop",
Address = "I0.1",
DataType = "BOOL",
Comment = "Pulsador de parada del motor"
},
new S7Symbol {
Name = "Motor_Running",
Address = "Q0.0",
DataType = "BOOL",
Comment = "Motor en marcha"
},
new S7Symbol {
Name = "Temperature",
Address = "IW64",
DataType = "INT",
Comment = "Temperatura del proceso"
},
};
foreach (var symbol in symbols)
{
symbol.Parent = symbolsFolder;
symbolsFolder.Children.Add(symbol);
}
}
private void ParseBlocks(S7Object dbFolder, S7Object fbFolder, S7Object fcFolder, S7Object obFolder, int subblockListId)
{
try
{
// Usar parser específico para las funciones (FC)
var fcParser = new FCParser(_projectDirectory);
var functions = fcParser.ParseFunctionBlocks(subblockListId);
// Añadir bloques FC al árbol de proyecto
foreach (var fc in functions)
{
fc.Parent = fcFolder;
fcFolder.Children.Add(fc);
}
// Para los otros tipos de bloques, seguimos usando los ejemplos por ahora
// En una implementación completa, crearíamos parsers específicos para cada tipo
AddSampleDataBlocks(dbFolder);
AddSampleFunctionBlocks(fbFolder);
AddSampleOrgBlocks(obFolder);
}
catch (Exception ex)
{
// En caso de error, añadimos un objeto de error informativo
var errorObject = new S7Object
{
Name = "Error en parseo de bloques",
Description = ex.Message,
ObjectType = S7ObjectType.Folder
};
// Lo añadimos a cada carpeta para que sea visible
dbFolder.Children.Add(errorObject);
fbFolder.Children.Add(new S7Object
{
Name = errorObject.Name,
Description = errorObject.Description,
ObjectType = errorObject.ObjectType
});
fcFolder.Children.Add(new S7Object
{
Name = errorObject.Name,
Description = errorObject.Description,
ObjectType = errorObject.ObjectType
});
obFolder.Children.Add(new S7Object
{
Name = errorObject.Name,
Description = errorObject.Description,
ObjectType = errorObject.ObjectType
});
}
}
private void AddSampleDataBlocks(S7Object dbFolder)
{
var dbs = new List<S7DataBlock>
{
new S7DataBlock {
Name = "Datos_proceso",
Number = "DB1",
Size = 124,
IsInstanceDb = false,
Description = "Datos de proceso",
Modified = DateTime.Now.AddDays(-5)
},
new S7DataBlock {
Name = "Parámetros",
Number = "DB2",
Size = 234,
IsInstanceDb = false,
Description = "Parámetros de configuración",
Modified = DateTime.Now.AddDays(-10)
},
new S7DataBlock {
Name = "Motor_Inst",
Number = "DB10",
Size = 86,
IsInstanceDb = true,
Description = "Instancia de FB1",
Modified = DateTime.Now.AddDays(-2)
}
};
foreach (var db in dbs)
{
db.Parent = dbFolder;
dbFolder.Children.Add(db);
}
}
private void AddSampleFunctionBlocks(S7Object fbFolder)
{
var fbs = new List<S7FunctionBlock>
{
new S7FunctionBlock {
Name = "Motor_Control",
Number = "FB1",
Size = 328,
Language = "SCL",
Description = "Control de motor",
Modified = DateTime.Now.AddDays(-15)
},
new S7FunctionBlock {
Name = "PID_Control",
Number = "FB2",
Size = 512,
Language = "SCL",
Description = "Controlador PID",
Modified = DateTime.Now.AddDays(-20)
}
};
foreach (var fb in fbs)
{
fb.Parent = fbFolder;
fbFolder.Children.Add(fb);
}
}
private void AddSampleFunctions(S7Object fcFolder)
{
var fcs = new List<S7Function>
{
new S7Function {
Name = "Calc_Setpoint",
Number = "FC1",
Size = 124,
ReturnType = "REAL",
Description = "Cálculo de punto de consigna",
Modified = DateTime.Now.AddDays(-8)
},
new S7Function {
Name = "Scale_Analog",
Number = "FC2",
Size = 68,
ReturnType = "REAL",
Description = "Escalado de valor analógico",
Modified = DateTime.Now.AddDays(-12)
}
};
foreach (var fc in fcs)
{
fc.Parent = fcFolder;
fcFolder.Children.Add(fc);
}
}
private void AddSampleOrgBlocks(S7Object obFolder)
{
var obs = new List<S7Block>
{
new S7Block {
Name = "Main",
Number = "OB1",
Size = 256,
ObjectType = S7ObjectType.Organization,
Description = "Ciclo principal",
Modified = DateTime.Now.AddDays(-1)
},
new S7Block {
Name = "Clock_100ms",
Number = "OB35",
Size = 124,
ObjectType = S7ObjectType.Organization,
Description = "Interrupción cíclica 100ms",
Modified = DateTime.Now.AddDays(-7)
}
};
foreach (var ob in obs)
{
ob.Parent = obFolder;
obFolder.Children.Add(ob);
}
}
// Clase interna para la información de los dispositivos
private class DeviceIdInfo
{
public string Name { get; set; }
public int? SubblockListId { get; set; }
public int? SymbolListId { get; set; }
}
}
}

BIN
Resources/db.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
Resources/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
Resources/device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Resources/fb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
Resources/fc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
Resources/folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
Resources/ob.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
Resources/project.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
Resources/symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -6,14 +6,50 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="DotNetProjects.DotNetSiemensPLCToolBoxLibrary" Version="4.2.3" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.7.25104.5739" />
<PackageReference Include="NDbfReader" Version="2.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.0-preview.2.25163.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\db.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\default.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\device.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\fb.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\fc.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\folder.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\ob.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\project.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\symbol.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

230
Services/ExportService.cs Normal file
View File

@ -0,0 +1,230 @@
using Newtonsoft.Json;
using S7Explorer.Models;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace S7Explorer.Services
{
public class ExportService
{
private readonly LogService _logService;
public ExportService()
{
_logService = LogService.Instance;
}
public async Task<bool> ExportProjectAsync(ProjectStructure project, ExportSettings settings)
{
return await Task.Run(() => ExportProject(project, settings));
}
private bool ExportProject(ProjectStructure project, ExportSettings settings)
{
try
{
_logService.LogInfo($"Starting export to {settings.ExportPath}");
string exportContent = GenerateExportContent(project, settings);
// Ensure directory exists
string directory = Path.GetDirectoryName(settings.ExportPath) ?? string.Empty;
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Write to file based on format
switch (settings.ExportFormat)
{
case ExportFormat.PlainText:
File.WriteAllText(settings.ExportPath, exportContent);
break;
case ExportFormat.MarkDown:
File.WriteAllText(settings.ExportPath, GenerateMarkdownContent(project, settings));
break;
case ExportFormat.HTML:
File.WriteAllText(settings.ExportPath, GenerateHtmlContent(project, settings));
break;
case ExportFormat.JSON:
File.WriteAllText(settings.ExportPath, GenerateJsonContent(project, settings));
break;
}
_logService.LogInfo($"Export completed successfully to {settings.ExportPath}");
return true;
}
catch (Exception ex)
{
_logService.LogError($"Error during export: {ex.Message}");
return false;
}
}
private string GenerateExportContent(ProjectStructure project, ExportSettings settings)
{
StringBuilder sb = new StringBuilder();
// Project info
sb.AppendLine("==========================================================");
sb.AppendLine($"PROJECT DOCUMENTATION: {project.Name}");
sb.AppendLine("==========================================================");
sb.AppendLine();
sb.AppendLine(project.GetExportText());
// Hardware configuration
if (settings.ExportHardware)
{
sb.AppendLine("==========================================================");
sb.AppendLine("HARDWARE CONFIGURATION");
sb.AppendLine("==========================================================");
sb.AppendLine();
ExportChildren(project.HardwareFolder, sb, settings, 0);
}
// Symbols
if (settings.ExportSymbols)
{
sb.AppendLine("==========================================================");
sb.AppendLine("SYMBOL TABLE");
sb.AppendLine("==========================================================");
sb.AppendLine();
ExportChildren(project.SymbolsFolder, sb, settings, 0);
}
// Blocks
if (settings.ExportBlocks)
{
sb.AppendLine("==========================================================");
sb.AppendLine("PROGRAM BLOCKS");
sb.AppendLine("==========================================================");
sb.AppendLine();
ExportChildren(project.BlocksFolder, sb, settings, 0);
}
return sb.ToString();
}
private void ExportChildren(ProjectItem item, StringBuilder sb, ExportSettings settings, int level)
{
// Don't export if no children
if (item.Children.Count == 0)
return;
string indent = new string(' ', level * 2);
foreach (var child in item.Children)
{
// Skip block code if settings say so
if (child is BlockItem blockItem && !settings.IncludeBlockCode)
{
string text = blockItem.GetExportText();
// Remove the block content section
int contentIndex = text.IndexOf("// Block Content");
if (contentIndex > 0)
{
text = text.Substring(0, contentIndex) + "// Block Content omitted per export settings\r\n\r\n";
}
sb.Append(indent);
sb.AppendLine(text.Replace("\r\n", "\r\n" + indent));
}
else
{
// For other item types, just get export text
sb.Append(indent);
sb.AppendLine(child.GetExportText().Replace("\r\n", "\r\n" + indent));
}
// Recursively export children
ExportChildren(child, sb, settings, level + 1);
}
}
private string GenerateMarkdownContent(ProjectStructure project, ExportSettings settings)
{
// This would generate Markdown-formatted content
// Simplified implementation
StringBuilder sb = new StringBuilder();
sb.AppendLine($"# PROJECT DOCUMENTATION: {project.Name}");
sb.AppendLine();
sb.AppendLine($"**Project Path:** {project.ProjectPath}");
sb.AppendLine($"**Version:** {project.ProjectVersion}");
sb.AppendLine($"**Created:** {project.CreationDate}");
sb.AppendLine($"**Last Modified:** {project.LastModifiedDate}");
sb.AppendLine();
// Rest of the implementation similar to plain text but with Markdown formatting
// ...
return sb.ToString();
}
private string GenerateHtmlContent(ProjectStructure project, ExportSettings settings)
{
// This would generate HTML-formatted content
// Simplified implementation
StringBuilder sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html>");
sb.AppendLine("<head>");
sb.AppendLine($"<title>Project Documentation: {project.Name}</title>");
sb.AppendLine("<style>");
sb.AppendLine("body { font-family: Arial, sans-serif; margin: 20px; }");
sb.AppendLine("h1, h2 { color: #0066cc; }");
sb.AppendLine("pre { background-color: #f5f5f5; padding: 10px; border: 1px solid #ddd; }");
sb.AppendLine("</style>");
sb.AppendLine("</head>");
sb.AppendLine("<body>");
sb.AppendLine($"<h1>PROJECT DOCUMENTATION: {project.Name}</h1>");
// Rest of the implementation similar to plain text but with HTML formatting
// ...
sb.AppendLine("</body>");
sb.AppendLine("</html>");
return sb.ToString();
}
private string GenerateJsonContent(ProjectStructure project, ExportSettings settings)
{
// Create an object structure to export to JSON
var exportObject = new
{
Project = new
{
Name = project.Name,
Path = project.ProjectPath,
Version = project.ProjectVersion,
CreationDate = project.CreationDate,
LastModifiedDate = project.LastModifiedDate
},
Hardware = settings.ExportHardware ? GetExportObjectForItem(project.HardwareFolder) : null,
Symbols = settings.ExportSymbols ? GetExportObjectForItem(project.SymbolsFolder) : null,
Blocks = settings.ExportBlocks ? GetExportObjectForItem(project.BlocksFolder) : null
};
return JsonConvert.SerializeObject(exportObject, Formatting.Indented);
}
private object GetExportObjectForItem(ProjectItem item)
{
// Create a JSON-friendly object structure for an item and its children
var result = new
{
Name = item.Name,
Children = item.Children.Select(c => GetExportObjectForItem(c)).ToArray()
};
return result;
}
}
}

64
Services/LogService.cs Normal file
View File

@ -0,0 +1,64 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
namespace S7Explorer.Services
{
public class LogService
{
private static LogService? _instance;
private ObservableCollection<LogEntry> _logs = new();
// Singleton instance
public static LogService Instance
{
get
{
if (_instance == null)
_instance = new LogService();
return _instance;
}
}
public ObservableCollection<LogEntry> Logs => _logs;
public void LogInfo(string message)
{
AddLog(LogLevel.Info, message);
}
public void LogWarning(string message)
{
AddLog(LogLevel.Warning, message);
}
public void LogError(string message)
{
AddLog(LogLevel.Error, message);
}
public void LogError(Exception ex)
{
AddLog(LogLevel.Error, $"Error: {ex.Message}");
AddLog(LogLevel.Debug, $"Stack trace: {ex.StackTrace}");
}
public void LogDebug(string message)
{
AddLog(LogLevel.Debug, message);
}
private void AddLog(LogLevel level, string message)
{
Application.Current.Dispatcher.Invoke(() =>
{
_logs.Add(new LogEntry
{
Timestamp = DateTime.Now,
Level = level,
Message = message
});
});
}
}
}

496
Services/ProjectService.cs Normal file
View File

@ -0,0 +1,496 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DotNetSiemensPLCToolBoxLibrary.DataTypes;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.Step7V5;
using DotNetSiemensPLCToolBoxLibrary.Projectfiles;
using S7Explorer.Models;
using ProjectFolder = DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.ProjectFolder;
namespace S7Explorer.Services
{
public class ProjectService
{
private readonly LogService _logService;
public ProjectService()
{
_logService = LogService.Instance;
}
public async Task<ProjectStructure?> LoadProjectAsync(string projectPath)
{
return await Task.Run(() => LoadProject(projectPath));
}
private ProjectStructure? LoadProject(string projectPath)
{
try
{
_logService.LogInfo($"Loading project from: {projectPath}");
if (!File.Exists(projectPath))
{
_logService.LogError($"Project file not found: {projectPath}");
return null;
}
// Load the Step7 project
var step7Project = Projects.GetStep7ProjectsFromDirectory(Path.GetDirectoryName(projectPath)).FirstOrDefault();
if (step7Project == null)
{
_logService.LogError("Could not load project. Make sure it's a valid S7 project file.");
return null;
}
_logService.LogInfo($"Project loaded: {step7Project.ProjectName}");
// Create the project structure
ProjectStructure projectStructure = new ProjectStructure
{
Name = step7Project.ProjectName,
ProjectData = step7Project,
ProjectPath = projectPath,
ProjectVersion = "Unknown", // Not directly available in the API
CreationDate = File.GetCreationTime(projectPath),
LastModifiedDate = File.GetLastWriteTime(projectPath)
};
try
{
// Process the project structure
ProcessProjectStructure(step7Project, projectStructure);
}
catch (Exception ex)
{
_logService.LogError($"Error processing project structure: {ex.Message}");
}
return projectStructure;
}
catch (Exception ex)
{
_logService.LogError(ex);
return null;
}
}
private void ProcessProjectStructure(Project project, ProjectStructure projectStructure)
{
// We need to carefully navigate the project structure
if (project.ProjectStructure == null)
{
_logService.LogWarning("Project structure is null");
return;
}
// Use reflection to find and process PLC folders
ProcessProjectFolder(project.ProjectStructure, projectStructure);
}
private void ProcessProjectFolder(ProjectFolder folder, ProjectStructure projectStructure)
{
try
{
// Check if this is a CPU/PLC folder
if (IsProgrammFolder(folder))
{
string folderName = GetFolderName(folder);
_logService.LogInfo($"Processing PLC folder: {folderName}");
// Process this folder
LoadHardwareConfiguration(folder, projectStructure.HardwareFolder);
// Try to find blocks folder
var blocksFolder = GetBlocksFolder(folder);
if (blocksFolder != null)
{
LoadBlocks(blocksFolder, projectStructure.BlocksFolder);
}
// Try to find symbol table
var symbolTable = GetSymbolTable(folder);
if (symbolTable != null)
{
LoadSymbolTable(symbolTable, projectStructure.SymbolsFolder);
}
}
// Try to process sub-folders
ProcessSubFolders(folder, projectStructure);
}
catch (Exception ex)
{
_logService.LogError($"Error processing folder: {ex.Message}");
}
}
private void ProcessSubFolders(ProjectFolder folder, ProjectStructure projectStructure)
{
try
{
// Use reflection to find sub-folders
var properties = folder.GetType().GetProperties();
foreach (var property in properties)
{
if (typeof(ProjectFolder).IsAssignableFrom(property.PropertyType))
{
// This property is a ProjectFolder
var subFolder = property.GetValue(folder) as ProjectFolder;
if (subFolder != null)
{
ProcessProjectFolder(subFolder, projectStructure);
}
}
else if (property.PropertyType.IsGenericType &&
typeof(IEnumerable<>).IsAssignableFrom(property.PropertyType.GetGenericTypeDefinition()))
{
// This might be a collection of folders
var collection = property.GetValue(folder) as System.Collections.IEnumerable;
if (collection != null)
{
foreach (var item in collection)
{
if (item is ProjectFolder subFolder)
{
ProcessProjectFolder(subFolder, projectStructure);
}
}
}
}
}
}
catch (Exception ex)
{
_logService.LogError($"Error processing sub-folders: {ex.Message}");
}
}
private void LoadBlocks(IBlocksFolder blocksFolder, S7Explorer.Models.ProjectItem parentFolder)
{
try
{
// IBlocksFolder doesn't have a Name property directly, so let's use a generic name
string folderName = "Blocks";
if (blocksFolder is ProjectFolder pf && pf.GetType().GetProperty("Name") != null)
folderName = GetFolderName(pf);
_logService.LogInfo($"Loading blocks from: {folderName}");
// Create a folder for this block type
S7Explorer.Models.ProjectItem blocksFolderItem = new S7Explorer.Models.ProjectItem
{
Name = folderName,
Parent = parentFolder
};
parentFolder.Children.Add(blocksFolderItem);
// Get all blocks
var blockInfos = blocksFolder.BlockInfos;
_logService.LogInfo($"Found {blockInfos.Count} blocks");
// Add each block to the tree
foreach (var blockInfo in blockInfos)
{
// Skip system blocks if needed
if (blockInfo.BlockType == PLCBlockType.SFC ||
blockInfo.BlockType == PLCBlockType.SFB)
continue;
try
{
// Load full block data
S7Block blockData = blocksFolder.GetBlock(blockInfo) as S7Block;
if (blockData != null)
{
// Need to extract block number from blockInfo
int blockNumber = 0;
// Try to parse block number from ToString() which typically returns something like "DB100"
string blockStr = blockInfo.ToString();
string numPart = string.Empty;
for (int i = 0; i < blockStr.Length; i++)
{
if (char.IsDigit(blockStr[i]))
numPart += blockStr[i];
}
if (!string.IsNullOrEmpty(numPart))
int.TryParse(numPart, out blockNumber);
S7Explorer.Models.BlockItem blockItem = new S7Explorer.Models.BlockItem
{
Name = blockInfo.ToString(),
BlockInfo = blockInfo,
BlockData = blockData,
BlockType = blockInfo.BlockType.ToString(),
BlockNumber = blockNumber,
BlockComment = blockData.Title ?? string.Empty,
Parent = blocksFolderItem
};
// Get block source code
try
{
if (blocksFolder is BlocksOfflineFolder bof)
{
var sourceCode = bof.GetSourceBlock(blockInfo, true);
blockItem.BlockContent = sourceCode ?? string.Empty;
}
else
{
blockItem.BlockContent = "// Unable to retrieve block source code";
}
}
catch (Exception)
{
blockItem.BlockContent = "// Unable to retrieve block source code";
}
blocksFolderItem.Children.Add(blockItem);
}
}
catch (Exception ex)
{
_logService.LogWarning($"Failed to load block {blockInfo}: {ex.Message}");
}
}
}
catch (Exception ex)
{
_logService.LogError($"Error loading blocks: {ex.Message}");
}
}
private void LoadSymbolTable(ISymbolTable symbolTable, S7Explorer.Models.ProjectItem parentFolder)
{
try
{
string tableName = "Symbols";
if (symbolTable is ProjectFolder pf)
tableName = GetFolderName(pf);
_logService.LogInfo($"Loading symbols from table: {tableName}");
// Create a folder for this symbol table
S7Explorer.Models.ProjectItem symbolTableItem = new S7Explorer.Models.ProjectItem
{
Name = tableName,
Parent = parentFolder
};
parentFolder.Children.Add(symbolTableItem);
// Try to get symbols using reflection to avoid direct property access
int count = LoadSymbolsUsingReflection(symbolTable, symbolTableItem);
if (count > 0)
{
_logService.LogInfo($"Loaded {count} symbols");
}
else
{
_logService.LogWarning("No symbols found or symbol table is empty");
}
}
catch (Exception ex)
{
_logService.LogError($"Error loading symbols: {ex.Message}");
}
}
private int LoadSymbolsUsingReflection(ISymbolTable symbolTable, S7Explorer.Models.ProjectItem parentFolder)
{
int count = 0;
try
{
// Find property that might contain symbols
var properties = symbolTable.GetType().GetProperties();
// Look for collection properties that might be symbol collections
foreach (var prop in properties)
{
// Skip non-collection properties
if (!typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) ||
prop.PropertyType == typeof(string))
continue;
// Try to get the collection
var collection = prop.GetValue(symbolTable) as IEnumerable;
if (collection == null)
continue;
// Check if this collection contains symbols
foreach (var item in collection)
{
if (item == null)
continue;
// Check if this looks like a symbol
var itemType = item.GetType();
var symbolProp = itemType.GetProperty("Symbol");
var operandProp = itemType.GetProperty("Operand") ?? itemType.GetProperty("Address");
// If it has Symbol and Operand properties, treat it as a symbol
if (symbolProp != null && operandProp != null)
{
var dataTypeProp = itemType.GetProperty("DataType");
var commentProp = itemType.GetProperty("Comment");
S7Explorer.Models.SymbolItem symbolItem = new S7Explorer.Models.SymbolItem
{
Name = symbolProp.GetValue(item)?.ToString() ?? "Unknown",
SymbolAddress = operandProp.GetValue(item)?.ToString() ?? "",
SymbolDataType = dataTypeProp?.GetValue(item)?.ToString() ?? "",
SymbolComment = commentProp?.GetValue(item)?.ToString() ?? "",
Parent = parentFolder
};
parentFolder.Children.Add(symbolItem);
count++;
}
}
// If we found symbols, stop looking through properties
if (count > 0)
break;
}
}
catch (Exception ex)
{
_logService.LogError($"Error loading symbols using reflection: {ex.Message}");
}
return count;
}
private void LoadHardwareConfiguration(object cpuFolder, S7Explorer.Models.ProjectItem parentFolder)
{
try
{
string folderName = GetFolderName(cpuFolder);
_logService.LogInfo($"Loading hardware configuration for CPU: {folderName}");
// Create a folder for this CPU
S7Explorer.Models.ProjectItem cpuItem = new S7Explorer.Models.ProjectItem
{
Name = folderName,
Parent = parentFolder
};
parentFolder.Children.Add(cpuItem);
// Add CPU information
S7Explorer.Models.HardwareItem cpuHardwareItem = new S7Explorer.Models.HardwareItem
{
Name = $"CPU {folderName}",
ModuleType = "CPU",
OrderNumber = "Unknown", // Would need to extract from hardware
Position = "Unknown",
Parent = cpuItem
};
cpuItem.Children.Add(cpuHardwareItem);
// We would need to analyze the hardware configuration in more detail
// to extract modules, but this requires more insight into the library's structure
_logService.LogInfo("Hardware configuration loaded");
}
catch (Exception ex)
{
_logService.LogError($"Error loading hardware configuration: {ex.Message}");
}
}
// Helper methods to safely access project structure
private bool IsProgrammFolder(object folder)
{
return folder is IProgrammFolder ||
folder.GetType().GetInterfaces().Any(i => i.Name == "IProgrammFolder");
}
private IBlocksFolder GetBlocksFolder(object folder)
{
try
{
// Try using reflection to access the BlocksOfflineFolder property
var property = folder.GetType().GetProperty("BlocksOfflineFolder");
if (property != null)
{
return property.GetValue(folder) as IBlocksFolder;
}
// If that fails, look for a property of type IBlocksFolder
foreach (var prop in folder.GetType().GetProperties())
{
if (typeof(IBlocksFolder).IsAssignableFrom(prop.PropertyType))
{
return prop.GetValue(folder) as IBlocksFolder;
}
}
}
catch (Exception ex)
{
_logService.LogError($"Error accessing blocks folder: {ex.Message}");
}
return null;
}
private ISymbolTable GetSymbolTable(object folder)
{
try
{
// Try using reflection to access the SymbolTable property
var property = folder.GetType().GetProperty("SymbolTable");
if (property != null)
{
return property.GetValue(folder) as ISymbolTable;
}
// If that fails, look for a property of type ISymbolTable
foreach (var prop in folder.GetType().GetProperties())
{
if (typeof(ISymbolTable).IsAssignableFrom(prop.PropertyType))
{
return prop.GetValue(folder) as ISymbolTable;
}
}
}
catch (Exception ex)
{
_logService.LogError($"Error accessing symbol table: {ex.Message}");
}
return null;
}
private string GetFolderName(object folder)
{
try
{
// Try to access Name property
var property = folder.GetType().GetProperty("Name");
if (property != null)
{
return property.GetValue(folder)?.ToString() ?? "Unknown";
}
}
catch
{
// Ignore errors
}
return "Unknown";
}
}
}

View File

@ -1,154 +1,141 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ookii.Dialogs.Wpf;
using S7Explorer.Helpers;
using S7Explorer.Models;
using S7Explorer.Parsers;
using S7Explorer.Services;
using S7Explorer.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
namespace S7Explorer.ViewModels
{
public class MainViewModel : ObservableObject
public partial class MainViewModel : ViewModelBase
{
private string _searchText;
private object _selectedObject;
private object _selectedTreeItem;
private S7Project _currentProject;
private ObservableCollection<S7Object> _projectStructure;
private string _projectInfo;
private readonly ProjectService _projectService;
private readonly ExportService _exportService;
private readonly SearchHelper _searchHelper;
[ObservableProperty]
private ProjectStructure? _projectStructure;
[ObservableProperty]
private ProjectItem? _selectedItem;
[ObservableProperty]
private object? _selectedItemDetails;
[ObservableProperty]
private string _projectPath = string.Empty;
[ObservableProperty]
private bool _isProjectLoaded;
[ObservableProperty]
private string _searchText = string.Empty;
[ObservableProperty]
private bool _isCaseSensitive;
[ObservableProperty]
private bool _useRegex;
[ObservableProperty]
private ObservableCollection<LogEntry> _logEntries = new();
[ObservableProperty]
private ExportSettings _exportSettings = new();
[ObservableProperty]
private bool _isLoading;
public string SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value);
}
public object SelectedObject
{
get => _selectedObject;
set => SetProperty(ref _selectedObject, value);
}
public object SelectedTreeItem
{
get => _selectedTreeItem;
set
{
if (SetProperty(ref _selectedTreeItem, value))
{
// Actualizar el objeto seleccionado para PropertyGrid
SelectedObject = _selectedTreeItem;
// Acciones específicas según el tipo de objeto seleccionado
if (_selectedTreeItem is S7Function fc)
{
// Si el objeto seleccionado es una función, actualizar su interfaz
UpdateFunctionInterface(fc);
}
}
}
}
/// <summary>
/// Actualiza la información de interfaz de una función seleccionada
/// </summary>
private void UpdateFunctionInterface(S7Function fc)
{
try
{
// Aquí podríamos cargar información adicional específica de la función
// Por ejemplo, cargar el código de la función, detalles de parámetros, etc.
// Actualizar estadísticas o análisis de la función
// Por ejemplo, mostrar uso de la función en otros bloques
// Este método se ejecutará cuando el usuario seleccione una FC en el árbol
}
catch (Exception)
{
// Ignorar errores para no interrumpir la experiencia del usuario
}
}
public ObservableCollection<S7Object> ProjectStructure
{
get => _projectStructure;
set => SetProperty(ref _projectStructure, value);
}
public string ProjectInfo
{
get => _projectInfo;
set => SetProperty(ref _projectInfo, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public IRelayCommand OpenProjectCommand { get; }
public IRelayCommand SearchCommand { get; }
public MainViewModel()
{
ProjectStructure = new ObservableCollection<S7Object>();
OpenProjectCommand = new RelayCommand(OpenProject);
SearchCommand = new RelayCommand(SearchInProject);
ProjectInfo = "No hay proyecto abierto";
_projectService = new ProjectService();
_exportService = new ExportService();
_searchHelper = new SearchHelper();
// Initialize log entries collection
LogEntries = LogService.Instance.Logs;
// Initialize export settings
ExportSettings.ExportPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"S7Documentation.txt");
}
private void OpenProject()
partial void OnSelectedItemChanged(ProjectItem? value)
{
var dialog = new VistaOpenFileDialog
{
Title = "Seleccionar archivo de proyecto STEP7",
Filter = "Proyectos STEP7 (*.s7p)|*.s7p|Todos los archivos (*.*)|*.*",
CheckFileExists = true
};
SelectedItemDetails = value?.GetDetailsObject();
}
if (dialog.ShowDialog() == true)
[RelayCommand]
private async Task LoadProject()
{
try
{
LoadProjectAsync(dialog.FileName);
var dialog = new VistaOpenFileDialog
{
Title = "Open Siemens S7 Project",
Filter = "S7 Projects (*.s7p)|*.s7p|All files (*.*)|*.*",
CheckFileExists = true
};
if (dialog.ShowDialog() == true)
{
ProjectPath = dialog.FileName;
await LoadProjectFromPath(ProjectPath);
}
}
catch (Exception ex)
{
_logService.LogError(ex);
MessageBox.Show($"Error loading project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void LoadProjectAsync(string filePath)
[RelayCommand]
private async Task ReloadProject()
{
if (string.IsNullOrEmpty(ProjectPath) || !File.Exists(ProjectPath))
{
_logService.LogWarning("No project loaded to reload");
return;
}
await LoadProjectFromPath(ProjectPath);
}
private async Task LoadProjectFromPath(string path)
{
try
{
IsLoading = true;
ProjectInfo = "Cargando proyecto...";
// Reiniciar estado
ProjectStructure.Clear();
_currentProject = null;
// Load the project
_logService.LogInfo($"Loading project from {path}");
ProjectStructure = await _projectService.LoadProjectAsync(path);
// Parsear proyecto
var parser = new S7ProjectParser(filePath);
_currentProject = await parser.ParseProjectAsync();
// Actualizar UI
ProjectStructure.Add(_currentProject);
_currentProject.IsExpanded = true;
// Actualizar info
ProjectInfo = $"Proyecto: {Path.GetFileNameWithoutExtension(filePath)}";
if (ProjectStructure != null)
{
IsProjectLoaded = true;
_logService.LogInfo("Project loaded successfully");
}
else
{
IsProjectLoaded = false;
_logService.LogError("Failed to load project");
MessageBox.Show("Failed to load project", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
MessageBox.Show($"Error al cargar el proyecto: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
ProjectInfo = "Error al cargar el proyecto";
_logService.LogError(ex);
MessageBox.Show($"Error loading project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
@ -156,125 +143,166 @@ namespace S7Explorer.ViewModels
}
}
private void SearchInProject()
[RelayCommand]
private void Search()
{
if (_currentProject == null || string.IsNullOrWhiteSpace(SearchText))
if (!IsProjectLoaded || ProjectStructure == null)
{
_logService.LogWarning("No project loaded to search");
return;
}
if (string.IsNullOrWhiteSpace(SearchText))
{
_logService.LogWarning("No search text specified");
return;
}
try
{
// Recopilar todos los objetos en el proyecto
var allObjects = GetAllObjects(_currentProject);
var results = _searchHelper.Search(ProjectStructure, SearchText, IsCaseSensitive, UseRegex);
// Lista para almacenar coincidencias
var matchingObjects = new List<S7Object>();
// Buscar en función del texto de búsqueda
string searchText = SearchText.Trim().ToLowerInvariant();
// Buscar objetos que coincidan con el texto
foreach (var obj in allObjects)
if (results.Count == 0)
{
// Comprobar si el texto de búsqueda parece ser una referencia a FC específica
if (searchText.StartsWith("fc") && searchText.Length >= 3)
{
// Si es una función específica (por ejemplo "fc5")
if (obj is S7Function func &&
func.Number.ToLowerInvariant() == searchText)
{
matchingObjects.Add(func);
}
}
// Comprobar si queremos encontrar todas las FCs
else if (searchText == "fc" || searchText == "función" || searchText == "funcion")
{
if (obj is S7Function)
{
matchingObjects.Add(obj);
}
}
// Búsqueda en parámetros de función
else if (obj is S7Function func)
{
if (func.ContainsText(searchText))
{
matchingObjects.Add(func);
}
else if (func.Parameters != null)
{
// Buscar en los parámetros de la función
foreach (var param in func.Parameters)
{
if (param.Name.ToLowerInvariant().Contains(searchText) ||
param.DataType.ToLowerInvariant().Contains(searchText) ||
(param.Description?.ToLowerInvariant().Contains(searchText) ?? false))
{
matchingObjects.Add(func);
break;
}
}
}
}
// Búsqueda general para otros objetos
else if (obj.ContainsText(searchText))
{
matchingObjects.Add(obj);
}
}
if (matchingObjects.Count == 0)
{
MessageBox.Show($"No se encontraron coincidencias para: {SearchText}",
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
_logService.LogInfo($"No results found for '{SearchText}'");
MessageBox.Show($"No results found for '{SearchText}'", "Search Results", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// Seleccionar el primer objeto coincidente y expandir su ruta
var firstMatch = matchingObjects.First();
SelectAndExpandToObject(firstMatch);
// Show search results dialog
ShowSearchResults(results);
}
catch (Exception ex)
{
_logService.LogError(ex);
MessageBox.Show($"Error during search: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// Informar al usuario
if (matchingObjects.Count > 1)
private void ShowSearchResults(List<SearchResult> results)
{
// In a full implementation, this would show a dialog or window with search results
// For demonstration, we'll just select the first result and expand to it
if (results.Count > 0)
{
var firstResult = results[0];
TreeViewHelper.ExpandToItem(firstResult.Item);
SelectedItem = firstResult.Item;
_logService.LogInfo($"Found {results.Count} results, highlighted first result");
MessageBox.Show($"Found {results.Count} results", "Search Results", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
[RelayCommand]
private async Task ExportDocumentation()
{
if (!IsProjectLoaded || ProjectStructure == null)
{
_logService.LogWarning("No project loaded to export");
return;
}
try
{
// Show export settings dialog
bool proceed = ShowExportSettings();
if (!proceed)
return;
IsLoading = true;
// Export the documentation
bool success = await _exportService.ExportProjectAsync(ProjectStructure, ExportSettings);
if (success)
{
MessageBox.Show($"Se encontraron {matchingObjects.Count} coincidencias. " +
"Mostrando la primera coincidencia.",
"Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information);
_logService.LogInfo($"Documentation exported to {ExportSettings.ExportPath}");
MessageBox.Show($"Documentation exported to {ExportSettings.ExportPath}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
_logService.LogError("Failed to export documentation");
MessageBox.Show("Failed to export documentation", "Export Failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
MessageBox.Show($"Error durante la búsqueda: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
_logService.LogError(ex);
MessageBox.Show($"Error exporting documentation: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsLoading = false;
}
}
private IEnumerable<S7Object> GetAllObjects(S7Object root)
private bool ShowExportSettings()
{
// Devolver el objeto raíz
yield return root;
// Recursivamente devolver todos los hijos
foreach (var child in root.Children)
// Show file save dialog for export path
var dialog = new VistaSaveFileDialog
{
foreach (var obj in GetAllObjects(child))
Title = "Export Documentation",
Filter = GetExportFilter(),
FileName = Path.GetFileName(ExportSettings.ExportPath),
InitialDirectory = Path.GetDirectoryName(ExportSettings.ExportPath)
};
if (dialog.ShowDialog() == true)
{
ExportSettings.ExportPath = dialog.FileName;
// Determine export format from file extension
string extension = Path.GetExtension(dialog.FileName).ToLower();
ExportSettings.ExportFormat = extension switch
{
yield return obj;
}
".md" => ExportFormat.MarkDown,
".html" => ExportFormat.HTML,
".json" => ExportFormat.JSON,
_ => ExportFormat.PlainText
};
return true;
}
return false;
}
private string GetExportFilter()
{
return "Text Files (*.txt)|*.txt|" +
"Markdown Files (*.md)|*.md|" +
"HTML Files (*.html)|*.html|" +
"JSON Files (*.json)|*.json|" +
"All Files (*.*)|*.*";
}
[RelayCommand]
private void ExpandAll()
{
if (ProjectStructure != null)
{
TreeViewHelper.ExpandAll(ProjectStructure);
_logService.LogInfo("Expanded all tree nodes");
}
}
private void SelectAndExpandToObject(S7Object obj)
[RelayCommand]
private void CollapseAll()
{
// Expandir todos los nodos padres hasta llegar al objeto
var parent = obj.Parent;
while (parent != null)
if (ProjectStructure != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
TreeViewHelper.CollapseAll(ProjectStructure);
_logService.LogInfo("Collapsed all tree nodes");
}
}
// Seleccionar el objeto
SelectedTreeItem = obj;
[RelayCommand]
private void ClearLog()
{
LogEntries.Clear();
_logService.LogInfo("Log cleared");
}
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace S7Explorer.ViewModels
{
class S7ObjectViewModel
{
}
}

View File

@ -0,0 +1,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
using S7Explorer.Services;
namespace S7Explorer.ViewModels
{
public class ViewModelBase : ObservableObject
{
protected readonly LogService _logService;
public ViewModelBase()
{
_logService = LogService.Instance;
}
}
}

View File

@ -1,67 +0,0 @@
<Window x:Class="S7Explorer.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:local="clr-namespace:S7Explorer" mc:Ignorable="d"
Title="S7 Project Explorer" Height="600" Width="900">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Grid Grid.Row="0" Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Abrir Proyecto" Command="{Binding OpenProjectCommand}" Padding="8,4"
Margin="0,0,8,0" />
<TextBox Grid.Column="1" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Padding="4"
Margin="0,0,8,0" VerticalContentAlignment="Center" KeyDown="SearchBox_KeyDown" />
<Button Grid.Column="2" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
<TextBlock Grid.Column="3" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
</Grid>
<!-- Main Content -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="200" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Project Structure TreeView -->
<TreeView Grid.Column="0" Margin="8,0,0,8" ItemsSource="{Binding ProjectStructure}"
SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding IconSource}" Width="16" Height="16" Margin="0,0,4,0" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Center" VerticalAlignment="Stretch" />
<!-- Property Grid -->
<xctk:PropertyGrid Grid.Column="2" Margin="0,0,8,8" SelectedObject="{Binding SelectedObject}"
AutoGenerateProperties="True" ShowSearchBox="True" ShowSortOptions="True" ShowTitle="True" />
</Grid>
</Grid>
</Window>

View File

@ -1,37 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using S7Explorer.ViewModels;
namespace S7Explorer.Views
{
public partial class MainWindow : Window
{
private MainViewModel ViewModel => (MainViewModel)DataContext;
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (ViewModel != null)
{
ViewModel.SelectedTreeItem = e.NewValue;
}
}
private void SearchBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && ViewModel != null)
{
ViewModel.SearchCommand.Execute(null);
e.Handled = true;
}
}
}
}