├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Documentation └── Images │ ├── Assistant_Plugins_dark.png │ ├── Assistant_Plugins_light.png │ ├── DeviceGeneration.gif │ ├── FileMenu.png │ ├── MainWindow.png │ ├── PluginUsage.gif │ ├── ProfinetMenu.png │ ├── ProjectMenu.png │ ├── ProjectSettings.png │ ├── ScanProfinet.png │ └── TaskGeneration.gif ├── LICENSE.md ├── OC.Assistant.slnx ├── OC.Assistant ├── App.xaml ├── AssemblyHelper.cs ├── AssemblyInfo.cs ├── Controls │ ├── DependencyInfo.xaml │ ├── DependencyInfo.xaml.cs │ ├── DteSelector.xaml │ ├── DteSelector.xaml.cs │ ├── FileMenu.xaml │ ├── FileMenu.xaml.cs │ ├── HelpMenu.xaml │ ├── HelpMenu.xaml.cs │ ├── NotConnectedOverlay.xaml │ ├── NotConnectedOverlay.xaml.cs │ ├── ProjectStateView.xaml │ ├── ProjectStateView.xaml.cs │ ├── VersionCheck.xaml │ └── VersionCheck.xaml.cs ├── Core │ ├── AppData.cs │ ├── BusyState.cs │ ├── DteSingleThread.cs │ ├── IProjectStateEvents.cs │ ├── IProjectStateSolution.cs │ ├── MessageFilter.cs │ ├── ProjectState.cs │ ├── TcDte.cs │ ├── TcShortcut.cs │ ├── TcSmTreeItemExtension.cs │ ├── TcSmTreeItemSubType.cs │ ├── TcStringExtension.cs │ ├── TcSysManagerExtension.cs │ ├── XmlFile.cs │ └── XmlTags.cs ├── Generator │ ├── EtherCat │ │ ├── EtherCatGenerator.cs │ │ ├── EtherCatInstance.cs │ │ ├── EtherCatTemplate.cs │ │ ├── EtherCatVariable.cs │ │ └── EtherCatVariables.cs │ ├── Generators │ │ ├── DeviceTemplate.cs │ │ ├── Hil.cs │ │ ├── Project.cs │ │ ├── Sil.cs │ │ └── Task.cs │ ├── Menu.xaml │ ├── Menu.xaml.cs │ ├── PouInstance.cs │ ├── Profinet │ │ ├── PlcAddress.cs │ │ ├── ProfinetGenerator.cs │ │ ├── ProfinetParser.cs │ │ ├── ProfinetVariable.cs │ │ ├── SafetyModule.cs │ │ └── SafetyProgram.cs │ ├── Settings.xaml │ ├── Settings.xaml.cs │ ├── SettingsPlcDropdown.cs │ ├── SettingsTaskDropdown.cs │ ├── Tags.cs │ └── XmlFile.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── OC.Assistant.csproj ├── Plugins │ ├── Plugin.xaml │ ├── Plugin.xaml.cs │ ├── PluginDropdown.cs │ ├── PluginEditor.xaml │ ├── PluginEditor.xaml.cs │ ├── PluginManager.xaml │ ├── PluginManager.xaml.cs │ ├── PluginParameter.xaml │ ├── PluginParameter.xaml.cs │ ├── PluginRegister.cs │ └── XmlFile.cs ├── PnGenerator │ ├── AdapterDropdown.cs │ ├── Control.cs │ ├── Menu.xaml │ ├── Menu.xaml.cs │ ├── Settings.cs │ ├── SettingsView.xaml │ └── SettingsView.xaml.cs ├── Resources │ ├── OC.TcTemplate.zip │ └── oc_logo.ico └── Settings.cs └── README.md /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup dotnet 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: '8.0.x' 21 | 22 | - name: Install Versionize 23 | run: dotnet tool install --global Versionize 24 | 25 | - name: Setup git 26 | run: | 27 | git config --global user.email opencommissioning@spiratec.com 28 | git config --global user.name "oc-bot" 29 | 30 | - name: Versioning 31 | id: versionize 32 | run: versionize --exit-insignificant-commits 33 | continue-on-error: true 34 | 35 | - name: Get current version 36 | if: steps.versionize.outcome == 'success' 37 | run: echo "VERSION=v$(versionize inspect)" >> $GITHUB_ENV 38 | 39 | - name: Get current changelog 40 | if: steps.versionize.outcome == 'success' 41 | run: echo "$(versionize changelog)" > latest_changelog.md 42 | 43 | - name: Push changes to GitHub 44 | if: steps.versionize.outcome == 'success' 45 | uses: ad-m/github-push-action@master 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | branch: ${{ github.ref }} 49 | tags: true 50 | 51 | - name: Dotnet publish 52 | if: steps.versionize.outcome == 'success' 53 | run: dotnet publish OC.Assistant --configuration release --runtime win-x64 -p:PublishSingleFile=true -p:EnableWindowsTargeting=true --self-contained false --output ./ 54 | 55 | - name: Publish new release 56 | if: steps.versionize.outcome == 'success' 57 | run: | 58 | gh release create ${{env.VERSION}} -t "Release ${{env.VERSION}}" -F latest_changelog.md 59 | zip ./OC.Assistant_${{env.VERSION}}.zip ./OC.Assistant.exe 60 | gh release upload ${{env.VERSION}} ./OC.Assistant_${{env.VERSION}}.zip 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | *.sln.docstates 5 | .vs/ 6 | .idea/ 7 | 8 | # Build results 9 | [Dd]ebug/ 10 | [Dd]ebugPublic/ 11 | [Rr]elease/ 12 | [Rr]eleases/ 13 | x64/ 14 | x86/ 15 | build/ 16 | bld/ 17 | [Bb]in/ 18 | [Oo]bj/ 19 | 20 | # Roslyn cache directories 21 | *.ide/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | #NUNIT 28 | *.VisualState.xml 29 | TestResult.xml 30 | 31 | # Build Results of an ATL Project 32 | [Dd]ebugPS/ 33 | [Rr]eleasePS/ 34 | dlldata.c 35 | 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Chutzpah Test files 62 | _Chutzpah* 63 | 64 | # Visual C++ cache files 65 | ipch/ 66 | *.aps 67 | *.ncb 68 | *.opensdf 69 | *.sdf 70 | *.cachefile 71 | 72 | # Visual Studio profiler 73 | *.psess 74 | *.vsp 75 | *.vspx 76 | 77 | # TFS 2012 Local Workspace 78 | $tf/ 79 | 80 | # Guidance Automation Toolkit 81 | *.gpState 82 | 83 | # ReSharper is a .NET coding add-in 84 | _ReSharper*/ 85 | *.[Rr]e[Ss]harper 86 | *.DotSettings.user 87 | 88 | # JustCode is a .NET coding addin-in 89 | .JustCode 90 | 91 | # TeamCity is a build add-in 92 | _TeamCity* 93 | 94 | # DotCover is a Code Coverage Tool 95 | *.dotCover 96 | 97 | # NCrunch 98 | _NCrunch_* 99 | .*crunch*.local.xml 100 | 101 | # MightyMoose 102 | *.mm.* 103 | AutoTest.Net/ 104 | 105 | # Web workbench (sass) 106 | .sass-cache/ 107 | 108 | # Installshield output folder 109 | [Ee]xpress/ 110 | 111 | # DocProject is a documentation generator add-in 112 | DocProject/buildhelp/ 113 | DocProject/Help/*.HxT 114 | DocProject/Help/*.HxC 115 | DocProject/Help/*.hhc 116 | DocProject/Help/*.hhk 117 | DocProject/Help/*.hhp 118 | DocProject/Help/Html2 119 | DocProject/Help/html 120 | 121 | # Click-Once directory 122 | publish/ 123 | 124 | # Publish Web Output 125 | *.[Pp]ublish.xml 126 | *.azurePubxml 127 | # TODO: Comment the next line if you want to checkin your web deploy settings 128 | # but database connection strings (with potential passwords) will be unencrypted 129 | *.pubxml 130 | *.publishproj 131 | 132 | # NuGet Packages 133 | *.nupkg 134 | # The packages folder can be ignored because of Package Restore 135 | **/packages/* 136 | # except build/, which is used as an MSBuild target. 137 | !**/packages/build/ 138 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 139 | #!**/packages/repositories.config 140 | 141 | # Windows Azure Build Output 142 | csx/ 143 | *.build.csdef 144 | 145 | # Windows Store app package directory 146 | AppPackages/ 147 | 148 | # Others 149 | sql/ 150 | *.Cache 151 | ClientBin/ 152 | [Ss]tyle[Cc]op.* 153 | ~$* 154 | *~ 155 | *.dbmdl 156 | *.dbproj.schemaview 157 | *.pfx 158 | *.publishsettings 159 | node_modules/ 160 | 161 | # RIA/Silverlight projects 162 | Generated_Code/ 163 | 164 | # Backup & report files from converting an old project file 165 | # to a newer Visual Studio version. Backup files are not needed, 166 | # because we have git ;-) 167 | _UpgradeReport_Files/ 168 | Backup*/ 169 | UpgradeLog*.XML 170 | UpgradeLog*.htm 171 | 172 | # SQL Server files 173 | *.mdf 174 | *.ldf 175 | 176 | # Business Intelligence projects 177 | *.rdl.data 178 | *.bim.layout 179 | *.bim_*.settings 180 | 181 | # Microsoft Fakes 182 | FakesAssemblies/ 183 | 184 | 185 | ### Windows ### 186 | # Windows image file caches 187 | Thumbs.db 188 | ehthumbs.db 189 | 190 | # Folder config file 191 | Desktop.ini 192 | 193 | # Recycle Bin used on file shares 194 | $RECYCLE.BIN/ 195 | 196 | # Windows Installer files 197 | *.cab 198 | *.msi 199 | *.msm 200 | *.msp -------------------------------------------------------------------------------- /Documentation/Images/Assistant_Plugins_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/Assistant_Plugins_dark.png -------------------------------------------------------------------------------- /Documentation/Images/Assistant_Plugins_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/Assistant_Plugins_light.png -------------------------------------------------------------------------------- /Documentation/Images/DeviceGeneration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/DeviceGeneration.gif -------------------------------------------------------------------------------- /Documentation/Images/FileMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/FileMenu.png -------------------------------------------------------------------------------- /Documentation/Images/MainWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/MainWindow.png -------------------------------------------------------------------------------- /Documentation/Images/PluginUsage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/PluginUsage.gif -------------------------------------------------------------------------------- /Documentation/Images/ProfinetMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/ProfinetMenu.png -------------------------------------------------------------------------------- /Documentation/Images/ProjectMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/ProjectMenu.png -------------------------------------------------------------------------------- /Documentation/Images/ProjectSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/ProjectSettings.png -------------------------------------------------------------------------------- /Documentation/Images/ScanProfinet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/ScanProfinet.png -------------------------------------------------------------------------------- /Documentation/Images/TaskGeneration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/Documentation/Images/TaskGeneration.gif -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Open Commissioning 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /OC.Assistant.slnx: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /OC.Assistant/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OC.Assistant/AssemblyHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace OC.Assistant; 6 | 7 | /// 8 | /// Static helper. 9 | /// 10 | public static class AssemblyHelper 11 | { 12 | private static readonly ConcurrentBag Directories = []; 13 | 14 | /// 15 | /// Adds a new directory to search dlls. 16 | /// The path of the directory. 17 | /// The . 18 | /// 19 | public static void AddDirectory(string? directory, SearchOption searchOption = SearchOption.TopDirectoryOnly) 20 | { 21 | if (!Directory.Exists(directory) || Directories.Any(x => x == directory)) return; 22 | Directories.Add(directory); 23 | 24 | AppDomain.CurrentDomain.AssemblyResolve += (_, resolveEventArgs) => 25 | { 26 | var assemblyFile = $"{resolveEventArgs.Name.Split(',')[0]}.dll"; 27 | return (from filePath in Directory.GetFiles(directory, assemblyFile, searchOption).Reverse() 28 | select Assembly.LoadFile(filePath)).FirstOrDefault(); 29 | }; 30 | } 31 | } -------------------------------------------------------------------------------- /OC.Assistant/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] -------------------------------------------------------------------------------- /OC.Assistant/Controls/DependencyInfo.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/DependencyInfo.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows; 3 | 4 | namespace OC.Assistant.Controls; 5 | 6 | public partial class DependencyInfo 7 | { 8 | private string? _url; 9 | 10 | public DependencyInfo(Type type) 11 | { 12 | InitializeComponent(); 13 | NameLabel.Content = type.Assembly.GetName().Name; 14 | VersionLabel.Content = type.Assembly.GetName().Version?.ToString(); 15 | } 16 | 17 | public DependencyInfo(string? name, string? version = null) 18 | { 19 | InitializeComponent(); 20 | NameLabel.Content = name; 21 | VersionLabel.Content = version; 22 | } 23 | 24 | public string? Url 25 | { 26 | get => _url; 27 | set 28 | { 29 | if (value is null) 30 | { 31 | UrlButton.Visibility = Visibility.Collapsed; 32 | return; 33 | } 34 | UrlButton.Visibility = Visibility.Visible; 35 | _url = value; 36 | UrlButton.ToolTip = value; 37 | } 38 | } 39 | 40 | public string? UrlName 41 | { 42 | set => UrlButton.Content = value; 43 | } 44 | 45 | 46 | private void UrlButton_OnClick(object sender, RoutedEventArgs e) 47 | { 48 | if (Url is null) return; 49 | 50 | Process.Start(new ProcessStartInfo 51 | { 52 | FileName = Url, 53 | UseShellExecute = true 54 | }); 55 | } 56 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/DteSelector.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/DteSelector.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using OC.Assistant.Core; 3 | 4 | namespace OC.Assistant.Controls; 5 | 6 | /// 7 | /// Dropdown for available solutions. 8 | /// 9 | public partial class DteSelector 10 | { 11 | private readonly struct Solution 12 | { 13 | public string? SolutionFullName { get; init; } 14 | public string? ProjectFolder { get; init; } 15 | } 16 | 17 | private readonly List _solutions = []; 18 | 19 | public DteSelector() 20 | { 21 | InitializeComponent(); 22 | SubmenuOpened += OnSubmenuOpened; 23 | } 24 | 25 | private void OnSubmenuOpened(object sender, EventArgs e) 26 | { 27 | Items.Clear(); 28 | _solutions.Clear(); 29 | 30 | DteSingleThread.Run(() => 31 | { 32 | foreach (var instance in TcDte.GetInstances()) 33 | { 34 | _solutions.Add(new Solution 35 | { 36 | SolutionFullName = instance.Solution?.FullName, 37 | ProjectFolder = instance.GetProjectFolder() 38 | }); 39 | 40 | instance.Finalize(false); 41 | } 42 | 43 | GC.Collect(); 44 | GC.WaitForPendingFinalizers(); 45 | }, 1000); 46 | 47 | 48 | foreach (var solution in _solutions) 49 | { 50 | var subMenuItem = new MenuItem 51 | { 52 | Header = solution.SolutionFullName, 53 | Tag = solution 54 | }; 55 | 56 | subMenuItem.Click += (obj, _) => 57 | { 58 | if (((MenuItem) obj).Tag is not Solution tag) return; 59 | if (string.IsNullOrEmpty(tag.SolutionFullName) || string.IsNullOrEmpty(tag.ProjectFolder)) return; 60 | ProjectState.Solution.Connect(tag.SolutionFullName, tag.ProjectFolder); 61 | }; 62 | 63 | Items.Add(subMenuItem); 64 | } 65 | 66 | if (Items.Count == 0) 67 | { 68 | Items.Add(new MenuItem {Header = "no open TwinCAT solution", IsEnabled = false}); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/FileMenu.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/FileMenu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Windows; 4 | using EnvDTE; 5 | using Microsoft.Win32; 6 | using OC.Assistant.Core; 7 | using OC.Assistant.Sdk; 8 | 9 | namespace OC.Assistant.Controls; 10 | 11 | internal partial class FileMenu 12 | { 13 | private static event RoutedEventHandler? OnOpenSolution; 14 | private static event RoutedEventHandler? OnCreateSolution; 15 | 16 | public FileMenu() 17 | { 18 | InitializeComponent(); 19 | OnOpenSolution += OpenSlnOnClick; 20 | OnCreateSolution += CreateSlnOnClick; 21 | } 22 | 23 | public static void OpenSolution(object sender, RoutedEventArgs e) 24 | { 25 | OnOpenSolution?.Invoke(sender, e); 26 | } 27 | 28 | public static void CreateSolution(object sender, RoutedEventArgs e) 29 | { 30 | OnCreateSolution?.Invoke(sender, e); 31 | } 32 | 33 | private void ExitOnClick(object sender, RoutedEventArgs e) 34 | { 35 | Application.Current.Shutdown(); 36 | } 37 | 38 | private void OpenSlnOnClick(object sender, RoutedEventArgs e) 39 | { 40 | var openFileDialog = new OpenFileDialog 41 | { 42 | Filter = "TwinCAT Solution (*.sln)|*.sln", 43 | RestoreDirectory = true 44 | }; 45 | 46 | if (openFileDialog.ShowDialog() == true) 47 | { 48 | OpenDte(openFileDialog.FileName); 49 | } 50 | } 51 | 52 | private void CreateSlnOnClick(object? sender = null, RoutedEventArgs? e = null) 53 | { 54 | var saveFileDialog = new SaveFileDialog 55 | { 56 | Filter = "TwinCAT Solution (*.sln)|*.sln", 57 | RestoreDirectory = true 58 | }; 59 | 60 | if (saveFileDialog.ShowDialog() == true) 61 | { 62 | CreateSolution(saveFileDialog.FileName); 63 | } 64 | } 65 | 66 | private void OpenDte(string path, Task? previousTask = null) 67 | { 68 | string? projectFolder = null; 69 | 70 | var thread = DteSingleThread.Run(() => 71 | { 72 | previousTask?.Wait(); 73 | if (previousTask?.IsFaulted == true) return; 74 | 75 | DTE? dte = null; 76 | 77 | try 78 | { 79 | dte = TcDte.Create(); 80 | Logger.LogInfo(this, $"Open project '{path}' ..."); 81 | dte.Solution?.Open(path); 82 | dte.UserControl = true; 83 | if (!dte.UserControl) return; 84 | projectFolder = dte.GetProjectFolder(); 85 | } 86 | catch (Exception e) 87 | { 88 | Logger.LogError(this, e.Message); 89 | } 90 | finally 91 | { 92 | dte?.Finalize(); 93 | } 94 | }); 95 | 96 | Task.Run(() => 97 | { 98 | thread.Join(); 99 | if (projectFolder is null) 100 | { 101 | Logger.LogError(this, "Failed to connect solution"); 102 | return; 103 | } 104 | ProjectState.Solution.Connect(path, projectFolder); 105 | }); 106 | } 107 | 108 | private void CreateSolution(string slnFilePath) 109 | { 110 | var task = Task.Run(() => 111 | { 112 | try 113 | { 114 | BusyState.Set(this); 115 | 116 | const string templateName = "OC.TcTemplate"; 117 | var rootFolder = Path.GetDirectoryName(slnFilePath); 118 | var projectName = Path.GetFileNameWithoutExtension(slnFilePath); 119 | 120 | if (rootFolder is null) 121 | { 122 | throw new ArgumentNullException(rootFolder); 123 | } 124 | 125 | //Get zip file from resource 126 | var assembly = typeof(FileMenu).Assembly; 127 | var resourceName = $"{assembly.GetName().Name}.Resources.{templateName}.zip"; 128 | var resourceStream = assembly.GetManifestResourceStream(resourceName); 129 | if (resourceStream is null) 130 | { 131 | throw new ArgumentNullException(resourceName); 132 | } 133 | 134 | //Extract resource to folder 135 | ZipFile.ExtractToDirectory(resourceStream, rootFolder); 136 | 137 | //Rename solution file 138 | File.Move($"{rootFolder}\\{templateName}.sln", slnFilePath); 139 | 140 | //Modify solution file 141 | var slnFileText = File.ReadAllText(slnFilePath); 142 | File.WriteAllText(slnFilePath, slnFileText.Replace(templateName, projectName)); 143 | 144 | //Rename project folder 145 | Directory.Move($"{rootFolder}\\{templateName}", $"{rootFolder}\\{projectName}"); 146 | 147 | //Rename project file 148 | File.Move($@"{rootFolder}\{projectName}\{templateName}.tsproj", 149 | $@"{rootFolder}\{projectName}\{projectName}.tsproj"); 150 | 151 | return Task.CompletedTask; 152 | } 153 | catch (Exception e) 154 | { 155 | Logger.LogError(this, e.Message); 156 | return Task.FromException(e); 157 | } 158 | finally 159 | { 160 | BusyState.Reset(this); 161 | } 162 | }); 163 | 164 | OpenDte(slnFilePath, task); 165 | } 166 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/HelpMenu.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | Verbose logging 7 | 8 | 9 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/HelpMenu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Reflection; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Input; 6 | 7 | namespace OC.Assistant.Controls; 8 | 9 | internal partial class HelpMenu 10 | { 11 | public HelpMenu() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | private static Assembly Assembly => typeof(MainWindow).Assembly; 17 | private static string? ProductName => Assembly.GetName().Name; 18 | private static string? Version => Assembly.GetName().Version?.ToString(); 19 | private static string? CompanyName 20 | { 21 | get 22 | { 23 | var company = Attribute 24 | .GetCustomAttribute(Assembly, typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute; 25 | return company?.Company; 26 | } 27 | } 28 | 29 | private class GitHubLink : Button 30 | { 31 | public GitHubLink() 32 | { 33 | const string url = "https://github.com/OpenCommissioning/OC_Assistant"; 34 | Style = Application.Current.FindResource("LinkButton") as Style; 35 | Margin = new Thickness(0, 10, 0, 20); 36 | Cursor = Cursors.Hand; 37 | Content = url; 38 | Click += (_, _) => Process.Start(new ProcessStartInfo {FileName = url, UseShellExecute = true}); 39 | } 40 | } 41 | 42 | private void AppDataOnClick(object sender, RoutedEventArgs e) 43 | { 44 | Process.Start("explorer.exe" , Core.AppData.Path); 45 | } 46 | 47 | private void VerboseOnClick(object sender, RoutedEventArgs e) 48 | { 49 | Sdk.Logger.Verbose = ((CheckBox) sender).IsChecked == true; 50 | } 51 | 52 | private void AboutOnClick(object sender, RoutedEventArgs e) 53 | { 54 | var content = new StackPanel(); 55 | var stack = content.Children; 56 | 57 | stack.Add(new Label {Content = $"\n{ProductName}\nVersion {Version}\n{CompanyName}"}); 58 | 59 | stack.Add(new GitHubLink()); 60 | 61 | stack.Add(new DependencyInfo(typeof(Sdk.Logger)) 62 | { 63 | Url = "https://www.nuget.org/packages/OC.Assistant.Sdk", 64 | UrlName = "nuget.org" 65 | }); 66 | 67 | stack.Add(new DependencyInfo(typeof(Theme.WindowStyle)) 68 | { 69 | Url = "https://www.nuget.org/packages/OC.Assistant.Theme", 70 | UrlName = "nuget.org" 71 | }); 72 | 73 | stack.Add(new Label{Content = "\n\nThird party software:\n"}); 74 | 75 | stack.Add(new DependencyInfo(typeof(EnvDTE.DTE)) 76 | { 77 | Url = "https://www.nuget.org/packages/envdte", 78 | UrlName = "nuget.org" 79 | }); 80 | 81 | stack.Add(new DependencyInfo(typeof(Microsoft.WindowsAPICodePack.Dialogs.DialogControl)) 82 | { 83 | Url = "https://www.nuget.org/packages/Microsoft-WindowsAPICodePack-Core", 84 | UrlName = "nuget.org" 85 | }); 86 | 87 | stack.Add(new DependencyInfo(typeof(TwinCAT.Ads.AdsClient)) 88 | { 89 | Url = "https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads", 90 | UrlName = "nuget.org" 91 | }); 92 | 93 | stack.Add(new DependencyInfo(typeof(TCatSysManagerLib.TcSysManager)) 94 | { 95 | Url = "https://www.nuget.org/packages/TCatSysManagerLib", 96 | UrlName = "nuget.org" 97 | }); 98 | 99 | stack.Add(new DependencyInfo("dsian.TcPnScanner.CLI", "") 100 | { 101 | Url = "https://www.nuget.org/packages/dsian.TcPnScanner.CLI", 102 | UrlName = "nuget.org" 103 | }); 104 | 105 | Theme.MessageBox.Show($"About {ProductName}", content, MessageBoxButton.OK, MessageBoxImage.Information); 106 | } 107 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/NotConnectedOverlay.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | 19 | 20 | 21 | 24 | 25 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/NotConnectedOverlay.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using OC.Assistant.Core; 3 | 4 | namespace OC.Assistant.Controls; 5 | 6 | public partial class NotConnectedOverlay 7 | { 8 | public NotConnectedOverlay() 9 | { 10 | InitializeComponent(); 11 | ProjectState.Events.Connected += OnConnect; 12 | ProjectState.Events.Disconnected += OnDisconnect; 13 | } 14 | 15 | private void OnConnect(string solutionFullName) 16 | { 17 | Visibility = Visibility.Hidden; 18 | } 19 | 20 | private void OnDisconnect() 21 | { 22 | Visibility = Visibility.Visible; 23 | } 24 | 25 | private void OpenOnClick(object sender, RoutedEventArgs e) 26 | { 27 | FileMenu.OpenSolution(sender, e); 28 | } 29 | 30 | private void CreateOnClick(object sender, RoutedEventArgs e) 31 | { 32 | FileMenu.CreateSolution(sender, e); 33 | } 34 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/ProjectStateView.xaml: -------------------------------------------------------------------------------- 1 |  11 | 16 | 21 | 22 | 27 | 32 | 33 | 37 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /OC.Assistant/Controls/ProjectStateView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace OC.Assistant.Controls; 5 | 6 | public partial class ProjectStateView 7 | { 8 | public ProjectStateView() 9 | { 10 | InitializeComponent(); 11 | IndicateDisconnected(); 12 | } 13 | 14 | protected void SetSolutionPath(string? path) 15 | { 16 | Dispatcher.Invoke(() => 17 | { 18 | SolutionLabel.Content = path; 19 | }); 20 | } 21 | 22 | protected void IndicateDisconnected() 23 | { 24 | Dispatcher.Invoke(() => 25 | { 26 | StateBorder.Background = Application.Current.Resources["White4Brush"] as SolidColorBrush; 27 | StateLabel.Content = "Offline"; 28 | NedIdLabel.Content = null; 29 | SolutionLabel.Content = null; 30 | }); 31 | } 32 | 33 | protected void IndicateRunMode() 34 | { 35 | Dispatcher.Invoke(() => 36 | { 37 | StateBorder.Background = Application.Current.Resources["Success1Brush"] as SolidColorBrush; 38 | StateLabel.Content = "Run"; 39 | NedIdLabel.Content = Sdk.ApiLocal.Interface.NetId; 40 | }); 41 | } 42 | 43 | protected void IndicateConfigMode() 44 | { 45 | Dispatcher.Invoke(() => 46 | { 47 | StateBorder.Background = Application.Current.Resources["AccentBrush"] as SolidColorBrush; 48 | StateLabel.Content = "Config"; 49 | NedIdLabel.Content = Sdk.ApiLocal.Interface.NetId; 50 | }); 51 | } 52 | } -------------------------------------------------------------------------------- /OC.Assistant/Controls/VersionCheck.xaml: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /OC.Assistant/Controls/VersionCheck.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Net.Http; 3 | using System.Reflection; 4 | using System.Text.Json; 5 | using System.Windows; 6 | using OC.Assistant.Sdk; 7 | 8 | namespace OC.Assistant.Controls; 9 | 10 | public partial class VersionCheck 11 | { 12 | public VersionCheck() 13 | { 14 | InitializeComponent(); 15 | Loaded += OnLoaded; 16 | return; 17 | 18 | void OnLoaded(object sender, RoutedEventArgs e) 19 | { 20 | Task.Run(async() => 21 | { 22 | try 23 | { 24 | const string api = "https://api.github.com/repos/opencommissioning/oc_assistant/releases/latest"; 25 | const string url = "https://github.com/opencommissioning/oc_assistant/releases/latest"; 26 | 27 | var version = Assembly.GetExecutingAssembly().GetName().Version; 28 | Logger.LogInfo(this, $"Current version {version}"); 29 | var current = $"v{version?.Major}.{version?.Minor}.{version?.Build}"; 30 | 31 | using var client = new HttpClient(); 32 | client.DefaultRequestHeaders.UserAgent.ParseAdd("request"); 33 | 34 | var latest = JsonDocument 35 | .Parse(await client.GetStringAsync(api)) 36 | .RootElement 37 | .GetProperty("tag_name") 38 | .GetString(); 39 | 40 | if (current != latest) 41 | { 42 | Dispatcher.Invoke(() => 43 | { 44 | Visibility = Visibility.Visible; 45 | Content = $"Release {latest} available!"; 46 | Click += (_, _) => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); 47 | }); 48 | } 49 | } 50 | catch 51 | { 52 | //Logger.LogWarning(this, $"Unable to fetch latest release from GitHub"); 53 | } 54 | }); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/AppData.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | public static class AppData 6 | { 7 | private static readonly string PathPreset = 8 | $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\OC.Assistant"; 9 | 10 | /// 11 | /// Gets the path of the user specific directory. 12 | /// 13 | public static string Path 14 | { 15 | get 16 | { 17 | Directory.CreateDirectory(PathPreset); 18 | return PathPreset; 19 | } 20 | } 21 | 22 | /// 23 | /// Gets the path of the global settings for the application. 24 | /// 25 | public static string SettingsFilePath => $"{Path}\\settings.json"; 26 | 27 | /// 28 | /// Gets the path of the log file. 29 | /// 30 | public static string LogFilePath => $"{Path}\\log.txt"; 31 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/BusyState.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Core; 2 | 3 | /// 4 | /// Represents a static class to set/reset a busy state. 5 | /// Each is responsible to reset the state after setting it, otherwise the state will remain busy. 6 | /// 7 | public static class BusyState 8 | { 9 | private static readonly HashSet HashCodes = []; 10 | private static readonly object HashCodesLock = new(); 11 | 12 | /// 13 | /// Returns true if any has set the state, otherwise false. 14 | /// 15 | public static bool IsSet 16 | { 17 | get 18 | { 19 | lock (HashCodesLock) 20 | { 21 | return HashCodes.Count > 0; 22 | } 23 | } 24 | } 25 | 26 | /// 27 | /// Checks whether the given has set the state. 28 | /// 29 | /// The to check. 30 | /// True if the given has set the state, otherwise false. 31 | public static bool Query(object sender) 32 | { 33 | lock (HashCodesLock) 34 | { 35 | return HashCodes.Contains(sender.GetHashCode()); 36 | } 37 | } 38 | 39 | /// 40 | /// Sets the busy state. 41 | /// 42 | /// The sender from where the state is set. 43 | public static void Set(object sender) 44 | { 45 | var hashCode = sender.GetHashCode(); 46 | lock (HashCodesLock) 47 | { 48 | if (!HashCodes.Add(hashCode)) return; 49 | } 50 | Sdk.Logger.LogInfo(sender, $"Set busy state. Object hashcode 0x{hashCode:x8}", true); 51 | Changed?.Invoke(true); 52 | } 53 | 54 | /// 55 | /// Resets the busy state. 56 | /// The sender from where the state is reset. 57 | /// 58 | public static void Reset(object sender) 59 | { 60 | var hashCode = sender.GetHashCode(); 61 | lock (HashCodesLock) 62 | { 63 | if (!HashCodes.Remove(hashCode)) return; 64 | } 65 | Sdk.Logger.LogInfo(sender, $"Reset busy state. Object hashcode 0x{hashCode:x8}", true); 66 | if (IsSet) return; 67 | Changed?.Invoke(false); 68 | } 69 | 70 | /// 71 | /// The state has been changed. 72 | /// 73 | public static event Action? Changed; 74 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/DteSingleThread.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | /// 6 | /// single-threaded invoker. 7 | /// 8 | public static class DteSingleThread 9 | { 10 | ///
11 | /// This overload automatically gets the interface of the currently connected solution. 12 | public static System.Threading.Thread Run(Action action, int millisecondsTimeout = 0) 13 | { 14 | return Run(() => 15 | { 16 | if (ProjectState.Solution.FullName is null) 17 | { 18 | Sdk.Logger.LogError(typeof(DteSingleThread), "No Solution selected"); 19 | return; 20 | } 21 | 22 | DTE? dte = null; 23 | 24 | try 25 | { 26 | dte = TcDte.GetInstance(ProjectState.Solution.FullName); 27 | if (dte is null) return; 28 | action(dte); 29 | } 30 | catch (Exception e) 31 | { 32 | Sdk.Logger.LogError(typeof(DteSingleThread), e.Message); 33 | } 34 | finally 35 | { 36 | dte?.Finalize(); 37 | } 38 | }, millisecondsTimeout); 39 | } 40 | 41 | /// 42 | /// Registers a and invokes the given delegate in a new 43 | /// with .
44 | /// Is used to invoke COM functions with interface. 45 | ///
46 | /// The action to be invoked in single-threaded apartment. 47 | /// Blocks the calling thread until this thread terminates or the timeout is reached. 48 | /// Value 0 disables blocking and activates the busy state. 49 | /// 50 | /// The instance of this . 51 | public static System.Threading.Thread Run(Action action, int millisecondsTimeout = 0) 52 | { 53 | var thread = new System.Threading.Thread(() => 54 | { 55 | try 56 | { 57 | if (millisecondsTimeout == 0) BusyState.Set(action); 58 | MessageFilter.Register(); 59 | action(); 60 | } 61 | catch (Exception e) 62 | { 63 | Sdk.Logger.LogError(typeof(DteSingleThread), e.Message); 64 | } 65 | finally 66 | { 67 | MessageFilter.Revoke(); 68 | if (millisecondsTimeout == 0) BusyState.Reset(action); 69 | } 70 | }); 71 | 72 | thread.SetApartmentState(ApartmentState.STA); 73 | thread.Start(); 74 | if (millisecondsTimeout > 0) thread.Join(millisecondsTimeout); 75 | return thread; 76 | } 77 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/IProjectStateEvents.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Core; 2 | 3 | /// 4 | /// Interface for the events. 5 | /// 6 | public interface IProjectStateEvents 7 | { 8 | /// 9 | /// Is raised with the solution full name when a project gets connected. 10 | /// 11 | public event Action? Connected; 12 | 13 | /// 14 | /// Is raised when the project gets disconnected. 15 | /// 16 | public event Action? Disconnected; 17 | 18 | /// 19 | /// Is raised a project is connected and TwinCAT started running. 20 | /// 21 | public event Action? StartedRunning; 22 | 23 | /// 24 | /// Is raised a project is connected and TwinCAT stopped running. 25 | /// 26 | public event Action? StoppedRunning; 27 | 28 | /// 29 | /// Is raised with True when the project gets disconnected or when TwinCAT started running.
30 | /// Is raised with False when a project is connected and TwinCAT stopped running. 31 | ///
32 | public event Action? Locked; 33 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/IProjectStateSolution.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | /// 6 | /// Interface for the solution. 7 | /// 8 | public interface IProjectStateSolution 9 | { 10 | /// 11 | /// Connects to a Visual Studio Solution via interface. 12 | /// 13 | /// The path of the Visual Studio Solution. 14 | /// The path of the project folder. 15 | public void Connect(string solutionFullName, string projectFolder); 16 | 17 | /// 18 | /// Gets the full name of the Visual Studio Solution. 19 | /// 20 | public string? FullName { get; } 21 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/MessageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | [ComImport, Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 6 | public interface IOleMessageFilter 7 | { 8 | [PreserveSig] 9 | int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo); 10 | 11 | 12 | [PreserveSig] 13 | int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType); 14 | 15 | 16 | [PreserveSig] 17 | int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType); 18 | } 19 | 20 | public class MessageFilter : IOleMessageFilter 21 | { 22 | public static void Register() 23 | { 24 | IOleMessageFilter newFilter = new MessageFilter(); 25 | var result = CoRegisterMessageFilter(newFilter, out _); 26 | 27 | if (result != 0) 28 | { 29 | Sdk.Logger.LogError(typeof(MessageFilter), $"CoRegisterMessageFilter failed with error {result}"); 30 | } 31 | } 32 | 33 | public static void Revoke() 34 | { 35 | _ = CoRegisterMessageFilter(null, out _); 36 | } 37 | 38 | int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo) 39 | { 40 | // return flag SERVERCALL_ISHANDLED 41 | return 0; 42 | } 43 | 44 | int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) 45 | { 46 | // Thread call was refused, try again 47 | if (dwRejectType == 2) // flag SERVERCALL_RETRYLATER 48 | { 49 | // retry thread call at once, if return value >= 0 & < 100 50 | return 100; 51 | } 52 | 53 | return -1; 54 | } 55 | 56 | int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) 57 | { 58 | // return flag PENDINGMSG_WAITDEFPROCESS 59 | return 2; 60 | } 61 | 62 | [DllImport("Ole32.dll")] 63 | private static extern int CoRegisterMessageFilter(IOleMessageFilter? newFilter, out IOleMessageFilter oldFilter); 64 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcDte.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.InteropServices.ComTypes; 4 | using EnvDTE; 5 | using OC.Assistant.Sdk; 6 | using TCatSysManagerLib; 7 | 8 | namespace OC.Assistant.Core; 9 | 10 | /// 11 | /// Represents a static class with methods to extend the interface for TwinCAT specific usings. 12 | /// 13 | public static class TcDte 14 | { 15 | /// 16 | /// Supported versions, prioritized in this order:

17 | /// TcXaeShell.DTE.17.0 : TwinCAT Shell based on VS2022
18 | /// TcXaeShell.DTE.15.0 : TwinCAT Shell based on VS2017
19 | /// VisualStudio.DTE.17.0 : Visual Studio 2022
20 | /// VisualStudio.DTE.16.0 : Visual Studio 2019
21 | /// VisualStudio.DTE.15.0 : Visual Studio 2017
22 | ///
23 | private static readonly Type? InstalledShell = 24 | Type.GetTypeFromProgID("TcXaeShell.DTE.17.0") ?? 25 | Type.GetTypeFromProgID("TcXaeShell.DTE.15.0") ?? 26 | Type.GetTypeFromProgID("VisualStudio.DTE.17.0") ?? 27 | Type.GetTypeFromProgID("VisualStudio.DTE.16.0") ?? 28 | Type.GetTypeFromProgID("VisualStudio.DTE.15.0"); 29 | 30 | /// 31 | /// Creates a new Visual Studio instance. 32 | /// 33 | /// The interface to the created instance. 34 | /// An appropriate shell is not installed on the computer. 35 | /// Creating an instance of the shell failed. 36 | public static DTE Create() 37 | { 38 | if (InstalledShell is null) 39 | { 40 | throw new Exception("No TwinCAT Shell installed"); 41 | } 42 | 43 | Logger.LogInfo(typeof(TcDte), "Create TwinCAT Shell instance ..."); 44 | 45 | if (Activator.CreateInstance(InstalledShell) is not DTE dte) 46 | { 47 | throw new Exception("Creating instance of TwinCAT Shell failed"); 48 | } 49 | 50 | return dte; 51 | } 52 | 53 | /// 54 | /// Releases all references to the given interface and forces a garbage collection. 55 | /// 56 | /// The given interface. 57 | /// Forces an immediate garbage collection of all generations. 58 | public static void Finalize(this DTE? dte, bool gcCollect = true) 59 | { 60 | if (dte is null) return; 61 | Marshal.FinalReleaseComObject(dte); 62 | if (!gcCollect) return; 63 | GC.Collect(); 64 | GC.WaitForPendingFinalizers(); 65 | } 66 | 67 | /// 68 | /// Tries to get the path of the project folder. 69 | /// 70 | /// The given interface. 71 | /// The path of the project folder if succeeded, otherwise . 72 | public static string? GetProjectFolder(this DTE? dte) 73 | { 74 | var project = GetTcProject(dte); 75 | return project is null ? null : Directory.GetParent(project.FullName)?.FullName; 76 | } 77 | 78 | /// 79 | /// Tries to get the . 80 | /// 81 | /// The given interface. 82 | /// The interface if succeeded, otherwise . 83 | public static ITcSysManager15? GetTcSysManager(this DTE? dte) 84 | { 85 | return GetTcProject(dte)?.Object as ITcSysManager15; 86 | } 87 | 88 | /// 89 | /// Tries to get the first of the given that implements . 90 | /// 91 | /// The given interface. 92 | /// The first implementing if succeeded, otherwise . 93 | public static Project? GetTcProject(this DTE? dte) 94 | { 95 | if (dte is null) return null; 96 | return (from Project project in dte.Solution.Projects select project) 97 | .FirstOrDefault(pro => pro.Object is ITcSysManager15); 98 | } 99 | 100 | /// 101 | /// Gets the interface of the given solution path. 102 | /// 103 | /// The full name of the solution file. 104 | /// The interface of the given solution path if any, otherwise null. 105 | public static DTE? GetInstance(string solutionFullName) 106 | { 107 | return GetInstances(solutionFullName).FirstOrDefault(); 108 | } 109 | 110 | /// 111 | /// Returns a collection of by querying all supported Visual Studio instances 112 | /// with a valid TwinCAT solution. 113 | /// 114 | /// A collection of interfaces with a valid TwinCAT solution. 115 | public static IEnumerable GetInstances(string? solutionFullName = null) 116 | { 117 | if (InstalledShell is null) yield break; 118 | if (GetRunningObjectTable(0, out var runningObjectTable) != 0) yield break; 119 | runningObjectTable.EnumRunning(out var enumMoniker); 120 | 121 | var fetched = IntPtr.Zero; 122 | var moniker = new IMoniker[1]; 123 | 124 | while (enumMoniker.Next(1, moniker, fetched) == 0) 125 | { 126 | DTE? dte = null; 127 | 128 | try 129 | { 130 | CreateBindCtx(0, out var bindCtx); 131 | moniker[0].GetDisplayName(bindCtx, null, out var displayName); 132 | 133 | if (!displayName.StartsWith("!TcXaeShell.DTE") && !displayName.StartsWith("!VisualStudio.DTE")) continue; 134 | if (runningObjectTable.GetObject(moniker[0], out var obj) != 0) continue; 135 | 136 | dte = (DTE?) obj; 137 | var fullName = dte?.Solution?.FullName; 138 | 139 | if (string.IsNullOrEmpty(fullName)) 140 | { 141 | dte?.Finalize(); 142 | continue; 143 | } 144 | 145 | if (!string.IsNullOrEmpty(solutionFullName) && 146 | !string.Equals(solutionFullName, fullName, StringComparison.OrdinalIgnoreCase)) 147 | { 148 | dte?.Finalize(); 149 | continue; 150 | } 151 | 152 | if (dte?.GetTcProject() is null) 153 | { 154 | dte?.Finalize(); 155 | continue; 156 | } 157 | } 158 | catch (Exception e) 159 | { 160 | dte?.Finalize(); 161 | Logger.LogError(typeof(TcDte), e.Message, true); 162 | continue; 163 | } 164 | 165 | yield return dte; 166 | } 167 | } 168 | 169 | [DllImport("ole32.dll")] 170 | private static extern void CreateBindCtx(int reserved, out IBindCtx bindCtx); 171 | 172 | [DllImport("ole32.dll")] 173 | private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable runningObjectTable); 174 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcShortcut.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Core; 2 | 3 | public struct TcShortcut 4 | { 5 | public const string BOX = "TIIB"; 6 | public const string IO_DEVICE = "TIID"; 7 | public const string TASK = "TIRT"; 8 | public const string PLC = "TIPC"; 9 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcSmTreeItemExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using TCatSysManagerLib; 3 | 4 | namespace OC.Assistant.Core; 5 | 6 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 7 | public static class TcSmTreeItemExtension 8 | { 9 | /// 10 | /// Searches for a child in the given tree by name. 11 | /// 12 | /// The parent 13 | /// The name of the child item. 14 | /// The if successful, otherwise null. 15 | public static ITcSmTreeItem? TryLookupChild(this ITcSmTreeItem parent, string? childName) 16 | { 17 | return parent.Cast().FirstOrDefault(c => c.Name == childName); 18 | } 19 | 20 | /// 21 | /// Searches for a child in the given tree by name and type. 22 | /// 23 | /// The parent 24 | /// The name of the child item. 25 | /// The of the child item. 26 | /// The if successful, otherwise null. 27 | public static ITcSmTreeItem? TryLookupChild(this ITcSmTreeItem parent, string? childName, TREEITEMTYPES type) 28 | { 29 | var child = parent.TryLookupChild(childName); 30 | if (child is null) return null; 31 | return child.ItemType == (int) type ? child : null; 32 | } 33 | 34 | /// 35 | /// Tries to find a recursive. 36 | /// 37 | /// The parent . 38 | /// The name of the child item. 39 | /// The of the child item. 40 | /// The if successful, otherwise null. 41 | public static ITcSmTreeItem? FindChildRecursive(this ITcSmTreeItem parent, string? childName, TREEITEMTYPES type) 42 | { 43 | foreach (ITcSmTreeItem child in parent) 44 | { 45 | if (child.Name.Equals(childName, StringComparison.CurrentCultureIgnoreCase) && child.ItemType == (int)type) 46 | { 47 | return child; 48 | } 49 | 50 | var grandChild = FindChildRecursive(child, childName, type); 51 | if (grandChild is not null) 52 | { 53 | return grandChild; 54 | } 55 | } 56 | 57 | return null; 58 | } 59 | 60 | /// 61 | /// Gets a child in the given tree by name and type. Creates the child if not existent. 62 | /// 63 | /// The parent 64 | /// The name of the child item. 65 | /// The of the child item. 66 | /// The if successful, otherwise null. 67 | public static ITcSmTreeItem? GetOrCreateChild(this ITcSmTreeItem parent, string? childName, TREEITEMTYPES type) 68 | { 69 | var compatibleName = childName?.MakePlcCompatible(); 70 | var item = parent.TryLookupChild(compatibleName, type); 71 | if (item is not null) return item; 72 | Thread.Sleep(1); //"Breathing room" for the COM interface 73 | return parent.CreateChild(compatibleName, nSubType: (int)type); 74 | } 75 | 76 | /// 77 | /// Creates or overwrites a GVL item with the given content. 78 | /// 79 | /// The parent . 80 | /// The name of the GVL. 'GVL_' is automatically added at the start. 81 | /// The variable declarations. 82 | /// The GVL if successful, otherwise null. 83 | public static ITcSmTreeItem? CreateGvl(this ITcSmTreeItem? parent, string name, string? variables) 84 | { 85 | if (parent?.GetOrCreateChild($"GVL_{name}", TREEITEMTYPES.TREEITEMTYPE_PLCGVL) is not { } gvlItem) 86 | { 87 | return null; 88 | } 89 | 90 | if (gvlItem is not ITcPlcDeclaration gvlDecl) 91 | { 92 | return null; 93 | } 94 | 95 | gvlDecl.DeclarationText = 96 | $"{{attribute 'linkalways'}}\n" + 97 | $"{{attribute 'qualified_only'}}\n" + 98 | $"{{attribute 'subsequent'}}\n" + 99 | $"VAR_GLOBAL\n{variables}END_VAR"; 100 | 101 | return gvlItem; 102 | } 103 | 104 | /// 105 | /// Creates or overwrites a DUT struct item with the given content. 106 | /// 107 | /// The parent . 108 | /// The name of the DUT. 'ST_' is automatically added at the start. 109 | /// The variable declarations. 110 | /// The DUT struct if successful, otherwise null. 111 | public static ITcSmTreeItem? CreateDutStruct(this ITcSmTreeItem? parent, string name, string? variables) 112 | { 113 | if (parent?.GetOrCreateChild($"ST_{name}", TREEITEMTYPES.TREEITEMTYPE_PLCDUTSTRUCT) is not { } dutItem) 114 | { 115 | return null; 116 | } 117 | 118 | if (dutItem is not ITcPlcDeclaration dutDecl) 119 | { 120 | return null; 121 | } 122 | 123 | dutDecl.DeclarationText = 124 | $"{{attribute 'pack_mode' := '0'}}\nTYPE ST_{name} :\nSTRUCT\n{variables}END_STRUCT\nEND_TYPE"; 125 | 126 | return dutItem; 127 | } 128 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcSmTreeItemSubType.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Core; 2 | 3 | /// 4 | /// TwinCAT TreeItem SubTypes. 5 | /// 6 | public enum TcSmTreeItemSubType 7 | { 8 | TaskWithImage = 0, 9 | TaskWithoutImage = 1, 10 | ProfinetIoDevice = 115, 11 | EtherCatSimulation = 144 12 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcStringExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | /// 6 | /// Provides extension methods for string manipulation tailored for PLC (Programmable Logic Controller) compatibility. 7 | /// This static class contains helper methods to validate and modify strings to comply with specific PLC requirements. 8 | /// 9 | public static partial class TcStringExtension 10 | { 11 | [GeneratedRegex("[^a-zA-Z0-9_]+")] 12 | private static partial Regex InvalidCharacters(); 13 | 14 | /// Converts the input string to a format compatible with PLC naming conventions. 15 | /// The input string to be converted. 16 | /// Returns the PLC-compatible string. If the input string is already PLC-compatible, it is returned unchanged; otherwise, it is wrapped with backticks (`). 17 | public static string MakePlcCompatible(this string input) 18 | { 19 | return input.IsPlcCompatible() ? input : $"`{input}`"; 20 | } 21 | 22 | /// 23 | /// Checks if the input string is compatible with PLC naming conventions. 24 | /// 25 | /// The input string to verify. 26 | /// Returns true if the input string is PLC compatible; otherwise, false. 27 | public static bool IsPlcCompatible(this string input) 28 | { 29 | return !InvalidCharacters().IsMatch(input); 30 | } 31 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/TcSysManagerExtension.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using OC.Assistant.Sdk; 3 | using TCatSysManagerLib; 4 | 5 | namespace OC.Assistant.Core; 6 | 7 | /// 8 | /// Extension for the interface. 9 | /// 10 | public static class TcSysManagerExtension 11 | { 12 | /// 13 | /// Saves the associated with the given . 14 | /// 15 | /// The interface. 16 | public static void SaveProject(this ITcSysManager15 sysManager) 17 | { 18 | (sysManager.VsProject as Project)?.Save(); 19 | } 20 | 21 | /// 22 | /// Gets an by the given name. 23 | /// 24 | /// The interface. 25 | /// The name of the root . 26 | /// The name of the to find. 27 | public static ITcSmTreeItem? TryGetItem(this ITcSysManager15 sysManager, string rootItemName, string? name) 28 | { 29 | var path = $"{rootItemName}^{name}"; 30 | if (sysManager.TryLookupTreeItem(path, out var item)) return item; 31 | Logger.LogError(typeof(TcSysManagerExtension), $"{path} not found"); 32 | return null; 33 | } 34 | 35 | /// 36 | /// Gets an enumeration of type . 37 | /// 38 | /// The interface. 39 | /// The name of the root . 40 | public static IEnumerable TryGetItems(this ITcSysManager15 sysManager, string rootItemName) 41 | { 42 | if (!sysManager.TryLookupTreeItem(rootItemName, out var rootItem)) 43 | { 44 | Logger.LogError(typeof(TcSysManagerExtension), $"RootItem {rootItemName} not found"); 45 | yield break; 46 | } 47 | 48 | foreach (var item in rootItem.Cast()) 49 | { 50 | yield return item; 51 | } 52 | } 53 | 54 | /// 55 | /// Gets the plc project as . 56 | /// 57 | /// The interface. 58 | public static ITcSmTreeItem? TryGetPlcProject(this ITcSysManager15 sysManager) 59 | { 60 | return sysManager 61 | .TryGetItem(TcShortcut.PLC, XmlFile.Instance.PlcProjectName)? 62 | .Cast() 63 | .FirstOrDefault(item => item.ItemType == (int) TREEITEMTYPES.TREEITEMTYPE_PLCAPP); 64 | } 65 | 66 | /// 67 | /// Gets the plc instance as . 68 | /// 69 | /// The interface. 70 | public static ITcSmTreeItem? TryGetPlcInstance(this ITcSysManager15 sysManager) 71 | { 72 | return sysManager 73 | .TryGetItem(TcShortcut.PLC, XmlFile.Instance.PlcProjectName)? 74 | .Cast() 75 | .FirstOrDefault(item => item.ItemType == (int) TREEITEMTYPES.TREEITEMTYPE_TCOMPLCOBJECT); 76 | } 77 | 78 | /// 79 | /// Updates or creates an IoDevice. 80 | /// 81 | /// The interface. 82 | /// The root name of the IoDevice. 83 | /// The xti file path of the IoDevice. 84 | /// The updated or created IoDevice as . 85 | public static ITcSmTreeItem? UpdateIoDevice(this ITcSysManager15 sysManager, string deviceName, string xtiFilePath) 86 | { 87 | if (sysManager.TryLookupTreeItem($"{TcShortcut.IO_DEVICE}^{deviceName}", out _)) 88 | { 89 | sysManager.LookupTreeItem(TcShortcut.IO_DEVICE).DeleteChild(deviceName); 90 | } 91 | 92 | return sysManager.LookupTreeItem(TcShortcut.IO_DEVICE).ImportChild(xtiFilePath); 93 | } 94 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/XmlFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Xml.Linq; 3 | 4 | namespace OC.Assistant.Core; 5 | 6 | public class XmlFile 7 | { 8 | private const string DEFAULT_FILE_NAME = "OC.Assistant.xml"; 9 | private static readonly Lazy LazyInstance = new(() => new XmlFile()); 10 | private XDocument? _doc; 11 | 12 | /// 13 | /// The private constructor. 14 | /// 15 | private XmlFile() 16 | { 17 | } 18 | 19 | /// 20 | /// Singleton instance of the . 21 | /// 22 | public static XmlFile Instance => LazyInstance.Value; 23 | 24 | /// 25 | /// Gets the path of the xml file. Can be null of not connected. 26 | /// 27 | public string? Path { get; private set; } 28 | 29 | /// 30 | /// Sets the directory of the xml file.
31 | /// The is set to the directory combined with the . 32 | ///
33 | public void SetDirectory(string value) 34 | { 35 | Path = System.IO.Path.Combine(value, DEFAULT_FILE_NAME); 36 | Reload(); 37 | } 38 | 39 | /// 40 | /// Is raised when the xml file has been reloaded. 41 | /// 42 | public event Action? Reloaded; 43 | 44 | /// 45 | /// Reloads the xml file. 46 | /// 47 | public void Reload() 48 | { 49 | if (Path is null) return; 50 | if (File.Exists(Path)) 51 | { 52 | _doc = XDocument.Load(Path); 53 | Reloaded?.Invoke(); 54 | return; 55 | } 56 | 57 | _doc = new XDocument( 58 | new XElement(XmlTags.ROOT, 59 | new XElement(XmlTags.SETTINGS, 60 | new XElement(XmlTags.PLC_PROJECT_NAME, "OC"), 61 | new XElement(XmlTags.PLC_TASK_NAME, "PlcTask"), 62 | new XElement(XmlTags.TASK_AUTO_UPDATE)), 63 | new XElement(XmlTags.PLUGINS), 64 | new XElement(XmlTags.PROJECT, 65 | new XElement(XmlTags.HIL), 66 | new XElement(XmlTags.MAIN)))); 67 | 68 | _doc.Save(Path); 69 | Reloaded?.Invoke(); 70 | } 71 | 72 | /// 73 | /// Saves the current configuration. 74 | /// 75 | public void Save() 76 | { 77 | if (Path is null) return; 78 | _doc?.Save(Path); 79 | } 80 | 81 | /// 82 | /// Returns the . 83 | /// 84 | public XElement? Settings => _doc?.Root?.Element(XmlTags.SETTINGS); 85 | 86 | /// 87 | /// Returns the . 88 | /// 89 | public XElement? Plugins => _doc?.Root?.Element(XmlTags.PLUGINS); 90 | 91 | /// 92 | /// Returns the . 93 | /// 94 | public XElement? Project => _doc?.Root?.Element(XmlTags.PROJECT); 95 | 96 | /// 97 | /// Tries to get a child from a given parent .
98 | /// Creates the child if it doesn't exist. 99 | ///
100 | /// The parent . 101 | /// The name of the child to get or create. 102 | /// The child with the given name. 103 | public static XElement GetOrCreateChild(XElement? parent, string childName) 104 | { 105 | var element = parent?.Element(childName); 106 | if (element is not null) return element; 107 | element = new XElement(childName); 108 | parent?.Add(element); 109 | return element; 110 | } 111 | 112 | /// 113 | /// Gets or sets the PlcProjectName value. 114 | /// 115 | public string PlcProjectName 116 | { 117 | get => Settings?.Element(XmlTags.PLC_PROJECT_NAME)?.Value ?? ""; 118 | set 119 | { 120 | var element = GetOrCreateChild(Settings, XmlTags.PLC_PROJECT_NAME); 121 | element.Value = value; 122 | Save(); 123 | } 124 | } 125 | 126 | /// 127 | /// Gets or sets the PlcTaskName value. 128 | /// 129 | public string PlcTaskName 130 | { 131 | get => Settings?.Element(XmlTags.PLC_TASK_NAME)?.Value ?? ""; 132 | set 133 | { 134 | var element = GetOrCreateChild(Settings, XmlTags.PLC_TASK_NAME); 135 | element.Value = value; 136 | Save(); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /OC.Assistant/Core/XmlTags.cs: -------------------------------------------------------------------------------- 1 | using OC.Assistant.Sdk.Plugin; 2 | 3 | namespace OC.Assistant.Core; 4 | 5 | /// 6 | /// Predefined strings for the xml file. 7 | /// 8 | public struct XmlTags 9 | { 10 | /// 11 | /// The name of the root node. 12 | /// 13 | public const string ROOT = "Config"; 14 | 15 | /// 16 | /// The node name for the plugin category. 17 | /// 18 | public const string PLUGINS = "Plugins"; 19 | 20 | /// 21 | /// The node name for the plc project category. 22 | /// 23 | public const string PROJECT = "Project"; 24 | 25 | /// 26 | /// The node name for the general settings. 27 | /// 28 | public const string SETTINGS = "Settings"; 29 | 30 | /// 31 | /// The node name for the projectName setting. 32 | /// 33 | public const string PLC_PROJECT_NAME = "PlcProjectName"; 34 | 35 | /// 36 | /// The node name for the taskName setting. 37 | /// 38 | public const string PLC_TASK_NAME = "PlcTaskName"; 39 | 40 | /// 41 | /// The node name for the TaskAutoUpdate setting. 42 | /// 43 | public const string TASK_AUTO_UPDATE = "TaskAutoUpdate"; 44 | 45 | /// 46 | /// The node name for Hil in the category. 47 | /// 48 | public const string HIL = "Hil"; 49 | 50 | /// 51 | /// The node name for the plc project root in the category. 52 | /// 53 | public const string MAIN = "Main"; 54 | 55 | /// 56 | /// The node name for a plugin in the category. 57 | /// 58 | public const string PLUGIN = "Plugin"; 59 | 60 | /// 61 | /// The node name for parameters in the category. 62 | /// 63 | public const string PLUGIN_PARAMETER = nameof(IPluginController.Parameter); 64 | 65 | /// 66 | /// The node name for the input structure in the category. 67 | /// 68 | public const string PLUGIN_INPUT_STRUCT = nameof(IPluginController.InputStructure); 69 | 70 | /// 71 | /// The node name for output structure in the category. 72 | /// 73 | public const string PLUGIN_OUTPUT_STRUCT = nameof(IPluginController.OutputStructure); 74 | 75 | /// 76 | /// The name of the name attribute for a . 77 | /// 78 | public const string PLUGIN_NAME = "Name"; 79 | 80 | /// 81 | /// The name of the type attribute for a . 82 | /// 83 | public const string PLUGIN_TYPE = "Type"; 84 | 85 | /// 86 | /// The name of the ioType attribute for a . 87 | /// 88 | public const string PLUGIN_IO_TYPE = "IoType"; 89 | 90 | /// 91 | /// The name of the predefined for the input address 92 | /// when is set to . 93 | /// 94 | public const string PLUGIN_PARAMETER_INPUT_ADDRESS = nameof(IPluginController.InputAddress); 95 | 96 | /// 97 | /// The name of the predefined for the output address 98 | /// when is set to . 99 | /// 100 | public const string PLUGIN_PARAMETER_OUTPUT_ADDRESS = nameof(IPluginController.OutputAddress); 101 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/EtherCat/EtherCatGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Xml.Linq; 3 | using EnvDTE; 4 | using OC.Assistant.Core; 5 | using OC.Assistant.Sdk; 6 | using TCatSysManagerLib; 7 | 8 | namespace OC.Assistant.Generator.EtherCat; 9 | 10 | /// 11 | /// Represents a generator to parse an EtherCAT bus and create linked variables. 12 | /// 13 | internal class EtherCatGenerator 14 | { 15 | private readonly DTE _dte; 16 | private readonly List _instance = []; 17 | private readonly string? _projectFolder; 18 | private readonly string _folderName; 19 | 20 | /// 21 | /// Instance of the . 22 | /// 23 | /// The interface of the connected project. 24 | /// The plc folder locating the created GVL(s). 25 | public EtherCatGenerator(DTE dte, string folderName) 26 | { 27 | _dte = dte; 28 | _projectFolder = dte.GetProjectFolder(); 29 | _folderName = folderName; 30 | } 31 | 32 | /// 33 | /// Parses EtherCAT bus node(s) and creates GVL(s) with linked variables. 34 | /// 35 | /// The of the plc project. 36 | public void Generate(ITcSmTreeItem plcProjectItem) 37 | { 38 | var tcSysManager =_dte.GetTcSysManager(); 39 | 40 | if (tcSysManager is null) return; 41 | 42 | //Get io area of project 43 | if (!tcSysManager.TryLookupTreeItem(TcShortcut.IO_DEVICE, out var ioItem)) 44 | { 45 | return; 46 | } 47 | 48 | foreach (ITcSmTreeItem item in ioItem) 49 | { 50 | //Is not etherCat simulation 51 | if (item.ItemSubType != (int) TcSmTreeItemSubType.EtherCatSimulation) continue; 52 | 53 | //Is disabled 54 | if (item.Disabled == DISABLED_STATE.SMDS_DISABLED) continue; 55 | 56 | //Export etherCat simulation to xti file 57 | var name = item.Name.TcRemoveBrackets(); 58 | var xtiFile = $"{AppData.Path}\\{name}.xti"; 59 | if (File.Exists(xtiFile)) File.Delete(xtiFile); 60 | item.Parent.ExportChild(item.Name, xtiFile); 61 | 62 | //Parse etherCat and implement plc devices with tcLink attributes 63 | ParseXti(XDocument.Load(xtiFile)); 64 | Implement(name, plcProjectItem); 65 | 66 | //Delete xti file 67 | File.Delete(xtiFile); 68 | } 69 | } 70 | 71 | private void ParseXti(XContainer xtiDocument) 72 | { 73 | var eCatName = xtiDocument.Element("TcSmItem")?.Element("Device")?.Attribute("RemoteName")?.Value.MakePlcCompatible(); 74 | if (eCatName is null) return; 75 | 76 | _instance.Clear(); 77 | 78 | var activeBoxes = (from elem in xtiDocument.Descendants("Box") 79 | where elem.Attribute("Disabled") is null 80 | select elem).ToList(); 81 | 82 | var eCatCollection = TcEtherCatTemplates.ToList(); 83 | var missingTypes = new List(); 84 | 85 | foreach (var box in activeBoxes) 86 | { 87 | var type = box.Element("EcatSimuBox")?.Attribute("Type")?.Value ?? ""; 88 | if (IgnoreList.Any(ignoredType => ignoredType is not null && type.StartsWith(ignoredType))) continue; 89 | 90 | var nativeName = box.Element("Name")?.Value ?? ""; 91 | var name = PlcCompatibleString(nativeName); 92 | var id = box.Attribute("Id")?.Value; 93 | var productCode = box.Element("EcatSimuBox")?.Element("BoxSettings")?.Attribute("ProductCode")?.Value; 94 | var template = eCatCollection.FirstOrDefault(x => type.StartsWith(x.ProductDescription)); 95 | 96 | if (template is null) 97 | { 98 | foreach (var variable in new EtherCatVariables(name, box)) 99 | { 100 | _instance.Add(new EtherCatInstance(variable)); 101 | } 102 | 103 | if (string.IsNullOrEmpty(type)) 104 | { 105 | type = $"unknown type {productCode}"; 106 | } 107 | 108 | if (missingTypes.Any(x => x == type)) continue; 109 | missingTypes.Add(type); 110 | Logger.LogWarning(this, $"{nativeName}: Missing EtherCAT type in bus {eCatName}. {type} not found in any *.ethml file"); 111 | continue; 112 | } 113 | 114 | _instance.Add(new EtherCatInstance(id, name, template)); 115 | } 116 | } 117 | 118 | private void Implement(string name, ITcSmTreeItem plcProjectItem) 119 | { 120 | var hilFolder = plcProjectItem.GetOrCreateChild(_folderName, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 121 | var busFolder = hilFolder?.GetOrCreateChild(name, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 122 | var prg = busFolder?.GetOrCreateChild($"PRG_{name}", TREEITEMTYPES.TREEITEMTYPE_PLCPOUPROG); 123 | 124 | var variables = _instance 125 | .Aggregate("", (current, next) => current + $"{next.DeclarationText}\n"); 126 | 127 | busFolder.CreateGvl(name, variables); 128 | 129 | // ReSharper disable once SuspiciousTypeConversion.Global 130 | if (prg is ITcPlcImplementation impl) 131 | { 132 | impl.ImplementationText = _instance 133 | .Where(x => x.CyclicCall) 134 | .Aggregate("", (current, device) => current + $"GVL_{name}.{device.InstanceName}();\n"); 135 | } 136 | 137 | XmlFile.AddHilProgram(name); 138 | } 139 | 140 | private IEnumerable TcEtherCatTemplates 141 | { 142 | get 143 | { 144 | return Directory.GetFiles($"{_projectFolder}", "*.ethml") 145 | .Select(XDocument.Load).SelectMany(doc => doc.Root?.Elements("Device") 146 | .Select(device => new EtherCatTemplate(device)) ?? new List()); 147 | } 148 | } 149 | 150 | private IEnumerable IgnoreList 151 | { 152 | get 153 | { 154 | return Directory.GetFiles($"{_projectFolder}", "*.ethml") 155 | .Select(XDocument.Load).SelectMany(doc => doc.Root?.Element("Ignore")?.Elements("Device") 156 | .Select(device => device.Attribute("ProductDescription")?.Value) ?? new List()); 157 | } 158 | } 159 | 160 | private static string PlcCompatibleString(string name) 161 | { 162 | //First remove the device type, e.g. KF2.1 (EL1008) => KF2.1 163 | var result = name.TcRemoveBrackets(); 164 | 165 | //Then replace any special character 166 | return result.TcPlcCompatibleString(); 167 | } 168 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/EtherCat/EtherCatInstance.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Generator.EtherCat; 2 | 3 | /// 4 | /// Class representing the plc instance of a EtherCAT linked variable, 5 | /// structure or function block. 6 | /// 7 | internal class EtherCatInstance 8 | { 9 | /// 10 | /// Constructor for template. 11 | /// 12 | /// The box id of the etherCat device. 13 | /// The instance name. 14 | /// The etherCat template. 15 | public EtherCatInstance(string? id, string name, EtherCatTemplate template) 16 | { 17 | InstanceName = name; 18 | 19 | DeclarationText = template.DeclarationTemplate 20 | .Replace(Tags.NAME, name) 21 | .Replace(Tags.BOX_NO, id); 22 | 23 | CyclicCall = template.CyclicCall; 24 | } 25 | 26 | /// 27 | /// Constructor for generic variable. 28 | /// 29 | /// Information of a single etherCat variable. 30 | public EtherCatInstance(EtherCatVariable variable) 31 | { 32 | InstanceName = variable.Name; 33 | var attribute = $"{{attribute 'TcLinkTo' := '{variable.LinkTo}'}}"; 34 | DeclarationText = $"{attribute}\n{variable.Name} {variable.Type};"; 35 | CyclicCall = false; 36 | } 37 | 38 | /// 39 | /// The plc instance name. 40 | /// 41 | public string InstanceName { get; } 42 | 43 | /// 44 | /// The plc declaration text including attributes. 45 | /// 46 | public string DeclarationText { get; } 47 | 48 | /// 49 | /// True when the plc instance is a function block and needs a cyclic call. 50 | /// 51 | public bool CyclicCall { get; } 52 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/EtherCat/EtherCatTemplate.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Xml.Linq; 3 | 4 | namespace OC.Assistant.Generator.EtherCat; 5 | 6 | internal partial class EtherCatTemplate 7 | { 8 | public string ProductDescription { get; } 9 | public string DeclarationTemplate { get; } 10 | public bool CyclicCall { get; } 11 | 12 | public EtherCatTemplate(XElement device) 13 | { 14 | ProductDescription = device.Attribute("ProductDescription")?.Value ?? ""; 15 | 16 | DeclarationTemplate = LeadingWhitespace() 17 | .Replace(device.Element("Declaration")?.Value ?? "", ""); 18 | 19 | CyclicCall = FunctionBlock().Match(DeclarationTemplate).Success; 20 | } 21 | 22 | [GeneratedRegex(@"^[\t ]+", RegexOptions.Multiline)] 23 | private static partial Regex LeadingWhitespace(); 24 | 25 | [GeneratedRegex(@"\$NAME\$\s*:\s*FB_\w+")] 26 | private static partial Regex FunctionBlock(); 27 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/EtherCat/EtherCatVariable.cs: -------------------------------------------------------------------------------- 1 | using OC.Assistant.Sdk; 2 | 3 | namespace OC.Assistant.Generator.EtherCat; 4 | 5 | /// 6 | /// Class representing a single EtherCAT linked variable. 7 | /// 8 | /// The name of the variable. 9 | /// The type of the variable. 10 | /// The link information for the 'TcLinkTo' attribute. 11 | internal class EtherCatVariable(string name, string type, string linkTo) 12 | { 13 | /// 14 | /// The name of the variable. 15 | /// 16 | public string Name { get; } = name.TcPlcCompatibleString(); 17 | 18 | /// 19 | /// The type of the variable. 20 | /// 21 | public string Type { get; } = type.Replace(TcType.Bit.Name(), TcType.Bool.Name()); 22 | 23 | /// 24 | /// The link information for the 'TcLinkTo' attribute. 25 | /// 26 | public string LinkTo { get; } = linkTo; 27 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/EtherCat/EtherCatVariables.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Core; 3 | 4 | namespace OC.Assistant.Generator.EtherCat; 5 | 6 | /// 7 | /// Represents an extended list of type . 8 | /// 9 | internal class EtherCatVariables : List 10 | { 11 | private readonly List _uniquePdoNames = []; 12 | 13 | /// 14 | /// Parses an EtherCAT box into a list of . 15 | /// 16 | /// The managed (plc compatible) name of the EtherCAT box. 17 | /// representing an EtherCAT box. 18 | public EtherCatVariables(string name, XElement box) 19 | { 20 | ParseBox(name, box); 21 | } 22 | 23 | private void ParseBox(string boxName, XElement box) 24 | { 25 | var id = box.Attribute("Id")?.Value; 26 | var settings = box.Element("EcatSimuBox")?.Element("BoxSettings"); 27 | var outputSize = settings?.Attribute("OutputSize"); 28 | var inputSize = settings?.Attribute("InputSize"); 29 | 30 | var pdoNodes = box.Descendants("Pdo") 31 | .Where(pdo => !string.IsNullOrEmpty(pdo.Attribute("SyncMan")?.Value)); 32 | 33 | foreach (var pdoNode in pdoNodes) 34 | { 35 | var pdoName = pdoNode.Attribute("Name")?.Value ?? ""; 36 | var uniquePdoName = GetUniquePdoName(pdoName); 37 | var entryNodes = pdoNode.Descendants("Entry"); 38 | var syncMan = pdoNode.Attribute("SyncMan")?.Value; 39 | 40 | /* 41 | Every Pdo element with a SyncMan attribute represents an in- or output 42 | - SyncMan is '2' or '3' when the box has inputs and outputs 43 | - SyncMan is '0' when the box has only inputs or only outputs 44 | -> we need to evaluate if InputSize or OutputSize attribute is available 45 | */ 46 | var inOut = syncMan switch 47 | { 48 | "0" when outputSize is not null => "AT %I*", 49 | "0" when inputSize is not null => "AT %Q*", 50 | "2" => "AT %I*", 51 | "3" => "AT %Q*", 52 | _ => null 53 | }; 54 | 55 | if (inOut is null) continue; 56 | 57 | foreach (var entryNode in entryNodes.Where(x => !string.IsNullOrEmpty(x.Attribute("Index")?.Value))) 58 | { 59 | var nativeName = entryNode.Attribute("Name")?.Value ?? ""; 60 | 61 | // Two underscores in a row usually means the entry is part of a structure 62 | // e.g. 'StructName__StructName EntryName' 63 | // -> we only need the substring starting at the end of the underscores 64 | var index = nativeName.IndexOf("__", StringComparison.Ordinal); 65 | var cleanedName = index > -1 ? nativeName[index..] : nativeName; 66 | 67 | //Shorten the name by replacing In- and Output with I and Q 68 | cleanedName = cleanedName.Replace("Output ", "Q").Replace("Input ", "I"); 69 | 70 | var name = $"{boxName}_{uniquePdoName}_{cleanedName}"; 71 | var type = $"{inOut} : {entryNode.Element("Type")?.Value}"; 72 | var linkTo = $"{TcShortcut.BOX}({id})^{uniquePdoName}^{nativeName.Replace("__", "^")}"; 73 | 74 | Add(new EtherCatVariable(name, type, linkTo)); 75 | } 76 | } 77 | } 78 | 79 | private string GetUniquePdoName(string originalName) 80 | { 81 | var modifiedName = originalName; 82 | var counter = 1; 83 | 84 | while (_uniquePdoNames.Contains(modifiedName)) 85 | { 86 | modifiedName = $"{originalName}_{counter}"; 87 | counter++; 88 | } 89 | 90 | _uniquePdoNames.Add(modifiedName); 91 | return modifiedName; 92 | } 93 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Generators/DeviceTemplate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using OC.Assistant.Core; 3 | using OC.Assistant.Sdk; 4 | using TCatSysManagerLib; 5 | 6 | namespace OC.Assistant.Generator.Generators; 7 | 8 | /// 9 | /// Generator for device templates. 10 | /// 11 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 12 | public static class DeviceTemplate 13 | { 14 | /// 15 | /// Creates a device template. 16 | /// 17 | /// The parent . Usually the plc project or a plc folder. 18 | /// The name of the device. 19 | public static void Create(ITcSmTreeItem parent, string name) 20 | { 21 | if (string.IsNullOrWhiteSpace(name)) 22 | { 23 | Logger.LogWarning(typeof(DeviceTemplate), "Name must not be empty"); 24 | return; 25 | } 26 | 27 | if (!name.IsPlcCompatible()) 28 | { 29 | Logger.LogWarning(typeof(DeviceTemplate), $"{name} is not a valid name. Allowed characters are a-z A-Z 0-9 and underscore"); 30 | return; 31 | } 32 | 33 | if (parent.TryLookupChild(name, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER) is not null) 34 | { 35 | Logger.LogWarning(typeof(DeviceTemplate), $"{name} already exists"); 36 | return; 37 | } 38 | 39 | //Create folder 40 | if (parent.GetOrCreateChild(name, 41 | TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER) is not {} folder) return; 42 | 43 | //Create function block 44 | if (folder.GetOrCreateChild($"FB_{name}", 45 | TREEITEMTYPES.TREEITEMTYPE_PLCPOUFB) is not {} fb) return; 46 | 47 | //Create structs 48 | if (folder.GetOrCreateChild($"ST_{name}_Control", 49 | TREEITEMTYPES.TREEITEMTYPE_PLCDUTSTRUCT) is not {} control) return; 50 | if (folder.GetOrCreateChild($"ST_{name}_Status", 51 | TREEITEMTYPES.TREEITEMTYPE_PLCDUTSTRUCT) is not {} status) return; 52 | if (folder.GetOrCreateChild($"ST_{name}_Config", 53 | TREEITEMTYPES.TREEITEMTYPE_PLCDUTSTRUCT) is not {} config) return; 54 | 55 | //Create methods 56 | if (fb.GetOrCreateChild("InitRun", 57 | TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD) is not {} initRun) return; 58 | if (fb.GetOrCreateChild("Cycle", 59 | TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD) is not {} cycle) return; 60 | if (fb.GetOrCreateChild("GetControlData", 61 | TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD) is not {} getControl) return; 62 | if (fb.GetOrCreateChild("SetStatusData", 63 | TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD) is not {} setStatus) return; 64 | 65 | //Fill with content 66 | fb.SetContent(DECLARATION.Replace(Tags.NAME, name), IMPLEMENTATION); 67 | control.SetContent(CONTROL_STRUCT.Replace(Tags.NAME, name)); 68 | status.SetContent(STATUS_STRUCT.Replace(Tags.NAME, name)); 69 | config.SetContent(CONFIG_STRUCT.Replace(Tags.NAME, name)); 70 | initRun.SetContent(INIT_RUN_DECLARATION, INIT_RUN_IMPLEMENTATION); 71 | cycle.SetContent(CYCLE_DECLARATION, CYCLE_IMPLEMENTATION); 72 | getControl.SetContent(GET_CONTROL_DATA_DECLARATION, GET_CONTROL_DATA_IMPLEMENTATION); 73 | setStatus.SetContent(SET_STATUS_DATA_DECLARATION, SET_STATUS_DATA_IMPLEMENTATION); 74 | 75 | Logger.LogInfo(typeof(DeviceTemplate), 76 | $"Device template {name} created. See folder {parent.Name} in TwinCAT project"); 77 | } 78 | 79 | private static void SetContent(this ITcSmTreeItem parent, string declText, string? implText = null) 80 | { 81 | if (parent is not ITcPlcDeclaration decl) return; 82 | decl.DeclarationText = declText; 83 | if (implText is null) return; 84 | 85 | if (parent is not ITcPlcImplementation impl) return; 86 | impl.ImplementationText = implText; 87 | } 88 | 89 | private const string CONTROL_STRUCT = 90 | """ 91 | {attribute 'pack_mode' := '0'} 92 | TYPE ST_$NAME$_Control: 93 | STRUCT 94 | //Custom device control structure (from fieldbus)... 95 | END_STRUCT 96 | END_TYPE 97 | """; 98 | 99 | private const string STATUS_STRUCT = 100 | """ 101 | {attribute 'pack_mode' := '0'} 102 | TYPE ST_$NAME$_Status: 103 | STRUCT 104 | //Custom device status structure (to fieldbus)... 105 | END_STRUCT 106 | END_TYPE 107 | """; 108 | 109 | private const string CONFIG_STRUCT = 110 | """ 111 | {attribute 'pack_mode' := '0'} 112 | TYPE ST_$NAME$_Config: 113 | STRUCT 114 | bForceMode : BOOL; //Ignore process data from fieldbus. 115 | bSwapProcessData : BOOL; //Reverse process data byte order from/to fieldbus. 116 | //Custom device config structure (parameters and settings)... 117 | END_STRUCT 118 | END_TYPE 119 | """; 120 | 121 | private const string DECLARATION = 122 | """ 123 | (* 124 | For Unity communication, extend a link from the OC_Core library. 125 | A link contains control and status variables: 126 | Control TwinCAT => Unity 127 | Status TwinCAT <= Unity 128 | 129 | Basic link: 130 | FB_LinkDevice Control and Status of type BYTE 131 | 132 | Extended links with additional data, all extending the FB_LinkDevice: 133 | FB_LinkDataByte ControlData and StatusData of type BYTE 134 | FB_LinkDataWord ControlData and StatusData of type WORD 135 | FB_LinkDataDWord ControlData and StatusData of type DWORD 136 | FB_LinkDataLWord ControlData and StatusData of type LWORD 137 | FB_LinkDataReal ControlData and StatusData of type REAL 138 | 139 | Example: 140 | The device is simulating a drive and writes a calculated position (fPosition : REAL) to Unity. 141 | Declaration FUNCTION_BLOCK FB_$NAME$ EXTENDS OC_Core.FB_LinkDataReal 142 | Implementation THIS^.ControlData := fPosition; 143 | *) 144 | {attribute 'reflection'} 145 | FUNCTION_BLOCK FB_$NAME$ EXTENDS OC_Core.FB_LinkDevice 146 | VAR_INPUT 147 | pControl : PVOID; //Pocess data from fieldbus. Size must be >= stControl. 148 | pStatus : PVOID; //Pocess data to fieldbus. Size must be >= stStatus. 149 | stControl : ST_$NAME$_Control; //Control structure. Is read from the fieldbus. 150 | stConfig : ST_$NAME$_Config; //Config structure. Contains parameters of the device. 151 | END_VAR 152 | VAR_OUTPUT 153 | stStatus : ST_$NAME$_Status; //Status structure. Is written to the fieldbus. 154 | END_VAR 155 | VAR 156 | //Is used in conjunction with attribute 'reflection' to indicate the function block's name at runtime. 157 | {attribute 'instance-path'} 158 | {attribute 'noinit'} 159 | _sPath : STRING; 160 | 161 | //Custom device variables... 162 | END_VAR 163 | """; 164 | 165 | private const string IMPLEMENTATION = 166 | """ 167 | InitRun(); 168 | 169 | GetControlData(); 170 | Cycle(); 171 | SetStatusData(); 172 | """; 173 | 174 | private const string INIT_RUN_DECLARATION = 175 | """ 176 | //Initializes device parameters. Is only called once. 177 | METHOD InitRun 178 | VAR_INST 179 | bInitRun : BOOL := TRUE; 180 | END_VAR 181 | """; 182 | 183 | private const string INIT_RUN_IMPLEMENTATION = 184 | """ 185 | IF NOT bInitRun THEN RETURN; END_IF 186 | bInitRun := FALSE; 187 | 188 | IF pControl = 0 OR pStatus = 0 THEN 189 | ADSLOGSTR(ADSLOG_MSGTYPE_WARN, CONCAT(sPath, ': %s'), 'Plc address not set.'); 190 | END_IF 191 | 192 | //Custom device initialization... 193 | 194 | """; 195 | 196 | private const string CYCLE_DECLARATION = 197 | """ 198 | //Processes cyclic device logic. 199 | METHOD Cycle 200 | """; 201 | 202 | private const string CYCLE_IMPLEMENTATION = 203 | """ 204 | //Custom device logic... 205 | 206 | """; 207 | 208 | private const string GET_CONTROL_DATA_DECLARATION = 209 | """ 210 | //Reads fieldbus process data and writes to the stControl structure. 211 | METHOD GetControlData 212 | """; 213 | 214 | private const string GET_CONTROL_DATA_IMPLEMENTATION = 215 | """ 216 | IF pControl <= 0 OR stConfig.bForceMode THEN RETURN; END_IF 217 | 218 | //This is just an example how to copy the fieldbus data to the stControl structure via memcpy 219 | F_Memcpy(ADR(stControl), pControl, SIZEOF(stControl), stConfig.bSwapProcessData); 220 | """; 221 | 222 | private const string SET_STATUS_DATA_DECLARATION = 223 | """ 224 | //Writes the stStatus structure to fieldbus process data. 225 | METHOD SetStatusData 226 | """; 227 | 228 | private const string SET_STATUS_DATA_IMPLEMENTATION = 229 | """ 230 | IF pStatus <= 0 THEN RETURN; END_IF 231 | 232 | //This is just an example how to copy the stStatus structure to the fieldbus data via memcpy 233 | F_Memcpy(pStatus, ADR(stStatus), SIZEOF(stStatus), stConfig.bSwapProcessData); 234 | """; 235 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Generators/Hil.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using OC.Assistant.Core; 3 | using OC.Assistant.Generator.EtherCat; 4 | using OC.Assistant.Generator.Profinet; 5 | using TCatSysManagerLib; 6 | 7 | namespace OC.Assistant.Generator.Generators; 8 | 9 | /// 10 | /// Generator for HiL signals. 11 | /// 12 | internal static class Hil 13 | { 14 | private const string FOLDER_NAME = nameof(Hil); 15 | 16 | /// 17 | /// Updates all HiL structures. 18 | /// 19 | public static void Update(DTE dte, ITcSmTreeItem plcProjectItem) 20 | { 21 | XmlFile.ClearHilPrograms(); 22 | 23 | if (plcProjectItem.TryLookupChild(FOLDER_NAME) is not null) 24 | { 25 | plcProjectItem.DeleteChild(FOLDER_NAME); 26 | } 27 | 28 | new ProfinetGenerator(dte, FOLDER_NAME).Generate(plcProjectItem); 29 | new EtherCatGenerator(dte, FOLDER_NAME).Generate(plcProjectItem); 30 | } 31 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Generators/Project.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.RegularExpressions; 3 | using System.Xml.Linq; 4 | using OC.Assistant.Core; 5 | using TCatSysManagerLib; 6 | 7 | namespace OC.Assistant.Generator.Generators; 8 | 9 | /// 10 | /// Generator for the plc project. 11 | /// 12 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 13 | internal static partial class Project 14 | { 15 | /// 16 | /// Updates the given plc project. 17 | /// 18 | public static void Update(ITcSmTreeItem plcProjectItem) 19 | { 20 | var main = XmlFile.Main; 21 | if (main is null) return; 22 | var instances = new List(); 23 | 24 | foreach (var child in main.Elements()) 25 | { 26 | switch (child.Name.LocalName) 27 | { 28 | case "Device": 29 | instances.Add(new PouInstance(child)); 30 | continue; 31 | case "Group": 32 | var childName = child.Attribute("Name")?.Value; 33 | instances.Add(new PouInstance(childName, childName)); 34 | CreateGroup(plcProjectItem, child); 35 | continue; 36 | } 37 | } 38 | 39 | CreateMainPrg(plcProjectItem, instances); 40 | } 41 | 42 | private static void CreateGroup(ITcSmTreeItem? parent, XElement? group, string? parentName = null) 43 | { 44 | if (group is null) return; 45 | var name = group.Attribute("Name")?.Value; 46 | var fbName = parentName is null ? name : $"{parentName}{name}"; 47 | var folder = parent?.GetOrCreateChild(name, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 48 | var instances = new List(); 49 | 50 | foreach (var child in group.Elements()) 51 | { 52 | switch (child.Name.LocalName) 53 | { 54 | case "Device": 55 | instances.Add(new PouInstance(child)); 56 | continue; 57 | case "Group": 58 | var childName = child.Attribute("Name")?.Value; 59 | instances.Add(new PouInstance(childName, $"{fbName}{childName}")); 60 | CreateGroup(folder, child, fbName); 61 | continue; 62 | } 63 | } 64 | 65 | CreateFb(folder, fbName, instances); 66 | } 67 | 68 | private static void CreateMainPrg(ITcSmTreeItem? parent, IReadOnlyCollection instances) 69 | { 70 | //Find or create main program 71 | var pou = parent?.FindChildRecursive("main", TREEITEMTYPES.TREEITEMTYPE_PLCPOUPROG); 72 | pou ??= parent?.GetOrCreateChild("MAIN", TREEITEMTYPES.TREEITEMTYPE_PLCPOUPROG); 73 | 74 | //Declaration 75 | const string declaration = "\tbInitRun : BOOL := TRUE;\n\tfbSystem : FB_System;\n"; 76 | pou.UpdateDeclaration(instances 77 | .Aggregate(declaration, (current, next) => $"{current}{next.DeclarationText}")); 78 | 79 | //Create or set InitRun method 80 | CreateInitRun(pou, instances, true); 81 | 82 | //InitRun() and fbSystem() 83 | var implementation = "\tInitRun();\n\tfbSystem();\n"; 84 | 85 | //HiL calls 86 | implementation = XmlFile.HilPrograms?. 87 | Aggregate(implementation, (current, next) => $"{current}\t{next}();\n"); 88 | 89 | //Instance calls 90 | implementation = instances 91 | .Aggregate(implementation, (current, next) => $"{current}{next.ImplementationText}"); 92 | 93 | //Implementation 94 | pou.UpdateImplementation(implementation); 95 | } 96 | 97 | private static void CreateFb(ITcSmTreeItem? parent, string? name, IReadOnlyCollection instances) 98 | { 99 | var pou = parent?.GetOrCreateChild(name, TREEITEMTYPES.TREEITEMTYPE_PLCPOUFB); 100 | 101 | //Declaration 102 | pou.UpdateDeclaration(instances 103 | .Aggregate("", (current, next) => $"{current}{next.DeclarationText}")); 104 | 105 | //Create or set InitRun method 106 | CreateInitRun(pou, instances); 107 | 108 | //Implementation 109 | pou.UpdateImplementation(instances 110 | .Aggregate("\tInitRun();\n", (current, next) => $"{current}{next.ImplementationText}")); 111 | } 112 | 113 | private static void CreateInitRun(ITcSmTreeItem? parent, IReadOnlyCollection instances, bool isProgram = false) 114 | { 115 | //Create method 'InitRun' 116 | var method = parent?.GetOrCreateChild("InitRun", TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD); 117 | 118 | //Set declaration 119 | if (!isProgram) 120 | { 121 | method?.UpdateDeclaration("\tbInitRun : BOOL := TRUE;\n"); 122 | } 123 | 124 | //Set implementation 125 | var text = instances.Aggregate("\tIF NOT bInitRun THEN RETURN; END_IF\n\tbInitRun := FALSE;\n", 126 | (current, next) => $"{current}{next.InitRunText}"); 127 | method.UpdateImplementation(text); 128 | } 129 | 130 | private static void UpdateDeclaration(this ITcSmTreeItem? item, string? text) 131 | { 132 | if (item is not ITcPlcDeclaration declaration) return; 133 | ReplaceGeneratedText(declaration, text, item.ItemType == (int) TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD); 134 | } 135 | 136 | private static void UpdateImplementation(this ITcSmTreeItem? item, string? text) 137 | { 138 | if (item is not ITcPlcImplementation implementation) return; 139 | ReplaceGeneratedText(implementation, text); 140 | } 141 | 142 | private static void ReplaceGeneratedText(ITcPlcImplementation? impl, string? text) 143 | { 144 | if (impl is null || string.IsNullOrEmpty(text)) return; 145 | 146 | var existingText = impl.ImplementationText; 147 | var generatedText = $"{{region generated code}}\n{text}{{endregion}}"; 148 | 149 | if (GetGeneratedText(existingText) == generatedText) return; 150 | impl.ImplementationText = $"{generatedText}\n{RemoveGeneratedText(existingText)}"; 151 | } 152 | 153 | private static void ReplaceGeneratedText(ITcPlcDeclaration? decl, string? text, bool isMethod = false) 154 | { 155 | if (decl is null || string.IsNullOrEmpty(text)) return; 156 | 157 | var existingText = decl.DeclarationText; 158 | var varType = isMethod ? "VAR_INST" : "VAR_INPUT"; 159 | var generatedText = $"{{region generated code}}\n{varType}\n{text}END_VAR\n{{endregion}}"; 160 | 161 | if (GetGeneratedText(existingText) == generatedText) return; 162 | decl.DeclarationText = $"{RemoveGeneratedText(existingText)}\n{generatedText}"; 163 | } 164 | 165 | [GeneratedRegex(@"\s*\{region generated code\}.*?\{endregion\}\s*", RegexOptions.Singleline)] 166 | private static partial Regex PatternRemoveGenerated(); 167 | 168 | [GeneratedRegex(@"{region generated code\}.*?\{endregion\}", RegexOptions.Singleline)] 169 | private static partial Regex PatternGetGenerated(); 170 | 171 | private static string GetGeneratedText(string input) 172 | { 173 | return PatternGetGenerated().Match(input).Value; 174 | } 175 | 176 | private static string RemoveGeneratedText(string input) 177 | { 178 | var result = PatternRemoveGenerated().Replace(input, "\n") 179 | //also remove empty variable declarations 180 | .Replace("\nVAR_INPUT\nEND_VAR", "") 181 | .Replace("\nVAR_OUTPUT\nEND_VAR", "") 182 | .Replace("\nVAR\nEND_VAR", ""); 183 | return result == "\n" ? "" : result; 184 | } 185 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Generators/Sil.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Xml.Linq; 3 | using OC.Assistant.Core; 4 | using OC.Assistant.Sdk; 5 | using OC.Assistant.Sdk.Plugin; 6 | using TCatSysManagerLib; 7 | 8 | namespace OC.Assistant.Generator.Generators; 9 | 10 | /// 11 | /// Generator for SiL signals. 12 | /// 13 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 14 | internal static class Sil 15 | { 16 | private const string FOLDER_NAME = nameof(Sil); 17 | 18 | /// 19 | /// Creates or removes a single SiL structure. 20 | /// 21 | /// The given plc project. 22 | /// The name of the SiL plugin. 23 | /// False: Updates the SiL structure. True: Deletes the SiL structure. 24 | public static void Update(ITcSmTreeItem plcProjectItem, string name, bool delete) 25 | { 26 | var sil = plcProjectItem.GetOrCreateChild(FOLDER_NAME, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 27 | if (sil?.TryLookupChild(name) is not null) sil.DeleteChild(name); 28 | if (delete) 29 | { 30 | if (sil?.ChildCount == 0) plcProjectItem.DeleteChild(sil.Name); 31 | return; 32 | } 33 | Generate(XmlFile.PluginElements?.First(x => x.Attribute(XmlTags.PLUGIN_NAME)?.Value == name), plcProjectItem); 34 | } 35 | 36 | /// 37 | /// Updates all SiL structures. 38 | /// 39 | /// The given plc project. 40 | public static void UpdateAll(ITcSmTreeItem plcProjectItem) 41 | { 42 | if (plcProjectItem.TryLookupChild(FOLDER_NAME) is not null) plcProjectItem.DeleteChild(FOLDER_NAME); 43 | if (XmlFile.PluginElements is null) return; 44 | foreach (var plugin in XmlFile.PluginElements) 45 | { 46 | Generate(plugin, plcProjectItem); 47 | } 48 | } 49 | 50 | private static void Generate(XElement? plugin, ITcSmTreeItem plcProjectItem) 51 | { 52 | if (plugin is null) return; 53 | if (!Enum.TryParse(plugin.Attribute(XmlTags.PLUGIN_IO_TYPE)?.Value, out IoType ioType)) return; 54 | if (ioType == IoType.None) return; 55 | var silFolder = plcProjectItem.GetOrCreateChild(FOLDER_NAME, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 56 | if (silFolder is null) return; 57 | 58 | switch (ioType) 59 | { 60 | case IoType.Address: 61 | AddressVariables(plugin, silFolder); 62 | break; 63 | case IoType.Struct: 64 | StructVariables(plugin, silFolder); 65 | break; 66 | } 67 | } 68 | 69 | private static void AddressVariables(XElement plugin, ITcSmTreeItem silFolder) 70 | { 71 | var pluginName = plugin.Attribute(XmlTags.PLUGIN_NAME)?.Value; 72 | if (pluginName is null) return; 73 | 74 | var pluginFolder = silFolder.GetOrCreateChild(pluginName, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 75 | if (pluginFolder is null) return; 76 | 77 | var gvlVariables = ""; 78 | 79 | //Inputs 80 | var request = plugin 81 | .Element(XmlTags.PLUGIN_PARAMETER)? 82 | .Element(XmlTags.PLUGIN_PARAMETER_INPUT_ADDRESS)?.Value.ToNumberList(); 83 | 84 | if (request is not null) 85 | { 86 | gvlVariables = request.Aggregate(gvlVariables, (current, t) => 87 | current + $"\tI{t}: {TcType.Byte.Name()};\n"); 88 | } 89 | 90 | //Outputs 91 | request = plugin 92 | .Element(XmlTags.PLUGIN_PARAMETER)? 93 | .Element(XmlTags.PLUGIN_PARAMETER_OUTPUT_ADDRESS)?.Value.ToNumberList(); 94 | 95 | if (request is null) return; 96 | gvlVariables = request.Aggregate(gvlVariables, (current, t) => 97 | current + $"\tQ{t}: {TcType.Byte.Name()};\n"); 98 | 99 | pluginFolder.CreateGvl(pluginName, gvlVariables); 100 | } 101 | 102 | private static void StructVariables(XElement plugin, ITcSmTreeItem silFolder) 103 | { 104 | var pluginName = plugin.Attribute(XmlTags.PLUGIN_NAME)?.Value; 105 | if (pluginName is null) return; 106 | 107 | var pluginFolder = silFolder.GetOrCreateChild(pluginName, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 108 | if (pluginFolder is null) return; 109 | 110 | var inputStruct = plugin.Element(XmlTags.PLUGIN_INPUT_STRUCT); 111 | var outputStruct = plugin.Element(XmlTags.PLUGIN_OUTPUT_STRUCT); 112 | 113 | //Input struct 114 | var variables = ""; 115 | variables = inputStruct?.Elements() 116 | .Aggregate(variables, (current, var) => 117 | current + $"\t{var.Element(XmlTags.PLUGIN_NAME)?.Value}: {var.Element(XmlTags.PLUGIN_TYPE)?.Value};\n"); 118 | pluginFolder.CreateDutStruct($"{pluginName}Inputs", variables); 119 | 120 | //Output struct 121 | variables = ""; 122 | variables = outputStruct?.Elements() 123 | .Aggregate(variables, (current, var) => 124 | current + $"\t{var.Element(XmlTags.PLUGIN_NAME)?.Value}: {var.Element(XmlTags.PLUGIN_TYPE)?.Value};\n"); 125 | pluginFolder.CreateDutStruct($"{pluginName}Outputs", variables); 126 | 127 | //GVL 128 | pluginFolder.CreateGvl(pluginName, 129 | $"\tInputs : ST_{pluginName}Inputs;\n\tOutputs : ST_{pluginName}Outputs;\n"); 130 | } 131 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Generators/Task.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Xml.Linq; 3 | using EnvDTE; 4 | using OC.Assistant.Core; 5 | using OC.Assistant.Sdk; 6 | using TCatSysManagerLib; 7 | 8 | namespace OC.Assistant.Generator.Generators; 9 | 10 | /// 11 | /// Generator for Task variables. 12 | /// 13 | internal static class Task 14 | { 15 | /// 16 | /// Creates variables for a task, based on the plc instance. 17 | /// 18 | public static void CreateVariables(DTE? dte) 19 | { 20 | var tcSysManager = dte?.GetTcSysManager(); 21 | tcSysManager?.SaveProject(); 22 | 23 | //Get plc instance 24 | var instance = tcSysManager?.TryGetPlcInstance(); 25 | if (instance is null) return; 26 | 27 | //Collect all symbols with 'simulation_interface' attribute 28 | var filter = instance.GetSymbolsWithAttribute("simulation_interface"); 29 | 30 | //Get task 31 | var task = tcSysManager?.TryGetItem(TcShortcut.TASK, XmlFile.XmlBase.PlcTaskName); 32 | if (task is null) 33 | { 34 | Logger.LogWarning(typeof(Task), "Task not found"); 35 | return; 36 | } 37 | 38 | //Task has no image 39 | if (task.ItemSubType == (int)TcSmTreeItemSubType.TaskWithoutImage) 40 | { 41 | Logger.LogWarning(typeof(Task), "Task has no image"); 42 | return; 43 | } 44 | 45 | var inputVariables = new List(); 46 | var outputVariables = new List(); 47 | 48 | var instanceVarGroups = instance.GetVarGroups(); 49 | 50 | //Collect variables from plc instance 51 | foreach (var varGroup in instanceVarGroups) 52 | { 53 | switch (varGroup.ItemSubType) 54 | { 55 | case 1: 56 | varGroup.CollectVariablesRecursive(inputVariables, filter); 57 | break; 58 | case 2: 59 | varGroup.CollectVariablesRecursive(outputVariables, filter); 60 | break; 61 | } 62 | } 63 | 64 | var taskVarGroups = task.GetVarGroups(); 65 | 66 | //Create and link task variables 67 | foreach (var varGroup in taskVarGroups) 68 | { 69 | switch (varGroup.ItemSubType) 70 | { 71 | case 1: 72 | varGroup.AddAndLinkVariables(outputVariables); 73 | break; 74 | case 2: 75 | varGroup.AddAndLinkVariables(inputVariables); 76 | break; 77 | } 78 | } 79 | 80 | Logger.LogInfo(typeof(Task), "Task variables have been updated."); 81 | } 82 | 83 | private static HashSet GetSymbolsWithAttribute(this ITcSmTreeItem instance, string attribute) 84 | { 85 | // ReSharper disable once SuspiciousTypeConversion.Global 86 | return XDocument.Parse(((ITcModuleInstance2) instance).ExportXml()) 87 | .Descendants("Symbol") 88 | .Where(symbol => 89 | symbol.Element("Properties")? 90 | .Element("Property")? 91 | .Element("Name")?.Value == attribute) 92 | .Select(symbol => symbol.Element("Name")?.Value) 93 | .Distinct() 94 | .ToHashSet(); 95 | } 96 | 97 | private static IEnumerable GetVarGroups(this IEnumerable item) 98 | { 99 | return item 100 | .Cast() 101 | .Where(varGroup => varGroup.ItemType == (int)TREEITEMTYPES.TREEITEMTYPE_VARGRP).ToList(); 102 | } 103 | 104 | private static void CollectVariablesRecursive(this IEnumerable item, ICollection variables, HashSet filter) 105 | { 106 | var childItems = item.Cast(); 107 | 108 | foreach (var childItem in childItems) 109 | { 110 | if (childItem.Name.EndsWith('.')) 111 | { 112 | childItem.CollectVariablesRecursive(variables, filter); 113 | continue; 114 | } 115 | 116 | if (filter.Contains(childItem.Name)) 117 | { 118 | variables.Add(childItem); 119 | } 120 | } 121 | } 122 | 123 | private static void DeleteAllVariables(this ITcSmTreeItem varGroup) 124 | { 125 | foreach (ITcSmTreeItem variable in varGroup) 126 | { 127 | varGroup.DeleteChild(variable.Name); 128 | } 129 | } 130 | 131 | private static void AddAndLinkVariables(this ITcSmTreeItem varGroup, List variables) 132 | { 133 | varGroup.DeleteAllVariables(); 134 | foreach (var variable in variables) 135 | { 136 | var xElement = XElement.Parse(variable.ProduceXml()); 137 | var type = xElement.Descendants("VarType").FirstOrDefault(); 138 | if (type is null) continue; 139 | // ReSharper disable once SuspiciousTypeConversion.Global 140 | var var = (ITcVariable2) varGroup.CreateChild(variable.Name, -1, null, type.Value); 141 | var.AddLinkToVariable(variable.PathName); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Menu.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /OC.Assistant/Generator/Menu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Xml.Linq; 4 | using OC.Assistant.Core; 5 | using OC.Assistant.Sdk; 6 | using EnvDTE; 7 | using TCatSysManagerLib; 8 | 9 | namespace OC.Assistant.Generator; 10 | 11 | public partial class Menu 12 | { 13 | public Menu() 14 | { 15 | InitializeComponent(); 16 | ProjectState.Events.Locked += isLocked => IsEnabled = !isLocked; 17 | ApiLocal.Interface.ConfigReceived += ApiOnConfigReceived; 18 | } 19 | 20 | private void CreateProjectOnClick(object sender, RoutedEventArgs e) 21 | { 22 | DteSingleThread.Run(dte => 23 | { 24 | if (GetPlcProject(dte) is not {} plcProjectItem) return; 25 | Core.XmlFile.Instance.Reload(); 26 | Generators.Hil.Update(dte, plcProjectItem); 27 | Generators.Project.Update(plcProjectItem); 28 | Logger.LogInfo(this, "Project update finished."); 29 | }); 30 | } 31 | 32 | private void CreatePluginsOnClick(object sender, RoutedEventArgs e) 33 | { 34 | DteSingleThread.Run(dte => 35 | { 36 | if (GetPlcProject(dte) is not {} plcProjectItem) return; 37 | Core.XmlFile.Instance.Reload(); 38 | Generators.Sil.UpdateAll(plcProjectItem); 39 | Logger.LogInfo(this, "Project update finished."); 40 | }); 41 | } 42 | 43 | private void CreateTaskOnClick(object sender, RoutedEventArgs e) 44 | { 45 | DteSingleThread.Run(dte => 46 | { 47 | Generators.Task.CreateVariables(dte); 48 | Logger.LogInfo(this, "Project update finished."); 49 | }); 50 | } 51 | 52 | private void CreateTemplateOnClick(object sender, RoutedEventArgs e) 53 | { 54 | var input = new TextBox { Height = 24, Text = "DeviceName" }; 55 | 56 | if (Theme.MessageBox 57 | .Show("Create device template", input, MessageBoxButton.OKCancel, MessageBoxImage.None) != 58 | MessageBoxResult.OK) 59 | { 60 | return; 61 | } 62 | 63 | var name = input.Text; 64 | 65 | DteSingleThread.Run(dte => 66 | { 67 | if (GetPlcProject(dte) is not {} plcProjectItem) return; 68 | if (plcProjectItem.GetOrCreateChild("_generated_templates_", 69 | TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER) is not {} folder) return; 70 | Generators.DeviceTemplate.Create(folder, name); 71 | }); 72 | } 73 | 74 | private void SettingsOnClick(object sender, RoutedEventArgs e) 75 | { 76 | var settings = new Settings(); 77 | 78 | if (Theme.MessageBox 79 | .Show("Project Settings", settings, MessageBoxButton.OKCancel, MessageBoxImage.None) == 80 | MessageBoxResult.OK) 81 | { 82 | settings.Save(); 83 | } 84 | } 85 | 86 | private void ApiOnConfigReceived(XElement config) 87 | { 88 | XmlFile.ClientUpdate(config); 89 | DteSingleThread.Run(dte => 90 | { 91 | if (GetPlcProject(dte) is not {} plcProjectItem) return; 92 | Generators.Project.Update(plcProjectItem); 93 | Logger.LogInfo(this, "Project update finished."); 94 | }); 95 | } 96 | 97 | private ITcSmTreeItem? GetPlcProject(DTE? dte) 98 | { 99 | var tcSysManager = dte?.GetTcSysManager(); 100 | tcSysManager?.SaveProject(); 101 | if (tcSysManager?.TryGetPlcProject() is {} plcProjectItem) return plcProjectItem; 102 | Logger.LogError(this, "No Plc project found"); 103 | return null; 104 | } 105 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/PouInstance.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Core; 3 | 4 | namespace OC.Assistant.Generator; 5 | 6 | internal class PouInstance 7 | { 8 | private readonly string? _name; 9 | private readonly string? _type; 10 | private readonly string? _label; 11 | private readonly string? _assignments; 12 | 13 | public PouInstance(string? name, string? type) 14 | { 15 | _name = name?.MakePlcCompatible(); 16 | _type = type?.MakePlcCompatible(); 17 | } 18 | 19 | public PouInstance(XElement element) 20 | { 21 | _name = element.Attribute("Name")?.Value.MakePlcCompatible(); 22 | _type = element.Attribute("Type")?.Value.MakePlcCompatible(); 23 | 24 | _label = element.Element("Label")?.Value; 25 | 26 | _assignments = element 27 | .Elements() 28 | .Where(x => x.Name.LocalName is "Control" or "In") 29 | .Aggregate("", (current, next) => 30 | current + $"\n\t\t{next.Attribute("Name")?.Value} := GVL_{next.Attribute("Assignment")?.Value},"); 31 | 32 | _assignments = element 33 | .Elements() 34 | .Where(x => x.Name.LocalName is "Address") 35 | .Aggregate(_assignments, (current, next) => 36 | current + $"\n\t\t{next.Attribute("Name")?.Value} := ADR(GVL_{next.Attribute("Assignment")?.Value}),"); 37 | 38 | _assignments = element 39 | .Elements() 40 | .Where(x => x.Name.LocalName is "Status" or "Out") 41 | .Aggregate(_assignments, (current, next) => 42 | current + $"\n\t\t{next.Attribute("Name")?.Value} => GVL_{next.Attribute("Assignment")?.Value},"); 43 | 44 | if (_assignments.Length > 0) 45 | { 46 | _assignments = _assignments.Remove(_assignments.Length - 1); 47 | } 48 | } 49 | 50 | public string DeclarationText 51 | { 52 | get 53 | { 54 | var declaration = $"\t{_name} : {_type};"; 55 | if (!string.IsNullOrEmpty(_label)) 56 | { 57 | declaration += $" //{_label}"; 58 | } 59 | return declaration + "\n"; 60 | } 61 | } 62 | 63 | public string ImplementationText => $"\t{_name}({_assignments});\n"; 64 | 65 | public string InitRunText => ""; 66 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/PlcAddress.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace OC.Assistant.Generator.Profinet; 4 | 5 | /// 6 | /// Represents a PLC address containing a direction and an address value. 7 | /// 8 | /// 9 | /// The class parses and validates the provided address string based on certain rules. 10 | /// 11 | public partial class PlcAddress 12 | { 13 | /// Represents a PLC address used in Profinet communication. 14 | /// This class parses and stores information about a PLC address 15 | /// given in a specific format. The address is extracted using a 16 | /// regular expression and is decomposed into the components 17 | /// direction (I or Q) and the numerical address. 18 | public PlcAddress(string name) 19 | { 20 | var regex = AddressRegex(); 21 | var match = regex.Match(name); 22 | if (!match.Success) return; 23 | 24 | Direction = match.Groups[1].Value; 25 | Address = int.Parse(match.Groups[2].Value); 26 | } 27 | 28 | /// 29 | /// Gets a value indicating whether the PLC address is valid. 30 | /// 31 | /// 32 | /// The address is considered valid if it is greater than or equal to zero. 33 | /// 34 | public bool IsValid => Address >= 0; 35 | 36 | /// 37 | /// Gets the direction of the PLC address. 38 | /// 39 | /// 40 | /// The direction represents the type of PLC address. It is typically represented 41 | /// by the first character in the address string, such as "I" for input or "Q" for output. 42 | /// This property is initialized using a regex match when the constructor is invoked. 43 | /// 44 | public string Direction { get; } = ""; 45 | 46 | /// Represents the address of the PLC variable. 47 | /// The address is only considered valid if it contains a non-negative value. 48 | public int Address { get; } = -1; 49 | 50 | [GeneratedRegex(@"^([I,Q])(\d+)$")] 51 | private static partial Regex AddressRegex(); 52 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/ProfinetGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.IO; 3 | using EnvDTE; 4 | using OC.Assistant.Core; 5 | using TCatSysManagerLib; 6 | 7 | namespace OC.Assistant.Generator.Profinet; 8 | 9 | [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 10 | internal class ProfinetGenerator(DTE dte, string folderName) 11 | { 12 | public void Generate(ITcSmTreeItem plcProjectItem) 13 | { 14 | var tcSysManager = dte.GetTcSysManager(); 15 | if (tcSysManager is null) return; 16 | 17 | if (!tcSysManager.TryLookupTreeItem(TcShortcut.IO_DEVICE, out var ioItem)) 18 | { 19 | return; 20 | } 21 | 22 | foreach (ITcSmTreeItem item in ioItem) 23 | { 24 | //Is not Profinet 25 | if (item.ItemSubType != (int) TcSmTreeItemSubType.ProfinetIoDevice) continue; 26 | 27 | //Is disabled 28 | if (item.Disabled == DISABLED_STATE.SMDS_DISABLED) continue; 29 | 30 | //Export profinet to xti file 31 | var xtiPath = $"{AppData.Path}\\{item.Name}.xti"; 32 | if (File.Exists(xtiPath)) File.Delete(xtiPath); 33 | item.Parent.ExportChild(item.Name, xtiPath); 34 | 35 | //Parse xti file and delete 36 | var profinetParser = new ProfinetParser(xtiPath, item.Name); 37 | File.Delete(xtiPath); 38 | 39 | GenerateFiles(plcProjectItem, item.Name, profinetParser.Variables, profinetParser.SafetyModules); 40 | } 41 | } 42 | 43 | private void GenerateFiles(ITcSmTreeItem plcProjectItem, string pnName, IEnumerable pnVars, IEnumerable safetyModules) 44 | { 45 | var gvlVariables = ""; 46 | 47 | foreach (var pnVar in pnVars) 48 | { 49 | gvlVariables += pnVar.CreateGvlDeclaration(); 50 | } 51 | 52 | //Create safety program 53 | var safetyProgram = new SafetyProgram(safetyModules, pnName); 54 | 55 | //Create global variable list 56 | var hil = plcProjectItem.GetOrCreateChild(folderName, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 57 | var pnFolder = hil?.GetOrCreateChild(pnName, TREEITEMTYPES.TREEITEMTYPE_PLCFOLDER); 58 | 59 | //Create program 60 | if (pnFolder?.GetOrCreateChild($"PRG_{pnName}", TREEITEMTYPES.TREEITEMTYPE_PLCPOUPROG) is not { } prg) return; 61 | if (prg.GetOrCreateChild("InitRun", TREEITEMTYPES.TREEITEMTYPE_PLCMETHOD) is not {} initRun) return; 62 | if (prg is not ITcPlcDeclaration prgDecl) return; 63 | if (prg is not ITcPlcImplementation prgImpl) return; 64 | if (initRun is not ITcPlcDeclaration initDecl) return; 65 | if (initRun is not ITcPlcImplementation initImpl) return; 66 | 67 | prgDecl.DeclarationText = 68 | $""" 69 | PROGRAM {prg.Name} 70 | VAR 71 | bInitRun : BOOL := TRUE; 72 | bReset : BOOL; 73 | {safetyProgram.Declaration} 74 | END_VAR 75 | """; 76 | 77 | prgImpl.ImplementationText = 78 | $""" 79 | InitRun(); 80 | {safetyProgram.Implementation} 81 | """; 82 | 83 | initDecl.DeclarationText = "METHOD PRIVATE InitRun"; 84 | 85 | initImpl.ImplementationText = 86 | $""" 87 | IF NOT bInitRun THEN RETURN; END_IF 88 | bInitRun := FALSE; 89 | {safetyProgram.Parameter} 90 | """; 91 | 92 | pnFolder.CreateGvl(pnName, gvlVariables); 93 | 94 | //Add program name to xml for project generator 95 | XmlFile.AddHilProgram(pnName); 96 | } 97 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/ProfinetParser.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Sdk; 3 | 4 | namespace OC.Assistant.Generator.Profinet; 5 | 6 | internal class ProfinetParser 7 | { 8 | public List Variables { get; } 9 | public List SafetyModules { get; } 10 | 11 | public ProfinetParser(string xtiPath, string pnName) 12 | { 13 | var xtiDoc = XDocument.Load(xtiPath); 14 | 15 | var activeBoxes = (from elem in xtiDoc.Descendants("Box") 16 | where elem.Attribute("Disabled") is null 17 | select elem).ToList(); 18 | 19 | var subModules = (from elem in activeBoxes.Descendants("Name") 20 | where elem.Parent?.Name == "SubModule" 21 | && !elem.Value.Contains("#ignore") 22 | select elem.Parent).ToList(); 23 | 24 | var inputs = from elem in subModules.Descendants("BitOffs") 25 | where elem.Parent?.Parent?.Attribute("VarGrpType")?.Value == "1" && 26 | elem.Parent.Parent.Parent?.Name != "Box" 27 | select new ProfinetVariable(elem.Parent, pnName, true); 28 | 29 | var outputs = from elem in subModules.Descendants("BitOffs") 30 | where elem.Parent?.Parent?.Attribute("VarGrpType")?.Value == "2" && 31 | elem.Parent.Parent.Parent?.Name != "Box" 32 | select new ProfinetVariable(elem.Parent, pnName, false); 33 | 34 | var variables = new Dictionary(); 35 | 36 | foreach (var variable in inputs.Concat(outputs)) 37 | { 38 | if (!variables.TryAdd(variable.Name, variable)) 39 | { 40 | Logger.LogWarning(this, $"{variable.Name} exists more than once"); 41 | } 42 | } 43 | 44 | SafetyModules = (from elem in subModules.Descendants("Name") 45 | where elem.Value.Contains("#failsafe") 46 | select new SafetyModule(elem.Parent, pnName)).ToList(); 47 | 48 | foreach (var module in SafetyModules) 49 | { 50 | if (variables.TryGetValue(module.HstDataName, out var hstVar)) 51 | { 52 | hstVar.SafetyFlag = true; 53 | module.HstVariable = hstVar; 54 | } 55 | if (variables.TryGetValue(module.DevDataName, out var devVar)) 56 | { 57 | devVar.SafetyFlag = true; 58 | module.DevVariable = devVar; 59 | } 60 | } 61 | 62 | Variables = variables.Values 63 | .OrderBy(x => GetCategoryOrder(x.Name)) 64 | .ThenBy(x => GetAddressNumber(x.Name)) 65 | .ToList(); 66 | } 67 | 68 | private static int GetCategoryOrder(string name) 69 | { 70 | if (name.StartsWith('I')) 71 | { 72 | return 0; 73 | } 74 | return name.StartsWith('Q') ? 1 : 2; 75 | } 76 | 77 | private static int GetAddressNumber(string name) 78 | { 79 | if (name.Length <= 1 || (!name.StartsWith('I') && !name.StartsWith('Q'))) 80 | { 81 | return int.MaxValue; 82 | } 83 | var numPart = name[1..]; 84 | return int.TryParse(numPart, out var number) ? number : int.MaxValue; 85 | } 86 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/ProfinetVariable.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Xml.Linq; 3 | using OC.Assistant.Core; 4 | using OC.Assistant.Sdk; 5 | 6 | namespace OC.Assistant.Generator.Profinet; 7 | 8 | internal partial class ProfinetVariable 9 | { 10 | /// 11 | /// Name of the variable 12 | /// 13 | public string Name { get; } 14 | 15 | /// 16 | /// Type of the variable 17 | /// 18 | public string Type { get; } 19 | 20 | /// 21 | /// Link to the bus variable 22 | /// 23 | public string Link { get; } 24 | 25 | /// 26 | /// The TwinCAT direction 'I' or 'Q'. 27 | /// 28 | public string Direction { get; } 29 | 30 | /// 31 | /// The PLC address, if any. 32 | /// 33 | public PlcAddress PlcAddress { get; } 34 | 35 | /// 36 | /// The byte array size, if any. -1 means no byte array. 37 | /// 38 | public int ByteArraySize { get; } 39 | 40 | /// 41 | /// Indicates that this variable is used by a safety module. 42 | /// 43 | public bool SafetyFlag { get; set; } 44 | 45 | public ProfinetVariable(XElement? element, string pnName, bool isInput) 46 | { 47 | var nameList = GetFullNamePath(element); 48 | 49 | Direction = isInput ? "I" : "Q"; 50 | Link = nameList 51 | .Aggregate($"{TcShortcut.IO_DEVICE}^{pnName}", (current, next) => $"{current}^{next}"); 52 | Type = element?.Element("Type")?.Value ?? TcType.Byte.Name(); 53 | 54 | Name = nameList[^1]; 55 | ByteArraySize = GetByteArraySize(Type); 56 | PlcAddress = new PlcAddress(Name); 57 | 58 | if (PlcAddress.IsValid) 59 | { 60 | return; 61 | } 62 | 63 | Name = nameList.Where(x => x != "API" && x != "Inputs" && x != "Outputs") 64 | .Aggregate("", (current, next) => $"{current}_{next}") 65 | .TcPlcCompatibleString(); 66 | } 67 | 68 | 69 | /// 70 | /// Generates a declaration string for the GVL. 71 | /// 72 | public string CreateGvlDeclaration() 73 | { 74 | var template = SafetyFlag ? 75 | $"{Tags.VAR_NAME} : {Tags.VAR_TYPE}; //FAILSAFE\n" : 76 | $"{{attribute 'TcLinkTo' := '{Tags.LINK}'}}\n{Tags.VAR_NAME} AT %{Tags.DIRECTION}* : {Tags.VAR_TYPE};\n"; 77 | 78 | if (!PlcAddress.IsValid || ByteArraySize < 0) 79 | { 80 | return template 81 | .Replace(Tags.VAR_TYPE, Type) 82 | .Replace(Tags.DIRECTION, Direction) 83 | .Replace(Tags.LINK, Link) 84 | .Replace(Tags.VAR_NAME, Name); 85 | } 86 | 87 | var declaration = ""; 88 | for (var i = 0; i < ByteArraySize; i++) 89 | { 90 | declaration += template 91 | .Replace(Tags.VAR_TYPE, TcType.Byte.Name()) 92 | .Replace(Tags.DIRECTION, Direction) 93 | .Replace(Tags.LINK, $"{Link}[{i}]") 94 | .Replace(Tags.VAR_NAME, $"{PlcAddress.Direction}{PlcAddress.Address + i}"); 95 | } 96 | 97 | return declaration; 98 | } 99 | 100 | /// 101 | /// Generates a declaration string for the safety program. 102 | /// 103 | public string CreatePrgDeclaration() 104 | { 105 | return $"\t{{attribute 'TcLinkTo' := '{Tags.LINK}'}}\n\t{Tags.VAR_NAME} AT %{Tags.DIRECTION}* : {Tags.VAR_TYPE};\n" 106 | .Replace(Tags.VAR_TYPE, Type) 107 | .Replace(Tags.DIRECTION, Direction) 108 | .Replace(Tags.LINK, Link) 109 | .Replace(Tags.VAR_NAME, Name); 110 | } 111 | 112 | private static int GetByteArraySize(string type) 113 | { 114 | var regex = ByteArrayRegex(); 115 | var match = regex.Match(type); 116 | if (!match.Success) return -1; 117 | return int.Parse(match.Groups[2].Value) - int.Parse(match.Groups[1].Value) + 1; 118 | } 119 | 120 | private static List GetFullNamePath(XElement? element) 121 | { 122 | 123 | var namePath = new List(); 124 | if (element is null) return namePath; 125 | 126 | while (true) 127 | { 128 | //Not valid anymore 129 | if (element is null) 130 | return namePath; 131 | //Top device -> we are done 132 | if (element.Name == "Device") 133 | return namePath; 134 | //Element has element 'Name' 135 | if (element.Element("Name") is not null) 136 | namePath.Insert(0, element.Element("Name")?.Value ?? ""); 137 | //Element has attribute 'Name' 138 | else if (element.Attribute("Name") is not null) 139 | namePath.Insert(0, element.Attribute("Name")?.Value ?? ""); 140 | //Set to parent hierarchy 141 | element = element.Parent; 142 | } 143 | } 144 | 145 | [GeneratedRegex(@"ARRAY\s*\[(\d+)\.\.(\d+)\]\s+OF\s+(BYTE)", RegexOptions.IgnoreCase)] 146 | private static partial Regex ByteArrayRegex(); 147 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/SafetyModule.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Sdk; 3 | 4 | namespace OC.Assistant.Generator.Profinet; 5 | 6 | internal class SafetyModule 7 | { 8 | public int Port { get; } 9 | public int Slot { get; } 10 | public int SubSlot { get; } 11 | public string Name { get; } 12 | public string BoxName { get; } 13 | public string PnName { get; } 14 | public string HstDataName { get; } = ""; 15 | public string DevDataName { get; } = ""; 16 | public int HstSize { get; } 17 | public int DevSize { get; } 18 | 19 | public ProfinetVariable? HstVariable { get; set; } 20 | public ProfinetVariable? DevVariable { get; set; } 21 | 22 | public bool IsValid => HstSize >= 0 && DevSize >= 0; 23 | 24 | public SafetyModule (XElement? element, string pnName) 25 | { 26 | Port = IsFirstBox(element) ? 65535 : GetPortNr(element); 27 | BoxName = GetBoxName(element); 28 | PnName = pnName; 29 | Slot = GetSlotNr(element?.Parent); 30 | SubSlot = GetSubSlotNr(element); 31 | Name = $"fb{Port}x{Slot}x{SubSlot}"; 32 | 33 | var inputs = new List(); 34 | var outputs = new List(); 35 | 36 | if (element is null) return; 37 | 38 | foreach (var elem in element.Descendants("BitOffs")) 39 | { 40 | if (elem.Parent?.Parent?.Attribute("VarGrpType")?.Value == "1") 41 | { 42 | inputs.Add(new ProfinetVariable(elem.Parent, pnName, true)); 43 | } 44 | 45 | if (elem.Parent?.Parent?.Attribute("VarGrpType")?.Value == "2") 46 | { 47 | outputs.Add(new ProfinetVariable(elem.Parent, pnName, false)); 48 | } 49 | } 50 | 51 | if (inputs.Count == 0 || outputs.Count == 0) return; 52 | 53 | foreach (var input in inputs) 54 | { 55 | HstSize += input.Type.TcBitSize() / 8; 56 | } 57 | 58 | foreach (var output in outputs) 59 | { 60 | DevSize += output.Type.TcBitSize() / 8; 61 | } 62 | 63 | HstDataName = inputs[0].Name; 64 | DevDataName = outputs[0].Name; 65 | } 66 | 67 | private static bool IsFirstBox(XObject? element) 68 | { 69 | var parent = element?.Parent; 70 | if (parent?.Name != "Box") 71 | { 72 | return IsFirstBox(element?.Parent); 73 | } 74 | var previous = parent.PreviousNode as XElement; 75 | return previous?.Name != "Box"; 76 | } 77 | 78 | private static string GetBoxName(XObject? element) 79 | { 80 | if (element?.Parent?.Name == "Box") return element.Parent?.Element("Name")?.Value ?? ""; 81 | return GetBoxName(element?.Parent); 82 | } 83 | 84 | private static int GetPortNr(XElement? element) 85 | { 86 | if (element?.Name == "Box") return int.Parse(element.Attribute("Id")?.Value ?? "0") + 0x1000; 87 | return GetPortNr(element?.Parent); 88 | } 89 | 90 | private static int GetSlotNr(XNode? element) 91 | { 92 | var elem = element?.PreviousNode as XElement; 93 | if (elem?.Name != "Module") return 0; 94 | return GetSlotNr(elem) + 1; 95 | } 96 | 97 | private static int GetSubSlotNr(XNode? element) 98 | { 99 | var elem = element?.PreviousNode as XElement; 100 | if (elem?.Name != "SubModule") return 1; 101 | return GetSubSlotNr(elem) + 1; 102 | } 103 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Profinet/SafetyProgram.cs: -------------------------------------------------------------------------------- 1 | using OC.Assistant.Core; 2 | 3 | namespace OC.Assistant.Generator.Profinet; 4 | 5 | internal class SafetyProgram 6 | { 7 | public string Declaration { get; } = ""; 8 | public string Implementation { get; } = ""; 9 | public string Parameter { get; } = ""; 10 | 11 | public SafetyProgram(IEnumerable modules, string pnName) 12 | { 13 | const string info = $"//Profisafe Device {Tags.DEVICE} - Slot {Tags.SLOT} - SubSlot {Tags.SUB_SLOT}\n"; 14 | const string fbDecl = $"\n\t{Tags.VAR_NAME}: FB_ProfisafeDevice; {info}"; 15 | const string configPort = $"{Tags.INSTANCE}.stConfig.nPort := {Tags.VALUE};\n"; 16 | const string configSlot = $"{Tags.INSTANCE}.stConfig.nSlot := {Tags.VALUE};\n"; 17 | const string configSubSlot = $"{Tags.INSTANCE}.stConfig.nSubSlot := {Tags.VALUE};\n"; 18 | const string configHstSize = $"{Tags.INSTANCE}.stConfig.nHstDatasize := {Tags.VALUE};\n"; 19 | const string configDevSize = $"{Tags.INSTANCE}.stConfig.nDevDatasize := {Tags.VALUE};\n"; 20 | const string configInputAddr = $"{Tags.INSTANCE}.stConfig.pControlData := ADR({Tags.VAR_NAME});\n"; 21 | const string configOutputAddr = $"{Tags.INSTANCE}.stConfig.pStatusData := ADR({Tags.VAR_NAME});\n"; 22 | const string configDevDataAddr = $"{Tags.INSTANCE}.stConfig.pDevData := ADR({Tags.NAME}.{Tags.VAR_NAME});\n"; 23 | const string configHstDataAddr = $"{Tags.INSTANCE}.stConfig.pHstData := ADR({Tags.NAME}.{Tags.VAR_NAME});\n"; 24 | const string call = $"{Tags.INSTANCE}(bReset := bReset);\n"; 25 | 26 | foreach (var module in modules.Where(module => module.PnName == pnName && module.IsValid)) 27 | { 28 | var hstVar = module.HstVariable; 29 | var devVar = module.DevVariable; 30 | if (hstVar is null || devVar is null) continue; 31 | 32 | Declaration += fbDecl 33 | .Replace(Tags.VAR_NAME, module.Name) 34 | .Replace(Tags.DEVICE, module.BoxName) 35 | .Replace(Tags.SLOT, module.Slot.ToString()) 36 | .Replace(Tags.SUB_SLOT, module.SubSlot.ToString()); 37 | 38 | Declaration += hstVar.CreatePrgDeclaration(); 39 | Declaration += devVar.CreatePrgDeclaration(); 40 | 41 | Implementation += call 42 | .Replace(Tags.INSTANCE, module.Name); 43 | 44 | Parameter += $"\n{info}" 45 | .Replace(Tags.DEVICE, module.BoxName) 46 | .Replace(Tags.SLOT, module.Slot.ToString()) 47 | .Replace(Tags.SUB_SLOT, module.SubSlot.ToString()); 48 | Parameter += configPort 49 | .Replace(Tags.VALUE, module.Port.ToString()); 50 | Parameter += configSlot 51 | .Replace(Tags.VALUE, module.Slot.ToString()); 52 | Parameter += configSubSlot 53 | .Replace(Tags.VALUE, module.SubSlot.ToString()); 54 | Parameter += configHstSize 55 | .Replace(Tags.VALUE, module.HstSize.ToString()); 56 | Parameter += configDevSize 57 | .Replace(Tags.VALUE, module.DevSize.ToString()); 58 | Parameter += configInputAddr 59 | .Replace(Tags.VAR_NAME, module.HstDataName); 60 | Parameter += configOutputAddr 61 | .Replace(Tags.VAR_NAME, module.DevDataName); 62 | Parameter += configHstDataAddr 63 | .Replace(Tags.VAR_NAME, module.HstDataName); 64 | Parameter += configDevDataAddr 65 | .Replace(Tags.VAR_NAME, module.DevDataName); 66 | Parameter = Parameter 67 | .Replace(Tags.NAME, $"GVL_{pnName}".MakePlcCompatible()) 68 | .Replace(Tags.INSTANCE, module.Name); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Settings.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /OC.Assistant/Generator/Settings.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace OC.Assistant.Generator; 4 | 5 | public partial class Settings 6 | { 7 | public Settings() 8 | { 9 | InitializeComponent(); 10 | if (Core.XmlFile.Instance.Path is null) return; 11 | ((ComboBoxItem)PlcDropdown.SelectedItem).Content = Core.XmlFile.Instance.PlcProjectName; 12 | ((ComboBoxItem)TaskDropdown.SelectedItem).Content = Core.XmlFile.Instance.PlcTaskName; 13 | } 14 | 15 | public void Save() 16 | { 17 | if (Core.XmlFile.Instance.Path is null) return; 18 | Dispatcher.Invoke(() => 19 | { 20 | Core.XmlFile.Instance.PlcProjectName = PlcDropdown.Selected; 21 | Core.XmlFile.Instance.PlcTaskName = TaskDropdown.Selected; 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/SettingsPlcDropdown.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using OC.Assistant.Core; 4 | 5 | namespace OC.Assistant.Generator; 6 | 7 | /// 8 | /// Dropdown for available plc projects. 9 | /// 10 | public class SettingsPlcDropdown : ComboBox 11 | { 12 | public string Selected { get; private set; } = Core.XmlFile.Instance.PlcProjectName; 13 | 14 | public SettingsPlcDropdown() 15 | { 16 | Style = Application.Current.Resources["DefaultComboBoxStyle"] as Style; 17 | Items.Add(new ComboBoxItem {Content = Core.XmlFile.Instance.PlcProjectName}); 18 | SelectedIndex = 0; 19 | DropDownOpened += OnOpened; 20 | return; 21 | 22 | void OnOpened(object? sender, EventArgs e) 23 | { 24 | var projects = new List(); 25 | DteSingleThread.Run(dte => 26 | { 27 | if (dte.GetTcSysManager() is not {} tcSysManager) return; 28 | projects.AddRange(tcSysManager 29 | .TryGetItems(TcShortcut.PLC) 30 | .Select(item => item.Name)); 31 | }, 1000); 32 | 33 | Items.Clear(); 34 | foreach (var project in projects) 35 | { 36 | var comboBoxItem = new ComboBoxItem 37 | { 38 | Content = project 39 | }; 40 | 41 | comboBoxItem.Selected += ComboBoxItem_Selected; 42 | Items.Add(comboBoxItem); 43 | } 44 | 45 | if (Items.Count == 0) 46 | { 47 | Items.Add(new ComboBoxItem {Content = "No plc found", IsEnabled = false}); 48 | Selected = ""; 49 | return; 50 | } 51 | 52 | foreach(var item in Items.Cast()) 53 | { 54 | if (item.Content as string != Selected) continue; 55 | SelectedIndex = Items.IndexOf(item); 56 | return; 57 | } 58 | } 59 | } 60 | 61 | private void ComboBoxItem_Selected(object sender, EventArgs e) 62 | { 63 | var comboBoxItem = (ComboBoxItem)sender; 64 | Selected = (string)comboBoxItem.Content; 65 | } 66 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/SettingsTaskDropdown.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using OC.Assistant.Core; 4 | 5 | namespace OC.Assistant.Generator; 6 | 7 | /// 8 | /// Dropdown for available tasks. 9 | /// 10 | public class SettingsTaskDropdown : ComboBox 11 | { 12 | public string Selected { get; private set; } = Core.XmlFile.Instance.PlcTaskName; 13 | 14 | public SettingsTaskDropdown() 15 | { 16 | Style = Application.Current.Resources["DefaultComboBoxStyle"] as Style; 17 | Items.Add(new ComboBoxItem {Content = Core.XmlFile.Instance.PlcTaskName}); 18 | SelectedIndex = 0; 19 | DropDownOpened += OnOpened; 20 | return; 21 | 22 | void OnOpened(object? sender, EventArgs e) 23 | { 24 | var tasks = new List(); 25 | DteSingleThread.Run(dte => 26 | { 27 | if (dte.GetTcSysManager() is not {} tcSysManager) return; 28 | 29 | tasks.AddRange(tcSysManager 30 | .TryGetItems(TcShortcut.TASK) 31 | .Where(item => item.ItemSubType == (int)TcSmTreeItemSubType.TaskWithImage) 32 | .Select(item => item.Name)); 33 | }, 1000); 34 | 35 | Items.Clear(); 36 | foreach (var task in tasks) 37 | { 38 | var comboBoxItem = new ComboBoxItem 39 | { 40 | Content = task 41 | }; 42 | 43 | comboBoxItem.Selected += ComboBoxItem_Selected; 44 | Items.Add(comboBoxItem); 45 | } 46 | 47 | if (Items.Count == 0) 48 | { 49 | Items.Add(new ComboBoxItem {Content = "No task found", IsEnabled = false}); 50 | Selected = ""; 51 | return; 52 | } 53 | 54 | foreach(var item in Items.Cast()) 55 | { 56 | if (item.Content as string != Selected) continue; 57 | SelectedIndex = Items.IndexOf(item); 58 | return; 59 | } 60 | } 61 | } 62 | 63 | private void ComboBoxItem_Selected(object sender, EventArgs e) 64 | { 65 | var comboBoxItem = (ComboBoxItem)sender; 66 | Selected = (string)comboBoxItem.Content; 67 | } 68 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/Tags.cs: -------------------------------------------------------------------------------- 1 | namespace OC.Assistant.Generator; 2 | 3 | internal static class Tags 4 | { 5 | public const string NAME = "$NAME$"; 6 | public const string VAR_NAME = "$VARNAME$"; 7 | public const string VAR_TYPE = "$VARTYPE$"; 8 | public const string DIRECTION = "$DIRECTION$"; 9 | public const string VALUE = "$VALUE$"; 10 | public const string INSTANCE = "$INSTANCE$"; 11 | public const string SLOT = "$SLOT$"; 12 | public const string SUB_SLOT = "$SUBSLOT$"; 13 | public const string DEVICE = "$DEVICE$"; 14 | public const string BOX_NO = "$BOXNO$"; 15 | public const string LINK = "$LINK$"; 16 | } -------------------------------------------------------------------------------- /OC.Assistant/Generator/XmlFile.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Core; 3 | 4 | namespace OC.Assistant.Generator; 5 | 6 | /// 7 | /// extension to write and read HiL configurations. 8 | /// 9 | internal static class XmlFile 10 | { 11 | public static Core.XmlFile XmlBase => Core.XmlFile.Instance; 12 | 13 | /// 14 | /// Gets the Main program. 15 | /// 16 | public static XElement? Main => XmlBase.Project?.Element(XmlTags.MAIN); 17 | 18 | /// 19 | /// Implements a new client configuration. 20 | /// 21 | public static void ClientUpdate(XElement config) 22 | { 23 | try 24 | { 25 | var main = XmlBase.Project?.Element(XmlTags.MAIN); 26 | if (main is null) return; 27 | main.ReplaceNodes(config.Elements()); 28 | XmlBase.Save(); 29 | } 30 | catch (Exception e) 31 | { 32 | Sdk.Logger.LogWarning(nameof(XmlFile), e.Message); 33 | } 34 | } 35 | 36 | /// 37 | /// Gets the HiL programs. 38 | /// 39 | public static IEnumerable? HilPrograms 40 | { 41 | get 42 | { 43 | return XmlBase.Project?.Element(XmlTags.HIL)?.Elements().Select(x => x.Value); 44 | } 45 | } 46 | 47 | public static IEnumerable? PluginElements => XmlBase.Plugins?.Elements(XmlTags.PLUGIN); 48 | 49 | /// 50 | /// Removes all HiL programs. 51 | /// 52 | public static void ClearHilPrograms() 53 | { 54 | XmlBase.Project?.Element(XmlTags.HIL)?.RemoveAll(); 55 | XmlBase.Save(); 56 | } 57 | 58 | /// 59 | /// Adds a HiL program with the given name. 60 | /// 61 | public static void AddHilProgram(string name) 62 | { 63 | XmlBase.Project?.Element(XmlTags.HIL)?.Add(new XElement("Program", $"PRG_{name}".MakePlcCompatible())); 64 | XmlBase.Save(); 65 | } 66 | } -------------------------------------------------------------------------------- /OC.Assistant/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /OC.Assistant/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Windows; 3 | using OC.Assistant.Core; 4 | using OC.Assistant.Sdk; 5 | using OC.Assistant.Theme; 6 | 7 | namespace OC.Assistant; 8 | 9 | public partial class MainWindow 10 | { 11 | public MainWindow() 12 | { 13 | InitializeComponent(); 14 | Footer.Children.Add(ProjectState.View); 15 | ReadSettings(); 16 | 17 | BusyState.Changed += BusyOverlay.SetState; 18 | LogViewer.LogFilePath = AppData.LogFilePath; 19 | Logger.Info += (sender, message) => LogViewer.Add(sender, message, MessageType.Info); 20 | Logger.Warning += (sender, message) => LogViewer.Add(sender, message, MessageType.Warning); 21 | Logger.Error += (sender, message) => LogViewer.Add(sender, message, MessageType.Error); 22 | } 23 | 24 | private void ReadSettings() 25 | { 26 | var settings = new Settings().Read(); 27 | Height = settings.Height < 100 ? 400 : settings.Height; 28 | Width = settings.Width < 150 ? 600 : settings.Width; 29 | Left = settings.PosX; 30 | Top = settings.PosY; 31 | ConsoleRow.Height = new GridLength(settings.ConsoleHeight); 32 | } 33 | 34 | private void WriteSettings() 35 | { 36 | if (WindowState == WindowState.Maximized) return; 37 | 38 | new Settings 39 | { 40 | Height = (int) Height, 41 | Width = (int) Width, 42 | PosX = (int) Left, 43 | PosY = (int) Top, 44 | ConsoleHeight = (int) ConsoleRow.Height.Value 45 | }.Write(); 46 | } 47 | 48 | private void MainWindowOnClosing(object sender, CancelEventArgs e) 49 | { 50 | WriteSettings(); 51 | } 52 | } -------------------------------------------------------------------------------- /OC.Assistant/OC.Assistant.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | enable 6 | enable 7 | true 8 | Spiratec AG 9 | default 10 | net8.0-windows 11 | Resources\oc_logo.ico 12 | 1.13.4 13 | 14 | 15 | 16 | none 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | MSBuild:Compile 34 | Wpf 35 | Designer 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /OC.Assistant/Plugins/Plugin.xaml: -------------------------------------------------------------------------------- 1 |  10 | 13 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /OC.Assistant/Plugins/Plugin.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Media; 4 | using System.Xml.Linq; 5 | using OC.Assistant.Sdk; 6 | using OC.Assistant.Sdk.Plugin; 7 | 8 | namespace OC.Assistant.Plugins; 9 | 10 | internal partial class Plugin 11 | { 12 | public Type? Type { get; private set; } 13 | public IPluginController? PluginController { get; private set; } 14 | public bool IsValid => PluginController is not null; 15 | 16 | public Plugin() 17 | { 18 | InitializeComponent(); 19 | DockPanel.SetDock(this, Dock.Top); 20 | Name = "MyPlugin"; 21 | } 22 | 23 | public Plugin(string name, Type type, XContainer parameter) : this() 24 | { 25 | if (!InitType(type)) return; 26 | PluginController?.Parameter.Update(parameter); 27 | Name = name; 28 | BtnEditText.Text = $"{Name} ({Type?.Name})"; 29 | PluginController?.Initialize(name); 30 | } 31 | 32 | public bool Save(string name) 33 | { 34 | if (!IsValid) return false; 35 | Name = name; 36 | PluginController?.Stop(); 37 | BtnEditText.Text = $"{Name} ({Type?.Name})"; 38 | return PluginController?.Save(name) == true; 39 | } 40 | 41 | public bool InitType(Type? type) 42 | { 43 | try 44 | { 45 | if (type is null) 46 | { 47 | throw new Exception($"{type} is null"); 48 | } 49 | PluginController?.Stop(); 50 | Type = type; 51 | PluginController = Activator.CreateInstance(type) as IPluginController; 52 | if (PluginController is null) return false; 53 | PluginController.Started += PluginOnStarted; 54 | PluginController.Stopped += PluginOnStopped; 55 | PluginController.Starting += PluginOnStarting; 56 | PluginController.Stopping += PluginOnStopping; 57 | return true; 58 | } 59 | catch (Exception e) 60 | { 61 | Logger.LogError(this, e.Message); 62 | return false; 63 | } 64 | } 65 | 66 | private void PluginOnStopped() 67 | { 68 | Dispatcher.Invoke(() => 69 | { 70 | StartStopButton.IsEnabled = true; 71 | StartStopButtonText.Text = "\xE768"; 72 | StartStopButtonText.Foreground = Application.Current.Resources["SuccessBrush"] as SolidColorBrush; 73 | }); 74 | } 75 | 76 | private void PluginOnStopping() 77 | { 78 | Dispatcher.Invoke(() => 79 | { 80 | StartStopButton.IsEnabled = false; 81 | StartStopButtonText.Text = "\xE71A"; 82 | StartStopButtonText.Foreground = Application.Current.Resources["White3Brush"] as SolidColorBrush; 83 | }); 84 | } 85 | 86 | private void PluginOnStarted() 87 | { 88 | Dispatcher.Invoke(() => 89 | { 90 | StartStopButton.IsEnabled = true; 91 | StartStopButtonText.Text = "\xE71A"; 92 | StartStopButtonText.Foreground = Application.Current.Resources["DangerBrush"] as SolidColorBrush; 93 | }); 94 | } 95 | 96 | private void PluginOnStarting() 97 | { 98 | Dispatcher.Invoke(() => 99 | { 100 | StartStopButton.IsEnabled = false; 101 | StartStopButtonText.Text = "\xE768"; 102 | StartStopButtonText.Foreground = Application.Current.Resources["White3Brush"] as SolidColorBrush; 103 | }); 104 | } 105 | 106 | public void Start() 107 | { 108 | if (PluginController?.IsRunning == true) return; 109 | PluginController?.Start(); 110 | } 111 | 112 | public void Stop() 113 | { 114 | if (PluginController?.IsRunning != true) return; 115 | PluginController?.Stop(); 116 | } 117 | 118 | private void StartStopButton_Click(object sender, RoutedEventArgs e) 119 | { 120 | if (PluginController?.IsRunning == true) 121 | { 122 | Stop(); 123 | return; 124 | } 125 | 126 | Start(); 127 | } 128 | 129 | private void EditButton_Click(object sender, RoutedEventArgs e) 130 | { 131 | OnEdit?.Invoke(this); 132 | } 133 | 134 | private void RemoveButton_Click(object sender, RoutedEventArgs name) 135 | { 136 | if (PluginController?.IsRunning == true) return; 137 | if (Theme.MessageBox.Show( 138 | "Delete?", 139 | Name, MessageBoxButton.OKCancel, MessageBoxImage.Warning) 140 | == MessageBoxResult.OK) 141 | { 142 | OnRemove?.Invoke(this); 143 | } 144 | } 145 | 146 | public new bool IsEnabled 147 | { 148 | set 149 | { 150 | Dispatcher.Invoke(() => 151 | { 152 | RemoveButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed; 153 | }); 154 | } 155 | } 156 | 157 | public bool IsSelected 158 | { 159 | set => EditButton.Tag = value ? "Selected" : null; 160 | } 161 | 162 | public event Action? OnRemove; 163 | public event Action? OnEdit; 164 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginDropdown.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace OC.Assistant.Plugins; 5 | 6 | /// 7 | /// Dropdown for available plugins. 8 | /// 9 | public class PluginDropdown : ComboBox 10 | { 11 | public Type? SelectedType { get; private set; } 12 | 13 | public PluginDropdown() 14 | { 15 | Style = Application.Current.Resources["DefaultComboBoxStyle"] as Style; 16 | Loaded += (_, _) => 17 | { 18 | Items.Clear(); 19 | foreach (var type in PluginRegister.Types) 20 | { 21 | var comboBoxItem = new ComboBoxItem 22 | { 23 | Content = type.Name, 24 | Tag = type 25 | }; 26 | 27 | comboBoxItem.Selected += ComboBoxItem_Selected; 28 | Items.Add(comboBoxItem); 29 | } 30 | SelectedIndex = 0; 31 | }; 32 | } 33 | 34 | private void ComboBoxItem_Selected(object sender, EventArgs e) 35 | { 36 | var comboBoxItem = (ComboBoxItem)sender; 37 | SelectedType = (Type)comboBoxItem.Tag; 38 | TypeSelected?.Invoke(SelectedType); 39 | } 40 | 41 | public event Action? TypeSelected; 42 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginEditor.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | MyPlugin 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginEditor.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using OC.Assistant.Core; 4 | using OC.Assistant.Sdk; 5 | using OC.Assistant.Sdk.Plugin; 6 | 7 | namespace OC.Assistant.Plugins; 8 | 9 | /// 10 | /// UI to show and edit a . 11 | /// 12 | internal partial class PluginEditor 13 | { 14 | private Plugin? _plugin; 15 | private IReadOnlyCollection _plugins = []; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public PluginEditor() 21 | { 22 | InitializeComponent(); 23 | } 24 | 25 | /// 26 | /// Enable/Disable the control. 27 | /// 28 | public new bool IsEnabled 29 | { 30 | set => ApplyButton.IsEnabled = EditorWindow.IsEnabled = value; 31 | } 32 | 33 | /// 34 | /// The selected plugin has been saved. 35 | /// 36 | public event Action? OnConfirm; 37 | 38 | /// 39 | /// The editor has been closed. 40 | /// 41 | public event Action? OnCancel; 42 | 43 | /// 44 | /// Show the editor. 45 | /// 46 | /// The selected plugin to show. 47 | /// All currently available plugins in the project. 48 | /// 49 | public bool Show(Plugin plugin, IReadOnlyCollection plugins) 50 | { 51 | var selection = (ComboBoxItem)TypeDropdown.SelectedItem; 52 | if (selection is null) 53 | { 54 | Logger.LogWarning(this, "No Plugins found"); 55 | return false; 56 | } 57 | 58 | _plugins = plugins; 59 | _plugin = plugin; 60 | Visibility = Visibility.Visible; 61 | 62 | ShowParameter(); 63 | PluginName.Text = _plugin.Name; 64 | selection.Content = _plugin?.Type?.Name ?? ""; 65 | 66 | //Disable the name input and the type dropdown if the plugin has been saved already 67 | var isNew = plugins.All(x => x != plugin); 68 | PluginName.IsEnabled = isNew; 69 | TypeDropdown.IsEnabled = isNew; 70 | 71 | return true; 72 | } 73 | 74 | private void TypeSelectorOnSelected(Type e) 75 | { 76 | if (e == _plugin?.Type) return; 77 | if (_plugin?.InitType(e) != true) return; 78 | ShowParameter(); 79 | } 80 | 81 | private void ShowParameter() 82 | { 83 | IndicateChanges = false; 84 | if (_plugin?.IsValid != true) 85 | { 86 | if (_plugin?.InitType(TypeDropdown.SelectedType) != true) return; 87 | } 88 | 89 | ParameterPanel.Children.Clear(); 90 | foreach (var parameter in _plugin.PluginController?.Parameter.ToList() ?? []) 91 | { 92 | var param = new PluginParameter(parameter); 93 | param.Changed += () => IndicateChanges = true; 94 | ParameterPanel.Children.Add(param); 95 | } 96 | } 97 | 98 | private bool IndicateChanges 99 | { 100 | set => ApplyButton.Content = value ? "Apply*" : "Apply"; 101 | } 102 | 103 | private void ApplyButton_Click(object sender, RoutedEventArgs e) 104 | { 105 | if (!PluginName.Text.IsPlcCompatible()) 106 | { 107 | Theme.MessageBox.Show(PluginName.Text, "Name is not TwinCAT PLC compatible", MessageBoxButton.OK, MessageBoxImage.Warning); 108 | return; 109 | } 110 | 111 | if (_plugins.Any(plugin => plugin.Name == PluginName.Text && plugin != _plugin)) 112 | { 113 | Theme.MessageBox.Show(PluginName.Text, "Name already exists", MessageBoxButton.OK, MessageBoxImage.Warning); 114 | return; 115 | } 116 | 117 | if (Theme.MessageBox.Show("Editor", $"Save {PluginName.Text}?", MessageBoxButton.OKCancel, 118 | MessageBoxImage.Question) == MessageBoxResult.Cancel) 119 | { 120 | return; 121 | } 122 | 123 | //Update parameters of selected plugin 124 | _plugin?.PluginController?.Parameter.Update(ParameterPanel.Children.OfType()); 125 | 126 | //Call the save method for the selected plugin 127 | if (_plugin?.Save(PluginName.Text) != true) return; 128 | 129 | Logger.LogInfo(this, $"'{_plugin.Name}' saved"); 130 | OnConfirm?.Invoke(_plugin); 131 | IndicateChanges = false; 132 | } 133 | 134 | private void CloseButton_Click(object sender, RoutedEventArgs e) 135 | { 136 | Visibility = Visibility.Hidden; 137 | OnCancel?.Invoke(); 138 | } 139 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginManager.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginManager.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using OC.Assistant.Core; 4 | using OC.Assistant.Sdk.Plugin; 5 | 6 | namespace OC.Assistant.Plugins; 7 | 8 | public partial class PluginManager 9 | { 10 | private List _plugins = []; 11 | 12 | public PluginManager() 13 | { 14 | InitializeComponent(); 15 | Core.XmlFile.Instance.Reloaded += XmlOnReloaded; 16 | ProjectState.Events.Disconnected += OnDisconnect; 17 | ProjectState.Events.StartedRunning += OnStartedRunning; 18 | ProjectState.Events.StoppedRunning += OnStoppedRunning; 19 | ProjectState.Events.Locked += OnLocked; 20 | } 21 | 22 | private void PluginManagerOnLoaded(object sender, RoutedEventArgs e) 23 | { 24 | PluginRegister.Initialize(); 25 | } 26 | 27 | private void OnLocked(bool value) 28 | { 29 | var isEnabled = !value; 30 | BtnAdd.Visibility = isEnabled ? Visibility.Visible : Visibility.Hidden; 31 | Editor.IsEnabled = isEnabled; 32 | foreach (var plugin in _plugins) 33 | { 34 | plugin.IsEnabled = isEnabled; 35 | } 36 | } 37 | 38 | private void XmlOnReloaded() 39 | { 40 | Dispatcher.Invoke(Initialize); 41 | } 42 | 43 | private void Initialize() 44 | { 45 | OnDisconnect(); 46 | ControlPanel.Children.Remove(BtnAdd); 47 | _plugins = XmlFile.LoadPlugins(); 48 | 49 | foreach (var plugin in _plugins) 50 | { 51 | AddPlugin(plugin); 52 | } 53 | 54 | ControlPanel.Children.Add(BtnAdd); 55 | ScrollView.ScrollToEnd(); 56 | BtnAdd.Visibility = Visibility.Visible; 57 | } 58 | 59 | private void OnDisconnect() 60 | { 61 | foreach (var plugin in _plugins) 62 | { 63 | RemovePlugin(plugin); 64 | } 65 | 66 | BtnAdd.Visibility = Visibility.Hidden; 67 | DeselectAll(); 68 | HideEditor(); 69 | _plugins.Clear(); 70 | } 71 | 72 | private void OnStoppedRunning() 73 | { 74 | if (BusyState.IsSet) return; 75 | Task.Run(async () => 76 | { 77 | foreach (var plugin in _plugins) 78 | { 79 | plugin.Stop(); 80 | await Task.Delay(100); 81 | } 82 | }); 83 | } 84 | 85 | private void OnStartedRunning() 86 | { 87 | if (BusyState.IsSet) return; 88 | Task.Run(async () => 89 | { 90 | foreach (var plugin in _plugins.Where(x => x.PluginController?.AutoStart == true)) 91 | { 92 | plugin.Start(); 93 | await Task.Delay(plugin.PluginController?.DelayAfterStart ?? 0); 94 | } 95 | }); 96 | } 97 | 98 | private void AddPlugin(Plugin plugin) 99 | { 100 | plugin.OnRemove += Plugin_OnRemove; 101 | plugin.OnEdit += Plugin_OnEdit; 102 | 103 | Dispatcher.Invoke(() => 104 | { 105 | ControlPanel.Children.Add(plugin); 106 | DockPanel.SetDock(plugin, Dock.Top); 107 | }); 108 | } 109 | 110 | private void RemovePlugin(Plugin plugin) 111 | { 112 | plugin.PluginController?.Stop(); 113 | plugin.OnRemove -= Plugin_OnRemove; 114 | plugin.OnEdit -= Plugin_OnEdit; 115 | 116 | Dispatcher.Invoke(() => 117 | { 118 | ControlPanel.Children.Remove(plugin); 119 | }); 120 | } 121 | 122 | private void BtnAdd_Click(object sender, RoutedEventArgs? e) 123 | { 124 | if (BusyState.IsSet) return; 125 | 126 | var plugin = new Plugin(); 127 | if (!Editor.Show(plugin, _plugins)) return; 128 | DeselectAll(); 129 | BtnAddIsSelected = true; 130 | ShowEditor(); 131 | } 132 | 133 | private void Plugin_OnEdit(Plugin plugin) 134 | { 135 | if (!Editor.Show(plugin, _plugins)) return; 136 | ShowEditor(); 137 | DeselectAll(); 138 | plugin.IsSelected = true; 139 | } 140 | 141 | private void Plugin_OnRemove(Plugin plugin) 142 | { 143 | BusyState.Set(this); 144 | RemovePlugin(plugin); 145 | XmlFile.UpdatePlugin(plugin, true); 146 | _plugins.Remove(plugin); 147 | BusyState.Reset(this); 148 | BtnAdd_Click(this, null); 149 | if (plugin.PluginController?.IoType == IoType.None) return; 150 | UpdateProject(plugin.Name, true); 151 | } 152 | 153 | private void Editor_OnConfirm(Plugin plugin) 154 | { 155 | BusyState.Set(this); 156 | ControlPanel.Children.Remove(BtnAdd); 157 | 158 | XmlFile.UpdatePlugin(plugin); 159 | 160 | if (_plugins.FirstOrDefault(x => x.Name == plugin.Name) is null) 161 | { 162 | _plugins.Add(plugin); 163 | AddPlugin(plugin); 164 | } 165 | else 166 | { 167 | plugin.PluginController?.Stop(); 168 | } 169 | 170 | ControlPanel.Children.Add(BtnAdd); 171 | ScrollView.ScrollToEnd(); 172 | BusyState.Reset(this); 173 | Plugin_OnEdit(plugin); 174 | if (plugin.PluginController is null) return; 175 | if (!plugin.PluginController.IoChanged) return; 176 | UpdateProject(plugin.Name, false); 177 | } 178 | 179 | private void Editor_OnCancel() 180 | { 181 | DeselectAll(); 182 | HideEditor(); 183 | } 184 | 185 | private void ShowEditor() 186 | { 187 | GridSplitter.Width = new GridLength(4); 188 | if (EditorColumn.ActualWidth <= 0) EditorColumn.Width = new GridLength(400); 189 | } 190 | 191 | private void HideEditor() 192 | { 193 | GridSplitter.Width = new GridLength(0); 194 | EditorColumn.Width = new GridLength(0); 195 | Editor.Visibility = Visibility.Collapsed; 196 | } 197 | 198 | private bool BtnAddIsSelected 199 | { 200 | set => BtnAdd.Tag = value ? "Selected" : null; 201 | } 202 | 203 | private void DeselectAll() 204 | { 205 | BtnAddIsSelected = false; 206 | foreach (var plugin in _plugins) 207 | { 208 | plugin.IsSelected = false; 209 | } 210 | } 211 | 212 | private void UpdateProject(string name, bool delete) 213 | { 214 | DteSingleThread.Run(dte => 215 | { 216 | var tcSysManager = dte.GetTcSysManager(); 217 | tcSysManager?.SaveProject(); 218 | if (tcSysManager?.TryGetPlcProject() is not { } plcProjectItem) 219 | { 220 | Sdk.Logger.LogError(this, "No Plc project found"); 221 | return; 222 | } 223 | Generator.Generators.Sil.Update(plcProjectItem, name, delete); 224 | }); 225 | } 226 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginParameter.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginParameter.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using Microsoft.Win32; 4 | using OC.Assistant.Sdk; 5 | using OC.Assistant.Sdk.Plugin; 6 | 7 | namespace OC.Assistant.Plugins; 8 | 9 | internal partial class PluginParameter : IParameter 10 | { 11 | private readonly string? _fileFilter; 12 | private object? _value; 13 | 14 | public PluginParameter(IParameter parameter) 15 | { 16 | InitializeComponent(); 17 | DockPanel.SetDock(this, Dock.Top); 18 | VerticalAlignment = VerticalAlignment.Top; 19 | Loaded += OnLoaded; 20 | 21 | Name = parameter.Name; 22 | Value = parameter.Value; 23 | ToolTip = parameter.ToolTip; 24 | FileFilter = parameter.FileFilter; 25 | } 26 | 27 | public new string Name 28 | { 29 | get => NameLabel.Content.ToString() ?? ""; 30 | private init => NameLabel.Content = value; 31 | } 32 | 33 | public object? Value 34 | { 35 | get => ValueTextBox.Text.ConvertTo(_value?.GetType() ?? null); 36 | set 37 | { 38 | Dispatcher.Invoke(() => 39 | { 40 | _value = value; 41 | ValueTextBox.Text = $"{_value}"; 42 | 43 | if (_value is bool boolValue) 44 | { 45 | ValueCheckBox.Visibility = Visibility.Visible; 46 | ValueCheckBox.IsChecked = boolValue; 47 | ValueTextBox.Visibility = Visibility.Hidden; 48 | return; 49 | } 50 | 51 | ValueCheckBox.Visibility = Visibility.Hidden; 52 | ValueTextBox.Visibility = Visibility.Visible; 53 | }); 54 | } 55 | } 56 | 57 | public string? FileFilter 58 | { 59 | get => _fileFilter; 60 | private init 61 | { 62 | _fileFilter = value; 63 | FileSelector.Visibility = _fileFilter is null || Value is bool ? Visibility.Collapsed : Visibility.Visible; 64 | } 65 | } 66 | 67 | private void OnLoaded(object sender, RoutedEventArgs e) 68 | { 69 | Margin = new Thickness(0,3,0,3); 70 | } 71 | 72 | private void FileSelector_OnClick(object sender, RoutedEventArgs e) 73 | { 74 | var openFileDialog = new OpenFileDialog {InitialDirectory = $"{Value}"}; 75 | if (!string.IsNullOrEmpty(FileFilter)) 76 | { 77 | openFileDialog.Filter = $"*.{FileFilter}|*.{FileFilter}"; 78 | } 79 | if (openFileDialog.ShowDialog() is not true) return; 80 | Value = openFileDialog.FileName; 81 | } 82 | 83 | private void ValueCheckBox_OnChecked(object sender, RoutedEventArgs e) 84 | { 85 | Value = true; 86 | } 87 | 88 | private void ValueCheckBox_OnUnchecked(object sender, RoutedEventArgs e) 89 | { 90 | Value = false; 91 | } 92 | 93 | private void ValueTextBox_OnTextChanged(object sender, TextChangedEventArgs e) 94 | { 95 | if (!ValueTextBox.IsKeyboardFocused) return; 96 | Changed?.Invoke(); 97 | } 98 | 99 | private void ValueCheckBox_OnClick(object sender, RoutedEventArgs e) 100 | { 101 | Changed?.Invoke(); 102 | } 103 | 104 | public event Action? Changed; 105 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/PluginRegister.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using System.Xml.Linq; 4 | using OC.Assistant.Sdk; 5 | using OC.Assistant.Sdk.Plugin; 6 | 7 | namespace OC.Assistant.Plugins; 8 | 9 | /// 10 | /// Available plugins. 11 | /// 12 | internal static class PluginRegister 13 | { 14 | /// 15 | /// The list of available plugin types. 16 | /// 17 | public static List Types { get; } = []; 18 | 19 | /// 20 | /// The full path of the plugins search directory. 21 | /// 22 | public static string SearchPath { get; } = Path.GetFullPath(@".\Plugins"); 23 | 24 | /// 25 | /// Tries to load available plugins depending on the current environment (debug or executable). 26 | /// 27 | public static void Initialize() 28 | { 29 | if (Directory.Exists(SearchPath)) 30 | { 31 | Directory 32 | .GetFiles(SearchPath, "*.plugin", SearchOption.AllDirectories) 33 | .ToList().ForEach(Load); 34 | } 35 | 36 | #if DEBUG 37 | /* 38 | Search outside the solution environment when debugging 39 | Path of the executable when debugging looks like this: 40 | \\OC.Assistant\bin\Debug\net8.0-windows\OC.Assistant.exe 41 | */ 42 | try 43 | { 44 | Directory 45 | .GetFiles(@"..\..\..\..\..\", "*.plugin", SearchOption.AllDirectories) 46 | .ToList().ForEach(Load); 47 | } 48 | catch (Exception e) 49 | { 50 | Logger.LogError(typeof(PluginRegister), e.Message); 51 | } 52 | #endif 53 | } 54 | 55 | /// 56 | /// Loads the plugin by the given plugin file. 57 | /// 58 | /// The path to the plugin file. 59 | private static void Load(string pluginFile) 60 | { 61 | try 62 | { 63 | var filePath = Path.GetFullPath(pluginFile); 64 | var dir = Path.GetDirectoryName(filePath); 65 | var doc = XDocument.Load(filePath).Root; 66 | var dll = doc?.Element("AssemblyFile")?.Value; 67 | var additional = doc?.Elements("AdditionalDirectory"); 68 | 69 | if (!File.Exists($"{dir}\\{dll}")) return; 70 | 71 | if (additional is not null) 72 | { 73 | foreach (var additionalDirectory in additional) 74 | { 75 | AssemblyHelper.AddDirectory(additionalDirectory.Value, SearchOption.AllDirectories); 76 | } 77 | } 78 | 79 | AssemblyHelper.AddDirectory(dir); 80 | var assembly = Assembly.LoadFile($"{dir}\\{dll}"); 81 | 82 | foreach (var type in assembly.ExportedTypes.Where(x => x.BaseType == typeof(PluginBase))) 83 | { 84 | if (Types.Any(x => x.FullName == type.FullName)) continue; 85 | Types.Add(type); 86 | } 87 | } 88 | catch (Exception e) 89 | { 90 | Logger.LogError(typeof(PluginRegister), e.Message); 91 | } 92 | } 93 | 94 | /// 95 | /// Gets the plugin by the given name. 96 | /// 97 | /// The name of the plugin type. 98 | /// The type of the plugin if available, otherwise default. 99 | public static Type? GetTypeByName(string? name) 100 | { 101 | return Types.FirstOrDefault(x => x.Name == name); 102 | } 103 | } -------------------------------------------------------------------------------- /OC.Assistant/Plugins/XmlFile.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using OC.Assistant.Core; 3 | using OC.Assistant.Sdk; 4 | using OC.Assistant.Sdk.Plugin; 5 | 6 | namespace OC.Assistant.Plugins; 7 | 8 | /// 9 | /// extension to write and read plugin configurations. 10 | /// 11 | internal static class XmlFile 12 | { 13 | private static Core.XmlFile XmlBase => Core.XmlFile.Instance; 14 | 15 | /// 16 | /// Updates or removes the given plugin. 17 | /// 18 | public static void UpdatePlugin(Plugin plugin, bool remove = false) 19 | { 20 | var pluginElements = XmlBase.Plugins?.Elements(); 21 | if (pluginElements is null) return; 22 | 23 | foreach (var item in pluginElements) 24 | { 25 | if (item.Attribute(XmlTags.PLUGIN_NAME)?.Value == plugin.Name) item.Remove(); 26 | } 27 | 28 | if (remove) 29 | { 30 | XmlBase.Save(); 31 | return; 32 | } 33 | 34 | var xElement = new XElement(XmlTags.PLUGIN, 35 | new XAttribute(XmlTags.PLUGIN_NAME, plugin.Name ?? ""), 36 | new XAttribute(XmlTags.PLUGIN_TYPE, plugin.Type?.Name ?? ""), 37 | new XAttribute(XmlTags.PLUGIN_IO_TYPE, plugin.PluginController?.IoType ?? IoType.None), 38 | plugin.PluginController?.Parameter.XElement, 39 | plugin.PluginController?.InputStructure.XElement, 40 | plugin.PluginController?.OutputStructure.XElement); 41 | 42 | XmlBase.Plugins?.Add(xElement); 43 | XmlBase.Save(); 44 | } 45 | 46 | /// 47 | /// Loads all plugins. 48 | /// 49 | public static List LoadPlugins() 50 | { 51 | var pluginElements = XmlBase.Plugins?.Elements().ToList(); 52 | var plugins = new List(); 53 | if (pluginElements is null) return plugins; 54 | 55 | foreach (var element in pluginElements) 56 | { 57 | var name = element.Attribute(XmlTags.PLUGIN_NAME)?.Value; 58 | var type = element.Attribute(XmlTags.PLUGIN_TYPE)?.Value; 59 | var parameter = element.Element(XmlTags.PLUGIN_PARAMETER); 60 | var pluginType = PluginRegister.GetTypeByName(type); 61 | if (pluginType is null) 62 | { 63 | Logger.LogWarning(typeof(XmlFile), 64 | $"Plugin for type '{type}' not found in directory {PluginRegister.SearchPath}"); 65 | continue; 66 | } 67 | if (name is null || parameter is null) continue; 68 | plugins.Add(new Plugin(name, pluginType, parameter)); 69 | } 70 | 71 | return plugins; 72 | } 73 | } -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/AdapterDropdown.cs: -------------------------------------------------------------------------------- 1 | using System.Net.NetworkInformation; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Media; 5 | using OC.Assistant.Sdk; 6 | 7 | namespace OC.Assistant.PnGenerator; 8 | 9 | /// 10 | /// Dropdown for network adapters, filtered by 'twincat-intel' 11 | /// 12 | internal class AdapterDropdown : ComboBox 13 | { 14 | public AdapterDropdown() 15 | { 16 | Style = Application.Current.Resources["DefaultComboBoxStyle"] as Style; 17 | Background = Application.Current.Resources["Dark1Brush"] as Brush; 18 | try 19 | { 20 | var defaultItem = SelectedItem; 21 | Items.Clear(); 22 | 23 | foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) 24 | { 25 | if (!adapter.Description.Contains("twincat-intel", StringComparison.CurrentCultureIgnoreCase)) continue; 26 | 27 | var comboBoxItem = new ComboBoxItem 28 | { 29 | Content = $"{adapter.Name} ({adapter.Description})", 30 | Tag = adapter 31 | }; 32 | 33 | Items.Add(comboBoxItem); 34 | } 35 | 36 | if (Items.Count == 0) Items.Add(defaultItem); 37 | SelectedIndex = 0; 38 | } 39 | catch (Exception e) 40 | { 41 | Logger.LogError(this, e.Message); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/Control.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Net.NetworkInformation; 4 | using System.Xml.Linq; 5 | using EnvDTE; 6 | using OC.Assistant.Core; 7 | using OC.Assistant.Sdk; 8 | using TCatSysManagerLib; 9 | using Process = System.Diagnostics.Process; 10 | 11 | namespace OC.Assistant.PnGenerator; 12 | 13 | public class Control(string scannerTool) 14 | { 15 | private Settings _settings; 16 | 17 | /// 18 | /// Starts capturing. 19 | /// 20 | internal void StartCapture(Settings settings) 21 | { 22 | if (BusyState.IsSet) return; 23 | _settings = settings; 24 | 25 | if (_settings.PnName == "") 26 | { 27 | Logger.LogError(this, "Empty profinet name"); 28 | return; 29 | } 30 | 31 | if (_settings.Adapter is null) 32 | { 33 | Logger.LogError(this, "No adapter selected"); 34 | return; 35 | } 36 | 37 | if (_settings.HwFilePath is null) 38 | { 39 | Logger.LogError(this, "TIA aml file not specified"); 40 | return; 41 | } 42 | 43 | DteSingleThread.Run(dte => 44 | { 45 | RunScanner(); 46 | ImportPnDevice(dte); 47 | }); 48 | } 49 | 50 | /// 51 | /// Runs the scanner application. 52 | /// 53 | private void RunScanner() 54 | { 55 | Logger.LogInfo(this, $"Running {scannerTool}..."); 56 | 57 | var filePath = $"{AppData.Path}\\{_settings.PnName}.xti"; 58 | 59 | using var process = new Process(); 60 | process.StartInfo = new ProcessStartInfo 61 | { 62 | FileName = "cmd", 63 | Arguments = $"/c {scannerTool} -d \"{_settings.Adapter?.Id}\" -o \"{filePath}\" --aml-file \"{_settings.HwFilePath}\" --gsd-path \"{_settings.GsdFolderPath}\"" 64 | //RedirectStandardOutput = true, 65 | //RedirectStandardError = true, 66 | //CreateNoWindow = true 67 | }; 68 | 69 | process.OutputDataReceived += (_, e) => 70 | { 71 | if (!string.IsNullOrEmpty(e.Data)) 72 | { 73 | Logger.LogInfo(this, e.Data); 74 | } 75 | }; 76 | 77 | process.ErrorDataReceived += (_, e) => 78 | { 79 | if (!string.IsNullOrEmpty(e.Data)) 80 | { 81 | Logger.LogError(this, e.Data); 82 | } 83 | }; 84 | 85 | try 86 | { 87 | process.Start(); 88 | //process.BeginOutputReadLine(); 89 | //process.BeginErrorReadLine(); 90 | process.WaitForExit(); 91 | if (process.ExitCode != 0) 92 | { 93 | Logger.LogError(this, $"{scannerTool} has stopped with exit code 0x{process.ExitCode:x8}"); 94 | return; 95 | } 96 | Logger.LogInfo(this, $"{scannerTool} has finished"); 97 | } 98 | catch (Exception e) 99 | { 100 | Logger.LogError(this, e.Message); 101 | } 102 | } 103 | 104 | /// 105 | /// Imports a xti-file. 106 | /// 107 | private void ImportPnDevice(DTE dte) 108 | { 109 | //No file found 110 | var xtiFilePath = $"{AppData.Path}\\{_settings.PnName}.xti"; 111 | if (!File.Exists(xtiFilePath)) 112 | { 113 | Logger.LogInfo(this, "Nothing created"); 114 | return; 115 | } 116 | 117 | //File is empty 118 | if (File.ReadAllText(xtiFilePath) == string.Empty) 119 | { 120 | Logger.LogInfo(this, "Nothing created"); 121 | File.Delete(xtiFilePath); 122 | return; 123 | } 124 | 125 | var tcSysManager = dte.GetTcSysManager(); 126 | tcSysManager?.SaveProject(); 127 | 128 | //Import and delete xti file 129 | Logger.LogInfo(this, $"Import {xtiFilePath}..."); 130 | var tcPnDevice = tcSysManager?.UpdateIoDevice(_settings.PnName, xtiFilePath); 131 | File.Delete(xtiFilePath); 132 | 133 | UpdateTcPnDevice(tcPnDevice); 134 | 135 | if (tcSysManager?.TryGetPlcProject() is {} plcProjectItem) 136 | { 137 | Logger.LogInfo(this, "Create HiL structure..."); 138 | Generator.Generators.Hil.Update(dte, plcProjectItem); 139 | } 140 | 141 | Logger.LogInfo(this, "Finished"); 142 | } 143 | 144 | /// 145 | /// Updates the PN-Device. 146 | /// 147 | private void UpdateTcPnDevice(ITcSmTreeItem? tcPnDevice) 148 | { 149 | if (tcPnDevice is null) return; 150 | 151 | // Add the *.aml filename for information 152 | if (!string.IsNullOrEmpty(_settings.HwFilePath)) tcPnDevice.Comment = _settings.HwFilePath; 153 | 154 | // Set the network adapter 155 | var deviceDesc = $"{_settings.Adapter?.Name} ({_settings.Adapter?.Description})"; 156 | foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) 157 | { 158 | if ($"{adapter.Name} ({adapter.Description})" != deviceDesc) continue; 159 | var pnDevice = XDocument.Parse(tcPnDevice.ProduceXml()); 160 | var pnp = pnDevice.Root?.Element("DeviceDef")?.Element("AddressInfo")?.Element("Pnp"); 161 | 162 | if (pnp?.Element("DeviceDesc") is { } desc) 163 | { 164 | desc.Value = deviceDesc; 165 | } 166 | 167 | if (pnp?.Element("DeviceName") is { } name) 168 | { 169 | name.Value = $@"\DEVICE\{adapter.Id}"; 170 | } 171 | 172 | if (pnp?.Element("DeviceData") is { } data) 173 | { 174 | data.Value = adapter.GetPhysicalAddress().ToString(); 175 | } 176 | 177 | tcPnDevice.ConsumeXml(pnDevice.ToString()); 178 | return; 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/Menu.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/Menu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Text; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using OC.Assistant.Core; 7 | using OC.Assistant.Sdk; 8 | 9 | namespace OC.Assistant.PnGenerator; 10 | 11 | public partial class Menu 12 | { 13 | private const string SCANNER_TOOL = "OC.TcPnScanner.CLI"; 14 | private bool _isScannerInstalled; 15 | 16 | private readonly Control _control = new (SCANNER_TOOL); 17 | 18 | public Menu() 19 | { 20 | InitializeComponent(); 21 | ProjectState.Events.Locked += e => IsEnabled = !e; 22 | Task.Run(IsScannerInstalled); 23 | } 24 | 25 | private void ScanOnClick(object sender, RoutedEventArgs e) 26 | { 27 | var settingsView = new SettingsView(); 28 | var result = Theme.MessageBox.Show("Scan Profinet", settingsView, MessageBoxButton.OKCancel, MessageBoxImage.None); 29 | if (result != MessageBoxResult.OK) return; 30 | _control.StartCapture(settingsView.Settings); 31 | } 32 | 33 | private void MenuOnSubmenuOpened(object sender, RoutedEventArgs e) 34 | { 35 | var menu = (Menu) sender; 36 | menu.Items.Clear(); 37 | 38 | if (!_isScannerInstalled) 39 | { 40 | var install = new MenuItem {Header = $"Install {SCANNER_TOOL}"}; 41 | install.Click += InstallOnClick; 42 | menu.Items.Add(install); 43 | return; 44 | } 45 | 46 | var scan = new MenuItem {Header = "Scan Profinet"}; 47 | scan.Click += ScanOnClick; 48 | menu.Items.Add(scan); 49 | 50 | var update = new MenuItem {Header = $"Update {SCANNER_TOOL}"}; 51 | update.Click += UpdateOnClick; 52 | menu.Items.Add(update); 53 | 54 | var uninstall = new MenuItem {Header = $"Uninstall {SCANNER_TOOL}"}; 55 | uninstall.Click += UninstallOnClick; 56 | menu.Items.Add(uninstall); 57 | } 58 | 59 | private async Task IsScannerInstalled() 60 | { 61 | var lines = (await Powershell($"dotnet tool list {SCANNER_TOOL} -g", false)).Split("\r\n"); 62 | _isScannerInstalled = lines.Length > 2; 63 | } 64 | 65 | private async void InstallOnClick(object sender, RoutedEventArgs e) 66 | { 67 | try 68 | { 69 | await Powershell($"dotnet tool install {SCANNER_TOOL} -g"); 70 | await IsScannerInstalled(); 71 | } 72 | catch (Exception exception) 73 | { 74 | Logger.LogError(this, exception.Message); 75 | } 76 | } 77 | 78 | private async void UpdateOnClick(object sender, RoutedEventArgs e) 79 | { 80 | try 81 | { 82 | await Powershell($"dotnet tool update {SCANNER_TOOL} -g"); 83 | await IsScannerInstalled(); 84 | } 85 | catch (Exception exception) 86 | { 87 | Logger.LogError(this, exception.Message); 88 | } 89 | } 90 | 91 | private async void UninstallOnClick(object sender, RoutedEventArgs e) 92 | { 93 | try 94 | { 95 | await Powershell($"dotnet tool uninstall {SCANNER_TOOL} -g"); 96 | await IsScannerInstalled(); 97 | } 98 | catch (Exception exception) 99 | { 100 | Logger.LogError(this, exception.Message); 101 | } 102 | } 103 | 104 | private async Task Powershell(string arguments, bool logOutput = true) 105 | { 106 | using var process = new Process(); 107 | process.StartInfo = new ProcessStartInfo 108 | { 109 | CreateNoWindow = true, 110 | RedirectStandardOutput = true, 111 | FileName = "powershell", 112 | Arguments = arguments, 113 | Environment = 114 | { 115 | ["DOTNET_CLI_UI_LANGUAGE"] = "en" 116 | } 117 | }; 118 | 119 | BusyState.Set(this); 120 | process.Start(); 121 | await process.WaitForExitAsync(); 122 | var streamReader = new StreamReader(process.StandardOutput.BaseStream, Encoding.UTF8); 123 | var output = (await streamReader.ReadToEndAsync()).TrimEnd('\n').TrimEnd('\r'); 124 | if (logOutput) Logger.LogInfo(this, output); 125 | BusyState.Reset(this); 126 | return output; 127 | } 128 | } -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Net.NetworkInformation; 2 | 3 | namespace OC.Assistant.PnGenerator; 4 | 5 | public struct Settings 6 | { 7 | public string PnName { get; set; } 8 | public NetworkInterface? Adapter { get; set; } 9 | public string? HwFilePath { get; set; } 10 | public string? GsdFolderPath { get; set; } 11 | } -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/SettingsView.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | PNIO 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /OC.Assistant/PnGenerator/SettingsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Net.NetworkInformation; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Forms; 5 | using OpenFileDialog = Microsoft.Win32.OpenFileDialog; 6 | 7 | namespace OC.Assistant.PnGenerator; 8 | 9 | public partial class SettingsView 10 | { 11 | private string? _hwFilePath; 12 | private string? _gsdFolderPath; 13 | 14 | public SettingsView() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | public Settings Settings => new() 20 | { 21 | PnName = PnName.Text, 22 | Adapter = SelectedAdapter, 23 | HwFilePath = _hwFilePath, 24 | GsdFolderPath = _gsdFolderPath 25 | }; 26 | 27 | private NetworkInterface? SelectedAdapter 28 | { 29 | get 30 | { 31 | if (AdapterDropdown.SelectedIndex == -1) return null; 32 | var selection = (ComboBoxItem) AdapterDropdown.SelectedItem; 33 | return selection?.Tag as NetworkInterface; 34 | } 35 | } 36 | 37 | private void SelectAmlFileOnClick(object sender, RoutedEventArgs e) 38 | { 39 | var openFileDialog = new OpenFileDialog 40 | { 41 | Filter = "TIA aml export (*.aml)|*.aml" 42 | }; 43 | 44 | if (openFileDialog.ShowDialog() != true) return; 45 | _hwFilePath = openFileDialog.FileName; 46 | HwFileTextBlock.Text = _hwFilePath; 47 | } 48 | 49 | private void SelectGsdFolderOnClick(object sender, RoutedEventArgs e) 50 | { 51 | var dialog = new FolderBrowserDialog 52 | { 53 | Description = "GSDML folder", 54 | UseDescriptionForTitle = true, 55 | ShowNewFolderButton = true 56 | }; 57 | 58 | if (dialog.ShowDialog() != DialogResult.OK) return; 59 | _gsdFolderPath = dialog.SelectedPath; 60 | GsdFolderTextBlock.Text = _gsdFolderPath; 61 | } 62 | } -------------------------------------------------------------------------------- /OC.Assistant/Resources/OC.TcTemplate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/OC.Assistant/Resources/OC.TcTemplate.zip -------------------------------------------------------------------------------- /OC.Assistant/Resources/oc_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenCommissioning/OC_Assistant/5814403f8dbf8284da61b98368a489ac575ff4ce/OC.Assistant/Resources/oc_logo.ico -------------------------------------------------------------------------------- /OC.Assistant/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using OC.Assistant.Core; 3 | using OC.Assistant.Sdk; 4 | 5 | namespace OC.Assistant; 6 | 7 | /// 8 | /// Represents the settings structure. 9 | /// 10 | public class Settings 11 | { 12 | /// 13 | /// Gets or sets the height of the application window. 14 | /// 15 | public int Height { get; set; } = 900; 16 | /// 17 | /// Gets or sets the width of the application window. 18 | /// 19 | public int Width { get; set; } = 1280; 20 | /// 21 | /// Gets or sets the X position of the application window. 22 | /// 23 | public int PosX { get; set; } 24 | /// 25 | /// Gets or sets the Y position of the application window. 26 | /// 27 | public int PosY { get; set; } 28 | /// 29 | /// Gets or sets the console height within the application window. 30 | /// 31 | public int ConsoleHeight { get; set; } = 140; 32 | } 33 | 34 | /// 35 | /// extension methods. 36 | /// 37 | public static class SettingsExtension 38 | { 39 | /// 40 | /// Reads and deserializes the from a specific path. 41 | /// 42 | public static Settings Read(this Settings settings) 43 | { 44 | try 45 | { 46 | settings = JsonSerializer 47 | .Deserialize(System.IO.File.ReadAllText(AppData.SettingsFilePath)) ?? 48 | settings; 49 | } 50 | catch (Exception e) 51 | { 52 | Logger.LogError(typeof(Settings), e.Message); 53 | } 54 | 55 | return settings; 56 | } 57 | 58 | /// 59 | /// Serializes and writes the to a specific path. 60 | /// 61 | public static void Write(this Settings settings) 62 | { 63 | try 64 | { 65 | var contents = JsonSerializer.Serialize(settings); 66 | System.IO.File.WriteAllText(AppData.SettingsFilePath, contents); 67 | } 68 | catch (Exception e) 69 | { 70 | Logger.LogError(typeof(Settings), e.Message); 71 | } 72 | } 73 | } --------------------------------------------------------------------------------