Merge pull request #2221 from larsbrubaker/design_tools
Put in regex read write translation code
This commit is contained in:
commit
54dcfba449
17 changed files with 173 additions and 34 deletions
|
|
@ -426,6 +426,9 @@
|
|||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.ValueTuple, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Core" />
|
||||
|
|
|
|||
|
|
@ -28,11 +28,13 @@ either expressed or implied, of the FreeBSD Project.
|
|||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Gaming.Game;
|
||||
|
|
@ -1499,6 +1501,7 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
timeSinceRecievedOk.Restart();
|
||||
}
|
||||
lastLineRead = dataLastRead.Substring(0, returnPosition);
|
||||
lastLineRead = ProcessReadRegEx(lastLineRead);
|
||||
dataLastRead = dataLastRead.Substring(returnPosition + 1);
|
||||
|
||||
// process this command
|
||||
|
|
@ -1853,7 +1856,7 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
if (lineToWrite.Trim().Length > 0)
|
||||
{
|
||||
// sometimes we need to send code without buffering (like when we are closing the program).
|
||||
WriteRawToPrinter(lineToWrite + "\n", lineToWrite);
|
||||
WriteRawToPrinter(ProcessWriteRegEx(lineToWrite) + "\n", lineToWrite);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -1867,6 +1870,89 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
}
|
||||
}
|
||||
|
||||
#region RegExProcess
|
||||
Regex getQuotedParts = new Regex(@"([""'])(\\?.)*?\1", RegexOptions.Compiled);
|
||||
#region ProcessRead
|
||||
string read_regex = "";
|
||||
private List<(Regex Regex, string Replacement)> ReadLineReplacements = new List<(Regex Regex, string Replacement)>();
|
||||
|
||||
private string ProcessReadRegEx(string lineBeingRead)
|
||||
{
|
||||
if (read_regex != ActiveSliceSettings.Instance.GetValue(SettingsKey.read_regex))
|
||||
{
|
||||
ReadLineReplacements.Clear();
|
||||
string splitString = "\\n";
|
||||
read_regex = ActiveSliceSettings.Instance.GetValue(SettingsKey.read_regex);
|
||||
foreach (string regExLine in read_regex.Split(splitString.ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var matches = getQuotedParts.Matches(regExLine);
|
||||
if (matches.Count == 2)
|
||||
{
|
||||
var search = matches[0].Value.Substring(1, matches[0].Value.Length - 2);
|
||||
var replace = matches[1].Value.Substring(1, matches[1].Value.Length - 2);
|
||||
ReadLineReplacements.Add((new Regex(search, RegexOptions.Compiled), replace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in ReadLineReplacements)
|
||||
{
|
||||
lineBeingRead = item.Regex.Replace(lineBeingRead, item.Replacement);
|
||||
}
|
||||
|
||||
return lineBeingRead;
|
||||
}
|
||||
#endregion // ProcessRead
|
||||
|
||||
#region ProcessWrite
|
||||
string write_regex = "";
|
||||
private List<(Regex Regex, string Replacement)> WriteLineReplacements = new List<(Regex Regex, string Replacement)>();
|
||||
|
||||
private string ProcessWriteRegEx(string lineToWrite)
|
||||
{
|
||||
if (write_regex != ActiveSliceSettings.Instance.GetValue(SettingsKey.write_regex))
|
||||
{
|
||||
WriteLineReplacements.Clear();
|
||||
string splitString = "\\n";
|
||||
write_regex = ActiveSliceSettings.Instance.GetValue(SettingsKey.write_regex);
|
||||
foreach (string regExLine in write_regex.Split(splitString.ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var matches = getQuotedParts.Matches(regExLine);
|
||||
if (matches.Count == 2)
|
||||
{
|
||||
var search = matches[0].Value.Substring(1, matches[0].Value.Length - 2);
|
||||
var replace = matches[1].Value.Substring(1, matches[1].Value.Length - 2);
|
||||
WriteLineReplacements.Add((new Regex(search, RegexOptions.Compiled), replace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in WriteLineReplacements)
|
||||
{
|
||||
var replaced = item.Regex.Replace(lineToWrite, item.Replacement);
|
||||
if (replaced != lineToWrite)
|
||||
{
|
||||
var lines = replaced.Split(',');
|
||||
if (lines.Length > 1)
|
||||
{
|
||||
lineToWrite = lines[0];
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
SendLineToPrinterNow(lines[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lineToWrite = replaced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lineToWrite;
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
public bool SerialPortIsAvailable(string portName)
|
||||
//Check is serial port is in the list of available serial ports
|
||||
{
|
||||
|
|
@ -2135,7 +2221,7 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
private void ClearQueuedGCode()
|
||||
{
|
||||
loadedGCode.Clear();
|
||||
WriteChecksumLineToPrinter("M110 N1");
|
||||
WriteChecksumLineToPrinter(ProcessWriteRegEx("M110 N1"));
|
||||
}
|
||||
|
||||
private void Connect_Thread()
|
||||
|
|
@ -2526,10 +2612,7 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
|
||||
if (currentSentLine != null)
|
||||
{
|
||||
string[] splitOnSemicolon = currentSentLine.Split(';');
|
||||
string trimedLine = splitOnSemicolon[0].Trim().ToUpper();
|
||||
|
||||
if (trimedLine.Contains("M114")
|
||||
if (currentSentLine.Contains("M114")
|
||||
&& PrinterIsConnected)
|
||||
{
|
||||
waitingForPosition.Restart();
|
||||
|
|
@ -2562,11 +2645,16 @@ namespace MatterHackers.MatterControl.PrinterCommunication
|
|||
secondsSinceUpdateHistory = secondsSinceStartedPrint;
|
||||
}
|
||||
|
||||
if (trimedLine.Length > 0)
|
||||
// Check if there is anything in front of the ;.
|
||||
if (currentSentLine.Split(';')[0].Trim().Length > 0)
|
||||
{
|
||||
WriteChecksumLineToPrinter(currentSentLine);
|
||||
currentSentLine = ProcessWriteRegEx(currentSentLine).Trim();
|
||||
if (currentSentLine.Length > 0)
|
||||
{
|
||||
WriteChecksumLineToPrinter(currentSentLine);
|
||||
|
||||
currentLineIndexToSend++;
|
||||
currentLineIndexToSend++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.PrintWasCanceled)
|
||||
|
|
|
|||
|
|
@ -908,7 +908,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
if (GetValue<bool>(SettingsKey.recover_is_enabled))
|
||||
{
|
||||
string location = "Location: 'Settings & Controls' -> 'Settings' -> 'Printer' -> 'Print Recovery' -> 'Enable Recovery'".Localize();
|
||||
string[] startGCode = GetValue("start_gcode").Replace("\\n", "\n").Split('\n');
|
||||
string[] startGCode = GetValue(SettingsKey.start_gcode).Replace("\\n", "\n").Split('\n');
|
||||
foreach (string startGCodeLine in startGCode)
|
||||
{
|
||||
if (startGCodeLine.StartsWith("G29"))
|
||||
|
|
@ -933,7 +933,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
if (GetValue<bool>(SettingsKey.print_leveling_enabled))
|
||||
{
|
||||
string location = "Location: 'Settings & Controls' -> 'Settings' -> 'Printer' -> 'Custom G-Code' -> 'Start G-Code'".Localize();
|
||||
string[] startGCode = GetValue("start_gcode").Replace("\\n", "\n").Split('\n');
|
||||
string[] startGCode = GetValue(SettingsKey.start_gcode).Replace("\\n", "\n").Split('\n');
|
||||
foreach (string startGCodeLine in startGCode)
|
||||
{
|
||||
if (startGCodeLine.StartsWith("G29"))
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
public const string show_reset_connection = nameof(show_reset_connection);
|
||||
public const string spiral_vase = nameof(spiral_vase);
|
||||
public const string start_gcode = nameof(start_gcode);
|
||||
public const string end_gcode = nameof(end_gcode);
|
||||
public const string write_regex = nameof(write_regex);
|
||||
public const string read_regex = nameof(read_regex);
|
||||
public const string temperature = nameof(temperature);
|
||||
public const string use_z_probe = nameof(use_z_probe);
|
||||
public const string z_probe_samples = nameof(z_probe_samples);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
SettingsKey.build_height,
|
||||
SettingsKey.cancel_gcode,
|
||||
SettingsKey.connect_gcode,
|
||||
SettingsKey.write_regex,
|
||||
SettingsKey.read_regex,
|
||||
SettingsKey.has_fan,
|
||||
SettingsKey.has_hardware_leveling,
|
||||
SettingsKey.has_heated_bed,
|
||||
|
|
|
|||
|
|
@ -104,8 +104,8 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
|
||||
new InfillTranslator("fill_density", "sparseInfillLineDistance"),
|
||||
|
||||
new MapStartGCode("start_gcode", "startCode", false),
|
||||
new MapEndGCode("end_gcode", "endCode"),
|
||||
new MapStartGCode(SettingsKey.start_gcode, "startCode", false),
|
||||
new MapEndGCode(SettingsKey.end_gcode, "endCode"),
|
||||
};
|
||||
|
||||
curaSettingNames = new HashSet<string>(curaSettings.Select(m => m.CanonicalSettingsName));
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
new AsPercentOfReferenceOrDirect(SettingsKey.first_layer_height, "firstLayerThickness", SettingsKey.layer_height),
|
||||
new ExtruderOffsets("extruder_offset", "extruderOffsets"),
|
||||
new GCodeForSlicer("before_toolchange_gcode", "beforeToolchangeCode"),
|
||||
new GCodeForSlicer("end_gcode", "endCode"),
|
||||
new GCodeForSlicer(SettingsKey.end_gcode, "endCode"),
|
||||
new GCodeForSlicer("toolchange_gcode", "toolChangeCode"),
|
||||
new MapFirstValue("retract_before_travel", "minimumTravelToCauseRetraction"),
|
||||
new MapFirstValue("retract_length", "retractionOnTravel"),
|
||||
|
|
@ -120,7 +120,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
new MappedToBoolString(SettingsKey.fill_thin_gaps, "fillThinGaps"),
|
||||
new MappedToBoolString(SettingsKey.spiral_vase, "continuousSpiralOuterPerimeter"),
|
||||
new MapPositionToPlaceObjectCenter(SettingsKey.print_center, "positionToPlaceObjectCenter"),
|
||||
new MapStartGCode("start_gcode", "startCode", true),
|
||||
new MapStartGCode(SettingsKey.start_gcode, "startCode", true),
|
||||
new MapLayerChangeGCode("layer_gcode", "layerChangeCode"),
|
||||
new ScaledSingleNumber("fill_density", "infillPercent", 100),
|
||||
new ScaledSingleNumber(SettingsKey.perimeter_start_end_overlap, "perimeterStartEndOverlapRatio", .01),
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
|
||||
public List<string> PreStartGCode(List<bool> extrudersUsed)
|
||||
{
|
||||
string startGCode = ActiveSliceSettings.Instance.GetValue("start_gcode");
|
||||
string startGCode = ActiveSliceSettings.Instance.GetValue(SettingsKey.start_gcode);
|
||||
string[] preStartGCodeLines = startGCode.Split(new string[] { "\\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
List<string> preStartGCode = new List<string>();
|
||||
|
|
@ -339,7 +339,7 @@ namespace MatterHackers.MatterControl.SlicerConfiguration
|
|||
|
||||
public List<string> PostStartGCode(List<bool> extrudersUsed)
|
||||
{
|
||||
string startGCode = ActiveSliceSettings.Instance.GetValue("start_gcode");
|
||||
string startGCode = ActiveSliceSettings.Instance.GetValue(SettingsKey.start_gcode);
|
||||
string[] postStartGCodeLines = startGCode.Split(new string[] { "\\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
List<string> postStartGCode = new List<string>();
|
||||
|
|
|
|||
|
|
@ -361,6 +361,10 @@ Advanced
|
|||
cancel_gcode
|
||||
On Connect G-Code
|
||||
connect_gcode
|
||||
Write Filters
|
||||
write_regex
|
||||
Read Filters
|
||||
read_regex
|
||||
Extruder
|
||||
Extruder X
|
||||
nozzle_diameter
|
||||
|
|
|
|||
|
|
@ -1325,6 +1325,32 @@
|
|||
"DataEditType": "MULTI_LINE_TEXT",
|
||||
"DefaultValue": "G28 ; home all axes\\nG1 Z5 F5000 ; lift nozzle"
|
||||
},
|
||||
{
|
||||
"QuickMenuSettings": [ ],
|
||||
"SetSettingsOnChange": [ ],
|
||||
"SlicerConfigName": "write_regex",
|
||||
"PresentationName": "Write RegEx",
|
||||
"HelpText": "This is a set of regular expressions to apply to lines prior to sending to a printer. They will be applied in the order listed before sending. To return more than one instruction separate them with comma.",
|
||||
"DataEditType": "MULTI_LINE_TEXT",
|
||||
"ExtraSettings": "",
|
||||
"ShowAsOverride": true,
|
||||
"ShowIfSet": null,
|
||||
"ResetAtEndOfPrint": false,
|
||||
"DefaultValue": ""
|
||||
},
|
||||
{
|
||||
"QuickMenuSettings": [ ],
|
||||
"SetSettingsOnChange": [ ],
|
||||
"SlicerConfigName": "read_regex",
|
||||
"PresentationName": "Recieve RegEx",
|
||||
"HelpText": "This is a set of regular expressions to apply to lines after they are recived from the printer. They will be applied in order to each line recieved.",
|
||||
"DataEditType": "MULTI_LINE_TEXT",
|
||||
"ExtraSettings": "",
|
||||
"ShowAsOverride": true,
|
||||
"ShowIfSet": null,
|
||||
"ResetAtEndOfPrint": false,
|
||||
"DefaultValue": ""
|
||||
},
|
||||
{
|
||||
"SlicerConfigName": "start_perimeters_at_concave_points",
|
||||
"PresentationName": "Start At Concave Points",
|
||||
|
|
|
|||
|
|
@ -4261,3 +4261,15 @@ Translated:HotEnd
|
|||
English:Heated Bed
|
||||
Translated:Heated Bed
|
||||
|
||||
English:This is a set of regular expressions to apply to lines prior to sending to a printer. They will be applied in the order listed before sending. To return more than one instruction separate them with comma.
|
||||
Translated:This is a set of regular expressions to apply to lines prior to sending to a printer. They will be applied in the order listed before sending. To return more than one instruction separate them with comma.
|
||||
|
||||
English:This is a set of regular expressions to apply to lines after they are recived from the printer. They will be applied in order to each line recieved.
|
||||
Translated:This is a set of regular expressions to apply to lines after they are recived from the printer. They will be applied in order to each line recieved.
|
||||
|
||||
English:Write Filters
|
||||
Translated:Write Filters
|
||||
|
||||
English:Read Filters
|
||||
Translated:Read Filters
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 3d0d8dd95263063982b25a954fffb7559ee60924
|
||||
Subproject commit 8d1960a5d4f24d3c99d7daed2029ac7f33ac077a
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="nunit.framework" publicKeyToken="2638cd05610744eb" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0"/>
|
||||
<assemblyIdentity name="nunit.framework" publicKeyToken="2638cd05610744eb" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/></startup></configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration>
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
ValidateOnAllPrinters((printer, settings) =>
|
||||
{
|
||||
// TODO: Why aren't we testing all gcode sections?
|
||||
string[] keysToTest = { "start_gcode", "end_gcode" };
|
||||
string[] keysToTest = { SettingsKey.start_gcode, SettingsKey.end_gcode };
|
||||
foreach (string gcodeKey in keysToTest)
|
||||
{
|
||||
string gcode = settings.GetValue(gcodeKey);
|
||||
|
|
@ -304,7 +304,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
{
|
||||
ValidateOnAllPrinters((printer, settings) =>
|
||||
{
|
||||
string startGcode = settings.GetValue("start_gcode");
|
||||
string startGcode = settings.GetValue(SettingsKey.start_gcode);
|
||||
Assert.False(startGcode.Contains("first_layer_bed_temperature"), "[start_gcode] should not contain [first_layer_bed_temperature]" + printer.RelativeFilePath);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
public void noCurlyBracketsInStartGcode(PrinterSettingsLayer layer, string sourceFile)
|
||||
{
|
||||
string settingValue;
|
||||
if (!layer.TryGetValue("start_gcode", out settingValue))
|
||||
if (!layer.TryGetValue(SettingsKey.start_gcode, out settingValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -277,7 +277,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
public void noCurlyBracketsInEndGcode(PrinterSettingsLayer layer, string sourceFile)
|
||||
{
|
||||
string settingValue;
|
||||
if (!layer.TryGetValue("end_gcode", out settingValue))
|
||||
if (!layer.TryGetValue(SettingsKey.end_gcode, out settingValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
public void testFirstLayerTempNotInStartGcode(PrinterSettingsLayer layer, string sourceFile)
|
||||
{
|
||||
string settingValue;
|
||||
if (!layer.TryGetValue("start_gcode", out settingValue))
|
||||
if (!layer.TryGetValue(SettingsKey.start_gcode, out settingValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -310,7 +310,7 @@ namespace MatterControl.Tests.MatterControl
|
|||
public void testFirstLayerBedTemperatureNotInStartGcode(PrinterSettingsLayer layer, string sourceFile)
|
||||
{
|
||||
string settingValue;
|
||||
if (!layer.TryGetValue("start_gcode", out settingValue))
|
||||
if (!layer.TryGetValue(SettingsKey.start_gcode, out settingValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="nunit.framework" publicKeyToken="2638cd05610744eb" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0"/>
|
||||
<assemblyIdentity name="nunit.framework" publicKeyToken="2638cd05610744eb" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/></startup></configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration>
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
<packages>
|
||||
<package id="Mindscape.Raygun4Net" version="5.4.1" targetFramework="net45" />
|
||||
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net45" />
|
||||
<package id="System.ValueTuple" version="4.3.1" targetFramework="net461" />
|
||||
</packages>
|
||||
Loading…
Add table
Add a link
Reference in a new issue