diff --git a/.vscode/launch.json b/.vscode/launch.json index bf6a0c2..ae35233 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/app/bin/Debug/net9.0/vicon", - "args": ["--interactive"], + "args": ["--interactive", "--check"], "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "integratedTerminal", diff --git a/README.md b/README.md index bd7ccd0..d8cabe7 100644 --- a/README.md +++ b/README.md @@ -364,5 +364,5 @@ This project is licensed under the MIT license. For more details please refer to [LICENSE](./LICENSE). This software depends on the following third party components: -- Spectre.Console (https://github.com/spectreconsole/spectre.console/LICENSE.md) +- Spectre.Console (https://github.com/spectreconsole/spectre.console/blob/main/LICENSE.md) - HidSharp (https://github.com/IntergatedCircuits/HidSharp/blob/master/License.txt) diff --git a/app/AliasedDevice.cs b/app/AliasedDevice.cs index 2f13c2b..90df60b 100644 --- a/app/AliasedDevice.cs +++ b/app/AliasedDevice.cs @@ -10,6 +10,9 @@ public class AliasedDevice [JsonPropertyName("serial")] public string Serial { get; set; } = string.Empty; + [JsonPropertyName("config")] + public ConfiguredState? Config { get; set; } + public override string ToString() { if (Alias == string.Empty) diff --git a/app/ConfiguredState.cs b/app/ConfiguredState.cs new file mode 100644 index 0000000..4997089 --- /dev/null +++ b/app/ConfiguredState.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; +using System.Security.Cryptography; +using LibDP100; + +namespace PowerSupplyApp +{ + public class ConfiguredState + { + [JsonPropertyName("hash")] + public string Hash { get; set; } = string.Empty; + + [JsonPropertyName("system-params")] + public PowerSupplySystemParams SystemParams { get; set; } = new(); + + [JsonPropertyName("presets")] + public List Presets { get; set; } = []; + + /// + /// The number of presets available on the device. + /// + public const byte NumPresets = 10; + + public void Print() + { + SystemParams.Print(); + foreach (var preset in Presets) + { + preset.Print(); + } + } + + public string ComputeConfiguredHash() + { + using var sha256 = SHA256.Create(); + var sb = new System.Text.StringBuilder(); + + // Include settings critical for safe operation. + sb.Append(SystemParams.OPP); + sb.Append(SystemParams.OTP); + sb.Append(SystemParams.RPP); + sb.Append(SystemParams.AutoOn); + foreach (var preset in Presets) + { + sb.Append(preset.Voltage); + sb.Append(preset.Current); + sb.Append(preset.OVP); + sb.Append(preset.OCP); + } + + // Compute the hash + var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(sb.ToString())); + return Convert.ToHexString(hashBytes); + } + + } +} diff --git a/app/Operation.cs b/app/Operation.cs index 08c16c2..3419642 100644 --- a/app/Operation.cs +++ b/app/Operation.cs @@ -3,6 +3,7 @@ namespace PowerSupplyApp public enum Operation { None, + Delay, ReadOutput, ReadActState, ReadSystem, diff --git a/app/PowerControllerApp.csproj b/app/PowerControllerApp.csproj index fc02407..ccf3cc7 100644 --- a/app/PowerControllerApp.csproj +++ b/app/PowerControllerApp.csproj @@ -48,4 +48,10 @@ + + + Always + + + diff --git a/app/ProcessArgs.cs b/app/ProcessArgs.cs index 2347598..c57f491 100644 --- a/app/ProcessArgs.cs +++ b/app/ProcessArgs.cs @@ -34,14 +34,20 @@ private static void ShowHelp() "Displays the version of the application."); grid.AddRow(" [white]--config[/]", "", "Prints the path to the user settings file used by this tool."); + grid.AddRow(" [white]--save[/]", "", + "Saves the devices configured state to the application's settings. This action takes place after all other mutable actions complete successfully."); + grid.AddRow(" [white]--load[/]", "", + "Loads the application's device settings into the device. This action takes place before all other mutable actions. If no settings exist for the device the application exits with an exit code."); + grid.AddRow(" [white]--check[/]", "", + "Verifies that the devices configured state matches the state stored in the the application's settings. If running non-interactively, the application will exit with an error code; otherwise the application will prompt the user for further action."); grid.AddRow(" [white]--debug[/]", "", "Enables debug output of underlying driver. (Only intended for CLI mode)"); grid.AddRow(" [white]--enumerate[/]", "", "Enumerates all connected power supplies and returns device information for each. If set, all other processing is ignored."); grid.AddRow(" [white]--blink[/]", "", "For visual identification, the lock indicator will blink 10x (~1x per second)."); - grid.AddRow(" [white]--serial[/], [white]--sn[/]", "[silver][/]", - "Connects to the power supply that matches the specified [white]SERIAL[/] number."); + grid.AddRow(" [white]--serial[/], [white]--sn[/]", "[silver] [[ALIAS]][/]", + "Connects to the power supply that matches the specified [white]SERIAL[/] number. Specify [white]ALIAS[/] to assign an alias which may be presented during device selection or enumeration (persistently stored in settings)."); grid.AddRow(" [white]--interactive[/]", "[[MS_POLL]]", "Switches into an interactive text-based user interface. Set [white]MS_POLL[/] to reduce the update/poll rate (default = 1, range 0-100), value persists between executions. This currently affects the perceived responsiveness of the UI but lowers the CPU utilization."); grid.AddRow(" [white]--theme[/]", "", @@ -80,8 +86,8 @@ private static void ShowHelp() "Sets the Over-Current Protection level. Please note, the current setpoint parameters will be used to update the currently configured preset. Reaching or exceeding this limit will switch the output OFF. (units: mA)"); grid.AddRow(" [white]--opp[/]", "[silver][/]", "Sets the Over-Power Protection level. Reaching or exceeding this limit will switch the output OFF. (units: 0.1 W, range: 0-1050)"); - grid.AddRow(" [white]--otp[/]", "[silver][/]", - "Sets the Over-Temperature Protection level. Reaching or exceeding this limit will switch the output OFF. (units: 0.1 C, range: 500-800)"); + grid.AddRow(" [white]--otp[/]", "[silver][/]", + "Sets the Over-Temperature Protection level. Reaching or exceeding this limit will switch the output OFF. (units: 1 C, range: 50-80)"); grid.AddRow(" [white]--rpp[/]", "[silver][/]", "Sets the Reverse Polarity Protection. (range: 0-1)"); grid.AddRow(" [white]--auto-on[/]", "[silver][/]", @@ -101,7 +107,7 @@ private static ProcessArgsResult PreProcessArgs(string[] args) { if (!settings.Load()) { - Console.WriteLine($"ERROR: Invalid settings, please correct." + Environment.NewLine + settings.GetUserSettingsFilePath()); + ShowError("Invalid settings, please correct." + Environment.NewLine + settings.GetUserSettingsFilePath()); return ProcessArgsResult.Error; } @@ -111,6 +117,7 @@ private static ProcessArgsResult PreProcessArgs(string[] args) int argIndex = 0; for (int i = 0; i < args.Length; i++) { + int numArgParams = 0; string arg = args[i].ToLower(); switch (arg) @@ -130,10 +137,19 @@ private static ProcessArgsResult PreProcessArgs(string[] args) case "--debug": debug = true; break; + case "--load": + loadConfiguration = true; + break; + case "--save": + saveConfiguration = true; + break; + case "--check": + checkConfiguration = true; + break; case "--theme": if ((i + 1 >= args.Length) || args[i + 1].StartsWith('-')) { - Console.WriteLine($"ERROR: Missing parameter for '{args[i]}'."); + ShowError($"Missing parameter for '{args[i]}'."); return ProcessArgsResult.MissingParameter; } @@ -145,23 +161,41 @@ private static ProcessArgsResult PreProcessArgs(string[] args) break; case "--serial": case "--sn": - if (argIndex + 1 < args.Length) + numArgParams = CountArgsBetweenFlags(args, i); + if (numArgParams >= 1) { psuSerialNumber = args[argIndex + 1]; - if (psuSerialNumber.StartsWith('-')) + if (numArgParams >= 2) { - psuSerialNumber = string.Empty; - return ProcessArgsResult.MissingParameter; + var existingAlias = settings.AliasedDevices.FirstOrDefault(a => a.Serial == psuSerialNumber); + if (existingAlias != null) + { + existingAlias.Alias = args[argIndex + 2]; + } + else + { + settings.AliasedDevices.Add(new AliasedDevice + { + Serial = psuSerialNumber, + Alias = args[argIndex + 2] + }); + } + + // For now, save immediately. + // It may be desired to wait until arguments are processed before saving. + settings.Save(); } } else { + psuSerialNumber = string.Empty; return ProcessArgsResult.MissingParameter; } break; case "--interactive": interactiveMode = true; - if ((i + 1 < args.Length) && !args[i + 1].StartsWith('-')) + numArgParams = CountArgsBetweenFlags(args, i); + if (numArgParams == 1) { bool result = uint.TryParse(args[i + 1], out uint ms); if (result) @@ -179,9 +213,10 @@ private static ProcessArgsResult PreProcessArgs(string[] args) if (!result) { - Console.WriteLine($"ERROR: Invalid parameter for '{args[i]}'."); + ShowError($"Invalid parameter for '{args[i]}'."); return ProcessArgsResult.InvalidParameter; } + i += numArgParams; } break; case "--json": @@ -198,13 +233,13 @@ private static ProcessArgsResult PreProcessArgs(string[] args) case "--wavegen": if ((i + 1 >= args.Length) || args[i + 1].StartsWith('-')) { - Console.WriteLine($"ERROR: Missing parameter for '{args[i]}'."); + ShowError($"Missing parameter for '{args[i]}'."); return ProcessArgsResult.MissingParameter; } if (!WaveGen.Load(args[i + 1])) { - Console.WriteLine(WaveGen.GetLastErrorMessage()); + ShowError(WaveGen.GetLastErrorMessage()); return ProcessArgsResult.Error; } break; @@ -245,8 +280,8 @@ private static ProcessArgsResult PreProcessArgs(string[] args) default: if (arg.StartsWith('-')) { - Console.WriteLine($"Unsupported argument '{arg}'." + Environment.NewLine + - "Use -?, -h, or --help for help information."); + ShowError($"Unsupported argument '{arg}'." + Environment.NewLine + + "Use -?, -h, or --help for help information."); return ProcessArgsResult.UnsupportedOption; } break; @@ -261,12 +296,18 @@ private static ProcessArgsResult PreProcessArgs(string[] args) return ProcessArgsResult.Error; } + if (loadConfiguration && checkConfiguration) + { + ShowError("--check and --load are mutually exclusive options."); + return ProcessArgsResult.InvalidParameter; + } + if (pollRateSet || themeSet) { - if (!settings.Store()) + if (!settings.Save()) { - Console.WriteLine("Failed to store settings!"); - return ProcessArgsResult.Error; + ShowError("Failed to store settings!"); + return ProcessArgsResult.StoreError; } } @@ -290,12 +331,15 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) switch (arg) { default: - Console.WriteLine($"Internal error: argument '{arg}' has not been implemented."); + ShowError($"Internal - argument '{arg}' has not been implemented."); return ProcessArgsResult.NotImplemented; case "--json": case "--json-list": case "--debug": + case "--load": + case "--save": + case "--check": case "--enumerate": // Do nothing, already handled in first pass. break; @@ -307,7 +351,7 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) case "--serial": case "--sn": // Only increment the index, already handled in first pass. - i++; + i += CountArgsBetweenFlags(args, i); break; case "--blink": @@ -328,7 +372,7 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) case "--wavegen": if (!WaveGen.Init(inst, sp) || !WaveGen.Restart()) { - Console.WriteLine(WaveGen.GetLastErrorMessage()); + ShowError(WaveGen.GetLastErrorMessage()); return ProcessArgsResult.Error; } @@ -362,21 +406,20 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) if ((i + 1 < args.Length) && (!args[i + 1].StartsWith('-'))) { - int milliseconds; - result = int.TryParse(args[i + 1], out milliseconds); + result = int.TryParse(args[i + 1], out int milliseconds); if (result) { Thread.Sleep(milliseconds); } - if (!CheckResult(result, args, ref i)) + if (!CheckResult(Operation.Delay, result, args, ref i)) { return ProcessArgsResult.InvalidParameter; } } else { - Console.WriteLine($"ERROR: Missing parameter for '{args[i]}'."); + ShowError($"Missing parameter for '{args[i]}'."); return ProcessArgsResult.MissingParameter; } break; @@ -482,14 +525,14 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) if (readOp) { - if (!CheckResult(ProcessRead(inst, op, args, i), args, ref i)) + if (!CheckResult(op, ProcessRead(inst, op, args, i), args, ref i)) { return ProcessArgsResult.ReadError; } } else if (writeOp) { - if (!CheckResult(ProcessWrite(inst, op, args, i), args, ref i)) + if (!CheckResult(op, ProcessWrite(inst, op, args, i), args, ref i)) { return ProcessArgsResult.WriteError; } @@ -502,7 +545,7 @@ private static ProcessArgsResult ProcessArgs(PowerSupply inst, string[] args) private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, int index) { - bool result; + bool result = false; ushort parsedValue = 0; string formatMessage = string.Empty; int argsToProcess = 0; @@ -524,6 +567,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.UsePreset: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -533,6 +578,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteVoltage: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -547,6 +594,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteCurrent: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -561,6 +610,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteOVP: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -577,6 +628,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteOCP: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -593,6 +646,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteOPP: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -607,6 +662,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteOTP: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -621,6 +678,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteRPP: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -635,6 +694,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteAutoOn: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -649,6 +710,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteVolume: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -663,6 +726,8 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i case Operation.WriteBacklight: argsToProcess = 2; + if (index + 1 >= args.Length) break; + result = ushort.TryParse(args[index + 1], out parsedValue); if (result) { @@ -678,13 +743,9 @@ private static int ProcessWrite(PowerSupply inst, Operation op, string[] args, i if (!result) { - // TODO: more detailed error codes should be reported here for debugging purposes. - // Ex: "Not connected", "Out of Range", "Invalid command", "Invalid state" - SerializeObject(new CommandResponse - { - Command = op, - Response = new { Error = "Operation Failed" } - }); + // For now, no additional details are captured here. + // A more helpful "result" datatype should be used to pass the + // error information up to the logic that reports errors. return 0; } @@ -905,7 +966,7 @@ private static int CountArgsBetweenFlags(string[] args, int index) for (int i = index; i < args.Length; i++) { - if (args[i].StartsWith("--") || args[i].StartsWith('-')) + if (args[i].StartsWith('-')) { if (betweenFlags) { @@ -1145,7 +1206,7 @@ private static int ProcessRead(PowerSupply inst, Operation op, string[] args, in return result ? argsToProcess : 0; } - private static bool CheckResult(int numArgsProcessed, string[] args, ref int index) + private static bool CheckResult(Operation op, int numArgsProcessed, string[] args, ref int index) { if (numArgsProcessed > 0) { @@ -1154,13 +1215,24 @@ private static bool CheckResult(int numArgsProcessed, string[] args, ref int ind } else { - // TODO improve, output each unprocessed arg. - Console.WriteLine($"Could not process '{args[index]}'"); + string msg = $"Could not process argument ({args[index]})"; + if (serializeAsJson) + { + SerializeObject(new CommandResponse + { + Command = op, + Response = new { Error = msg } + }); + } + else + { + ShowError(msg); + } return false; } } - private static bool CheckResult(bool result, string arg) + private static bool CheckResult(Operation op, bool result, string arg) { if (result) { @@ -1168,12 +1240,24 @@ private static bool CheckResult(bool result, string arg) } else { - Console.WriteLine($"Could not process '{arg}'"); + string msg = $"Could not process argument ({arg})"; + if (serializeAsJson) + { + SerializeObject(new CommandResponse + { + Command = op, + Response = new { Error = msg } + }); + } + else + { + ShowError(msg); + } return false; } } - private static bool CheckResult(bool result, string[] args, ref int index) + private static bool CheckResult(Operation op, bool result, string[] args, ref int index) { if (result) { @@ -1182,7 +1266,19 @@ private static bool CheckResult(bool result, string[] args, ref int index) } else { - Console.WriteLine($"Could not process '{args[index]}' ({args[index + 1]})"); + string msg = $"Could not process argument ({args[index]}) value ({args[index + 1]})"; + if (serializeAsJson) + { + SerializeObject(new CommandResponse + { + Command = op, + Response = new { Error = msg } + }); + } + else + { + ShowError(msg); + } return false; } } diff --git a/app/ProcessArgsResult.cs b/app/ProcessArgsResult.cs index 5b59ee4..88e1619 100644 --- a/app/ProcessArgsResult.cs +++ b/app/ProcessArgsResult.cs @@ -10,6 +10,13 @@ public enum ProcessArgsResult NotImplemented, MissingParameter, InvalidParameter, - UnsupportedOption + UnsupportedOption, + SerialNumberRequired, + DeviceNotPresent, + ConfigurationMismatch, + InitializationFailed, + NoAliasedDeviceFound, + NoConfigurationPresent, + StoreError } } diff --git a/app/Program.cs b/app/Program.cs index 87d3152..bdbe508 100644 --- a/app/Program.cs +++ b/app/Program.cs @@ -1,10 +1,14 @@ using LibDP100; +using System.Security.Cryptography; namespace PowerSupplyApp { internal partial class Program { private static bool debug = false; + private static bool saveConfiguration = false; + private static bool loadConfiguration = false; + private static bool checkConfiguration = false; private static bool serializeAsJson = false; private static bool serializeAsJsonArray = false; private static int serializedOutput = 0; @@ -35,8 +39,8 @@ private static int Main(string[] args) if (psuCount == 0) { - Console.WriteLine("ERROR: No DP100 detected!"); - return 1; + ShowError("No DP100 detected!"); + return (int)ProcessArgsResult.DeviceNotPresent; } if ((psuSerialNumber == string.Empty) && (psuCount > 1)) @@ -49,8 +53,8 @@ private static int Main(string[] args) } else { - Console.WriteLine("ERROR: Multiple DP100s detected. Please provide the --serial option!"); - return 1; + ShowError("Multiple DP100s detected. Please provide the --serial option!"); + return (int)ProcessArgsResult.SerialNumberRequired; } } @@ -68,16 +72,61 @@ private static int Main(string[] args) // applications may connect to them. Enumerator.Done(); + AliasedDevice? devSettings = null; + if (psu != null) { psu.GetDeviceInfo(); psu.GetSystemParams(); psu.Reload(); + + if (loadConfiguration || saveConfiguration || checkConfiguration) + { + devSettings = settings.FindDeviceBySerialNumber(psu.Device.SerialNumber); + if (devSettings == null) + { + return (int)ProcessArgsResult.NoAliasedDeviceFound; + } + else if (devSettings.Config == null) + { + return (int)ProcessArgsResult.NoConfigurationPresent; + } + } + + if (loadConfiguration) + { + if (devSettings?.Config == null) + { + return (int)ProcessArgsResult.NoConfigurationPresent; + } + LoadConfiguration(psu, devSettings.Config); + psu.Reload(); + } + + if (checkConfiguration) + { + if (devSettings == null) + { + return (int)ProcessArgsResult.NoAliasedDeviceFound; + } + + (string computedConfigHash, string computedDeviceHash, string recordedHash) = GetHashes(psu, devSettings); + if ((recordedHash != computedConfigHash) || (recordedHash != computedDeviceHash)) + { + ExitAlternateScreenBuffer(); + result = CheckConfiguration(psu, devSettings, computedConfigHash, computedDeviceHash, recordedHash); + if (result != ProcessArgsResult.Ok) + { + return (int)result; + } + EnterAlternateScreenBuffer(); + } + } } else { - Console.WriteLine("ERROR: Could not initialize DP100!"); - return 1; + ShowError("Could not initialize DP100!"); + return (int)ProcessArgsResult.InitializationFailed; } // Only permit debug mode in non-interactive mode. @@ -95,11 +144,233 @@ private static int Main(string[] args) result = ProcessArgs(psu, args); + if (saveConfiguration && result == ProcessArgsResult.Ok) + { + if (devSettings == null) + { + return (int)ProcessArgsResult.NoAliasedDeviceFound; + } + + SaveConfiguration(psu, devSettings); + } + psu.Disconnect(); return (int)result; } + private static (string computedConfigHash, string computedDeviceHash, string recordedHash) GetHashes(PowerSupply psu, AliasedDevice devSettings) + { + string computedConfigHash = devSettings.Config == null ? string.Empty : devSettings.Config.ComputeConfiguredHash(); + string computedDeviceHash = GetConfigurationHash(psu.SystemParams, psu.Presets); + string recordedHash = devSettings.Config == null ? string.Empty : devSettings.Config.Hash; + return (computedConfigHash, computedDeviceHash, recordedHash); + } + + private static ProcessArgsResult CheckConfiguration(PowerSupply psu, AliasedDevice devSettings, string computedConfigHash, string computedDeviceHash, string recordedHash) + { + ProcessArgsResult result = ProcessArgsResult.Ok; + + if ((recordedHash == computedConfigHash) && (recordedHash == computedDeviceHash)) + { + return result; + } + + if (!interactiveMode) + { + ShowError("Configured state requires review!" + Environment.NewLine); + if (devSettings.Config == null) + { + Console.WriteLine("No configuration has been saved to check against." + Environment.NewLine); + Console.WriteLine("Possible actions:"); + Console.WriteLine(" 1. Remove --check to skip the check and use the current device state."); + Console.WriteLine(" 2. Use --interactive to interactively resolve the the issue."); + Console.WriteLine(" 3. Use --save to save this configuration to disk."); + result = ProcessArgsResult.NoConfigurationPresent; + } + else + { + Console.WriteLine("The device state does not match saved configuration." + Environment.NewLine); + Console.WriteLine("Possible actions:"); + Console.WriteLine(" 1. Remove --check to skip the check and use the current device state."); + Console.WriteLine(" 2. Use --interactive to interactively resolve the the issue."); + Console.WriteLine(" 3. Use --save to save this configuration to disk."); + Console.WriteLine(" 4. Use --load to load the device state from disk."); + result = ProcessArgsResult.ConfigurationMismatch; + } + } + else + { + ShowWarning("Configured state requires review!"); + } + + if (devSettings.Config != null) + { + Console.WriteLine(); + Console.WriteLine($"Device Hash : {computedDeviceHash}"); + Console.WriteLine($"Recorded Hash : {(string.IsNullOrWhiteSpace(recordedHash) ? "None" : recordedHash)}"); + Console.WriteLine($"Config Hash : {(string.IsNullOrWhiteSpace(computedConfigHash) ? "None" : computedConfigHash)}"); + } + + if (recordedHash != computedConfigHash) + { + if (!string.IsNullOrWhiteSpace(recordedHash)) + { + Console.WriteLine(); + ShowWarning("Possible corruption detected in stored settings!"); + } + } + + if (computedConfigHash != computedDeviceHash) + { + Console.WriteLine(); + PrintConfigurationDiff(psu, devSettings.Config); + } + + if (result != ProcessArgsResult.Ok) + { + return result; + } + + Console.WriteLine(); + + List choices = ["Exit now", "Continue", "Save device state (to disk)"]; + if (devSettings.Config != null) + { + choices.Add("Load device state (from disk)"); + } + + int choice = GetChoice("Select an action:", choices); + switch (choice) + { + default: + case 1: // Exit application, make no change. + Console.WriteLine("Exiting."); + return ProcessArgsResult.OkExitNow; + case 2: // Continue application, make no change. + Console.WriteLine("Continuing."); + return ProcessArgsResult.Ok; + case 3: // Save device state to disk + if (!SaveConfiguration(psu, devSettings)) + { + Console.WriteLine("Failed to store settings!"); + return ProcessArgsResult.StoreError; + } + Console.WriteLine("Saved configuration."); + break; + case 4: // Load device state from disk. + if (devSettings.Config == null) + { + return ProcessArgsResult.OkExitNow; + } + LoadConfiguration(psu, devSettings.Config); + if (devSettings.Config.Hash != computedConfigHash) + { + // The settings appear to also need a corrected hash, save now. + devSettings.Config.Hash = computedConfigHash; + if (!SaveConfiguration(psu, devSettings)) + { + Console.WriteLine("Failed to store settings!"); + return ProcessArgsResult.StoreError; + } + } + Console.WriteLine("Loaded configuration."); + break; + } + + return ProcessArgsResult.Ok; + } + + static bool SaveConfiguration(PowerSupply psu, AliasedDevice devSettings) + { + if (devSettings.Config == null) + { + devSettings.Config = new ConfiguredState(); + } + + devSettings.Config.SystemParams.OPP = psu.SystemParams.OPP; + devSettings.Config.SystemParams.OTP = psu.SystemParams.OTP; + devSettings.Config.SystemParams.RPP = psu.SystemParams.RPP; + devSettings.Config.SystemParams.AutoOn = psu.SystemParams.AutoOn; + devSettings.Config.SystemParams.Backlight = psu.SystemParams.Backlight; + devSettings.Config.SystemParams.Volume = psu.SystemParams.Volume; + + devSettings.Config.Presets = new List(psu.Presets.Length); + devSettings.Config.Presets.Clear(); + + for (int i = 0; i < psu.Presets.Length; i++) + { + devSettings.Config.Presets.Add(psu.Presets[i]); + } + + devSettings.Config.Hash = devSettings.Config.ComputeConfiguredHash(); + if (!settings.Save()) + { + return false; + } + + return true; + } + + static void LoadConfiguration(PowerSupply psu, ConfiguredState? config) + { + if (config == null) + { + return; + } + + psu.SetSystemParams(config.SystemParams); + foreach (var preset in config.Presets) + { + psu.SetPreset(preset.GetIndex(), preset); + } + } + + // Prints all settings which do not match between device and config + static int PrintConfigurationDiff(PowerSupply psu, ConfiguredState? config) + { + if (config == null) + { + Console.WriteLine("Current device configuration:"); + Console.WriteLine(); + ShowConfiguration(psu.SystemParams, psu.Presets); + return 1; + } + + Console.WriteLine("Checking configuration differences:"); + + int diffCount = ShowDifferences(psu, config); + if (diffCount == 0) + { + Console.WriteLine("- No differences found."); + } + + return diffCount; + } + + static string GetConfigurationHash(PowerSupplySystemParams systemParams, PowerSupplySetpoint[] presets) + { + using var sha256 = SHA256.Create(); + var sb = new System.Text.StringBuilder(); + + // Include settings critical for safe operation. + sb.Append(systemParams.OPP); + sb.Append(systemParams.OTP); + sb.Append(systemParams.RPP); + sb.Append(systemParams.AutoOn); + foreach (var preset in presets) + { + sb.Append(preset.Voltage); + sb.Append(preset.Current); + sb.Append(preset.OVP); + sb.Append(preset.OCP); + } + + // Compute the hash + var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(sb.ToString())); + return Convert.ToHexString(hashBytes); + } + private static int PrintEnumeration() { numSerializedOutputs = Enumerator.GetDeviceCount(); diff --git a/app/TUI/JsonSettings.cs b/app/TUI/JsonSettings.cs index 8d8dd62..a61fec9 100644 --- a/app/TUI/JsonSettings.cs +++ b/app/TUI/JsonSettings.cs @@ -41,6 +41,11 @@ public string Theme /// private const string settingsFilename = "vicon.settings.json"; + /// + /// The file defining the JSON schema for user settings. + /// + private const string settingsSchemaFilename = "vicon.settings.schema.json"; + /// /// The subdirectory where user settings will be saved. /// @@ -66,8 +71,10 @@ public string Theme /// private static JsonSerializerOptions serializationOptions = new() { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNameCaseInsensitive = true, - WriteIndented = true + WriteIndented = true, + MaxDepth = 8 }; /// @@ -82,7 +89,7 @@ public ColorTheme GetTheme() /// /// The file path where user settings are stored. /// - /// The settings file path path. + /// The settings file path. public string GetUserSettingsFilePath() { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @@ -90,14 +97,31 @@ public string GetUserSettingsFilePath() Path.Combine(nixLocalSettingsDir, settingsFilename); } + /// + /// The file path where the settings' JSON schema file is stored. + /// + /// The settings' schema file path. + public string GetSettingsSchemaFilePath() + { + return Path.Combine(AppContext.BaseDirectory, settingsSchemaFilename); + } + /// /// Stores settings to disk. /// /// /// True if the store to disk succeeded; otherwise false. /// - public bool Store() + public bool Save() { + foreach (var dev in AliasedDevices) + { + if (dev.Config != null && string.IsNullOrWhiteSpace(dev.Config.Hash)) + { + // Remove the config from the object. + dev.Config = null; + } + } string text = JsonSerializer.Serialize(this, serializationOptions); string settingsFilePath = GetUserSettingsFilePath(); string? settingsFileDir = Path.GetDirectoryName(settingsFilePath); @@ -136,6 +160,21 @@ public bool Load() { theme = ColorThemes.GetTheme(loadedSettings.Theme); PollRate = loadedSettings.PollRate; + + foreach (var dev in loadedSettings.AliasedDevices) + { + if (dev.Config == null) + { + continue; + } + + byte index = 0; + foreach (var preset in dev.Config.Presets) + { + // Restore the proper index. + preset.SetIndex(index++); + } + } AliasedDevices = loadedSettings.AliasedDevices; } } @@ -147,5 +186,15 @@ public bool Load() return true; } + + /// + /// Finds and returns the matching aliased device based on a serial number. + /// + /// The serial number to search for. + /// The matching aliased device, or null if no match is found. + public AliasedDevice? FindDeviceBySerialNumber(string serialNumber) + { + return AliasedDevices.FirstOrDefault(device => device.Serial == serialNumber); + } } } diff --git a/app/TUI/Model.cs b/app/TUI/Model.cs index 4e09ffa..ad13f87 100644 --- a/app/TUI/Model.cs +++ b/app/TUI/Model.cs @@ -9,7 +9,7 @@ internal partial class Program private const ushort powerOutputMax = 1050; // 1050 * 0.1 = 105W private const ushort powerOutputMin = 0; private const ushort tempOutputMax = 80; // 50 = 50C - private const ushort tempOutputMin = 0; + private const ushort tempOutputMin = 50; private static int VoltageOutput { diff --git a/app/TUI/View.cs b/app/TUI/View.cs index 6666cf2..01c8768 100644 --- a/app/TUI/View.cs +++ b/app/TUI/View.cs @@ -77,15 +77,21 @@ private static bool ControlsLocked private static bool faulted = false; private static bool controlsLocked = false; + private static bool altBufferState = false; /// /// Write the ANSI sequence to enter the alternate screen buffer. /// private static void EnterAlternateScreenBuffer() { - // Enter alt-screen buffer and hide cursor. - Console.Write($"{(char)27}[?1049h"); + // Hide cursor. Console.Write($"{(char)27}[?25l"); + if (!interactiveMode || altBufferState) + return; + + // Enter alt-screen buffer. + Console.Write($"{(char)27}[?1049h"); + altBufferState = true; } /// @@ -93,9 +99,142 @@ private static void EnterAlternateScreenBuffer() /// private static void ExitAlternateScreenBuffer() { + if (!interactiveMode || !altBufferState) + return; + // Exit alt-screen buffer and show cursor. Console.Write($"{(char)27}[?1049l"); Console.Write($"{(char)27}[?25h"); + altBufferState = false; + } + + /// + /// Shows a warning message. + /// + /// The message to show. + private static void ShowWarning(string message) + { + AnsiConsole.Write(new Markup($"[yellow]WARNING[/] {message}{Environment.NewLine}")); + } + + /// + /// Shows an error message. + /// + /// The message to show. + private static void ShowError(string message) + { + AnsiConsole.Write(new Markup($"[white on darkred] ERROR [/] {message}{Environment.NewLine}")); + } + + /// + /// Shows the differences between the device and stored configuration. + /// + /// The device. + /// The stored configuration. + /// The number of differences detected. + private static int ShowDifferences(PowerSupply supply, ConfiguredState config) + { + int AddDiff(Table table, string scope, string name, object deviceValue, object configValue) + { + if (!Equals(deviceValue, configValue)) + { + table.AddRow( + scope?.ToString() ?? "", + name?.ToString() ?? "", + deviceValue?.ToString() ?? "", + configValue?.ToString() ?? "" + ); + return 1; + } + + return 0; + } + + var diffTable = new Table(); + diffTable.AddColumn(new TableColumn(new Markup("[white]Scope[/]")).Centered()); + diffTable.AddColumn(new TableColumn(new Markup("[white]Parameter[/]")).Centered()); + diffTable.AddColumn(new TableColumn(new Markup("[white]Device[/]")).Centered()); + diffTable.AddColumn(new TableColumn(new Markup("[white]Config[/]")).Centered()); + int diffCount = 0; + diffCount += AddDiff(diffTable, "System", "OPP (W)", $"{supply.SystemParams.OPP / 10.0:0.0}", $"{config.SystemParams.OPP / 10.0:0.0}"); + diffCount += AddDiff(diffTable, "System", "OTP (C)", $"{supply.SystemParams.OTP}", $"{config.SystemParams.OTP}"); + diffCount += AddDiff(diffTable, "System", "RPP", $"{supply.SystemParams.RPP}", $"{config.SystemParams.RPP}"); + diffCount += AddDiff(diffTable, "System", "AutoOn", $"{supply.SystemParams.AutoOn}", $"{config.SystemParams.AutoOn}"); + diffCount += AddDiff(diffTable, "System", "Backlight", $"{supply.SystemParams.Backlight}", $"{config.SystemParams.Backlight}"); + diffCount += AddDiff(diffTable, "System", "Volume", $"{supply.SystemParams.Volume}", $"{config.SystemParams.Volume}"); + + for (int i = 0; i < supply.Presets.Length; i++) + { + var devPreset = supply.Presets[i]; + string cfgVoltage = "--"; + string cfgOvp = "--"; + string cfgCurrent = "--"; + string cfgOcp = "--"; + + if (i < config.Presets.Count) + { + cfgVoltage = $"{config.Presets[i].Voltage / 1000.0,6:0.000}"; + cfgOvp = $"{config.Presets[i].OVP / 1000.0,6:0.000}"; + cfgCurrent = $"{config.Presets[i].Current / 1000.0,6:0.000}"; + cfgOcp = $"{config.Presets[i].OCP / 1000.0,6:0.000}"; + } + diffCount += AddDiff(diffTable, $"Preset {i}", "Voltage (V)", $"{devPreset.Voltage / 1000.0,6:0.000}", cfgVoltage); + diffCount += AddDiff(diffTable, $"Preset {i}", "OVP (V)", $"{devPreset.OVP / 1000.0,6:0.000}", cfgOvp); + diffCount += AddDiff(diffTable, $"Preset {i}", "Current (A)", $"{devPreset.Current / 1000.0,6:0.000}", cfgCurrent); + diffCount += AddDiff(diffTable, $"Preset {i}", "OCP (A)", $"{devPreset.OCP / 1000.0,6:0.000}", cfgOcp); + } + + if (diffCount != 0) + { + AnsiConsole.Write(diffTable); + } + + return diffCount; + } + + /// + /// Shows the configuration for system parameters and presets. + /// + /// The system parameters. + /// The presets. + private static void ShowConfiguration(PowerSupplySystemParams sysParams, PowerSupplySetpoint[] presets) + { + var sysParamTable = new Table(); + var presetsTable = new Table(); + + sysParamTable.AddColumn(new TableColumn(new Markup("[white]OPP (W)[/]")).Centered()); + sysParamTable.AddColumn(new TableColumn(new Markup("[white]OTP (C)[/]")).Centered()); + sysParamTable.AddColumn(new TableColumn(new Markup("[white]RPP[/]")).Centered()); + sysParamTable.AddColumn(new TableColumn(new Markup("[white]Auto-ON[/]")).Centered()); + sysParamTable.AddColumn(new TableColumn(new Markup("[white]Backlight[/]")).Centered()); + sysParamTable.AddColumn(new TableColumn(new Markup("[white]Volume[/]")).Centered()); + + sysParamTable.AddRow( + $"{sysParams.OPP / 10.0:0.0}", + $"{sysParams.OTP}", + sysParams.RPP.ToString(), + sysParams.AutoOn.ToString(), + sysParams.Backlight.ToString(), + sysParams.Volume.ToString()); + + presetsTable.AddColumn(new TableColumn(new Markup("[white]Preset[/]")).Centered()); + presetsTable.AddColumn(new TableColumn(new Markup("[white]Voltage (V)[/]")).Centered()); + presetsTable.AddColumn(new TableColumn(new Markup("[white]OVP (V)[/]")).Centered()); + presetsTable.AddColumn(new TableColumn(new Markup("[white]Current (A)[/]")).Centered()); + presetsTable.AddColumn(new TableColumn(new Markup("[white]OCP (A)[/]")).Centered()); + + foreach (var preset in presets) + { + presetsTable.AddRow( + preset.GetIndex().ToString(), + $"{preset.Voltage / 1000.0,6:0.000}", + $"{preset.OVP / 1000.0,6:0.000}", + $"{preset.Current / 1000.0,6:0.000}", + $"{preset.OCP / 1000.0,6:0.000}"); + } + + AnsiConsole.Write(sysParamTable); + AnsiConsole.Write(presetsTable); } /// @@ -475,6 +614,32 @@ private static string GetDeviceSelection(List devices) return serialNumber.Split(':')[0].Trim(); } + /// + /// Ask the user to select an option from one of the specified choices. + /// + /// The title/prompt to show + /// The list of choices. + /// The choice selected (1-based). + private static int GetChoice(string title, List choices) + { + List formattedChoices = new(); + foreach (var choice in choices) + { + formattedChoices.Add($"{formattedChoices.Count + 1}: {choice}"); + } + + SelectionPrompt prompt = new SelectionPrompt() + .Title(title) + .PageSize(10) + .EnableSearch() + .MoreChoicesText("[grey](Move up and down to reveal more devices)[/]") + .AddChoices(formattedChoices); + prompt.SearchHighlightStyle = new Style(Color.White, Color.Blue); + string userChoice = AnsiConsole.Prompt(prompt); + + return int.Parse(userChoice.Split(':')[0].Trim()); + } + /// /// Get the main grid that presents the power supply data to the user. /// diff --git a/app/vicon.settings.schema.json b/app/vicon.settings.schema.json new file mode 100644 index 0000000..221a91b --- /dev/null +++ b/app/vicon.settings.schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "poll-rate-ms": { + "type": "integer", + "minimum": 0 + }, + "theme": { + "type": "string", + "enum": ["classic", "black-and-white", "grey", "dark-red", "dark-green", "dark-magenta", "cyan", "gold", "blue", "blue-violet"] + }, + "devices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "serial": { + "type": "string", + "pattern": "^[0-9A-Fa-f]{8}$" + }, + "config": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "pattern": "^[0-9A-Fa-f]*$" + }, + "system-params": { + "type": "object", + "properties": { + "opp": { + "type": "integer", + "minimum": 0, + "maximum": 1050 + }, + "otp": { + "type": "integer", + "minimum": 50, + "maximum": 80 + }, + "rpp": { + "type": "boolean" + }, + "auto-on": { + "type": "boolean" + }, + "backlight": { + "type": "integer", + "minimum": 0, + "maximum": 4 + }, + "volume": { + "type": "integer", + "minimum": 0, + "maximum": 4 + } + }, + "required": ["opp","otp","rpp","auto-on","backlight","volume"], + "additionalProperties": false + }, + "presets": { + "type":"array", + "minItems": 10, + "maxItems": 10, + "items": { + "type": "object", + "properties": { + "voltage": { + "type": "integer", + "minimum": 0, + "maximum": 30500 + }, + "current": { + "type": "integer", + "minimum": 0, + "maximum": 5050 + }, + "ovp": { + "type": "integer", + "minimum": 0, + "maximum": 30500 + }, + "ocp": { + "type": "integer", + "minimum": 0, + "maximum": 5050 + } + }, + "required": ["voltage","current","ovp","ocp"], + "additionalProperties": false + } + } + }, + "required": ["hash","system-params","presets"], + "additionalProperties": false + } + }, + "required": ["alias", "serial"], + "additionalProperties": false + } + } + }, + "required": [], + "additionalProperties": false +} diff --git a/libdp100/PowerSupply.cs b/libdp100/PowerSupply.cs index 53dfc33..c8eafab 100644 --- a/libdp100/PowerSupply.cs +++ b/libdp100/PowerSupply.cs @@ -533,11 +533,16 @@ public PowerSupplyResult SetOutput(bool outputOn, ushort millivolts, ushort mill if (Presets[Output.Preset] == null) { Presets[Output.Preset] = new PowerSupplySetpoint(Output.Preset); + result = GetPreset(Output.Preset); } + } + if (result == PowerSupplyResult.OK) + { // Output state affects the volatile state of a preset (group). - Presets[Output.Preset].Copy(Output.Setpoint); - presetsValid[Output.Preset] = true; + // This does not include OVP/OCP. + Presets[Output.Preset].Voltage = Output.Setpoint.Voltage; + Presets[Output.Preset].Current = Output.Setpoint.Current; } return result; @@ -760,6 +765,11 @@ public PowerSupplyResult SetPresetOVP(byte preset, ushort ovp) return PowerSupplyResult.DeviceNotConnected; } + if (preset >= NumPresets) + { + return PowerSupplyResult.OutOfRange; + } + PowerSupplyResult result = PowerSupplyResult.OK; if (!presetsValid[preset]) @@ -791,6 +801,11 @@ public PowerSupplyResult SetPresetOCP(byte preset, ushort ocp) return PowerSupplyResult.DeviceNotConnected; } + if (preset >= NumPresets) + { + return PowerSupplyResult.OutOfRange; + } + PowerSupplyResult result = PowerSupplyResult.OK; if (!presetsValid[preset]) @@ -839,6 +854,11 @@ public PowerSupplyResult SetPreset(byte preset, ushort millivolts, ushort millia return PowerSupplyResult.DeviceNotConnected; } + if (preset >= NumPresets) + { + return PowerSupplyResult.OutOfRange; + } + PowerSupplyResult result = PowerSupplyResult.OK; if (!presetsValid[preset]) @@ -877,6 +897,11 @@ public PowerSupplyResult UsePreset(byte preset) return PowerSupplyResult.DeviceNotConnected; } + if (preset >= NumPresets) + { + return PowerSupplyResult.OutOfRange; + } + PowerSupplyResult result = PowerSupplyResult.OK; if (Output.On) @@ -1022,6 +1047,11 @@ public PowerSupplyResult GetPreset(byte preset) return PowerSupplyResult.DeviceNotConnected; } + if (preset >= NumPresets) + { + return PowerSupplyResult.OutOfRange; + } + byte[] outputReport = GetBasicSetCommand(BasicSetSubOpCode.GetGroupInfo, preset); PrintOutputReportHex(outputReport); return ProcessTransaction(outputReport, diff --git a/libdp100/PowerSupplySetpoint.cs b/libdp100/PowerSupplySetpoint.cs index 27038f5..93a7d95 100644 --- a/libdp100/PowerSupplySetpoint.cs +++ b/libdp100/PowerSupplySetpoint.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace LibDP100 { /// @@ -8,21 +10,25 @@ public class PowerSupplySetpoint /// /// Output voltage. Unit: mV /// + [JsonPropertyName("voltage")] public ushort Voltage { get; set; } = 0; /// /// Output current. Unit: mA /// + [JsonPropertyName("current")] public ushort Current { get; set; } = 0; /// /// Over voltage protection. Unit: mV. /// + [JsonPropertyName("ovp")] public ushort OVP { get; set; } = 0; /// /// Over current protection. Unit: mA. /// + [JsonPropertyName("ocp")] public ushort OCP { get; set; } = 0; /// @@ -30,6 +36,11 @@ public class PowerSupplySetpoint /// private byte Index { get; set; } = 0; + /// + /// Default constructor. Not to be used except for deserialization. + /// + public PowerSupplySetpoint() { } + /// /// Default constructor. /// @@ -76,6 +87,16 @@ public byte GetIndex() return Index; } + /// + /// Sets the index of the preset. + /// This is primarily intended for deserialization logic. + /// + /// The index to set. + public void SetIndex(byte index) + { + Index = index; + } + /// /// Make a copy of the provided setpoint object. The is not modified. /// diff --git a/libdp100/PowerSupplySystemParams.cs b/libdp100/PowerSupplySystemParams.cs index 8609ba5..e735262 100644 --- a/libdp100/PowerSupplySystemParams.cs +++ b/libdp100/PowerSupplySystemParams.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace LibDP100 { /// @@ -8,31 +10,37 @@ public class PowerSupplySystemParams /// /// Over power protection. Unit: 0.1W. /// + [JsonPropertyName("opp")] public ushort OPP { get; set; } = 0; /// - /// Over temperature protection. Unit Celsius. Value range 50-80. + /// Over temperature protection. Unit Celsius. Value range 40-80. /// + [JsonPropertyName("otp")] public ushort OTP { get; set; } = 0; /// /// Reverse polarity protection enable/disable. /// + [JsonPropertyName("rpp")] public bool RPP { get; set; } = false; /// /// Enable/Disable automatic output-on. /// + [JsonPropertyName("auto-on")] public bool AutoOn { get; set; } = false; /// /// Backlight level value 0-4. /// + [JsonPropertyName("backlight")] public byte Backlight { get; set; } = 0; /// /// Volume level ranges from 0-4. /// + [JsonPropertyName("volume")] public byte Volume { get; set; } = 0; /// diff --git a/publish/build-installer.iss b/publish/build-installer.iss index d7f73f6..19292fe 100644 --- a/publish/build-installer.iss +++ b/publish/build-installer.iss @@ -42,6 +42,7 @@ Source: "build\win-x64\Spectre.Console.dll"; DestDir: "{app}\bin"; Flags: ignore Source: "build\win-x64\vicon.dll"; DestDir: "{app}\bin"; Flags: ignoreversion Source: "build\win-x64\vicon.exe"; DestDir: "{app}\bin"; Flags: ignoreversion Source: "build\win-x64\vicon.runtimeconfig.json"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "build\win-x64\vicon.settings.schema.json"; DestDir: "{app}\bin"; Flags: ignoreversion ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Code]