/// <summary> /// Print the diagnostics /// </summary> /// <param name="result">Target to write to</param> /// <returns>Asynchronous task</returns> private static async Task Diagnostics(CodeResult result) { StringBuilder builder = new StringBuilder(); builder.AppendLine("=== Duet Control Server ==="); builder.AppendLine($"Duet Control Server v{Assembly.GetExecutingAssembly().GetName().Version}"); await SPI.Interface.Diagnostics(builder); SPI.DataTransfer.Diagnostics(builder); await Print.Diagnostics(builder); result.Add(MessageType.Success, builder.ToString().TrimEnd()); }
/// <summary> /// Print the diagnostics /// </summary> /// <param name="result">Target to write to</param> /// <returns>Asynchronous task</returns> private static async Task Diagnostics(CodeResult result) { StringBuilder builder = new StringBuilder(); builder.AppendLine("=== Duet Control Server ==="); builder.AppendLine($"Duet Control Server v{Program.Version}"); await SPI.Interface.Diagnostics(builder); SPI.DataTransfer.Diagnostics(builder); await FileExecution.Job.Diagnostics(builder); result.Add(MessageType.Success, builder.ToString().TrimEnd()); }
/// <summary> /// Converts simple G/M/T-codes to a regular Code instances, executes them and returns the result as text /// </summary> /// <returns>Code result as text</returns> /// <exception cref="OperationCanceledException">Code has been cancelled</exception> public override async Task <string> Execute() { CodeResult result = new CodeResult(); try { List <Code> codes = Commands.Code.ParseMultiple(Code); foreach (Code code in codes) { // M112, M122, and M999 always go to the Daemon channel so we (hopefully) get a low-latency response if (code.Type == CodeType.MCode && (code.MajorNumber == 112 || code.MajorNumber == 122 || code.MajorNumber == 999)) { code.Channel = DuetAPI.CodeChannel.Daemon; code.Flags |= CodeFlags.IsPrioritized; } else { code.Channel = Channel; } // Execute the code and append the result CodeResult codeResult = await code.Execute(); result.AddRange(codeResult); } } catch (CodeParserException e) { // Report parsing errors as an error message result = new CodeResult(DuetAPI.MessageType.Error, e.Message); } catch (OperationCanceledException) { // Report this code as cancelled result.Add(DuetAPI.MessageType.Error, "Code has been cancelled"); } return(result.ToString()); }
/// <summary> /// React to an executed G-code before its result is returend /// </summary> /// <param name="code">Code processed by RepRapFirmware</param> /// <param name="result">Result that it generated</param> /// <returns>Result to output</returns> /// <remarks>This method shall be used only to update values that are time-critical. Others are supposed to be updated via the object model</remarks> public static async Task <CodeResult> CodeExecuted(Code code, CodeResult result) { if (!result.IsSuccessful) { return(result); } switch (code.MajorNumber) { // Rapid/Regular positioning case 0: case 1: CodeParameter feedrate = code.Parameter('F'); if (feedrate != null) { using (await Model.Provider.AccessReadWriteAsync()) { if (Model.Provider.Get.Channels[code.Channel].UsingInches) { Model.Provider.Get.Channels[code.Channel].Feedrate = feedrate / 25.4F; } else { Model.Provider.Get.Channels[code.Channel].Feedrate = feedrate; } } } break; // Use inches case 20: using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Channels[code.Channel].UsingInches = true; } break; // Use millimetres case 21: using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Channels[code.Channel].UsingInches = false; } break; // Save heightmap case 29: if (code.Parameter('S', 0) == 0) { string file = code.Parameter('P', "heightmap.csv"); try { Heightmap map = await SPI.Interface.GetHeightmap(); await map.Save(await FilePath.ToPhysicalAsync(file, "sys")); result.Add(DuetAPI.MessageType.Success, $"Height map saved to file {file}"); } catch (AggregateException ae) { result.Add(DuetAPI.MessageType.Error, $"Failed to save height map to file {file}: {ae.InnerException.Message}"); } } break; // Absolute positioning case 90: using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Channels[code.Channel].RelativePositioning = false; } break; // Relative positioning case 91: using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Channels[code.Channel].RelativePositioning = true; } break; } return(result); }
/// <summary> /// Converts simple G/M/T-codes to a regular Code instances, executes them and returns the result as text /// </summary> /// <returns>Code result as text</returns> /// <exception cref="OperationCanceledException">Code has been cancelled</exception> public override async Task <string> Execute() { // Check if the corresponding code channel has been disabled using (await Model.Provider.AccessReadOnlyAsync()) { if (Model.Provider.Get.Inputs[Channel] == null) { throw new InvalidOperationException("Requested code channel has been disabled"); } } // Parse the input string List <Code> codes = new List <Code>(), priorityCodes = new List <Code>(); try { await foreach (Code code in ParseAsync()) { // M108, M112, M122, and M999 always go to an idle channel so we (hopefully) get a low-latency response if (code.Type == CodeType.MCode && (code.MajorNumber == 108 || code.MajorNumber == 112 || code.MajorNumber == 122 || code.MajorNumber == 999)) { code.Channel = await SPI.Interface.GetIdleChannel(); code.Flags |= CodeFlags.IsPrioritized; priorityCodes.Add(code); } else if (IPC.Processors.CodeInterception.IsInterceptingConnection(Connection)) { // Need to bypass the code order lock for codes being inserted... priorityCodes.Add(code); } else { codes.Add(code); } } } catch (CodeParserException e) { // Report parsing errors as an error message return((new CodeResult(MessageType.Error, e.Message)).ToString()); } CodeResult result = new CodeResult(); try { // Execute priority codes first foreach (Code priorityCode in priorityCodes) { CodeResult codeResult = await priorityCode.Execute(); try { if (codeResult != null) { result.AddRange(codeResult); } } catch (OperationCanceledException) { // not logged } } // Execute normal codes next. Use a lock here because multiple codes may be queued for the same channel if (codes.Count > 0) { Task <CodeResult>[] codeTasks = new Task <CodeResult> [codes.Count]; using (await _channelLocks[(int)Channel].LockAsync(Program.CancellationToken)) { for (int i = 0; i < codes.Count; i++) { codeTasks[i] = codes[i].Execute(); } } foreach (Task <CodeResult> codeTask in codeTasks) { try { CodeResult codeResult = await codeTask; if (codeResult != null) { result.AddRange(codeResult); } } catch (OperationCanceledException) { // not logged } } } } catch (CodeParserException cpe) { result.Add(MessageType.Error, cpe.Message); } return(result.ToString()); }
/// <summary> /// Converts simple G/M/T-codes to a regular Code instances, executes them and returns the result as text /// </summary> /// <returns>Code result as text</returns> /// <exception cref="OperationCanceledException">Code has been cancelled</exception> public override async Task <string> Execute() { // Parse the input string List <Code> codes = new List <Code>(), priorityCodes = new List <Code>(); try { foreach (Code code in Parse()) { // M108, M112, M122, and M999 always go to an idle channel so we (hopefully) get a low-latency response if (code.Type == CodeType.MCode && (code.MajorNumber == 108 || code.MajorNumber == 112 || code.MajorNumber == 122 || code.MajorNumber == 999)) { code.Channel = await SPI.Interface.GetIdleChannel(); code.Flags |= CodeFlags.IsPrioritized; priorityCodes.Add(code); } else if (IPC.Processors.Interception.IsInterceptingConnection(SourceConnection)) { priorityCodes.Add(code); } else { codes.Add(code); } } } catch (CodeParserException e) { // Report parsing errors as an error message return((new CodeResult(MessageType.Error, e.Message)).ToString()); } CodeResult result = new CodeResult(); try { // Execute priority codes first foreach (Code priorityCode in priorityCodes) { CodeResult codeResult = await priorityCode.Execute(); if (codeResult != null) { result.AddRange(codeResult); } } // Execute normal codes next. Use a lock here because multiple codes may be queued for the same channel if (codes.Count > 0) { using (await _channelLocks[(int)Channel].LockAsync()) { foreach (Code code in codes) { CodeResult codeResult = await code.Execute(); if (codeResult != null) { result.AddRange(codeResult); } } } } } catch (OperationCanceledException) { // Report when a code is cancelled result.Add(MessageType.Error, "Code has been cancelled"); } return(result.ToString()); }
/// <summary> /// Process a G-code that should be interpreted by the control server /// </summary> /// <param name="code">Code to process</param> /// <returns>Result of the code if the code completed, else null</returns> public static async Task <CodeResult> Process(Commands.Code code) { if (code.Channel == CodeChannel.File && FileExecution.Job.IsSimulating) { // Ignore M-codes from files in simulation mode... return(null); } switch (code.MajorNumber) { // Save or load heightmap case 29: CodeParameter cp = code.Parameter('S', 0); if (cp == 1 || cp == 3) { if (await SPI.Interface.Flush(code)) { string file = code.Parameter('P', FilePath.DefaultHeightmapFile); string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.System); try { Heightmap map = null; if (cp == 1) { map = new Heightmap(); await map.Load(physicalFile); } if (await SPI.Interface.LockMovementAndWaitForStandstill(code.Channel)) { if (cp == 1) { try { await SPI.Interface.SetHeightmap(map); } finally { await SPI.Interface.UnlockAll(code.Channel); } string virtualFile = await FilePath.ToVirtualAsync(physicalFile); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Move.Compensation.File = virtualFile; } CodeResult result = new CodeResult(); using (await Model.Provider.AccessReadOnlyAsync()) { if (Model.Provider.Get.Move.Axes.Any(axis => axis.Letter == 'Z' && !axis.Homed)) { result.Add(MessageType.Warning, "The height map was loaded when the current Z=0 datum was not determined. This may result in a height offset."); } } result.Add(MessageType.Success, $"Height map loaded from file {file}"); return(result); } else { try { map = await SPI.Interface.GetHeightmap(); } finally { await SPI.Interface.UnlockAll(code.Channel); } if (map.NumX * map.NumY > 0) { await map.Save(physicalFile); string virtualFile = await FilePath.ToVirtualAsync(physicalFile); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Move.Compensation.File = virtualFile; } return(new CodeResult(MessageType.Success, $"Height map saved to file {file}")); } return(new CodeResult()); } } } catch (Exception e) { _logger.Debug(e, "Failed to access height map file"); if (e is AggregateException ae) { e = ae.InnerException; } return(new CodeResult(MessageType.Error, $"Failed to {(cp == 1 ? "load" : "save")} height map {(cp == 1 ? "from" : "to")} file {file}: {e.Message}")); } } throw new OperationCanceledException(); } break; } return(null); }
/// <summary> /// Process an M-code that should be interpreted by the control server /// </summary> /// <param name="code">Code to process</param> /// <returns>Result of the code if the code completed, else null</returns> public static async Task <CodeResult> Process(Commands.Code code) { if (code.Channel == CodeChannel.File && FileExecution.Job.IsSimulating) { // Ignore M-codes from files in simulation mode... return(null); } switch (code.MajorNumber) { // Stop or Unconditional stop // Sleep or Conditional stop case 0: case 1: if (await SPI.Interface.Flush(code)) { using (await FileExecution.Job.LockAsync()) { if (FileExecution.Job.IsFileSelected) { // M0/M1 may be used in a print file to terminate it if (code.Channel != CodeChannel.File && !FileExecution.Job.IsPaused) { return(new CodeResult(MessageType.Error, "Pause the print before attempting to cancel it")); } code.CancellingPrint = true; } } break; } throw new OperationCanceledException(); // List SD card case 20: if (await SPI.Interface.Flush(code)) { // Resolve the directory string virtualDirectory = code.Parameter('P'); if (virtualDirectory == null) { using (await Model.Provider.AccessReadOnlyAsync()) { virtualDirectory = Model.Provider.Get.Directories.GCodes; } } string physicalDirectory = await FilePath.ToPhysicalAsync(virtualDirectory); // Make sure to stay within limits if it is a request from the firmware int maxSize = -1; if (code.Flags.HasFlag(CodeFlags.IsFromFirmware)) { maxSize = SPI.Communication.Consts.MaxMessageLength; } // Check if JSON file lists were requested int startAt = Math.Max(code.Parameter('R') ?? 0, 0); CodeParameter sParam = code.Parameter('S', 0); if (sParam == 2) { string json = FileLists.GetFiles(virtualDirectory, physicalDirectory, startAt, true, maxSize); return(new CodeResult(MessageType.Success, json)); } if (sParam == 3) { string json = FileLists.GetFileList(virtualDirectory, physicalDirectory, startAt, maxSize); return(new CodeResult(MessageType.Success, json)); } // Print standard G-code response Compatibility compatibility; using (await Model.Provider.AccessReadOnlyAsync()) { compatibility = Model.Provider.Get.Inputs[code.Channel].Compatibility; } StringBuilder result = new StringBuilder(); if (compatibility == Compatibility.Default || compatibility == Compatibility.RepRapFirmware) { result.AppendLine("GCode files:"); } else if (compatibility == Compatibility.Marlin || compatibility == Compatibility.NanoDLP) { result.AppendLine("Begin file list:"); } int numItems = 0; bool itemFound = false; foreach (string file in Directory.EnumerateFileSystemEntries(physicalDirectory)) { if (numItems++ >= startAt) { string filename = Path.GetFileName(file); if (compatibility == Compatibility.Marlin || compatibility == Compatibility.NanoDLP) { result.AppendLine(filename); } else { if (itemFound) { result.Append(','); } result.Append($"\"{filename}\""); } itemFound = true; } } if (compatibility == Compatibility.Marlin || compatibility == Compatibility.NanoDLP) { if (!itemFound) { result.AppendLine("NONE"); } result.Append("End file list"); } return(new CodeResult(MessageType.Success, result.ToString())); } throw new OperationCanceledException(); // Initialize SD card case 21: throw new NotSupportedException(); // Release SD card case 22: throw new NotSupportedException(); // Select a file to print case 23: case 32: if (await SPI.Interface.Flush(code)) { string file = code.GetUnprecedentedString(); if (string.IsNullOrWhiteSpace(file)) { return(new CodeResult(MessageType.Error, "Filename expected")); } string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.GCodes); if (!File.Exists(physicalFile)) { return(new CodeResult(MessageType.Error, $"Could not find file {file}")); } using (await FileExecution.Job.LockAsync()) { if (code.Channel != CodeChannel.File && FileExecution.Job.IsProcessing) { return(new CodeResult(MessageType.Error, "Cannot set file to print, because a file is already being printed")); } await FileExecution.Job.SelectFile(physicalFile); } if (await code.EmulatingMarlin()) { return(new CodeResult(MessageType.Success, "File opened\nFile selected")); } return(new CodeResult(MessageType.Success, $"File {file} selected for printing")); } throw new OperationCanceledException(); // Resume a file print case 24: if (await SPI.Interface.Flush(code)) { using (await FileExecution.Job.LockAsync()) { if (!FileExecution.Job.IsFileSelected) { return(new CodeResult(MessageType.Error, "Cannot print, because no file is selected!")); } } // Let RepRapFirmware process this request so it can invoke resume.g. When M24 completes, the file is resumed break; } throw new OperationCanceledException(); // Set SD position case 26: if (await SPI.Interface.Flush(code)) { using (await FileExecution.Job.LockAsync()) { if (!FileExecution.Job.IsFileSelected) { return(new CodeResult(MessageType.Error, "Not printing a file")); } CodeParameter sParam = code.Parameter('S'); if (sParam != null) { if (sParam < 0L || sParam > FileExecution.Job.FileLength) { return(new CodeResult(MessageType.Error, "Position is out of range")); } await FileExecution.Job.SetFilePosition(sParam); } } // P is not supported yet return(new CodeResult()); } throw new OperationCanceledException(); // Report SD print status case 27: if (await SPI.Interface.Flush(code)) { using (await FileExecution.Job.LockAsync()) { if (FileExecution.Job.IsFileSelected) { long filePosition = await FileExecution.Job.GetFilePosition(); return(new CodeResult(MessageType.Success, $"SD printing byte {filePosition}/{FileExecution.Job.FileLength}")); } return(new CodeResult(MessageType.Success, "Not SD printing.")); } } throw new OperationCanceledException(); // Begin write to SD card case 28: if (await SPI.Interface.Flush(code)) { int numChannel = (int)code.Channel; using (await Commands.Code.FileLocks[numChannel].LockAsync(code.CancellationToken)) { if (Commands.Code.FilesBeingWritten[numChannel] != null) { return(new CodeResult(MessageType.Error, "Another file is already being written to")); } string file = code.GetUnprecedentedString(); if (string.IsNullOrWhiteSpace(file)) { return(new CodeResult(MessageType.Error, "Filename expected")); } string prefix = (await code.EmulatingMarlin()) ? "ok\n" : string.Empty; string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.GCodes); try { FileStream fileStream = new FileStream(physicalFile, FileMode.Create, FileAccess.Write); StreamWriter writer = new StreamWriter(fileStream); Commands.Code.FilesBeingWritten[numChannel] = writer; return(new CodeResult(MessageType.Success, prefix + $"Writing to file: {file}")); } catch (Exception e) { _logger.Debug(e, "Failed to open file for writing"); return(new CodeResult(MessageType.Error, prefix + $"Can't open file {file} for writing.")); } } } throw new OperationCanceledException(); // End write to SD card case 29: if (await SPI.Interface.Flush(code)) { int numChannel = (int)code.Channel; using (await Commands.Code.FileLocks[numChannel].LockAsync(code.CancellationToken)) { if (Commands.Code.FilesBeingWritten[numChannel] != null) { Stream stream = Commands.Code.FilesBeingWritten[numChannel].BaseStream; Commands.Code.FilesBeingWritten[numChannel].Dispose(); Commands.Code.FilesBeingWritten[numChannel] = null; stream.Dispose(); if (await code.EmulatingMarlin()) { return(new CodeResult(MessageType.Success, "Done saving file.")); } return(new CodeResult()); } break; } } throw new OperationCanceledException(); // Delete a file on the SD card case 30: if (await SPI.Interface.Flush(code)) { string file = code.GetUnprecedentedString(); string physicalFile = await FilePath.ToPhysicalAsync(file); try { File.Delete(physicalFile); } catch (Exception e) { _logger.Debug(e, "Failed to delete file"); return(new CodeResult(MessageType.Error, $"Failed to delete file {file}: {e.Message}")); } } throw new OperationCanceledException(); // For case 32, see case 23 // Return file information case 36: if (code.Parameters.Count > 0) { if (await SPI.Interface.Flush(code)) { string file = await FilePath.ToPhysicalAsync(code.GetUnprecedentedString(), FileDirectory.GCodes); try { ParsedFileInfo info = await InfoParser.Parse(file); string json = JsonSerializer.Serialize(info, JsonHelper.DefaultJsonOptions); return(new CodeResult(MessageType.Success, "{\"err\":0," + json.Substring(1))); } catch (Exception e) { _logger.Debug(e, "Failed to return file information"); return(new CodeResult(MessageType.Success, "{\"err\":1}")); } } throw new OperationCanceledException(); } break; // Simulate file case 37: if (await SPI.Interface.Flush(code)) { CodeParameter pParam = code.Parameter('P'); if (pParam != null) { string file = pParam; if (string.IsNullOrWhiteSpace(file)) { return(new CodeResult(MessageType.Error, "Filename expected")); } string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.GCodes); if (!File.Exists(physicalFile)) { return(new CodeResult(MessageType.Error, $"GCode file \"{file}\" not found\n")); } using (await FileExecution.Job.LockAsync()) { if (code.Channel != CodeChannel.File && FileExecution.Job.IsProcessing) { return(new CodeResult(MessageType.Error, "Cannot set file to simulate, because a file is already being printed")); } await FileExecution.Job.SelectFile(physicalFile, true); // Simulation is started when M37 has been processed by the firmware } } break; } throw new OperationCanceledException(); // Compute SHA1 hash of target file case 38: if (await SPI.Interface.Flush(code)) { string file = code.GetUnprecedentedString(); string physicalFile = await FilePath.ToPhysicalAsync(file); try { using FileStream stream = new FileStream(file, FileMode.Open, FileAccess.Read); byte[] hash; using System.Security.Cryptography.SHA1 sha1 = System.Security.Cryptography.SHA1.Create(); hash = await Task.Run(() => sha1.ComputeHash(stream), code.CancellationToken); return(new CodeResult(MessageType.Success, BitConverter.ToString(hash).Replace("-", string.Empty))); } catch (Exception e) { _logger.Debug(e, "Failed to compute SHA1 checksum"); if (e is AggregateException ae) { e = ae.InnerException; } return(new CodeResult(MessageType.Error, $"Could not compute SHA1 checksum for file {file}: {e.Message}")); } } throw new OperationCanceledException(); // Report SD card information case 39: if (await SPI.Interface.Flush(code)) { using (await Model.Provider.AccessReadOnlyAsync()) { int index = code.Parameter('P', 0); if (code.Parameter('S', 0) == 2) { if (index < 0 || index >= Model.Provider.Get.Volumes.Count) { return(new CodeResult(MessageType.Success, $"{{\"SDinfo\":{{\"slot\":{index},present:0}}}}")); } Volume storage = Model.Provider.Get.Volumes[index]; var output = new { SDinfo = new { slot = index, present = 1, capacity = storage.Capacity, free = storage.FreeSpace, speed = storage.Speed } }; return(new CodeResult(MessageType.Success, JsonSerializer.Serialize(output, JsonHelper.DefaultJsonOptions))); } else { if (index < 0 || index >= Model.Provider.Get.Volumes.Count) { return(new CodeResult(MessageType.Error, $"Bad SD slot number: {index}")); } Volume storage = Model.Provider.Get.Volumes[index]; return(new CodeResult(MessageType.Success, $"SD card in slot {index}: capacity {storage.Capacity / (1000 * 1000 * 1000):F2}Gb, free space {storage.FreeSpace / (1000 * 1000 * 1000):F2}Gb, speed {storage.Speed / (1000 * 1000):F2}MBytes/sec")); } } } throw new OperationCanceledException(); // Emergency Stop - unconditional and interpreteted immediately when read case 112: await SPI.Interface.EmergencyStop(); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.State.Status = MachineStatus.Halted; } return(new CodeResult()); // Immediate DSF diagnostics case 122: if (code.Parameter('B', 0) == 0 && code.GetUnprecedentedString() == "DSF") { CodeResult result = new CodeResult(); await Diagnostics(result); return(result); } break; // Save heightmap case 374: if (await SPI.Interface.Flush(code)) { string file = code.Parameter('P', FilePath.DefaultHeightmapFile); string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.System); try { if (await SPI.Interface.LockMovementAndWaitForStandstill(code.Channel)) { Heightmap map; try { map = await SPI.Interface.GetHeightmap(); } finally { await SPI.Interface.UnlockAll(code.Channel); } if (map.NumX * map.NumY > 0) { await map.Save(physicalFile); string virtualFile = await FilePath.ToVirtualAsync(physicalFile); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Move.Compensation.File = virtualFile; } return(new CodeResult(MessageType.Success, $"Height map saved to file {file}")); } return(new CodeResult()); } } catch (Exception e) { _logger.Debug(e, "Failed to save height map"); if (e is AggregateException ae) { e = ae.InnerException; } return(new CodeResult(MessageType.Error, $"Failed to save height map to file {file}: {e.Message}")); } } throw new OperationCanceledException(); // Load heightmap case 375: if (await SPI.Interface.Flush(code)) { string file = code.Parameter('P', FilePath.DefaultHeightmapFile); string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.System); try { Heightmap map = new Heightmap(); await map.Load(physicalFile); if (await SPI.Interface.LockMovementAndWaitForStandstill(code.Channel)) { try { await SPI.Interface.SetHeightmap(map); } finally { await SPI.Interface.UnlockAll(code.Channel); } string virtualFile = await FilePath.ToVirtualAsync(physicalFile); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Move.Compensation.File = virtualFile; } CodeResult result = new CodeResult(); using (await Model.Provider.AccessReadOnlyAsync()) { if (Model.Provider.Get.Move.Axes.Any(axis => axis.Letter == 'Z' && !axis.Homed)) { result.Add(MessageType.Warning, "The height map was loaded when the current Z=0 datum was not determined. This may result in a height offset."); } } result.Add(MessageType.Success, $"Height map loaded from file {file}"); return(result); } } catch (Exception e) { _logger.Debug(e, "Failed to load height map"); if (e is AggregateException ae) { e = ae.InnerException; } return(new CodeResult(MessageType.Error, $"Failed to load height map from file {file}: {e.Message}")); } } throw new OperationCanceledException(); // Create Directory on SD-Card case 470: if (await SPI.Interface.Flush(code)) { string path = code.Parameter('P'); if (path == null) { return(new CodeResult(MessageType.Error, "Missing directory name")); } string physicalPath = await FilePath.ToPhysicalAsync(path); try { Directory.CreateDirectory(physicalPath); } catch (Exception e) { _logger.Debug(e, "Failed to create directory"); return(new CodeResult(MessageType.Error, $"Failed to create directory {path}: {e.Message}")); } } throw new OperationCanceledException(); // Rename File/Directory on SD-Card case 471: if (await SPI.Interface.Flush(code)) { string from = code.Parameter('S'); string to = code.Parameter('T'); try { string source = await FilePath.ToPhysicalAsync(from); string destination = await FilePath.ToPhysicalAsync(to); if (File.Exists(source)) { if (File.Exists(destination) && code.Parameter('D', false)) { File.Delete(destination); } File.Move(source, destination); } else if (Directory.Exists(source)) { if (Directory.Exists(destination) && code.Parameter('D', false)) { // This could be recursive but at the moment we mimic RRF's behaviour Directory.Delete(destination); } Directory.Move(source, destination); } throw new FileNotFoundException(); } catch (Exception e) { _logger.Debug(e, "Failed to rename file or directory"); return(new CodeResult(MessageType.Error, $"Failed to rename file or directory {from} to {to}: {e.Message}")); } } throw new OperationCanceledException(); // Store parameters case 500: if (await SPI.Interface.Flush(code)) { await ConfigOverride.Save(code); return(new CodeResult()); } throw new OperationCanceledException(); // Print settings case 503: if (await SPI.Interface.Flush(code)) { string configFile = await FilePath.ToPhysicalAsync(FilePath.ConfigFile, FileDirectory.System); if (File.Exists(configFile)) { string content = await File.ReadAllTextAsync(configFile); return(new CodeResult(MessageType.Success, content)); } string configFileFallback = await FilePath.ToPhysicalAsync(FilePath.ConfigFileFallback, FileDirectory.System); if (File.Exists(configFileFallback)) { string content = await File.ReadAllTextAsync(configFileFallback); return(new CodeResult(MessageType.Success, content)); } return(new CodeResult(MessageType.Error, "Configuration file not found")); } throw new OperationCanceledException(); // Set configuration file folder case 505: if (await SPI.Interface.Flush(code)) { string directory = code.Parameter('P'); if (!string.IsNullOrEmpty(directory)) { string physicalDirectory = await FilePath.ToPhysicalAsync(directory, "sys"); if (Directory.Exists(physicalDirectory)) { string virtualDirectory = await FilePath.ToVirtualAsync(physicalDirectory); using (await Model.Provider.AccessReadWriteAsync()) { Model.Provider.Get.Directories.System = virtualDirectory; } return(new CodeResult()); } return(new CodeResult(MessageType.Error, "Directory not found")); } using (await Model.Provider.AccessReadOnlyAsync()) { return(new CodeResult(MessageType.Success, $"Sys file path is {Model.Provider.Get.Directories.System}")); } } throw new OperationCanceledException(); // Set Name case 550: if (await SPI.Interface.Flush(code)) { // Verify the P parameter string pParam = code.Parameter('P'); if (!string.IsNullOrEmpty(pParam)) { if (pParam.Length > 40) { return(new CodeResult(MessageType.Error, "Machine name is too long")); } // Strip letters and digits from the machine name string machineName = string.Empty; foreach (char c in Environment.MachineName) { if (char.IsLetterOrDigit(c)) { machineName += c; } } // Strip letters and digits from the desired name string desiredName = string.Empty; foreach (char c in pParam) { if (char.IsLetterOrDigit(c)) { desiredName += c; } } // Make sure the subset of letters and digits is equal if (!machineName.Equals(desiredName, StringComparison.CurrentCultureIgnoreCase)) { return(new CodeResult(MessageType.Error, "Machine name must consist of the same letters and digits as configured by the Linux hostname")); } // Hostname is legit - pretend we didn't see this code so RRF can interpret it } break; } throw new OperationCanceledException(); // Configure filament case 703: if (await SPI.Interface.Flush(code)) { await Model.Updater.WaitForFullUpdate(Program.CancellationToken); break; } throw new OperationCanceledException(); // Set current RTC date and time case 905: if (await SPI.Interface.Flush(code)) { bool seen = false; CodeParameter pParam = code.Parameter('P'); if (pParam != null) { if (DateTime.TryParseExact(pParam, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date)) { System.Diagnostics.Process.Start("timedatectl", $"set-time {date:yyyy-MM-dd}").WaitForExit(); seen = true; } else { return(new CodeResult(MessageType.Error, "Invalid date format")); } } CodeParameter sParam = code.Parameter('S'); if (sParam != null) { if (DateTime.TryParseExact(sParam, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime time)) { System.Diagnostics.Process.Start("timedatectl", $"set-time {time:HH:mm:ss}").WaitForExit(); seen = true; } else { return(new CodeResult(MessageType.Error, "Invalid time format")); } } if (!seen) { return(new CodeResult(MessageType.Success, $"Current date and time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}")); } } throw new OperationCanceledException(); // Start/stop event logging to SD card case 929: if (await SPI.Interface.Flush(code)) { CodeParameter sParam = code.Parameter('S'); if (sParam == null) { using (await Model.Provider.AccessReadOnlyAsync()) { return(new CodeResult(MessageType.Success, $"Event logging is {(Model.Provider.Get.State.LogFile != null ? "enabled" : "disabled")}")); } } if (sParam > 0) { string file = code.Parameter('P', Utility.Logger.DefaultLogFile); if (string.IsNullOrWhiteSpace(file)) { return(new CodeResult(MessageType.Error, "Missing filename in M929 command")); } string physicalFile = await FilePath.ToPhysicalAsync(file, FileDirectory.System); await Utility.Logger.Start(physicalFile); } else { await Utility.Logger.Stop(); } return(new CodeResult()); } throw new OperationCanceledException(); // Update the firmware case 997: if (((int[])code.Parameter('S', new int[] { 0 })).Contains(0) && code.Parameter('B', 0) == 0) { if (await SPI.Interface.Flush(code)) { string iapFile, firmwareFile; using (await Model.Provider.AccessReadOnlyAsync()) { if (Model.Provider.Get.Boards.Count == 0) { return(new CodeResult(MessageType.Error, "No boards have been detected")); } // There are now two different IAP binaries, check which one to use iapFile = Model.Provider.Get.Boards[0].IapFileNameSBC; firmwareFile = Model.Provider.Get.Boards[0].FirmwareFileName; } iapFile = await FilePath.ToPhysicalAsync(iapFile, FileDirectory.Firmware); if (!File.Exists(iapFile)) { return(new CodeResult(MessageType.Error, $"Failed to find IAP file {iapFile}")); } firmwareFile = await FilePath.ToPhysicalAsync(firmwareFile, FileDirectory.Firmware); if (!File.Exists(firmwareFile)) { return(new CodeResult(MessageType.Error, $"Failed to find firmware file {firmwareFile}")); } using FileStream iapStream = new FileStream(iapFile, FileMode.Open, FileAccess.Read); using FileStream firmwareStream = new FileStream(firmwareFile, FileMode.Open, FileAccess.Read); await SPI.Interface.UpdateFirmware(iapStream, firmwareStream); return(new CodeResult()); } throw new OperationCanceledException(); } break; // Request resend of line case 998: throw new NotSupportedException(); // Reset controller - unconditional and interpreteted immediately when read case 999: if (code.Parameters.Count == 0) { await SPI.Interface.Reset(); return(new CodeResult()); } break; } return(null); }