Files
UVtools/UVtools.Core/FileFormats/ZCodeFile.cs
T
Tiago Conceição a648610d59 v2.15.0
- **File formats:**
   - (Add) Wait time before cure: The time to rest/wait in seconds before cure a new layer
   - (Add) Wait time before after: The time to rest/wait in seconds after cure a new layer
   - (Add) Wait time after lift: The time to rest/wait in seconds after a lift/peel move
   - (Change) All gcode file formats dropped light-off delay field in favor of new 'Wait time before cure' field, setting light-off delay still valid but it redirects to the new field
   - (Change) Reorder 'Light-off delay' before 'Exposure time'
   - (Improvement) Recalculate the print time when a related property changes
   - (Fix) Generic time estimation calculation was ignoring exposure times
   - (Fix) Unable to load files with uppercase extensions
   - (Fix) ZIP: Use G1 at end of gcode instead of G0
   - (Fix) CWS: Use G1 for movements
   - **(Fix) CXDLP:** (#240)
      - Layer area calculation
      - Validation checksum
   - **(Fix) ZCODE:**
      - Use G1 at end of gcode instead of G0 to prevent crash to top bug on firmware
      - Put back the M18 motors off at end of gcode
- **GCode Builder/Parser:**
   - (Add) Allow to choose between G0 and G1 for layer movements and end gcode
   - (Fix) Safe guard: If the total print height is larger than set machine Z, do not raise/lower print on completeness
   - (Fix) Light-off delay is the real delay time and not the calculated movement of the lifts plus the extra time
   - (Improvement) Parse gcode line by line instead searching on a group of layers to allow a better control and identification
- **Tools:**
   - **Change print parameters:**
      - (Add) Tooltips to labels
      - (Add) Sun UTF-8 to the Light PWM value unit to describe intensity
   - (Improvement) Dynamic lifts: Round lift height and speed to 1 decimal
   - (Fix) Exposure time finder: Time estimation when using 'Use different settings for layers with same Z positioning'
- **Prusa Slicer:**
  - (Add) Note keyword: BottomWaitBeforeCure_xxx
  - (Add) Note keyword: WaitBeforeCure_xxx
  - (Add) Note keyword: BottomWaitAfterCure_xxx
  - (Add) Note keyword: WaitAfterCure_xxx
  - (Add) Note keyword: BottomWaitAfterLift_xxx
  - (Add) Note keyword: WaitAfterLift_xxx
  - (Change) Uniz IBEE: Remove light-off delay and implement wait time keywords
- **macOS:** (#236)
  - (Remove) osx legacy packages from build and downloads
  - (Fix) macOS: Simplify the libcvextern.dylib and remove libtesseract dependency
  - (Fix) macOS: Include libusb-1.0.0.dylib
  - Note: `brew install libusb` still required
- **UI:**
  - (Fix) Refresh gcode does not update text on UI for ZIP, CWS, ZCODEX files
  - (Fix) Operations: Import a .uvtop file by drag and drop into the UI would not load the layer range
  - (Change) When convert a file, the result dialog will have Yes, No and Cancel actions, where No will open the converted file on current window, while Cancel will not perform any action (The old No behaviour)
2021-07-16 22:40:11 +01:00

676 lines
24 KiB
C#

/*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.OpenSsl;
using UVtools.Core.Extensions;
using UVtools.Core.GCode;
using UVtools.Core.Operations;
namespace UVtools.Core.FileFormats
{
[Serializable]
[XmlRoot(ElementName = "Print")]
public class ZCodePrint
{
[Serializable]
[XmlRoot(ElementName = "Device")]
public class ZcodePrintDevice
{
[XmlAttribute("z")]
public float MachineZ { get; set; } = 220;
[XmlAttribute("height")]
public uint ResolutionY { get; set; } = 2400;
[XmlAttribute("width")]
public uint ResolutionX { get; set; } = 3840;
[XmlAttribute("type")]
public string MachineModel { get; set; } = "IBEE";
[XmlAttribute("pixel_size")]
public float PixelSize { get; set; } = 50;
}
[Serializable]
[XmlRoot(ElementName = "Profile")]
public class ZcodePrintProfile
{
[XmlAttribute("name")]
public string Name { get; set; } = "UVtools";
[Serializable]
[XmlRoot(ElementName = "Slice")]
public class ZcodePrintProfileSlice
{
[XmlAttribute("bottom_layers")]
public ushort BottomLayerCount { get; set; } = FileFormat.DefaultBottomLayerCount;
[XmlAttribute("exposure_bottom")]
public uint BottomExposureTime { get; set; } = (uint) (FileFormat.DefaultBottomExposureTime * 1000);
[XmlAttribute("exposure")]
public uint ExposureTime { get; set; } = (uint) (FileFormat.DefaultExposureTime * 1000);
[XmlAttribute("height_bottom")]
public float BottomLiftHeight { get; set; } = FileFormat.DefaultBottomLiftHeight;
[XmlAttribute("speed_bottom")]
public float BottomLiftSpeed { get; set; } = FileFormat.DefaultBottomLiftSpeed;
[XmlAttribute("height")]
public float LiftHeight { get; set; } = FileFormat.DefaultLiftHeight;
[XmlAttribute("speed")]
public float LiftSpeed { get; set; } = FileFormat.DefaultLiftSpeed;
[XmlAttribute("cooldown_bottom")]
public uint BottomLightOffDelay { get; set; } = (uint) (FileFormat.DefaultBottomLightOffDelay * 1000);
[XmlAttribute("cooldown")]
public uint LightOffDelay { get; set; } = (uint) (FileFormat.DefaultLightOffDelay * 1000);
[XmlAttribute("thickness")]
public float LayerHeight { get; set; } = FileFormat.DefaultLayerHeight;
[XmlAttribute("anti_aliasing_level")]
public byte AntiAliasing { get; set; }
[XmlAttribute("anti_aliasing_grey_level")]
public byte AntiAliasingGrey { get; set; }
[XmlAttribute("led_power")]
public ushort LedPower { get; set; } = ZCodeFile.MaxLEDPower;
}
public ZcodePrintProfileSlice Slice { get; set; } = new();
}
[Serializable]
[XmlRoot(ElementName = "Job")]
public class ZcodePrintJob
{
[XmlElement("name")]
public string StlName { get; set; }
[XmlElement("previewImage")]
public string PreviewImage { get; set; } = ZCodeFile.PreviewFilename;
[XmlElement("layers")]
public uint LayerCount { get; set; }
[XmlElement("time")]
public uint PrintTime { get; set; }
[XmlElement("volumn")]
public float VolumeMl { get; set; }
[XmlElement("thickness")]
public float LayerHeight { get; set; } = FileFormat.DefaultLayerHeight;
[XmlElement("price")]
public float Price { get; set; }
[XmlElement("weight")]
public float WeightG { get; set; }
}
public ZcodePrintDevice Device = new();
public ZcodePrintProfile Profile = new();
public ZcodePrintJob Job = new();
}
public class ZCodeFile : FileFormat
{
#region Constants
public const string GCodeFilename = "lcd.gcode";
public const string ManifestFilename = "task.xml";
public const string PreviewFilename = "preview.png";
public const ushort MaxLEDPower = 300;
public const string GCodeStart = "G21;{0}" + // Set units to be mm
"G90;{0}" + // Absolute Positioning
"M106 S0;{0}" + // Light Off
"G28 Z0;{0}"; // Home
public const string GCodeEnd = "M106 S0{0}" + // Light Off
"G1 Z{1} F10;{0}" + // Raize Z
"M18;{0}"; // Disable Motors
public const string GCodeRSAPrivateKey =
"-----BEGIN PRIVATE KEY-----\n" +
"MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA6BnF3oT9Ap+OyZFs" +
"p02ZRtTpbMUA96PpctTXev1cPCclLuowXWNa3f2JBpPbPrzMwLam3JSQN3HjvYfS" +
"Vx7e6wIDAQABAkBibCNzQ/PCfAThxxBLNeXMmpbNsBDD8rcZIdaqaewF+UjUlqI7" +
"eI0Yp2V2Ez28FjKCEiM34DxU9PZTzYS3rCShAiEA9qp9+RwYvcvQUXlT2bqKIo3s" +
"xu7LQFM4VIddQ1SMVhsCIQDw4i74LqF285Iz77vw1MXE06LHKKBBBa3Air1QNBhn" +
"cQIgYfO4TLk8lfoewovkoVyzSB+F/EWNjwC9KMwMXBVyGSsCIQDisxudOtV+y3C3" +
"LFHmL3kI6lxxrsxTJXMGmAvfJYgqIQIhAKBtzYeZB46EToDSpeYlrMQitSWvjgNG" +
"mw+8aB/HYEIr\n" +
"-----END PRIVATE KEY-----";
public const string GCodeRSAPublicKey =
"-----BEGIN PUBLIC KEY-----\n" +
"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOgZxd6E/QKfjsmRbKdNmUbU6WzFAPej" +
"6XLU13r9XDwnJS7qMF1jWt39iQaT2z68zMC2ptyUkDdx472H0lce3usCAwEAAQ==\n" +
"-----END PUBLIC KEY-----";
#endregion
#region Sub Classes
#endregion
#region Properties
public ZCodePrint ManifestFile { get; set; } = new ();
public override FileFormatType FileType => FileFormatType.Archive;
public override FileExtension[] FileExtensions { get; } = {
new("zcode", "UnizMaker ZCode")
};
public override PrintParameterModifier[] PrintParameterModifiers { get; } = {
PrintParameterModifier.BottomLayerCount,
PrintParameterModifier.BottomWaitTimeBeforeCure,
PrintParameterModifier.WaitTimeBeforeCure,
PrintParameterModifier.BottomExposureTime,
PrintParameterModifier.ExposureTime,
PrintParameterModifier.BottomWaitTimeAfterCure,
PrintParameterModifier.WaitTimeAfterCure,
PrintParameterModifier.BottomLiftHeight,
PrintParameterModifier.BottomLiftSpeed,
PrintParameterModifier.LiftHeight,
PrintParameterModifier.LiftSpeed,
PrintParameterModifier.BottomWaitTimeAfterLift,
PrintParameterModifier.WaitTimeAfterLift,
PrintParameterModifier.RetractSpeed,
PrintParameterModifier.BottomLightPWM,
PrintParameterModifier.LightPWM,
};
public override PrintParameterModifier[] PrintParameterPerLayerModifiers { get; } = {
PrintParameterModifier.WaitTimeBeforeCure,
PrintParameterModifier.ExposureTime,
PrintParameterModifier.WaitTimeAfterCure,
PrintParameterModifier.LiftHeight,
PrintParameterModifier.LiftSpeed,
PrintParameterModifier.WaitTimeAfterLift,
PrintParameterModifier.RetractSpeed,
PrintParameterModifier.LightPWM,
};
public override Size[] ThumbnailsOriginalSize { get; } = {new(640, 480)};
public override uint ResolutionX
{
get => ManifestFile.Device.ResolutionX;
set
{
ManifestFile.Device.ResolutionX = value;
RaisePropertyChanged();
}
}
public override uint ResolutionY
{
get => ManifestFile.Device.ResolutionY;
set
{
ManifestFile.Device.ResolutionY = value;
RaisePropertyChanged();
}
}
public override float DisplayWidth
{
get => (float) Math.Round(ManifestFile.Device.ResolutionX * ManifestFile.Device.PixelSize / 1000, 2);
set => RaisePropertyChanged();
}
public override float DisplayHeight
{
get => (float)Math.Round(ManifestFile.Device.ResolutionY * ManifestFile.Device.PixelSize / 1000, 2);
set => RaisePropertyChanged();
}
public override float MachineZ
{
get => ManifestFile.Device.MachineZ > 0 ? ManifestFile.Device.MachineZ : base.MachineZ;
set
{
ManifestFile.Device.MachineZ = value;
RaisePropertyChanged();
}
}
public override bool MirrorDisplay => true;
public override byte AntiAliasing
{
get => ManifestFile.Profile.Slice.AntiAliasing;
set
{
ManifestFile.Profile.Slice.AntiAliasing = value.Clamp(1, 16);
RaisePropertyChanged();
}
}
public override float LayerHeight
{
get => ManifestFile.Job.LayerHeight > 0 ? ManifestFile.Job.LayerHeight : ManifestFile.Profile.Slice.LayerHeight;
set
{
ManifestFile.Job.LayerHeight = ManifestFile.Profile.Slice.LayerHeight = Layer.RoundHeight(value);
RaisePropertyChanged();
}
}
public override uint LayerCount
{
get => base.LayerCount;
set => base.LayerCount = ManifestFile.Job.LayerCount = base.LayerCount;
}
public override ushort BottomLayerCount
{
get => ManifestFile.Profile.Slice.BottomLayerCount;
set => base.BottomLayerCount = ManifestFile.Profile.Slice.BottomLayerCount = value;
}
public override float BottomLightOffDelay
{
get => BottomWaitTimeBeforeCure;
set => BottomWaitTimeBeforeCure = value;
}
public override float LightOffDelay
{
get => WaitTimeBeforeCure;
set => WaitTimeBeforeCure = value;
}
public override float BottomWaitTimeBeforeCure
{
get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomLightOffDelay);
set
{
ManifestFile.Profile.Slice.BottomLightOffDelay = TimeExtensions.SecondsToMillisecondsUint(value);
base.BottomWaitTimeBeforeCure = base.BottomLightOffDelay = value;
}
}
public override float WaitTimeBeforeCure
{
get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.LightOffDelay);
set
{
ManifestFile.Profile.Slice.LightOffDelay = TimeExtensions.SecondsToMillisecondsUint(value);
base.WaitTimeBeforeCure = base.LightOffDelay = value;
}
}
public override float BottomExposureTime
{
get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomExposureTime);
set
{
ManifestFile.Profile.Slice.BottomExposureTime = TimeExtensions.SecondsToMillisecondsUint(value);
base.BottomExposureTime = value;
}
}
public override float ExposureTime
{
get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.ExposureTime);
set
{
ManifestFile.Profile.Slice.ExposureTime = TimeExtensions.SecondsToMillisecondsUint(value);
base.ExposureTime = value;
}
}
public override float BottomLiftHeight
{
get => ManifestFile.Profile.Slice.BottomLiftHeight;
set => base.BottomLiftHeight = ManifestFile.Profile.Slice.BottomLiftHeight = (float)Math.Round(value, 2);
}
public override float LiftHeight
{
get => ManifestFile.Profile.Slice.LiftHeight;
set => base.LiftHeight = ManifestFile.Profile.Slice.LiftHeight = (float)Math.Round(value, 2);
}
public override float BottomLiftSpeed
{
get => ManifestFile.Profile.Slice.BottomLiftSpeed;
set => base.BottomLiftSpeed = ManifestFile.Profile.Slice.BottomLiftSpeed = (float)Math.Round(value, 2);
}
public override float LiftSpeed
{
get => ManifestFile.Profile.Slice.LiftSpeed;
set => base.LiftSpeed = ManifestFile.Profile.Slice.LiftSpeed = (float)Math.Round(value, 2);
}
public override byte LightPWM
{
get => (byte)(byte.MaxValue * ManifestFile.Profile.Slice.LedPower / MaxLEDPower);
set
{
ManifestFile.Profile.Slice.LedPower = (ushort)(MaxLEDPower * value / byte.MaxValue);
base.LightPWM = value;
RaisePropertyChanged(nameof(BottomLightPWM));
}
}
public override float PrintTime
{
get => base.PrintTime;
set
{
base.PrintTime = value;
ManifestFile.Job.PrintTime = (uint) base.PrintTime;
}
}
public override float MaterialMilliliters
{
get => base.MaterialMilliliters;
set
{
base.MaterialMilliliters = value;
ManifestFile.Job.VolumeMl = base.MaterialMilliliters;
}
}
public override float MaterialGrams
{
get => ManifestFile.Job.WeightG;
set => base.MaterialGrams = ManifestFile.Job.WeightG = (float)Math.Round(value, 3);
}
public override float MaterialCost
{
get => ManifestFile.Job.Price;
set => base.MaterialCost = ManifestFile.Job.Price = (float)Math.Round(value, 3);
}
/*public override string MaterialName
{
get => HeaderSettings.Resin;
set
{
HeaderSettings.Resin = value;
RaisePropertyChanged();
}
}*/
public override string MachineName
{
get => ManifestFile.Device.MachineModel;
set => base.MachineName = ManifestFile.Device.MachineModel = value;
}
public override object[] Configs => new object[] { ManifestFile.Device, ManifestFile.Job, ManifestFile.Profile.Slice };
#endregion
#region Constructor
public ZCodeFile()
{
GCode = new GCodeBuilder
{
UseTailComma = true,
UseComments = false,
GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Absolute,
GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.CentimetersPerMinute,
GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds,
GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenameNonZeroPNG,
LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G0,
EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1,
MaxLEDPower = MaxLEDPower,
CommandClearImage = {Enabled = false},
};
}
#endregion
#region Methods
protected override void EncodeInternally(string fileFullPath, OperationProgress progress)
{
using ZipArchive outputFile = ZipFile.Open(fileFullPath, ZipArchiveMode.Create);
if (Thumbnails.Length > 0 && Thumbnails[0] is not null)
{
using var thumbnailsStream = outputFile.CreateEntry(PreviewFilename).Open();
thumbnailsStream.WriteBytes(Thumbnails[0].GetPngByes());
thumbnailsStream.Close();
}
for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++)
{
progress.Token.ThrowIfCancellationRequested();
var layer = this[layerIndex];
outputFile.PutFileContent($"{layerIndex + 1}.png", layer.CompressedBytes, ZipArchiveMode.Create);
progress++;
}
XmlSerializer serializer = new(ManifestFile.GetType());
XmlSerializerNamespaces ns = new();
ns.Add("", "");
var entry = outputFile.CreateEntry(ManifestFilename);
using (var stream = entry.Open())
{
serializer.Serialize(stream, ManifestFile, ns);
}
outputFile.PutFileContent(GCodeFilename, EncryptGCode(progress), ZipArchiveMode.Create);
}
protected override void DecodeInternally(string fileFullPath, OperationProgress progress)
{
using var inputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Read);
var entry = inputFile.GetEntry(ManifestFilename);
if (entry is null)
{
Clear();
throw new FileLoadException($"{ManifestFilename} not found", fileFullPath);
}
try
{
var serializer = new XmlSerializer(ManifestFile.GetType());
using var stream = entry.Open();
ManifestFile = (ZCodePrint)serializer.Deserialize(stream);
}
catch (Exception e)
{
Clear();
throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", fileFullPath);
}
entry = inputFile.GetEntry(GCodeFilename);
if (entry is null)
{
Clear();
throw new FileLoadException($"{GCodeFilename} not found", fileFullPath);
}
var encryptEngine = new RsaEngine();
using var txtreader = new StringReader(GCodeRSAPublicKey);
var keyParameter = (AsymmetricKeyParameter)new PemReader(txtreader).ReadObject();
encryptEngine.Init(true, keyParameter);
using (TextReader tr = new StreamReader(entry.Open()))
{
string line;
progress.Reset("Decrypting GCode", (uint) (entry.Length / 88));
while ((line = tr.ReadLine()) != null)
{
if (string.IsNullOrEmpty(line)) continue;
if (!line.EndsWith("==")) continue;
byte[] data = System.Convert.FromBase64String(line);
var decodedBytes = encryptEngine.ProcessBlock(data, 0, data.Length);
decodedBytes = decodedBytes.Skip(2).SkipWhile(b => b is 255 or 0).ToArray();
GCode.AppendLine(Encoding.UTF8.GetString(decodedBytes));
progress++;
}
tr.Close();
}
LayerManager.Init(ManifestFile.Job.LayerCount);
progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount);
//var gcode = GCode.ToString();
//float lastPostZ = LayerHeight;
for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++)
{
if (progress.Token.IsCancellationRequested) break;
entry = inputFile.GetEntry($"{layerIndex+1}.png");
if (entry is null)
{
Clear();
throw new FileLoadException($"Layer {layerIndex+1} not found", fileFullPath);
}
using var stream = entry.Open();
this[layerIndex] = new Layer(layerIndex, stream, LayerManager);
progress++;
}
GCode.ParseLayersFromGCode(this);
entry = inputFile.GetEntry(PreviewFilename);
if (entry is not null)
{
Thumbnails[0] = new Mat();
CvInvoke.Imdecode(entry.Open().ToArray(), ImreadModes.AnyColor, Thumbnails[0]);
}
LayerManager.GetBoundingRectangle(progress);
}
public override void SaveAs(string filePath = null, OperationProgress progress = null)
{
if (RequireFullEncode)
{
if (!string.IsNullOrEmpty(filePath))
{
FileFullPath = filePath;
}
Encode(FileFullPath, progress);
return;
}
if (!string.IsNullOrEmpty(filePath))
{
File.Copy(FileFullPath, filePath, true);
FileFullPath = filePath;
}
using var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update);
bool deleted;
do
{
deleted = false;
foreach (var zipentry in outputFile.Entries)
{
if (!zipentry.Name.EndsWith(".gcode") && !zipentry.Name.EndsWith(".xml")) continue;
zipentry.Delete();
deleted = true;
break;
}
} while (deleted);
XmlSerializer serializer = new(ManifestFile.GetType());
XmlSerializerNamespaces ns = new();
ns.Add("", "");
var entry = outputFile.CreateEntry(ManifestFilename);
using var stream = entry.Open();
serializer.Serialize(stream, ManifestFile, ns);
outputFile.PutFileContent(GCodeFilename, EncryptGCode(progress), ZipArchiveMode.Update);
//Decode(FileFullPath, progress);
}
private string EncryptGCode(OperationProgress progress)
{
RebuildGCode();
progress.Reset("Encrypting GCode", (uint) GCode.Length);
StringBuilder sb = new();
var encryptEngine = new RsaEngine();
using var txtreader = new StringReader(GCodeRSAPrivateKey);
var keyParameter = (AsymmetricKeyParameter)new PemReader(txtreader).ReadObject();
encryptEngine.Init(true, keyParameter);
using StringReader sr = new(GCodeStr);
string line;
while ((line = sr.ReadLine()) != null)
{
line = line.Trim();
if (line == string.Empty || line[0] == ';') continue; // No empty lines nor comment start lines
progress += (uint)line.Length;
byte[] data = Encoding.UTF8.GetBytes(line);
List<byte> padData = new(64) {0, 1, 0};
padData.AddRange(data);
if (padData.Count > 64)
{
throw new ArgumentOutOfRangeException($"Too long gcode line to encrypt, got: {padData.Count} bytes while expecting less than 64 bytes");
}
while (padData.Count < 64)
{
padData.Insert(2, 255);
}
var padDataArray = padData.ToArray();
var encodedBytes = encryptEngine.ProcessBlock(padDataArray, 0, padDataArray.Length);
sb.AppendLine(System.Convert.ToBase64String(encodedBytes));
}
return sb.ToString();
}
#endregion
}
}