├── .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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------