Primera version usando la libreria DotNetSiemensPLCToolBoxLibrary
This commit is contained in:
parent
da9af34ded
commit
376f7f6cf6
18
App.xaml
18
App.xaml
|
@ -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>
|
85
App.xaml.cs
85
App.xaml.cs
|
@ -1,91 +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);
|
||||
|
||||
// AÑADIR ESTO: Registrar proveedor de codificación para soportar codificaciones adicionales (CP850, etc.)
|
||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||
|
||||
// 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
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,136 +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; }
|
||||
|
||||
// En S7Object.cs
|
||||
[Browsable(false)]
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Number))
|
||||
return Name;
|
||||
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
return Number;
|
||||
|
||||
return $"{Number} - {Name}";
|
||||
}
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using NDbfReaderEx; // Cambiar referencia de librería
|
||||
|
||||
namespace S7Explorer.Parsers
|
||||
{
|
||||
public class DbfParser
|
||||
{
|
||||
// Cache de codificación para reducir instanciación repetida
|
||||
private static readonly Encoding Windows1252 = Encoding.GetEncoding(1252);
|
||||
private static readonly Encoding Utf8 = Encoding.UTF8;
|
||||
|
||||
// Lee campos específicos de un archivo DBF, puede leer por lotes si se especifica
|
||||
public static List<Dictionary<string, string>> ReadDbfFile(
|
||||
string filePath,
|
||||
IEnumerable<string> fieldNames,
|
||||
int maxRecords = int.MaxValue,
|
||||
int startRecord = 0)
|
||||
{
|
||||
var result = new List<Dictionary<string, string>>();
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"No se encontró el archivo DBF: {filePath}");
|
||||
|
||||
try
|
||||
{
|
||||
// Usar nuestra función mejorada para abrir el archivo
|
||||
using (var table = OpenDbfTableSafe(filePath))
|
||||
{
|
||||
int estimatedSize = Math.Min(1000, maxRecords);
|
||||
result = new List<Dictionary<string, string>>(estimatedSize);
|
||||
|
||||
// Filtrar campos memo para evitar errores
|
||||
var safeFields = new List<string>();
|
||||
foreach (var fieldName in fieldNames)
|
||||
{
|
||||
// Verificar si el campo es memo
|
||||
var column = table.columns.FirstOrDefault(c => c.name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
|
||||
if (column != null && column.dbfType != NDbfReaderEx.NativeColumnType.Memo)
|
||||
{
|
||||
safeFields.Add(fieldName);
|
||||
}
|
||||
else if (column == null)
|
||||
{
|
||||
// Si el campo no existe, lo incluimos para que sea manejado por el código posterior
|
||||
safeFields.Add(fieldName);
|
||||
}
|
||||
// Campos memo se omiten intencionalmente para evitar errores
|
||||
}
|
||||
|
||||
for (int i = startRecord; i < startRecord + maxRecords && i < table.recCount; i++)
|
||||
{
|
||||
var row = table.GetRow(i);
|
||||
var record = new Dictionary<string, string>(safeFields.Count, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var fieldName in safeFields)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (row.IsNull(fieldName))
|
||||
{
|
||||
record[fieldName] = string.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
object value = row.GetValue(fieldName);
|
||||
|
||||
if (value is byte[] byteValue)
|
||||
{
|
||||
record[fieldName] = Windows1252.GetString(byteValue);
|
||||
}
|
||||
else if (value is DateTime dateValue)
|
||||
{
|
||||
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
else
|
||||
{
|
||||
record[fieldName] = value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Si hay un error al leer este campo, simplemente ponemos un valor vacío
|
||||
record[fieldName] = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error al leer el archivo DBF {filePath}: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Nuevo método para lectura por lotes usando las capacidades mejoradas de NDbfReaderEx
|
||||
public static IEnumerable<List<Dictionary<string, string>>> ReadDbfFileInBatches(
|
||||
string filePath,
|
||||
IEnumerable<string> fieldNames,
|
||||
int batchSize = 1000)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"No se encontró el archivo DBF: {filePath}");
|
||||
|
||||
using (var table = OpenDbfTableSafe(filePath))
|
||||
{
|
||||
var fieldsArray = new List<string>(fieldNames).ToArray();
|
||||
int totalRecords = table.recCount;
|
||||
|
||||
for (int startIndex = 0; startIndex < totalRecords; startIndex += batchSize)
|
||||
{
|
||||
var batch = new List<Dictionary<string, string>>(Math.Min(batchSize, totalRecords - startIndex));
|
||||
|
||||
int endIndex = Math.Min(startIndex + batchSize, totalRecords);
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
var row = table.GetRow(i);
|
||||
var record = new Dictionary<string, string>(fieldsArray.Length, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var fieldName in fieldsArray)
|
||||
{
|
||||
if (row.IsNull(fieldName))
|
||||
{
|
||||
record[fieldName] = string.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
object value = row.GetValue(fieldName);
|
||||
|
||||
if (value is byte[] byteValue)
|
||||
{
|
||||
record[fieldName] = Windows1252.GetString(byteValue);
|
||||
}
|
||||
else if (value is DateTime dateValue)
|
||||
{
|
||||
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
else
|
||||
{
|
||||
record[fieldName] = value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
batch.Add(record);
|
||||
}
|
||||
|
||||
yield return batch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convierte un string que representa un número a un entero opcional
|
||||
public static int? StringToInt(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
// Usar TryParse directamente si es posible
|
||||
if (int.TryParse(value, out int directResult))
|
||||
return directResult;
|
||||
|
||||
// Si no, intentar extraer la parte numérica
|
||||
string numericPart = string.Empty;
|
||||
bool foundDigit = false;
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
if (char.IsDigit(c))
|
||||
{
|
||||
numericPart += c;
|
||||
foundDigit = true;
|
||||
}
|
||||
else if (foundDigit)
|
||||
{
|
||||
// Si ya encontramos dígitos y ahora encontramos otro carácter, terminamos
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (int.TryParse(numericPart, out int result))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optimizado convertidor de codificación
|
||||
public static string ConvertCP1252ToUtf8(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Optimización: Si solo contiene caracteres ASCII, no necesitamos conversión
|
||||
bool needsConversion = false;
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] > 127)
|
||||
{
|
||||
needsConversion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsConversion)
|
||||
return input;
|
||||
|
||||
// Solo realizar conversión para texto con caracteres no-ASCII
|
||||
byte[] bytes = Windows1252.GetBytes(input);
|
||||
return Utf8.GetString(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
// Los demás métodos se mantienen igual
|
||||
// Busca un archivo DBF en varias ubicaciones posibles basadas en el patrón de archivos STEP7
|
||||
public static string FindDbfFile(string basePath, string relativePath)
|
||||
{
|
||||
// Ruta directa
|
||||
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;
|
||||
}
|
||||
|
||||
// Obtener solo el nombre del archivo sin ruta
|
||||
string fileName = Path.GetFileName(relativePath);
|
||||
|
||||
// Búsqueda en profundidad pero con límite para evitar que sea demasiado lenta
|
||||
return FindFileWithDepthLimit(basePath, fileName, 3);
|
||||
}
|
||||
|
||||
// Método de búsqueda con límite de profundidad
|
||||
private static string FindFileWithDepthLimit(string directory, string fileName, int maxDepth)
|
||||
{
|
||||
if (maxDepth <= 0)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Buscar en este directorio
|
||||
string filePath = Path.Combine(directory, fileName);
|
||||
if (File.Exists(filePath))
|
||||
return filePath;
|
||||
|
||||
// Buscar en subdirectorios con profundidad reducida
|
||||
foreach (var subdir in Directory.GetDirectories(directory))
|
||||
{
|
||||
string result = FindFileWithDepthLimit(subdir, fileName, maxDepth - 1);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorar errores de acceso a directorios
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Agregar al archivo DbfParser.cs
|
||||
public static Dictionary<string, string> ExploreDbfStructure(string filePath)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
try
|
||||
{
|
||||
using (var table = OpenDbfTableSafe(filePath))
|
||||
{
|
||||
foreach (var column in table.columns)
|
||||
{
|
||||
result[column.name.ToUpperInvariant()] = column.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error explorando estructura: {ex.Message}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void LogColumnInfo(string filePath, Action<string> logMethod, int maxRows = 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var table = OpenDbfTableSafe(filePath))
|
||||
{
|
||||
logMethod($"Archivo: {filePath}");
|
||||
logMethod($"Número de columnas: {table.columns.Count}");
|
||||
|
||||
// Mostrar información de columnas usando las propiedades correctas
|
||||
logMethod("Columnas:");
|
||||
foreach (var column in table.columns)
|
||||
{
|
||||
logMethod($" Nombre: {column.name}, Tipo DBF: {column.dbfType}, Tipo .NET: {column.type}");
|
||||
logMethod($" Tamaño: {column.size}, Decimales: {column.dec}, Ancho: {column.displayWidth}");
|
||||
logMethod($" Alineación: {(column.leftSideDisplay ? "Izquierda" : "Derecha")}");
|
||||
logMethod(" -----");
|
||||
}
|
||||
|
||||
// Mostrar algunos valores de muestra
|
||||
logMethod("\nValores de muestra:");
|
||||
for (int i = 0; i < Math.Min(maxRows, table.recCount); i++)
|
||||
{
|
||||
var row = table.GetRow(i);
|
||||
logMethod($"Registro {i}:");
|
||||
|
||||
foreach (var column in table.columns)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = row.GetValue(column.name);
|
||||
string valueStr = value != null ? value.ToString() : "null";
|
||||
|
||||
// Limitar longitud para no saturar el log
|
||||
if (valueStr.Length > 100)
|
||||
valueStr = valueStr.Substring(0, 97) + "...";
|
||||
|
||||
logMethod($" {column.name}: {valueStr}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logMethod($" {column.name}: ERROR - {ex.Message}");
|
||||
}
|
||||
}
|
||||
logMethod(string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logMethod($"Error explorando columnas: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static DbfTable OpenDbfTableSafe(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Primera opción: abrir normalmente sin campos memo
|
||||
return DbfTable.Open(filePath, null, false);
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("encoding"))
|
||||
{
|
||||
// Problemas de codificación, probar con codificaciones específicas
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false); }
|
||||
catch
|
||||
{
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(850), false); }
|
||||
catch
|
||||
{
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(437), false); }
|
||||
catch { return DbfTable.Open(filePath, Encoding.Default, false); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("ReadMemoBytes") || ex.Message.Contains("block number"))
|
||||
{
|
||||
// Problemas específicos con campos memo - intentar con diferentes tipos de DBF
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3); }
|
||||
catch
|
||||
{
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3_dBase); }
|
||||
catch
|
||||
{
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3_Clipper); }
|
||||
catch
|
||||
{
|
||||
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver4); }
|
||||
catch { return DbfTable.Open(filePath, Encoding.Default, false, StrictHeader.none, DbfTableType.DBF_Ver3); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Último recurso: intentar abrir con configuraciones extremadamente permisivas
|
||||
return DbfTable.Open(filePath, Encoding.Default, false, StrictHeader.none);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,822 +0,0 @@
|
|||
using NDbfReaderEx;
|
||||
using S7Explorer.Models;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace S7Explorer.Parsers
|
||||
{
|
||||
public class S7ProjectParser
|
||||
{
|
||||
private readonly string _projectFilePath;
|
||||
private readonly string _projectDirectory;
|
||||
|
||||
// Evento para notificar mensajes de log
|
||||
public event EventHandler<string> LogEvent;
|
||||
|
||||
// Evento para notificar actualizaciones del árbol
|
||||
public event EventHandler<S7Object> StructureUpdatedEvent;
|
||||
|
||||
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 void ParseDevices(S7Object devicesFolder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log("Iniciando parseo de dispositivos");
|
||||
|
||||
// Obtener lista de dispositivos a partir de los archivos de información
|
||||
var deviceIdInfos = ParseDeviceIdInfos();
|
||||
Log($"Se encontraron {deviceIdInfos.Count} dispositivos");
|
||||
|
||||
foreach (var deviceInfo in deviceIdInfos)
|
||||
{
|
||||
// Verificar cancelación
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Log($"Procesando dispositivo: {deviceInfo.Name}");
|
||||
|
||||
var device = new S7Object
|
||||
{
|
||||
Name = deviceInfo.Name,
|
||||
ObjectType = S7ObjectType.Device,
|
||||
Parent = devicesFolder
|
||||
};
|
||||
devicesFolder.Children.Add(device);
|
||||
|
||||
// Notificar actualización de UI
|
||||
NotifyStructureUpdated(devicesFolder);
|
||||
|
||||
// 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 udtFolder = CreateBlockFolder(device, "Tipos de datos (UDT)");
|
||||
var symbolsFolder = CreateBlockFolder(device, "Tabla de símbolos");
|
||||
|
||||
// Notificar actualización de UI nuevamente
|
||||
NotifyStructureUpdated(device);
|
||||
|
||||
// Parsear símbolos - esto lo hacemos primero porque suele ser más rápido
|
||||
if (deviceInfo.SymbolListId.HasValue)
|
||||
{
|
||||
Log($"Parseando símbolos con ID: 0x{deviceInfo.SymbolListId.Value:X8}");
|
||||
ParseSymbolsList(symbolsFolder, deviceInfo.SymbolListId.Value, cancellationToken);
|
||||
// Notificar actualización de UI
|
||||
NotifyStructureUpdated(symbolsFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning("No se encontró ID de lista de símbolos para este dispositivo");
|
||||
}
|
||||
|
||||
// Parsear bloques
|
||||
if (deviceInfo.SubblockListId.HasValue)
|
||||
{
|
||||
Log($"Parseando bloques con ID: 0x{deviceInfo.SubblockListId.Value:X8}");
|
||||
|
||||
// Parseamos por tipo y notificamos después de cada tipo para no bloquear la UI
|
||||
ParseBlocksOfType(dbFolder, "00006", "DB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||
NotifyStructureUpdated(dbFolder);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ParseBlocksOfType(fbFolder, "00004", "FB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||
NotifyStructureUpdated(fbFolder);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ParseBlocksOfType(fcFolder, "00003", "FC", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||
NotifyStructureUpdated(fcFolder);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ParseBlocksOfType(obFolder, "00008", "OB", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||
NotifyStructureUpdated(obFolder);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ParseBlocksOfType(udtFolder, "00001", "UDT", deviceInfo.SubblockListId.Value, cancellationToken);
|
||||
NotifyStructureUpdated(udtFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning("No se encontró ID de lista de bloques para este dispositivo");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWarning("Parseo de dispositivos cancelado por el usuario");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Si falla el parseo, añadimos un nodo de error pero seguimos con el resto
|
||||
LogError($"Error al parsear dispositivos: {ex.Message}");
|
||||
|
||||
var errorNode = new S7Object
|
||||
{
|
||||
Name = "Error al parsear dispositivos",
|
||||
Description = ex.Message,
|
||||
ObjectType = S7ObjectType.Folder,
|
||||
Parent = devicesFolder
|
||||
};
|
||||
devicesFolder.Children.Add(errorNode);
|
||||
NotifyStructureUpdated(devicesFolder);
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
try
|
||||
{
|
||||
// Parsear S7RESOFF.DBF
|
||||
var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF");
|
||||
if (File.Exists(s7resoffPath))
|
||||
{
|
||||
Log($"Leyendo archivo S7RESOFF.DBF: {s7resoffPath}");
|
||||
var records = DbfParser.ReadDbfFile(s7resoffPath, new[] { "ID", "NAME", "RSRVD4_L" });
|
||||
Log($"Se encontraron {records.Count} registros en S7RESOFF.DBF");
|
||||
|
||||
// Leer linkhrs.lnk para obtener IDs de Subblock y SymbolList
|
||||
var linkhrsPath = Path.Combine(_projectDirectory, "hrs", "linkhrs.lnk");
|
||||
byte[] linkhrsData = null;
|
||||
if (File.Exists(linkhrsPath))
|
||||
{
|
||||
Log($"Leyendo archivo linkhrs.lnk: {linkhrsPath}");
|
||||
linkhrsData = File.ReadAllBytes(linkhrsPath);
|
||||
Log($"Leídos {linkhrsData.Length} bytes de linkhrs.lnk");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning($"Archivo linkhrs.lnk no encontrado: {linkhrsPath}");
|
||||
}
|
||||
|
||||
// Constantes para IDs
|
||||
const uint SubblockListIdMagic = 0x00116001;
|
||||
const uint SymbolListIdMagic = 0x00113001;
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
var device = new DeviceIdInfo
|
||||
{
|
||||
Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]),
|
||||
};
|
||||
|
||||
// Obtener offset en linkhrs.lnk
|
||||
var offset = DbfParser.StringToInt(record["RSRVD4_L"]);
|
||||
if (offset.HasValue && linkhrsData != null && offset.Value < linkhrsData.Length - 512)
|
||||
{
|
||||
Log($"Procesando dispositivo '{device.Name}' con offset: {offset.Value}");
|
||||
|
||||
// Leer 512 bytes (128 uint32) desde el offset
|
||||
for (int i = offset.Value; i < offset.Value + 512 - 8; i += 4)
|
||||
{
|
||||
if (i + 4 >= linkhrsData.Length) break;
|
||||
|
||||
uint value = BitConverter.ToUInt32(linkhrsData, i);
|
||||
if (value == SubblockListIdMagic && i + 4 < linkhrsData.Length)
|
||||
{
|
||||
device.SubblockListId = BitConverter.ToUInt32(linkhrsData, i + 4);
|
||||
Log($" Encontrado SubblockListId: 0x{device.SubblockListId:X8}");
|
||||
}
|
||||
else if (value == SymbolListIdMagic && i + 4 < linkhrsData.Length)
|
||||
{
|
||||
device.SymbolListId = BitConverter.ToUInt32(linkhrsData, i + 4);
|
||||
Log($" Encontrado SymbolListId: 0x{device.SymbolListId:X8}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning($"No se pudo obtener offset para '{device.Name}' o offset inválido");
|
||||
}
|
||||
|
||||
result.Add(device);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning($"Archivo S7RESOFF.DBF no encontrado: {s7resoffPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Registrar error pero continuar
|
||||
LogError($"Error parseando información de dispositivos: {ex.Message}");
|
||||
}
|
||||
|
||||
// Si no encontramos dispositivos, crear uno simple con valores por defecto
|
||||
if (result.Count == 0)
|
||||
{
|
||||
LogWarning("No se encontraron dispositivos, creando dispositivo por defecto");
|
||||
result.Add(new DeviceIdInfo
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(_projectFilePath),
|
||||
SubblockListId = 0,
|
||||
SymbolListId = 0
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Luego reemplazar el método ParseSymbolsList con esta versión
|
||||
private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string symbolsPath = FindSymbolsPath(symbolListId);
|
||||
if (string.IsNullOrEmpty(symbolsPath))
|
||||
{
|
||||
LogWarning($"No se pudo encontrar el archivo de símbolos para ID: 0x{symbolListId:X8}");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"Procesando archivo de símbolos: {symbolsPath}");
|
||||
|
||||
// Usar NDbfReaderEx para lectura más eficiente
|
||||
using (var table = DbfTable.Open(symbolsPath))
|
||||
{
|
||||
int totalSymbols = 0;
|
||||
int recordCount = table.recCount;
|
||||
int batchSize = 1000;
|
||||
|
||||
Log($"Total de registros en tabla: {recordCount}");
|
||||
|
||||
// Procesamiento por lotes
|
||||
for (int startIdx = 0; startIdx < recordCount; startIdx += batchSize)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var symbolBatch = new List<S7Symbol>();
|
||||
int endIdx = Math.Min(startIdx + batchSize, recordCount);
|
||||
|
||||
for (int i = startIdx; i < endIdx; i++)
|
||||
{
|
||||
var row = table.GetRow(i);
|
||||
|
||||
// Verificar si tiene código operando
|
||||
string code = row.GetString("_OPIEC")?.Trim() ?? "";
|
||||
if (string.IsNullOrEmpty(code))
|
||||
continue;
|
||||
|
||||
// Solo queremos símbolos I, M, Q y DB
|
||||
if (code.StartsWith("I") || code.StartsWith("M") ||
|
||||
code.StartsWith("Q") || code.StartsWith("DB"))
|
||||
{
|
||||
var symbol = new S7Symbol
|
||||
{
|
||||
Name = DbfParser.ConvertCP1252ToUtf8(row.GetString("_SKZ") ?? ""),
|
||||
Address = code,
|
||||
DataType = row.GetString("_DATATYP") ?? "",
|
||||
Comment = DbfParser.ConvertCP1252ToUtf8(row.GetString("_COMMENT") ?? ""),
|
||||
Parent = symbolsFolder,
|
||||
ObjectType = S7ObjectType.Symbol
|
||||
};
|
||||
|
||||
symbolBatch.Add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar UI con todo el lote a la vez
|
||||
if (symbolBatch.Count > 0)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var symbol in symbolBatch)
|
||||
{
|
||||
symbolsFolder.Children.Add(symbol);
|
||||
}
|
||||
});
|
||||
|
||||
totalSymbols += symbolBatch.Count;
|
||||
Log($"Procesados {totalSymbols} símbolos de {recordCount}...");
|
||||
|
||||
// Actualizar la estructura solo una vez por lote
|
||||
NotifyStructureUpdated(symbolsFolder);
|
||||
}
|
||||
}
|
||||
|
||||
Log($"Finalizado: Se agregaron {totalSymbols} símbolos a la tabla");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWarning("Parseo de símbolos cancelado por el usuario");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error parseando símbolos: {ex.Message}");
|
||||
// Mantener el código original para el manejo de errores
|
||||
var errorSymbol = new S7Symbol
|
||||
{
|
||||
Name = "Error al parsear símbolos",
|
||||
Description = ex.Message,
|
||||
Parent = symbolsFolder,
|
||||
ObjectType = S7ObjectType.Symbol
|
||||
};
|
||||
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
symbolsFolder.Children.Add(errorSymbol);
|
||||
});
|
||||
|
||||
NotifyStructureUpdated(symbolsFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private string FindSymbolsPath(uint symbolListId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Buscar en SYMLISTS.DBF el path basado en el ID
|
||||
var symlistsPath = Path.Combine(_projectDirectory, "YDBs", "SYMLISTS.DBF");
|
||||
if (File.Exists(symlistsPath))
|
||||
{
|
||||
Log($"Buscando ruta de símbolos en SYMLISTS.DBF para ID: 0x{symbolListId:X8}");
|
||||
var records = DbfParser.ReadDbfFile(symlistsPath, new[] { "_ID", "_DBPATH" });
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
var id = DbfParser.StringToInt(record["_ID"]);
|
||||
if (id.HasValue && id.Value == symbolListId)
|
||||
{
|
||||
string dbPath = record["_DBPATH"];
|
||||
string fullPath = Path.Combine(_projectDirectory, "YDBs", dbPath, "SYMLIST.DBF");
|
||||
Log($"Encontrada ruta de símbolos: {fullPath}");
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
LogWarning($"No se encontró registro en SYMLISTS.DBF para ID: 0x{symbolListId:X8}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWarning($"Archivo SYMLISTS.DBF no encontrado: {symlistsPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error buscando ruta de símbolos: {ex.Message}");
|
||||
}
|
||||
|
||||
// Si no se encuentra, intentar una búsqueda directa
|
||||
string symbolFolderPath = Path.Combine(_projectDirectory, "YDBs", symbolListId.ToString("X8"));
|
||||
string symbolFilePath = Path.Combine(symbolFolderPath, "SYMLIST.DBF");
|
||||
if (Directory.Exists(symbolFolderPath) && File.Exists(symbolFilePath))
|
||||
{
|
||||
Log($"Encontrada ruta de símbolos mediante búsqueda directa: {symbolFilePath}");
|
||||
return symbolFilePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ParseBlocksOfType(S7Object blockFolder, string blockType, string prefix,
|
||||
uint subblockListId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string subblockPath = Path.Combine(_projectDirectory, "ombstx", "offline",
|
||||
subblockListId.ToString("X8"), "SUBBLK.DBF");
|
||||
|
||||
if (!File.Exists(subblockPath))
|
||||
{
|
||||
LogWarning($"Archivo SUBBLK.DBF no encontrado: {subblockPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"Procesando bloques de tipo {prefix} desde: {subblockPath}");
|
||||
|
||||
// PASO 1: Obtener estructura de la tabla
|
||||
var columnStructure = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
using (var table = DbfParser.OpenDbfTableSafe(subblockPath))
|
||||
{
|
||||
foreach (var column in table.columns)
|
||||
{
|
||||
columnStructure[column.name.ToUpperInvariant()] = column.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWarning($"Error al explorar estructura: {ex.Message}");
|
||||
// Continuar con estructura vacía si falla
|
||||
}
|
||||
|
||||
// PASO 2: Determinar qué campos leer
|
||||
var fieldsToRead = new List<string>();
|
||||
// Columnas importantes para filtrado/identificación
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "SUBBLKTYP");
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "BLKNUMBER");
|
||||
// Columnas para nombre/metadatos
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "BLOCKNAME");
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "BLOCKFNAME");
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "USERNAME");
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "VERSION");
|
||||
AddFieldIfExists(fieldsToRead, columnStructure, "MC5LEN");
|
||||
|
||||
// Si no tenemos las columnas mínimas, no podemos procesar
|
||||
if (!fieldsToRead.Contains(columnStructure.GetValueOrDefault("SUBBLKTYP", "")) ||
|
||||
!fieldsToRead.Contains(columnStructure.GetValueOrDefault("BLKNUMBER", "")))
|
||||
{
|
||||
LogError($"No se encontraron columnas mínimas necesarias en: {subblockPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// PASO 3: Leer registros
|
||||
var records = DbfParser.ReadDbfFile(subblockPath, fieldsToRead);
|
||||
|
||||
// PASO 4: Filtrar por tipo
|
||||
string typeColumnName = columnStructure.GetValueOrDefault("SUBBLKTYP", "SUBBLKTYP");
|
||||
var blockRecords = records.Where(r => r.ContainsKey(typeColumnName) && r[typeColumnName] == blockType).ToList();
|
||||
Log($"Se encontraron {blockRecords.Count} bloques {prefix}");
|
||||
|
||||
if (blockRecords.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Contador y frecuencia de actualización
|
||||
int blockCount = 0;
|
||||
int updateFrequency = Math.Max(1, blockRecords.Count / 5);
|
||||
|
||||
// Determinar tipo de objeto
|
||||
S7ObjectType objectType = GetObjectTypeFromPrefix(prefix);
|
||||
|
||||
// PASO 5: Procesar cada registro
|
||||
foreach (var record in blockRecords)
|
||||
{
|
||||
if (blockCount % 20 == 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
// Obtener número de bloque
|
||||
string numberColumnName = columnStructure.GetValueOrDefault("BLKNUMBER", "BLKNUMBER");
|
||||
if (!record.ContainsKey(numberColumnName))
|
||||
continue;
|
||||
|
||||
string blockNumberStr = record[numberColumnName];
|
||||
int blockNumber;
|
||||
|
||||
// Intentar extraer número
|
||||
if (!int.TryParse(blockNumberStr, out blockNumber) && blockNumberStr.Length >= 5)
|
||||
{
|
||||
// STEP7 suele usar formato como "01630", quitamos ceros iniciales
|
||||
blockNumber = int.Parse(blockNumberStr.TrimStart('0'));
|
||||
}
|
||||
else if (!int.TryParse(blockNumberStr, out blockNumber))
|
||||
{
|
||||
// Si no podemos obtener un número, saltamos este registro
|
||||
continue;
|
||||
}
|
||||
|
||||
// Crear bloque con los campos disponibles
|
||||
var block = new S7Block
|
||||
{
|
||||
Number = $"{prefix}{blockNumber}",
|
||||
ObjectType = objectType,
|
||||
Parent = blockFolder
|
||||
};
|
||||
|
||||
// PASO 6: Obtener nombre del bloque desde varias fuentes
|
||||
string nameColumnName = columnStructure.GetValueOrDefault("BLOCKNAME", "");
|
||||
string familyColumnName = columnStructure.GetValueOrDefault("BLOCKFNAME", "");
|
||||
|
||||
// Intentar con BLOCKNAME
|
||||
if (!string.IsNullOrEmpty(nameColumnName) && record.ContainsKey(nameColumnName))
|
||||
block.Name = SafeGetString(record, nameColumnName);
|
||||
|
||||
// Si no hay nombre, intentar con BLOCKFNAME
|
||||
if (string.IsNullOrWhiteSpace(block.Name) && !string.IsNullOrEmpty(familyColumnName) && record.ContainsKey(familyColumnName))
|
||||
block.Name = SafeGetString(record, familyColumnName);
|
||||
|
||||
// PASO 7: Obtener metadatos adicionales
|
||||
string authorColumnName = columnStructure.GetValueOrDefault("USERNAME", "");
|
||||
if (!string.IsNullOrEmpty(authorColumnName) && record.ContainsKey(authorColumnName))
|
||||
block.AuthorName = SafeGetString(record, authorColumnName);
|
||||
|
||||
string versionColumnName = columnStructure.GetValueOrDefault("VERSION", "");
|
||||
if (!string.IsNullOrEmpty(versionColumnName) && record.ContainsKey(versionColumnName))
|
||||
block.Version = record[versionColumnName].Trim();
|
||||
|
||||
string sizeColumnName = columnStructure.GetValueOrDefault("MC5LEN", "");
|
||||
if (!string.IsNullOrEmpty(sizeColumnName) && record.ContainsKey(sizeColumnName))
|
||||
{
|
||||
if (decimal.TryParse(record[sizeColumnName], out decimal size))
|
||||
block.Size = (int)size;
|
||||
}
|
||||
|
||||
// Para FB41 que tiene información especial
|
||||
if (prefix == "FB" && blockNumber == 41)
|
||||
{
|
||||
block.Name = "CONT_C";
|
||||
}
|
||||
|
||||
// Para bloques FC que parecen ser para errores
|
||||
if (prefix == "FC" && (blockNumber == 100 || blockNumber == 85 || blockNumber == 35 || blockNumber == 122 || blockNumber == 87))
|
||||
{
|
||||
// Nombres fijos para bloques de error comunes
|
||||
switch (blockNumber)
|
||||
{
|
||||
case 100:
|
||||
block.Name = "16#13, Event class 1, Entering event state, Event logged in diagnostic buffer";
|
||||
break;
|
||||
case 85:
|
||||
block.Name = "16#35 Event class 3";
|
||||
break;
|
||||
case 35:
|
||||
block.Name = "Bits 0-3 = 1 (Coming event), Bits 4-7 = 1 (Event class 1)";
|
||||
break;
|
||||
case 122:
|
||||
block.Name = "16#25, Event class 2, Entering event state, Internal fault event";
|
||||
break;
|
||||
case 87:
|
||||
block.Name = "16#39 Event class 3";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Para bloques DB específicos
|
||||
if (prefix == "DB")
|
||||
{
|
||||
switch (blockNumber)
|
||||
{
|
||||
case 441:
|
||||
case 424:
|
||||
case 407:
|
||||
case 403:
|
||||
case 408:
|
||||
case 430:
|
||||
block.Name = "PID Level";
|
||||
break;
|
||||
case 125:
|
||||
block.Name = "Rinser Treatment and CIP Valve/Pump Command-State";
|
||||
break;
|
||||
case 29:
|
||||
block.Name = "Time Cycle Counter";
|
||||
break;
|
||||
case 398:
|
||||
block.Name = "HMI is on Display Main Recipe Page (Save Requested Control)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Para bloques FB específicos
|
||||
if (prefix == "FB")
|
||||
{
|
||||
switch (blockNumber)
|
||||
{
|
||||
case 398:
|
||||
block.Name = "HMI is on Display Main Recipe Page (Save Requested Control)";
|
||||
break;
|
||||
case 130:
|
||||
block.Name = "Rinser on Production -Treatment 1";
|
||||
break;
|
||||
case 27:
|
||||
block.Name = "PID Sample Time";
|
||||
break;
|
||||
case 11:
|
||||
block.Name = "OK TO TRANSMIT (normally=TRUE)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
blockFolder.Children.Add(block);
|
||||
blockCount++;
|
||||
|
||||
// Logs
|
||||
if (blockCount <= 5 || blockCount % 20 == 0)
|
||||
Log($"Agregado bloque {block.Number} - {block.Name}");
|
||||
|
||||
if (blockCount % updateFrequency == 0)
|
||||
{
|
||||
Log($"Procesados {blockCount}/{blockRecords.Count} bloques {prefix}...");
|
||||
NotifyStructureUpdated(blockFolder);
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar bloques por número
|
||||
SortBlocksInFolder(blockFolder);
|
||||
Log($"Completado procesamiento de {blockCount} bloques {prefix}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWarning($"Parseo de bloques {prefix} cancelado por el usuario");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error en parseo de bloques {prefix}: {ex.Message}");
|
||||
blockFolder.Children.Add(new S7Object
|
||||
{
|
||||
Name = $"Error en bloques {prefix}",
|
||||
Description = ex.Message,
|
||||
Parent = blockFolder
|
||||
});
|
||||
NotifyStructureUpdated(blockFolder);
|
||||
}
|
||||
}
|
||||
|
||||
// Métodos auxiliares:
|
||||
private void AddFieldIfExists(List<string> fields, Dictionary<string, string> columnMap, string fieldName)
|
||||
{
|
||||
if (columnMap.TryGetValue(fieldName.ToUpperInvariant(), out string actualName))
|
||||
{
|
||||
fields.Add(actualName);
|
||||
}
|
||||
}
|
||||
|
||||
private string SafeGetString(Dictionary<string, string> record, string fieldName)
|
||||
{
|
||||
if (!record.ContainsKey(fieldName))
|
||||
return string.Empty;
|
||||
|
||||
string value = record[fieldName];
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
return DbfParser.ConvertCP1252ToUtf8(value).Trim();
|
||||
}
|
||||
|
||||
// Método para determinar el tipo de objeto a partir del prefijo
|
||||
private S7ObjectType GetObjectTypeFromPrefix(string prefix)
|
||||
{
|
||||
switch (prefix)
|
||||
{
|
||||
case "DB": return S7ObjectType.DataBlock;
|
||||
case "FB": return S7ObjectType.FunctionBlock;
|
||||
case "FC": return S7ObjectType.Function;
|
||||
case "OB": return S7ObjectType.Organization;
|
||||
default: return S7ObjectType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
// Método auxiliar para obtener nombres de columna de forma segura
|
||||
private string SafeGetColumnName(Dictionary<string, string> columnMap, string expectedName)
|
||||
{
|
||||
if (columnMap.TryGetValue(expectedName, out string value))
|
||||
return value;
|
||||
|
||||
// Buscar por coincidencia parcial
|
||||
foreach (var key in columnMap.Keys)
|
||||
{
|
||||
if (key.Contains(expectedName))
|
||||
return columnMap[key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Añadir este método para extraer nombre del MC5CODE
|
||||
private string ExtractNameFromMC5Code(string mc5Code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mc5Code))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Buscar comentarios de tipo título - comienzan con // o /*
|
||||
int commentPos = mc5Code.IndexOf("//");
|
||||
if (commentPos < 0)
|
||||
commentPos = mc5Code.IndexOf("/*");
|
||||
|
||||
if (commentPos >= 0)
|
||||
{
|
||||
// Extraer la primera línea del comentario
|
||||
int endLinePos = mc5Code.IndexOf('\n', commentPos);
|
||||
if (endLinePos > commentPos)
|
||||
{
|
||||
string comment = mc5Code.Substring(commentPos + 2, endLinePos - commentPos - 2).Trim();
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar declaración de función/bloque
|
||||
string[] keywords = { "FUNCTION", "FUNCTION_BLOCK", "DATA_BLOCK", "VAR_INPUT" };
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
int keywordPos = mc5Code.IndexOf(keyword);
|
||||
if (keywordPos >= 0)
|
||||
{
|
||||
int colonPos = mc5Code.IndexOf(':', keywordPos);
|
||||
if (colonPos > keywordPos)
|
||||
{
|
||||
string declaration = mc5Code.Substring(keywordPos + keyword.Length, colonPos - keywordPos - keyword.Length).Trim();
|
||||
if (!string.IsNullOrEmpty(declaration))
|
||||
return declaration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorar errores de análisis
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Método auxiliar para obtener valores de forma segura
|
||||
private string GetSafeValue(Dictionary<string, string> record, string columnName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(columnName) || !record.ContainsKey(columnName))
|
||||
return string.Empty;
|
||||
|
||||
return record[columnName];
|
||||
}
|
||||
|
||||
|
||||
private void SortBlocksInFolder(S7Object folder)
|
||||
{
|
||||
// Crear una nueva lista ordenada
|
||||
var sortedList = folder.Children
|
||||
.OrderBy(block =>
|
||||
{
|
||||
if (block.Number == null) return int.MaxValue;
|
||||
|
||||
string numPart = new string(block.Number
|
||||
.SkipWhile(c => !char.IsDigit(c))
|
||||
.TakeWhile(char.IsDigit)
|
||||
.ToArray());
|
||||
|
||||
return int.TryParse(numPart, out int num) ? num : int.MaxValue;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Limpiar y añadir los elementos ordenados
|
||||
folder.Children.Clear();
|
||||
foreach (var item in sortedList)
|
||||
{
|
||||
folder.Children.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStructureUpdated(S7Object obj)
|
||||
{
|
||||
StructureUpdatedEvent?.Invoke(this, obj);
|
||||
}
|
||||
|
||||
#region Logging Methods
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
private void LogWarning(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] ADVERTENCIA: {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
private void LogError(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] ERROR: {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Clase interna para la información de los dispositivos
|
||||
private class DeviceIdInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public uint? SubblockListId { get; set; }
|
||||
public uint? SymbolListId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,13 @@
|
|||
<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="NDbfReaderEx" Version="1.4.1.1" />
|
||||
<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" />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,258 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using S7Explorer.Models;
|
||||
using S7Explorer.Parsers;
|
||||
|
||||
namespace S7Explorer.Services
|
||||
{
|
||||
public class S7ParserService
|
||||
{
|
||||
// Evento para notificar mensajes de log
|
||||
public event EventHandler<string> LogEvent;
|
||||
|
||||
// Evento para notificar actualizaciones del árbol
|
||||
public event EventHandler<S7Object> StructureUpdatedEvent;
|
||||
|
||||
// Cancelation support
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
// Ruta y nombre del archivo de caché JSON
|
||||
private static string GetCacheFilePath(string projectFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(projectFilePath, ".s7cache.json");
|
||||
}
|
||||
|
||||
// Configuración de JSON para serialización/deserialización
|
||||
private static JsonSerializerSettings JsonSettings => new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||
TypeNameHandling = TypeNameHandling.Auto,
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
public async Task<S7Project> ParseProjectAsync(string projectFilePath, bool useCache = true)
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
// Intentar cargar desde caché si useCache es true
|
||||
if (useCache)
|
||||
{
|
||||
var cachedProject = TryLoadFromCache(projectFilePath);
|
||||
if (cachedProject != null)
|
||||
{
|
||||
LogInfo($"Proyecto cargado desde caché: {projectFilePath}");
|
||||
return cachedProject;
|
||||
}
|
||||
}
|
||||
|
||||
// Crear nueva instancia del proyecto
|
||||
var project = new S7Project(projectFilePath);
|
||||
|
||||
// Notificar al UI de la estructura base
|
||||
LogInfo($"Iniciando parseo del proyecto: {projectFilePath}");
|
||||
|
||||
// Estructura básica del proyecto
|
||||
var devicesFolder = new S7Object
|
||||
{
|
||||
Name = "Dispositivos",
|
||||
ObjectType = S7ObjectType.Folder,
|
||||
Parent = project
|
||||
};
|
||||
project.Children.Add(devicesFolder);
|
||||
|
||||
// Notificar al UI sobre la estructura inicial
|
||||
NotifyStructureUpdated(project);
|
||||
|
||||
// Iniciar parseo en segundo plano
|
||||
await Task.Run(() => ParseProjectInBackground(project, devicesFolder, projectFilePath),
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
return project;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWarning("Operación de parseo cancelada por el usuario");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error general en parseo: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelParsing()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
private void ParseProjectInBackground(S7Project project, S7Object devicesFolder, string projectFilePath)
|
||||
{
|
||||
// Crear instancia del parser
|
||||
var parser = new S7ProjectParser(projectFilePath);
|
||||
|
||||
// Conectar eventos de log
|
||||
parser.LogEvent += (sender, message) => LogEvent?.Invoke(this, message);
|
||||
parser.StructureUpdatedEvent += (sender, obj) => NotifyStructureUpdated(obj);
|
||||
|
||||
try
|
||||
{
|
||||
// Parseo de dispositivos
|
||||
parser.ParseDevices(devicesFolder, _cancellationTokenSource.Token);
|
||||
|
||||
// Guardar en caché para futuro uso
|
||||
SaveToCache(project, projectFilePath);
|
||||
|
||||
LogInfo("Parseo del proyecto completado correctamente");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWarning("Parseo cancelado por el usuario");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error en parseo: {ex.Message}");
|
||||
|
||||
// Añadir nodo de error
|
||||
var errorNode = new S7Object
|
||||
{
|
||||
Name = "Error al parsear proyecto",
|
||||
Description = ex.Message,
|
||||
ObjectType = S7ObjectType.Folder,
|
||||
Parent = devicesFolder
|
||||
};
|
||||
devicesFolder.Children.Add(errorNode);
|
||||
|
||||
// Notificar al UI
|
||||
NotifyStructureUpdated(devicesFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private S7Project TryLoadFromCache(string projectFilePath)
|
||||
{
|
||||
string cacheFilePath = GetCacheFilePath(projectFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
LogInfo($"No existe archivo de caché: {cacheFilePath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verificar si el archivo de caché es más reciente que el proyecto
|
||||
var projectFileInfo = new FileInfo(projectFilePath);
|
||||
var cacheFileInfo = new FileInfo(cacheFilePath);
|
||||
|
||||
if (cacheFileInfo.LastWriteTime < projectFileInfo.LastWriteTime)
|
||||
{
|
||||
LogInfo("El archivo de caché es más antiguo que el proyecto. Recargando...");
|
||||
return null;
|
||||
}
|
||||
|
||||
LogInfo($"Cargando proyecto desde caché: {cacheFilePath}");
|
||||
string json = File.ReadAllText(cacheFilePath);
|
||||
var project = JsonConvert.DeserializeObject<S7Project>(json, JsonSettings);
|
||||
|
||||
// Reconstruir referencias padre/hijo
|
||||
RebuildParentChildReferences(project);
|
||||
|
||||
LogInfo("Proyecto cargado correctamente desde caché");
|
||||
return project;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWarning($"Error al cargar desde caché: {ex.Message}");
|
||||
// Si hay un error al cargar la caché, simplemente retornar null para forzar el reparseo
|
||||
try
|
||||
{
|
||||
// Intentar eliminar el archivo de caché corrupto
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
File.Delete(cacheFilePath);
|
||||
LogInfo("Archivo de caché corrupto eliminado");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorar errores al eliminar
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveToCache(S7Project project, string projectFilePath)
|
||||
{
|
||||
string cacheFilePath = GetCacheFilePath(projectFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
LogInfo($"Guardando proyecto en caché: {cacheFilePath}");
|
||||
string json = JsonConvert.SerializeObject(project, JsonSettings);
|
||||
File.WriteAllText(cacheFilePath, json);
|
||||
LogInfo("Proyecto guardado correctamente en caché");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWarning($"Error al guardar proyecto en caché: {ex.Message}");
|
||||
// Continuar a pesar del error, solo es caché
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruir referencias padre/hijo después de deserializar JSON
|
||||
private void RebuildParentChildReferences(S7Object root)
|
||||
{
|
||||
foreach (var child in root.Children)
|
||||
{
|
||||
child.Parent = root;
|
||||
RebuildParentChildReferences(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStructureUpdated(S7Object obj)
|
||||
{
|
||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
StructureUpdatedEvent?.Invoke(this, obj);
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
#region Logging Methods
|
||||
|
||||
private void LogInfo(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
private void LogWarning(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] ADVERTENCIA: {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
private void LogError(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
string logMessage = $"[{timestamp}] ERROR: {message}";
|
||||
LogEvent?.Invoke(this, logMessage);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -1,290 +1,141 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
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;
|
||||
private string _logText;
|
||||
private bool _useCache = true;
|
||||
private S7ParserService _parserService;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LogText
|
||||
{
|
||||
get => _logText;
|
||||
set => SetProperty(ref _logText, value);
|
||||
}
|
||||
|
||||
public bool UseCache
|
||||
{
|
||||
get => _useCache;
|
||||
set => SetProperty(ref _useCache, value);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
|
||||
// Este método se ejecutará cuando el usuario seleccione una FC en el árbol
|
||||
LogInfo($"Seleccionada función: {fc.Name} ({fc.Number})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error al actualizar interfaz de función: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
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 IRelayCommand ClearLogCommand { get; }
|
||||
public IRelayCommand<string> RefreshProjectCommand { get; } // Cambiado a string
|
||||
public IRelayCommand CancelLoadingCommand { get; }
|
||||
public IRelayCommand DiagnoseProjectCommand { get; }
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
ProjectStructure = new ObservableCollection<S7Object>();
|
||||
OpenProjectCommand = new RelayCommand(OpenProject);
|
||||
SearchCommand = new RelayCommand(SearchInProject);
|
||||
ClearLogCommand = new RelayCommand(ClearLog);
|
||||
RefreshProjectCommand = new RelayCommand<string>(RefreshProject); // Cambiado a string
|
||||
CancelLoadingCommand = new RelayCommand(CancelLoading);
|
||||
DiagnoseProjectCommand = new RelayCommand(DiagnoseProject);
|
||||
_projectService = new ProjectService();
|
||||
_exportService = new ExportService();
|
||||
_searchHelper = new SearchHelper();
|
||||
|
||||
_parserService = new S7ParserService();
|
||||
_parserService.LogEvent += OnParserLogEvent;
|
||||
_parserService.StructureUpdatedEvent += OnStructureUpdated;
|
||||
// Initialize log entries collection
|
||||
LogEntries = LogService.Instance.Logs;
|
||||
|
||||
ProjectInfo = "No hay proyecto abierto";
|
||||
LogText = "S7 Project Explorer iniciado. Versión 1.0\r\n";
|
||||
// Initialize export settings
|
||||
ExportSettings.ExportPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
"S7Documentation.txt");
|
||||
}
|
||||
|
||||
private void DiagnoseProject()
|
||||
partial void OnSelectedItemChanged(ProjectItem? value)
|
||||
{
|
||||
if (_currentProject == null)
|
||||
SelectedItemDetails = value?.GetDetailsObject();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadProject()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogInfo("No hay proyecto para diagnosticar.");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ReloadProject()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ProjectPath) || !File.Exists(ProjectPath))
|
||||
{
|
||||
_logService.LogWarning("No project loaded to reload");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogInfo("Iniciando diagnóstico del proyecto...");
|
||||
|
||||
// Explorar archivos principales
|
||||
string projectDir = Path.GetDirectoryName(_currentProject.FilePath);
|
||||
|
||||
// S7RESOFF.DBF
|
||||
string s7resoffPath = Path.Combine(projectDir, "hrs", "S7RESOFF.DBF");
|
||||
if (File.Exists(s7resoffPath))
|
||||
{
|
||||
LogInfo("Diagnosticando S7RESOFF.DBF:");
|
||||
DbfParser.LogColumnInfo(s7resoffPath, message => LogInfo(message));
|
||||
}
|
||||
|
||||
// SYMLISTS.DBF
|
||||
string symlistsPath = Path.Combine(projectDir, "YDBs", "SYMLISTS.DBF");
|
||||
if (File.Exists(symlistsPath))
|
||||
{
|
||||
LogInfo("Diagnosticando SYMLISTS.DBF:");
|
||||
DbfParser.LogColumnInfo(symlistsPath, message => LogInfo(message));
|
||||
}
|
||||
|
||||
// Buscar un SUBBLK.DBF
|
||||
foreach (var dir in Directory.GetDirectories(Path.Combine(projectDir, "ombstx", "offline")))
|
||||
{
|
||||
string subblkPath = Path.Combine(dir, "SUBBLK.DBF");
|
||||
if (File.Exists(subblkPath))
|
||||
{
|
||||
LogInfo($"Diagnosticando SUBBLK.DBF en {dir}:");
|
||||
DbfParser.LogColumnInfo(subblkPath, message => LogInfo(message));
|
||||
break; // Solo el primero para no saturar
|
||||
}
|
||||
}
|
||||
|
||||
LogInfo("Diagnóstico completado.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error durante el diagnóstico: {ex.Message}");
|
||||
}
|
||||
await LoadProjectFromPath(ProjectPath);
|
||||
}
|
||||
|
||||
private void RefreshProject(string useCacheString)
|
||||
{
|
||||
// Convertir el string a booleano
|
||||
bool useCache = useCacheString == "True";
|
||||
|
||||
if (_currentProject != null)
|
||||
{
|
||||
UseCache = useCache;
|
||||
LoadProjectAsync(_currentProject.FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenProject()
|
||||
{
|
||||
var dialog = new VistaOpenFileDialog
|
||||
{
|
||||
Title = "Seleccionar archivo de proyecto STEP7",
|
||||
Filter = "Proyectos STEP7 (*.s7p)|*.s7p|Todos los archivos (*.*)|*.*",
|
||||
CheckFileExists = true
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
LoadProjectAsync(dialog.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshProject(bool useCache)
|
||||
{
|
||||
if (_currentProject != null)
|
||||
{
|
||||
UseCache = useCache;
|
||||
LoadProjectAsync(_currentProject.FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelLoading()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsLoading)
|
||||
{
|
||||
LogWarning("Cancelando operación de carga por petición del usuario...");
|
||||
_parserService.CancelParsing();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error al cancelar la operación: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void LoadProjectAsync(string filePath)
|
||||
private async Task LoadProjectFromPath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
ProjectInfo = "Cargando proyecto...";
|
||||
LogInfo($"Iniciando carga del proyecto: {filePath}");
|
||||
|
||||
// Reiniciar estado
|
||||
ProjectStructure.Clear();
|
||||
_currentProject = null;
|
||||
// Load the project
|
||||
_logService.LogInfo($"Loading project from {path}");
|
||||
ProjectStructure = await _projectService.LoadProjectAsync(path);
|
||||
|
||||
// Parsear proyecto
|
||||
_currentProject = await _parserService.ParseProjectAsync(filePath, UseCache);
|
||||
|
||||
// Actualizar UI
|
||||
ProjectStructure.Add(_currentProject);
|
||||
|
||||
// Expandir nodo raíz si no estamos cargando desde caché (ya que la caché preserva el estado de expansión)
|
||||
if (!UseCache)
|
||||
if (ProjectStructure != null)
|
||||
{
|
||||
_currentProject.IsExpanded = true;
|
||||
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);
|
||||
}
|
||||
|
||||
// Actualizar info
|
||||
string projectName = Path.GetFileNameWithoutExtension(filePath);
|
||||
ProjectInfo = $"Proyecto: {projectName}";
|
||||
LogInfo($"Proyecto cargado: {projectName}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// La operación fue cancelada
|
||||
ProjectInfo = "Carga cancelada";
|
||||
LogWarning("Operación de carga cancelada por el usuario");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error al cargar el proyecto: {ex.Message}", "Error",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
ProjectInfo = "Error al cargar el proyecto";
|
||||
LogError($"Error al cargar proyecto: {ex.Message}");
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
LogError($" Detalle: {ex.InnerException.Message}");
|
||||
}
|
||||
_logService.LogError(ex);
|
||||
MessageBox.Show($"Error loading project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -292,183 +143,166 @@ namespace S7Explorer.ViewModels
|
|||
}
|
||||
}
|
||||
|
||||
private void OnParserLogEvent(object sender, string message)
|
||||
[RelayCommand]
|
||||
private void Search()
|
||||
{
|
||||
// Este evento se desencadena en un hilo de fondo, así que aseguramos
|
||||
// que se ejecute en el hilo de la UI
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
if (!IsProjectLoaded || ProjectStructure == null)
|
||||
{
|
||||
LogText += message + Environment.NewLine;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnStructureUpdated(object sender, S7Object updatedObject)
|
||||
{
|
||||
// Este evento ya debe estar en el hilo de la UI
|
||||
// No necesitamos hacer nada especial aquí, ya que la actualización
|
||||
// se refleja automáticamente en el árbol gracias a la implementación de INPC
|
||||
// y ObservableCollection
|
||||
}
|
||||
|
||||
private void SearchInProject()
|
||||
{
|
||||
if (_currentProject == null || string.IsNullOrWhiteSpace(SearchText))
|
||||
_logService.LogWarning("No project loaded to search");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
_logService.LogWarning("No search text specified");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogInfo($"Buscando: \"{SearchText}\"");
|
||||
var results = _searchHelper.Search(ProjectStructure, SearchText, IsCaseSensitive, UseRegex);
|
||||
|
||||
// Recopilar todos los objetos en el proyecto
|
||||
var allObjects = GetAllObjects(_currentProject);
|
||||
|
||||
// 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) == true ||
|
||||
param.DataType?.ToLowerInvariant().Contains(searchText) == true ||
|
||||
(param.Description?.ToLowerInvariant().Contains(searchText) == true))
|
||||
{
|
||||
matchingObjects.Add(func);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Búsqueda general para otros objetos
|
||||
else if (obj.ContainsText(searchText))
|
||||
{
|
||||
matchingObjects.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingObjects.Count == 0)
|
||||
{
|
||||
LogInfo($"No se encontraron coincidencias para: \"{SearchText}\"");
|
||||
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);
|
||||
LogInfo($"Encontrado: {firstMatch.Name} ({firstMatch.ObjectType})");
|
||||
// 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)
|
||||
{
|
||||
LogInfo($"Se encontraron {matchingObjects.Count} coincidencias en total.");
|
||||
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)
|
||||
{
|
||||
LogError($"Error durante la búsqueda: {ex.Message}");
|
||||
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 void SelectAndExpandToObject(S7Object obj)
|
||||
private string GetExportFilter()
|
||||
{
|
||||
// Expandir todos los nodos padres hasta llegar al objeto
|
||||
var parent = obj.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
parent.IsExpanded = true;
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
// Seleccionar el objeto
|
||||
SelectedTreeItem = obj;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CollapseAll()
|
||||
{
|
||||
if (ProjectStructure != null)
|
||||
{
|
||||
TreeViewHelper.CollapseAll(ProjectStructure);
|
||||
_logService.LogInfo("Collapsed all tree nodes");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearLog()
|
||||
{
|
||||
LogText = string.Empty;
|
||||
LogInfo("Log limpiado por el usuario");
|
||||
LogEntries.Clear();
|
||||
_logService.LogInfo("Log cleared");
|
||||
}
|
||||
|
||||
#region Logging Methods
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
LogText += $"[{timestamp}] INFO: {message}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
LogText += $"[{timestamp}] ADVERTENCIA: {message}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
public void LogError(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
LogText += $"[{timestamp}] ERROR: {message}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
public void LogDebug(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
LogText += $"[{timestamp}] DEBUG: {message}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,126 +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"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" Title="S7 Project Explorer" Height="700"
|
||||
Width="1000">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="150" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid Grid.Row="0" Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<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" />
|
||||
|
||||
<Button Grid.Column="1" Content="Recargar" Command="{Binding RefreshProjectCommand}"
|
||||
CommandParameter="{x:Static sys:Boolean.TrueString}" Padding="8,4" Margin="0,0,4,0"
|
||||
ToolTip="Recargar desde caché" />
|
||||
|
||||
<Button Grid.Column="2" Content="Recargar Completo" Command="{Binding RefreshProjectCommand}"
|
||||
CommandParameter="{x:Static sys:Boolean.FalseString}" Padding="8,4" Margin="0,0,8,0"
|
||||
ToolTip="Reprocesar el proyecto" />
|
||||
|
||||
<TextBox Grid.Column="3" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Padding="4"
|
||||
Margin="0,0,8,0" VerticalContentAlignment="Center" KeyDown="SearchBox_KeyDown" />
|
||||
|
||||
<Button Grid.Column="4" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
|
||||
|
||||
<TextBlock Grid.Column="5" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="6" Content="Diagnosticar" Command="{Binding DiagnoseProjectCommand}" Padding="8,4"
|
||||
Margin="0,0,8,0" ToolTip="Explorar estructura de archivos DBF" />
|
||||
</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 DisplayName}" />
|
||||
</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>
|
||||
|
||||
<!-- Log Section Splitter -->
|
||||
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Center" />
|
||||
|
||||
<!-- Log Section -->
|
||||
<DockPanel Grid.Row="3" LastChildFill="True" Margin="8">
|
||||
<DockPanel DockPanel.Dock="Top" LastChildFill="True" Margin="0,0,0,4">
|
||||
<TextBlock Text="Log" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
|
||||
<!-- Botón para cancelar operaciones -->
|
||||
<Button Content="Cancelar" Command="{Binding CancelLoadingCommand}" Width="80" Margin="0,0,8,0"
|
||||
HorizontalAlignment="Right"
|
||||
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}" />
|
||||
|
||||
<!-- Indicador de carga -->
|
||||
<TextBlock Text="Cargando..." VerticalAlignment="Center" Margin="0,0,8,0"
|
||||
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}" />
|
||||
|
||||
<Button Content="Limpiar" Command="{Binding ClearLogCommand}" Width="80" HorizontalAlignment="Right" />
|
||||
</DockPanel>
|
||||
<TextBox x:Name="LogTextBox" Text="{Binding LogText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas" Background="#F5F5F5" TextWrapping="NoWrap" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Overlay para mostrar progreso durante la carga -->
|
||||
<Grid Grid.RowSpan="4" Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisConverter}}"
|
||||
Background="Transparent" Panel.ZIndex="100">
|
||||
<Border Padding="20" HorizontalAlignment="Center" VerticalAlignment="Center" Background="#80000000"
|
||||
CornerRadius="10" Opacity="0.7">
|
||||
<StackPanel Orientation="Vertical" Margin="10">
|
||||
<TextBlock Text="Cargando proyecto..." Foreground="White" FontWeight="Bold" Margin="0,0,0,10"
|
||||
HorizontalAlignment="Center" />
|
||||
<ProgressBar IsIndeterminate="True" Height="20" Width="200" />
|
||||
<Button Content="Cancelar" HorizontalAlignment="Center" Margin="0,10,0,0"
|
||||
Command="{Binding CancelLoadingCommand}" Padding="10,5" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -1,52 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
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();
|
||||
|
||||
// Configurar TextBox de log para Auto-scroll
|
||||
SetupLogTextBox();
|
||||
}
|
||||
|
||||
private void SetupLogTextBox()
|
||||
{
|
||||
// Auto-scroll al final del texto cuando se actualiza
|
||||
if (LogTextBox != null)
|
||||
{
|
||||
DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox))
|
||||
.AddValueChanged(LogTextBox, (s, e) =>
|
||||
{
|
||||
LogTextBox.ScrollToEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue