From 8b288d07720154982de01136c8792c5f7c1ffc41 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 25 Oct 2023 09:49:02 -0500 Subject: [PATCH 001/141] local connection creating proper public key in Windows and Linux --- .../Connections/LocalConnection.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index 6ff0c0dd..8e8c9a4a 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -58,7 +58,7 @@ public LocalConnection() _deviceInfo = new DeviceInfo(info); } - return Task.FromResult< DeviceInfo?>(_deviceInfo); + return Task.FromResult(_deviceInfo); } private string ExecuteBashCommandLine(string command) @@ -79,8 +79,27 @@ private string ExecuteBashCommandLine(string command) return process?.StandardOutput.ReadToEnd() ?? string.Empty; } + private string ExecuteWindowsCommandLine(string command, string args) + { + var psi = new ProcessStartInfo() + { + FileName = command, + Arguments = args, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + + process?.WaitForExit(); + + return process?.StandardOutput.ReadToEnd() ?? string.Empty; + } + public override Task GetPublicKey(CancellationToken? cancellationToken = null) { + // DEV NOTE: this *must* be in PEM format: i.e. -----BEGIN RSA PUBLIC KEY----- ... -----END RSA PUBLIC KEY---- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -98,13 +117,27 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = throw new Exception("Public key not found"); } - return Task.FromResult(File.ReadAllText(pkFile)); + var pkFileContent = File.ReadAllText(pkFile); + if (!pkFileContent.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) + { + // need to convert + pkFileContent = ExecuteWindowsCommandLine("ssh-keygen", $"-e -m pem -f {pkFile}"); + } + + return Task.FromResult(pkFileContent); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - // ssh-agent sh -c 'ssh-add; ssh-add -L' - throw new PlatformNotSupportedException(); + // this will generate a PEM output *assuming* the key has already been created + var keygenOutput = ExecuteBashCommandLine("ssh-keygen -f id_rsa -e -m pem"); + if (!keygenOutput.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) + { + // probably no key generated + throw new Exception("Unable to retrieve a public key. Please run 'ssh-keygen -t rsa'"); + } + + return Task.FromResult(keygenOutput); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { From f3202f169f935e64302980b3140862702e4c7e47 Mon Sep 17 00:00:00 2001 From: ctacke Date: Wed, 25 Oct 2023 10:06:58 -0500 Subject: [PATCH 002/141] Mac public key to PEM format --- Source/v2/Meadow.Hcom/Connections/LocalConnection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index 8e8c9a4a..9d6a2cc6 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -143,6 +143,11 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = { // ssh-agent sh -c 'ssh-add; ssh-add -L' var pubkey = this.ExecuteBashCommandLine("ssh-agent sh -c 'ssh-add; ssh-add -L'"); + if (!pubkey.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh", "id_rsa.pub"); + pubkey = ExecuteBashCommandLine($"ssh-keygen -f {path} -e -m pem"); + } return Task.FromResult(pubkey); } else From 69cf913e3d9cf28780666edd7f85a49c915b2920 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 25 Oct 2023 17:54:23 -0500 Subject: [PATCH 003/141] more simulation work --- .../Current/Device/DeviceProvisionCommand.cs | 18 +++++++++++++++--- .../Meadow.Hcom/Connections/LocalConnection.cs | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index d51bd1cc..1e36a798 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -21,7 +21,7 @@ public class DeviceProvisionCommand : BaseDeviceCommand [CommandOption("name", 'n', Description = "Device friendly name", IsRequired = false)] public string? Name { get; set; } - [CommandOption("host", 'h', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] + [CommandOption("host", 'e', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) @@ -85,9 +85,21 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); - var delim = "-----END PUBLIC KEY-----\n"; - publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + var delimList = new string[] + { + "-----END PUBLIC KEY-----", + "-----END RSA PUBLIC KEY-----" + }; + foreach (var delim in delimList) + { + var i = publicKey.IndexOf(delim); + if (i >= 0) + { + publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + break; + } + } Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index 8e8c9a4a..07c5257b 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -39,7 +39,7 @@ public LocalConnection() } using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography\")) { - info.Add("SerialNo", (key?.GetValue("MachineGuid")?.ToString() ?? "Unknown").Trim()); + info.Add("SerialNo", (key?.GetValue("MachineGuid")?.ToString() ?? "Unknown").Trim().ToUpper()); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) From 0b382f73c674b65e8264393be36ecf320e257513 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 27 Oct 2023 09:37:22 -0500 Subject: [PATCH 004/141] simulator connection --- .../v2/Meadow.Cli/MeadowConnectionManager.cs | 4 + .../Connections/SimulatorConnection.cs | 144 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 559e7b72..57ac0529 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -39,6 +39,10 @@ public MeadowConnectionManager(ISettingsManager settingsManager) { _currentConnection = new LocalConnection(); } + if (route == "simulator") + { + _currentConnection = new SimulatorConnection(); + } else { string? uri = null; diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs new file mode 100644 index 00000000..90e2d140 --- /dev/null +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -0,0 +1,144 @@ +using Microsoft.Extensions.Logging; + +namespace Meadow.Hcom; + +public class SimulatorConnection : ConnectionBase +{ + public override string Name => "Simulator"; + + private HttpClient? _client = null; + + public override Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + // TODO: use some config our environment variable to launch the simulator process if it's not running + + _client = new HttpClient(); + + throw new NotImplementedException(); + } + + public override Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task EraseFlash(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetPublicKey(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetRtcTime(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ResetDevice(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task TraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task TraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } +} From b7da94a10b185cc5ff2358e899b94591d9717722 Mon Sep 17 00:00:00 2001 From: Adam Patridge Date: Fri, 3 Nov 2023 17:06:17 -0600 Subject: [PATCH 005/141] Add install instructions/links up top --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6604d024..739eaa31 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,17 @@ The CLI tool supports DFU flashing for `nuttx.bin` and `nuttx_user.bin`. When th The CLI tool also supports device and file management including file transfers, flash partitioning, and MCU reset. -To run Meadow.CLI on Windows, run meadow.exe from the command prompt. On Mac and Windows, call **mono meadow.exe**. +To install the latest Meadow.CLI release, run the .NET tool install command to get the latest package from NuGet. + +```console +dotnet tool install WildernessLabs.Meadow.CLI --global +``` + +For the latest getting started instructions with Meadow and Meadow.CLI, check out the [Meadow guides](https://developer.wildernesslabs.co/Meadow/Getting_Started/Deploying_Meadow/) in the Wilderness Labs documentations. Additionally, there are instructions there for updating an existing Meadow.CLI install. + +If you want to develop or build a Meadow.CLI directly, or install a pre-release version, follow the [instructions to install a pre-release Meadow.CLI](https://github.com/WildernessLabs/Meadow.CLI/blob/develop/README.md#install-a-downloaded-pre-release-version). + +Once installed, run the Meadow.CLI from a command line with `meadow`. ## Options From bffd1dc7d1753f328596ceb86128361c2791e3c4 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 1 Nov 2023 17:16:04 +0000 Subject: [PATCH 006/141] Add Helpers. --- .../Commands/Helper/ConsoleSpinner.cs | 27 ++++++++++++++++++ .../Commands/Helper/ExtensionMethods.cs | 28 +++++++++++++++++++ Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs b/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs new file mode 100644 index 00000000..744a62e3 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs @@ -0,0 +1,27 @@ + +using CliFx.Infrastructure; + +namespace Meadow.CLI +{ + public class ConsoleSpinner + { + private int counter = 0; + private char[] sequence = { '|', '/', '-', '\\' }; + private IConsole console; + + public ConsoleSpinner(IConsole console) + { + this.console = console; + } + + public async Task Turn(int delay = 100, CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + { + counter++; + console?.Output.WriteAsync($"{sequence[counter % 4]} \r"); + await Task.Delay(delay, CancellationToken.None); // Not propogating the token intentionally. + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs new file mode 100644 index 00000000..738ed1ff --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs @@ -0,0 +1,28 @@ + +namespace Meadow.CLI +{ + public static class ExtensionMethods + { + public const string ConsoleColourBlack = "\u001b[30m"; + public const string ConsoleColourBlue = "\u001b[34m"; + public const string ConsoleColourCyan = "\u001b[36m"; + public const string ConsoleColourGreen = "\u001b[32m"; + public const string ConsoleColourMagenta = "\u001b[35m"; + public const string ConsoleColourRed = "\u001b[31m"; + public const string ConsoleColourReset = "\u001b[0m"; + public const string ConsoleColourWhite = "\u001b[37m"; + public const string ConsoleColourYellow = "\u001b[33m"; + + public static string ColourConsoleText(this string textToColour, string textColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return textColour + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 19a77d02..0d884816 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Chris Tacke, Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 2.0.0-alpha.1 + 2.0.0-alpha.2 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From 5b1f16ab4a55cf704b15e99845e77fd5ab508055 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 1 Nov 2023 17:20:28 +0000 Subject: [PATCH 007/141] Remove duplicate extension methods. --- .../v2/Meadow.Cli/Properties/AssemblyInfo.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index c2127cd4..0e39f0aa 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -8,29 +8,4 @@ public static class Constants { public const string CLI_VERSION = "2.0.0.0"; } - - public static class StringExtensions - { - public const string ConsoleColourBlack = "\u001b[30m"; - public const string ConsoleColourBlue = "\u001b[34m"; - public const string ConsoleColourCyan = "\u001b[36m"; - public const string ConsoleColourGreen = "\u001b[32m"; - public const string ConsoleColourMagenta = "\u001b[35m"; - public const string ConsoleColourRed = "\u001b[31m"; - public const string ConsoleColourReset = "\u001b[0m"; - public const string ConsoleColourWhite = "\u001b[37m"; - public const string ConsoleColourYellow = "\u001b[33m"; - - public static string ColourConsoleText(this string textToColour, string textColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return textColour + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - } } \ No newline at end of file From 837f31272a3c5e98c75059dbefa8ade864a2be29 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 1 Nov 2023 17:25:55 +0000 Subject: [PATCH 008/141] Point to new ExtensionMethods class. --- .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 8 ++++---- Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 644dc8f3..0c2daa0e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,6 +1,6 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; @@ -185,9 +185,9 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"{Environment.NewLine}Firmware Write Status:"); foreach (var item in flashStatus) { - var textColour = StringExtensions.ConsoleColourRed; + var textColour = ExtensionMethods.ConsoleColourRed; if (item.Value.Contains(FileWriteComplete)) { - textColour = StringExtensions.ConsoleColourGreen; + textColour = ExtensionMethods.ConsoleColourGreen; } Logger?.LogInformation($"Serial Number: {item.Key} - {item.Value}".ColourConsoleText(textColour)); } diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index 3b4d4f0a..b0a6d097 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -24,16 +24,16 @@ private void OnDeviceMessageReceived(object? sender, (string message, string? so switch (e.source) { case "stdout": - textColour = StringExtensions.ConsoleColourBlue; + textColour = ExtensionMethods.ConsoleColourBlue; break; case "info": - textColour = StringExtensions.ConsoleColourGreen; + textColour = ExtensionMethods.ConsoleColourGreen; break; case "stderr": - textColour = StringExtensions.ConsoleColourRed; + textColour = ExtensionMethods.ConsoleColourRed; break; default: - textColour = StringExtensions.ConsoleColourReset; + textColour = ExtensionMethods.ConsoleColourReset; break; } From 33875e9418e32d4a9d081d40fed8e11fe6353772 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 1 Nov 2023 17:28:18 +0000 Subject: [PATCH 009/141] Normalise reference to CLI namespace instead of Cli. --- .github/workflows/dotnet.yml | 4 ++-- Source/v2/Meadow.Cli/AppManager.cs | 2 +- .../v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs | 2 +- .../Meadow.Cli/Commands/Current/App/AppDeployCommand.cs | 4 ++-- .../v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs | 6 +++--- .../v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs | 2 +- .../v2/Meadow.Cli/Commands/Current/BaseSettingsCommand.cs | 4 ++-- .../Current/Cloud/Package/CloudPackageCreateCommand.cs | 8 ++++---- .../Meadow.Cli/Commands/Current/Config/ConfigCommand.cs | 2 +- .../Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareDefaultCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareDeleteCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareDownloadCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareListCommand.cs | 2 +- .../Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs | 2 +- Source/v2/Meadow.Cli/IPackageManager.cs | 2 +- Source/v2/Meadow.Cli/ISettingsManager.cs | 2 +- Source/v2/Meadow.Cli/MeadowConnectionManager.cs | 2 +- Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs | 2 +- Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs | 2 +- Source/v2/Meadow.Cli/PackageManager.cs | 2 +- Source/v2/Meadow.Cli/Program.cs | 2 +- Source/v2/Meadow.Cli/Properties/launchSettings.json | 2 +- Source/v2/Meadow.Cli/SettingsManager.cs | 2 +- .../InMemorySettingsManager.cs | 2 +- .../Meadow.HCom.Integration.Tests.csproj | 2 +- Source/v2/Meadow.Hcom/Meadow.Hcom.sln | 2 +- 33 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 25223ac6..b63b3fa8 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -122,12 +122,12 @@ jobs: uses: actions/upload-artifact@v2 with: name: Meadow.CLI.V2.nuget.2.0.0 - path: 'main\Source\v2\Meadow.Cli\bin\Release\*.nupkg' + path: 'main\Source\v2\Meadow.CLI\bin\Release\*.nupkg' - if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} name: Publish Meadow.CLI v2.0 Nuget publically run: | - nuget push main\Source\v2\Meadow.Cli\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} + nuget push main\Source\v2\Meadow.CLI\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} build-vswin-2019: runs-on: windows-2019 diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index dbd146dc..e0456712 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -2,7 +2,7 @@ using Meadow.Software; using Microsoft.Extensions.Logging; -namespace Meadow.Cli; +namespace Meadow.CLI; public static class AppManager { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index fcb18e50..91f45cf1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 4930ad38..af62308d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -71,7 +71,7 @@ protected override async ValueTask ExecuteCommand() if (!file.Exists) { // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); if (candidates.Length == 0) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 269fb844..1997eaec 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Hcom; using Microsoft.Extensions.Logging; @@ -105,7 +105,7 @@ private Task BuildApplication(string path, CancellationToken cancellationT private async Task TrimApplication(string path, CancellationToken cancellationToken) { // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); if (candidates.Length == 0) { @@ -127,7 +127,7 @@ private async Task DeployApplication(IMeadowConnection connection, string { connection.FileWriteProgress += OnFileWriteProgress; - var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); if (candidates.Length == 0) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 1ebfca94..0b4b047d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs index 8953a9a3..e995f156 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -1,4 +1,4 @@ -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseSettingsCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseSettingsCommand.cs index 6f65341c..74a75b36 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseSettingsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseSettingsCommand.cs @@ -1,5 +1,5 @@ -using CliFx.Infrastructure; -using Meadow.Cli; +using CliFx.Infrastructure; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index af2cb13e..5b6e1245 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Cloud; using Meadow.Cloud.Identity; using Meadow.Software; @@ -54,7 +54,7 @@ protected override async ValueTask ExecuteCommand() return; } - var candidates = Cli.PackageManager.GetAvailableBuiltConfigurations(ProjectPath, "App.dll"); + var candidates = PackageManager.GetAvailableBuiltConfigurations(ProjectPath, "App.dll"); if (candidates.Length == 0) { @@ -71,8 +71,8 @@ protected override async ValueTask ExecuteCommand() await _packageManager.TrimApplication(file, cancellationToken: CancellationToken); // package - var packageDir = Path.Combine(file.Directory?.FullName ?? string.Empty, Cli.PackageManager.PackageOutputDirectoryName); - var postlinkDir = Path.Combine(file.Directory?.FullName ?? string.Empty, Cli.PackageManager.PostLinkDirectoryName); + var packageDir = Path.Combine(file.Directory?.FullName ?? string.Empty, PackageManager.PackageOutputDirectoryName); + var postlinkDir = Path.Combine(file.Directory?.FullName ?? string.Empty, PackageManager.PostLinkDirectoryName); Logger?.LogInformation($"Assembling the MPAK..."); var packagePath = await _packageManager.AssemblePackage(postlinkDir, packageDir, osVersion, Filter, true, Logger, CancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs index 68fca753..589c4c4e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs @@ -2,7 +2,7 @@ using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Infrastructure; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs index c3c69d4e..5c1b173e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -1,6 +1,6 @@ using CliFx.Attributes; using CliFx.Infrastructure; -using Meadow.Cli; +using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.Software; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 75d76cda..4de18847 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 1dcadf68..77059364 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index a897e3be..d959fd0d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index a0935fbf..adfb0283 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,7 +1,7 @@ using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index a19666f0..d324d8e4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs index 4c262134..b1804fcf 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index 4e88bfd9..297937a1 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.LibUsb; using Meadow.Software; diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs index 0f7d55d1..97613771 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs index 2c5053d0..696c0b44 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs index 92b72295..87e4d06e 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs @@ -1,5 +1,5 @@ using CliFx.Attributes; -using Meadow.Cli; +using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 01a69f7f..918704a0 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace Meadow.Cli; +namespace Meadow.CLI; public interface IPackageManager { diff --git a/Source/v2/Meadow.Cli/ISettingsManager.cs b/Source/v2/Meadow.Cli/ISettingsManager.cs index 4874e295..7c0bce4e 100644 --- a/Source/v2/Meadow.Cli/ISettingsManager.cs +++ b/Source/v2/Meadow.Cli/ISettingsManager.cs @@ -1,4 +1,4 @@ -namespace Meadow.Cli; +namespace Meadow.CLI; public interface ISettingsManager { diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index b388ba0a..04e6e8fe 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,4 +1,4 @@ -using Meadow.Cli; +using Meadow.CLI; using Meadow.Hcom; using Meadow.LibUsb; using System.Diagnostics; diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index c372eb1e..fc689294 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Reflection; -namespace Meadow.Cli; +namespace Meadow.CLI; public partial class PackageManager { diff --git a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs index f05e3f94..5bec4adc 100644 --- a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs +++ b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs @@ -1,4 +1,4 @@ -namespace Meadow.Cli; +namespace Meadow.CLI; public partial class PackageManager { diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 0c3d3fca..05f4e549 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -8,7 +8,7 @@ using System.Text.Json; using YamlDotNet.Serialization; -namespace Meadow.Cli; +namespace Meadow.CLI; public partial class PackageManager : IPackageManager { diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index bf789832..609418a5 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -1,5 +1,5 @@ using CliFx; -using Meadow.Cli; +using Meadow.CLI; using Meadow.CLI.Commands.DeviceManagement; using Meadow.Cloud; using Meadow.Cloud.Identity; diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 2597d467..e0ad7a7b 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "Meadow.Cli": { + "Meadow.CLI": { "commandName": "Project" }, "Help": { diff --git a/Source/v2/Meadow.Cli/SettingsManager.cs b/Source/v2/Meadow.Cli/SettingsManager.cs index e1e91ec9..aa3ffbee 100644 --- a/Source/v2/Meadow.Cli/SettingsManager.cs +++ b/Source/v2/Meadow.Cli/SettingsManager.cs @@ -1,7 +1,7 @@ using System.Configuration; using System.Text.Json; -namespace Meadow.Cli; +namespace Meadow.CLI; public class SettingsManager : ISettingsManager { diff --git a/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs b/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs index d4712ef4..e0bfa5c0 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs @@ -1,4 +1,4 @@ -using Meadow.Cli; +using Meadow.CLI; namespace Meadow.HCom.Integration.Tests { diff --git a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj index 9f107b3a..1391a119 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj +++ b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj @@ -26,7 +26,7 @@ - + diff --git a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln index 93b78ab2..06e0064a 100644 --- a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln +++ b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Hcom", "Meadow.Hcom. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.HCom.Integration.Tests", "..\Meadow.HCom.Integration.Tests\Meadow.HCom.Integration.Tests.csproj", "{F8830C1D-8343-4700-A849-B22537411E98}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cli", "..\Meadow.Cli\Meadow.Cli.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.CLI", "..\Meadow.CLI\Meadow.CLI.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 56c5285b5fc2314fd725ea19c42ac0ea5978a028 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 12:30:22 +0000 Subject: [PATCH 010/141] Make sure app.config.json and meadow.config.yaml are sent to device. --- Source/v2/Meadow.Cli/AppManager.cs | 41 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index e0456712..5ba90319 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -1,4 +1,5 @@ -using Meadow.Hcom; +using System.Drawing; +using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -47,19 +48,23 @@ public static async Task DeployApplication( { // TODO: add any other filtering capability here - if (!includePdbs && IsPdb(file)) continue; - if (!includeXmlDocs && IsXmlDoc(file)) continue; + if (!includePdbs && IsPdb(file)) + continue; + if (!includeXmlDocs && IsXmlDoc(file)) + continue; - // read the file data so we can generate a CRC - using FileStream fs = File.Open(file, FileMode.Open); - var len = (int)fs.Length; - var bytes = new byte[len]; - - await fs.ReadAsync(bytes, 0, len, cancellationToken); + //Populate out LocalFile Dictionary with this entry + await AddToLocalFiles(localFiles, file, cancellationToken); + } - var crc = CrcTools.Crc32part(bytes, len, 0); + if (File.Exists(Path.Combine(localBinaryDirectory, "app.config.json"))) + { + await AddToLocalFiles(localFiles, Path.Combine(localBinaryDirectory, "app.config.json"), cancellationToken); + } - localFiles.Add(file, crc); + if (File.Exists(Path.Combine(localBinaryDirectory, "meadow.config.yaml"))) + { + await AddToLocalFiles(localFiles, Path.Combine(localBinaryDirectory, "meadow.config.yaml"), cancellationToken); } if (localFiles.Count() == 0) @@ -129,4 +134,18 @@ public static async Task DeployApplication( } while (!success); } } + + private static async Task AddToLocalFiles(Dictionary localFiles, string file, CancellationToken cancellationToken) + { + // read the file data so we can generate a CRC + using FileStream fs = File.Open(file, FileMode.Open); + var len = (int)fs.Length; + var bytes = new byte[len]; + + await fs.ReadAsync(bytes, 0, len, cancellationToken); + + var crc = CrcTools.Crc32part(bytes, len, 0); + + localFiles.Add(file, crc); + } } From 11b38e9ea60d9a70f251ab02914bb250aa9f6194 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 12:32:47 +0000 Subject: [PATCH 011/141] make EncodeAndSendPacketawaitable. --- .../SerialConnection.ListenerProc.cs | 2 +- .../Connections/SerialConnection.cs | 188 +++++++++--------- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 7186ca55..b2be8d3e 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -219,7 +219,7 @@ private async Task ListenerProc() _readFileInfo.FileStream = File.Create(_readFileInfo.LocalFileName); var uploadRequest = RequestBuilder.Build(); - EncodeAndSendPacket(uploadRequest.Serialize()); + await EncodeAndSendPacket(uploadRequest.Serialize()); } else if (response is UploadDataPacketResponse udp) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 5448f18c..28ed952d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -231,7 +231,7 @@ private void Close() private async void CommandManager() { - await Task.Run(() => + await Task.Run(async () => { while (!_isDisposed) { @@ -246,7 +246,7 @@ await Task.Run(() => if (command != null) { var payload = command.Serialize(); - EncodeAndSendPacket(payload); + await EncodeAndSendPacket(payload); } // TODO: re-queue on fail? @@ -291,120 +291,120 @@ public void EnqueueRequest(IRequest command) _pendingCommands.Enqueue(command); } - private void EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) + private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = default) { - EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); + await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); } - private void EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = null) + private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = default) { - Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - - while (!_port.IsOpen) + if (messageBytes != null) { - State = ConnectionState.Disconnected; - Thread.Sleep(100); - // wait for the port to open - } - - State = ConnectionState.Connected; + Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - try - { - int encodedToSend; - byte[] encodedBytes; + while (!_port.IsOpen) + { + State = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } - // For file download this is a LOT of messages - // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + State = ConnectionState.Connected; - // For testing calculate the crc including the sequence number - //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); try { - // The encoded size using COBS is just a bit more than the original size adding 1 byte - // every 254 bytes plus 1 and need room for beginning and ending delimiters. - var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; - encodedBytes = new byte[l + 2]; + int encodedToSend; + byte[] encodedBytes; + + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + + // For testing calculate the crc including the sequence number + //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + try + { + // The encoded size using COBS is just a bit more than the original size adding 1 byte + // every 254 bytes plus 1 and need room for beginning and ending delimiters. + var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; + encodedBytes = new byte[l + 2]; - // Skip over first byte so it can be a start delimiter - encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + + // DEBUG TESTING + if (encodedToSend == -1) + { + _logger?.LogError($"Error - encodedToSend == -1"); + return; + } - // DEBUG TESTING - if (encodedToSend == -1) + if (_port == null) + { + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); + } + } + catch (Exception except) { - _logger?.LogError($"Error - encodedToSend == -1"); - return; + string msg = string.Format("Send setup Exception: {0}", except); + _logger?.LogError(msg); + throw; } - if (_port == null) + // Add delimiters to packet boundaries + try { - _logger?.LogError($"Error - SerialPort == null"); - throw new Exception("Port is null"); + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + } + catch (Exception encodedBytesEx) + { + // This should drop the connection and retry + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; } - } - catch (Exception except) - { - string msg = string.Format("Send setup Exception: {0}", except); - _logger?.LogError(msg); - throw; - } - // Add delimiters to packet boundaries - try - { - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; + try + { + // Send the data to Meadow + await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken.HasValue ? cancellationToken.Value : default); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); + throw; + } } - catch (Exception encodedBytesEx) + catch (Exception except) { + // DID YOU RESTART MEADOW? // This should drop the connection and retry - Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); - Thread.Sleep(500); // Place for break point - throw; - } - - try - { - // Send the data to Meadow - // Debug.Write($"Sending {encodedToSend} bytes..."); - //await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken ?? CancellationToken.None); - _port.Write(encodedBytes, 0, encodedToSend); - // Debug.WriteLine($"sent"); - } - catch (InvalidOperationException ioe) // Port not opened - { - string msg = string.Format("Write but port not opened. Exception: {0}", ioe); - _logger?.LogError(msg); - throw; - } - catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer - { - string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); - _logger?.LogError(msg); - throw; - } - catch (ArgumentException ae) // offset plus count > buffer length - { - string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); - _logger?.LogError(msg); - throw; - } - catch (TimeoutException te) // Took too long to send - { - string msg = string.Format("Write took too long to send. Exception: {0}", te); - _logger?.LogError(msg); + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); throw; } } - catch (Exception except) - { - // DID YOU RESTART MEADOW? - // This should drop the connection and retry - _logger?.LogError($"EncodeAndSendPacket threw: {except}"); - throw; - } } @@ -1073,7 +1073,7 @@ void OnFileRetry(object? sender, EventArgs e) Array.Copy(fileBytes, progress, packet, 2, toRead); try { - EncodeAndSendPacket(packet, toRead + 2, cancellationToken); + await EncodeAndSendPacket(packet, toRead + 2, cancellationToken); } catch (Exception) { @@ -1095,7 +1095,7 @@ void OnFileRetry(object? sender, EventArgs e) var request = RequestBuilder.Build(); request.SetRequestType(endRequestType); var p = request.Serialize(); - EncodeAndSendPacket(p, cancellationToken); + await EncodeAndSendPacket(p, cancellationToken); } FileWriteAccepted -= OnFileWriteAccepted; From 4adeacf4ac9f75b8abd2904d63ead44a9abdbb6a Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 12:35:28 +0000 Subject: [PATCH 012/141] Refactor to re-use existing/shared command. Also added ConsoleSpinner tasks that longer. --- .../Commands/Current/App/BaseAppCommand.cs | 14 +++ .../Commands/Current/App/AppBuildCommand.cs | 20 +++- .../Commands/Current/App/AppDeployCommand.cs | 47 +++++--- .../Commands/Current/App/AppRunCommand.cs | 109 ++---------------- .../Commands/Current/App/AppTrimCommand.cs | 17 ++- .../PackageManager.AssemblyManager.cs | 2 +- 6 files changed, 84 insertions(+), 125 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs diff --git a/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs new file mode 100644 index 00000000..2c0f9d66 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseAppCommand : BaseDeviceCommand +{ + protected IPackageManager _packageManager; + + public BaseAppCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + _packageManager = packageManager; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 91f45cf1..ef1479de 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -23,7 +23,7 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact protected override async ValueTask ExecuteCommand() { - await Task.Run(() => + await Task.Run(async () => { string path = Path == null ? Environment.CurrentDirectory @@ -40,12 +40,24 @@ await Task.Run(() => } } - if (Configuration == null) Configuration = "Release"; + if (Configuration == null) + Configuration = "Release"; - Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); + Logger?.LogInformation($"Building {Configuration} configuration of {path} (this may take a few seconds)..."); + + // Get out spinner ready + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(Console!); + Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); // TODO: enable cancellation of this call - var success = _packageManager.BuildApplication(path, Configuration); + var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)); + + // Cancel the spinner as soon as EraseFlash finishes + spinnerCancellationTokenSource.Cancel(); + + // Let's start spinning + await consoleSpinnerTask; if (!success) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index af62308d..7ef86487 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -5,17 +5,16 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app deploy", Description = "Deploys a built Meadow application to a target device")] -public class AppDeployCommand : BaseDeviceCommand +public class AppDeployCommand : BaseAppCommand { - private IPackageManager _packageManager; + private string lastFile = string.Empty; [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] public string? Path { get; set; } = default!; public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) + : base(packageManager, connectionManager, loggerFactory) { - _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() @@ -31,7 +30,7 @@ protected override async ValueTask ExecuteCommand() // is the path a file? FileInfo file; - var lastFile = string.Empty; + lastFile = string.Empty; // in order to deploy, the runtime must be disabled var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); @@ -43,20 +42,6 @@ protected override async ValueTask ExecuteCommand() await Connection.RuntimeDisable(CancellationToken); } - Connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; - - if (e.fileName != lastFile) - { - Console?.Output.WriteAsync("\n"); - lastFile = e.fileName; - } - - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; - if (!File.Exists(path)) { // is it a valid directory? @@ -92,8 +77,18 @@ protected override async ValueTask ExecuteCommand() if (Logger != null && !string.IsNullOrEmpty(targetDirectory)) { + var trimApplicationCommand = new AppTrimCommand(_packageManager, LoggerFactory!) + { + Path = path + }; + await trimApplicationCommand.ExecuteAsync(Console!); + + Connection.FileWriteProgress += Connection_FileWriteProgress; + await AppManager.DeployApplication(_packageManager, Connection, targetDirectory, true, false, Logger, CancellationToken); Console?.Output.WriteAsync("\n"); + + Connection.FileWriteProgress -= Connection_FileWriteProgress; } if (wasRuntimeEnabled) @@ -105,4 +100,18 @@ protected override async ValueTask ExecuteCommand() } } } + + private void Connection_FileWriteProgress(object? sender, (string fileName, long completed, long total) e) + { + var p = (e.completed / (double)e.total) * 100d; + + if (e.fileName != lastFile) + { + Console?.Output.WriteAsync("\n"); + lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 1997eaec..4e0cbe6d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -6,11 +6,8 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app run", Description = "Builds, trims and deploys a Meadow application to a target device")] -public class AppRunCommand : BaseDeviceCommand +public class AppRunCommand : BaseAppCommand { - private IPackageManager _packageManager; - private string _lastFile = string.Empty; - [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] public bool NoPrefix { get; set; } @@ -21,13 +18,14 @@ public class AppRunCommand : BaseDeviceCommand public string? Path { get; set; } = default!; public AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) + : base(packageManager, connectionManager, loggerFactory) { - _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { + await base.ExecuteCommand(); + string path = Path == null ? Environment.CurrentDirectory : Path; @@ -40,19 +38,11 @@ protected override async ValueTask ExecuteCommand() var lastFile = string.Empty; - if (!await BuildApplication(path, CancellationToken)) + var buildmApplicationCommand = new AppBuildCommand(_packageManager, LoggerFactory!) { - Logger?.LogError("Build failed."); - return; - } - - if (!await TrimApplication(path, CancellationToken)) - { - Logger?.LogError("Trimming failed."); - return; - } - - await base.ExecuteCommand(); + Path = path + }; + await buildmApplicationCommand.ExecuteAsync(Console!); if (Connection != null) @@ -72,10 +62,11 @@ protected override async ValueTask ExecuteCommand() s.CommandTimeoutSeconds = 60; } - if (!await DeployApplication(Connection, path, CancellationToken)) + var deployApplication = new AppDeployCommand(_packageManager, ConnectionManager, LoggerFactory!) { - return; - } + Path = path + }; + await deployApplication.ExecuteAsync(Console!); Logger?.LogInformation("Enabling the runtime..."); await Connection.RuntimeEnable(CancellationToken); @@ -92,82 +83,6 @@ protected override async ValueTask ExecuteCommand() } } - private Task BuildApplication(string path, CancellationToken cancellationToken) - { - if (Configuration == null) Configuration = "Debug"; - - Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); - - // TODO: enable cancellation of this call - return Task.FromResult(_packageManager.BuildApplication(path, Configuration, logger: Logger)); - } - - private async Task TrimApplication(string path, CancellationToken cancellationToken) - { - // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{path}'"); - return false; - } - - var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - - // if no configuration was provided, find the most recently built - Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); - - await _packageManager.TrimApplication(file, false, null, Logger, cancellationToken); - - return true; - } - - private async Task DeployApplication(IMeadowConnection connection, string path, CancellationToken cancellationToken) - { - connection.FileWriteProgress += OnFileWriteProgress; - - var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{path}'"); - return false; - } - - var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - var directoryName = file.DirectoryName; - - Logger?.LogInformation($"Deploying app from {directoryName}..."); - - if (!string.IsNullOrEmpty(directoryName) && Logger != null) - { - await AppManager.DeployApplication(_packageManager, connection, directoryName, true, false, Logger, CancellationToken); - } - else - { - Logger?.LogError($"Invalid DirectoryName"); - } - - connection.FileWriteProgress -= OnFileWriteProgress; - - return true; - } - - private void OnFileWriteProgress(object? sender, (string fileName, long completed, long total) e) - { - var p = (e.completed / (double)e.total) * 100d; - - if (e.fileName != _lastFile) - { - Console?.Output.Write("\n"); - _lastFile = e.fileName; - } - - // Console instead of Logger due to line breaking for progress bar - Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); - } - private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) { if (NoPrefix) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 0b4b047d..1e524d65 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,4 +1,5 @@ -using CliFx.Attributes; +using System.Threading; +using CliFx.Attributes; using Meadow.CLI; using Microsoft.Extensions.Logging; @@ -55,10 +56,18 @@ protected override async ValueTask ExecuteCommand() file = new FileInfo(path); } - // if no configuration was provided, find the most recently built - Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + // Get out spinner ready + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(Console!); + Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); // TODO: support `nolink` command line args await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken); + + // Cancel the spinner as soon as EraseFlash finishes + spinnerCancellationTokenSource.Cancel(); + + // Let's start spinning + await consoleSpinnerTask; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index fc689294..67c961f8 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -125,7 +125,7 @@ private string? MeadowAssembliesPath var monolinker_args = $"\"{illinker_path}\" -x \"{descriptor_path}\" {no_link_args} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlink_dir}\" -r \"{prelink_app}\" -a \"{prelink_os}\" -d \"{prelink_dir}\""; - logger?.LogInformation("Trimming assemblies to reduce size (may take several seconds)..."); + logger?.LogInformation($"Trimming assemblies associated with {fileName} to reduce upload size (this may take a few seconds)..."); if (!string.IsNullOrWhiteSpace(no_link_args)) { logger?.LogInformation($"no-link args:'{no_link_args}'"); From b563be7a8cc9fbe76f3f545cc060c161816e655d Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 13:09:04 +0000 Subject: [PATCH 013/141] Add wifi.config.yaml to our list of AdditionalFiles. --- Source/v2/Meadow.Cli/AppManager.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 5ba90319..9d755f8e 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -40,6 +40,13 @@ public static async Task DeployApplication( var localFiles = new Dictionary(); + var additionFilesList = new string[] + { + "app.config.json", + "meadow.config.yaml", + "wifi.config.yaml", + }; + // get a list of files to send var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); @@ -57,14 +64,13 @@ public static async Task DeployApplication( await AddToLocalFiles(localFiles, file, cancellationToken); } - if (File.Exists(Path.Combine(localBinaryDirectory, "app.config.json"))) + foreach (var item in additionFilesList) { - await AddToLocalFiles(localFiles, Path.Combine(localBinaryDirectory, "app.config.json"), cancellationToken); - } - - if (File.Exists(Path.Combine(localBinaryDirectory, "meadow.config.yaml"))) - { - await AddToLocalFiles(localFiles, Path.Combine(localBinaryDirectory, "meadow.config.yaml"), cancellationToken); + var file = Path.Combine(localBinaryDirectory, item); + if (File.Exists(file)) + { + await AddToLocalFiles(localFiles, file, cancellationToken); + } } if (localFiles.Count() == 0) @@ -146,6 +152,7 @@ private static async Task AddToLocalFiles(Dictionary localFiles, s var crc = CrcTools.Crc32part(bytes, len, 0); - localFiles.Add(file, crc); + if (!localFiles.ContainsKey(file)) + localFiles.Add(file, crc); } } From f5e31d6bf84f759e8b1158db1884c662aff32104 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 13:52:35 +0000 Subject: [PATCH 014/141] Make sure we order the list correctly. --- .../Commands/Current/Firmware/FirmwareListCommand.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index adfb0283..ced95701 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,4 +1,5 @@ -using CliFx; +using System.Linq; +using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Meadow.CLI; @@ -42,9 +43,9 @@ private async Task DisplayVerboseResults(FileManager manager) foreach (var name in manager.Firmware.CollectionNames) { Logger?.LogInformation($" {name}"); - var collection = manager.Firmware[name]; + var collection = manager.Firmware[name.ToString()]; - foreach (var package in collection) + foreach (var package in collection.OrderByDescending(s=> s.Version)) { if (package == collection.DefaultPackage) { @@ -83,9 +84,9 @@ private async Task DisplayTerseResults(FileManager manager) foreach (var name in manager.Firmware.CollectionNames) { Logger?.LogInformation($" {name}"); - var collection = manager.Firmware[name]; + var collection = manager.Firmware[name.ToString()]; - foreach (var package in collection) + foreach (var package in collection.OrderByDescending(s => s.Version)) { if (package == collection.DefaultPackage) { From bacf04915560d09622900f4453a3934b1ab21f2d Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 18:19:28 +0000 Subject: [PATCH 015/141] Add Trimmed and TrimmedDependencies properties for use during deployment. --- Source/v2/Meadow.Cli/IPackageManager.cs | 5 ++++- Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs | 3 +++ Source/v2/Meadow.Cli/PackageManager.cs | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 918704a0..5960c5a7 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -29,4 +29,7 @@ Task AssemblePackage( ILogger? logger = null, CancellationToken? cancellationToken = null); -} + IEnumerable? TrimmedDependencies { get; set; } + + bool Trimmed { get; set; } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index 67c961f8..d0fe4387 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -46,6 +46,9 @@ private string? MeadowAssembliesPath } } + public IEnumerable? TrimmedDependencies { get; set; } + public bool Trimmed { get; set; } = false; + public async Task?> TrimDependencies(FileInfo file, List dependencies, IList? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) { var directoryName = file.DirectoryName; diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 05f4e549..49c1b1b5 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -131,6 +131,8 @@ public async Task TrimApplication( throw new FileNotFoundException($"{applicationFilePath} not found"); } + Trimmed = false; + // does an app.build.yaml file exist? var buildOptionsFile = Path.Combine( applicationFilePath.DirectoryName ?? string.Empty, @@ -159,13 +161,15 @@ public async Task TrimApplication( .Where(x => x.Contains("App.") == false) .ToList(); - await TrimDependencies( + TrimmedDependencies = await TrimDependencies( applicationFilePath, dependencies, noLink, logger, includePdbs, verbose: false); + + Trimmed = true; } public const string PackageMetadataFileName = "info.json"; From 9dfe53c2dcf3585d53155e27951ead3edb7b79ca Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 2 Nov 2023 18:20:19 +0000 Subject: [PATCH 016/141] Separate out Deployment List generation. --- Source/v2/Meadow.Cli/AppManager.cs | 74 ++++++++++++++----- .../Commands/Current/App/AppDeployCommand.cs | 16 +++- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 9d755f8e..56393ba7 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -1,4 +1,5 @@ using System.Drawing; +using System.Threading; using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -27,8 +28,7 @@ private static bool IsXmlDoc(string file) return false; } - public static async Task DeployApplication( - IPackageManager packageManager, + public static async Task> GenerateDeployList(IPackageManager packageManager, IMeadowConnection connection, string localBinaryDirectory, bool includePdbs, @@ -38,6 +38,8 @@ public static async Task DeployApplication( { // TODO: add sub-folder support when HCOM supports it + logger.LogInformation("Generating the list of files to deploy..."); + var localFiles = new Dictionary(); var additionFilesList = new string[] @@ -47,21 +49,44 @@ public static async Task DeployApplication( "wifi.config.yaml", }; + string[] dllLinkIngoreList = { "System.Threading.Tasks.Extensions.dll" };//, "Microsoft.Extensions.Primitives.dll" }; + string[] pdbLinkIngoreList = { "System.Threading.Tasks.Extensions.pdb" };//, "Microsoft.Extensions.Primitives.pdb" }; + // get a list of files to send var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); - logger.LogInformation("Generating the list of files to deploy..."); - foreach (var file in dependencies) + if (packageManager.Trimmed && packageManager.TrimmedDependencies != null) { - // TODO: add any other filtering capability here + var trimmedDependencies = packageManager.TrimmedDependencies.Where(x => x.Contains("App.") == false) + .Where(x => dllLinkIngoreList.Any(f => x.Contains(f)) == false) + .Where(x => pdbLinkIngoreList.Any(f => x.Contains(f)) == false) + .ToList(); + + //crawl trimmed dependencies + foreach (var file in trimmedDependencies) + { + if (!includePdbs && IsPdb(file)) + continue; + if (!includeXmlDocs && IsXmlDoc(file)) + continue; - if (!includePdbs && IsPdb(file)) - continue; - if (!includeXmlDocs && IsXmlDoc(file)) - continue; + await AddToLocalFiles(localFiles, file, cancellationToken); + } + } + else + { + foreach (var file in dependencies) + { + // TODO: add any other filtering capability here + + if (!includePdbs && IsPdb(file)) + continue; + if (!includeXmlDocs && IsXmlDoc(file)) + continue; - //Populate out LocalFile Dictionary with this entry - await AddToLocalFiles(localFiles, file, cancellationToken); + //Populate out LocalFile Dictionary with this entry + await AddToLocalFiles(localFiles, file, cancellationToken); + } } foreach (var item in additionFilesList) @@ -99,20 +124,31 @@ public static async Task DeployApplication( await connection.DeleteFile(file, cancellationToken); } - // now send all files with differing CRCs - foreach (var localFile in localFiles) + foreach (var deviceFile in deviceFiles) { - var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == Path.GetFileName(localFile.Key)); - - if (existing != null && existing.Crc != null) + var exists = localFiles.FirstOrDefault(l => Path.GetFileName(deviceFile.Name) == Path.GetFileName(l.Key)); + if (deviceFile.Crc != null) { - if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) + if (uint.Parse(deviceFile.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == exists.Value) { // exists and has a matching CRC, skip it - continue; + localFiles.Remove(exists.Key); } } + } + + return localFiles; + } + public static async Task DeployApplication( + IMeadowConnection connection, + Dictionary localFiles, + ILogger logger, + CancellationToken cancellationToken) + { + // now send all files with differing CRCs + foreach (var localFile in localFiles) + { bool success; do @@ -155,4 +191,4 @@ private static async Task AddToLocalFiles(Dictionary localFiles, s if (!localFiles.ContainsKey(file)) localFiles.Add(file, crc); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 7ef86487..886c0a52 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -83,9 +83,23 @@ protected override async ValueTask ExecuteCommand() }; await trimApplicationCommand.ExecuteAsync(Console!); + // Get out spinner ready + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(Console!); + Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); + + var localFiles = await AppManager.GenerateDeployList(_packageManager, Connection, targetDirectory, true, false, Logger, CancellationToken); + Console?.Output.WriteAsync("\n"); + + // Cancel the spinner as soon as EraseFlash finishes + spinnerCancellationTokenSource.Cancel(); + + // Let's start spinning + await consoleSpinnerTask; + Connection.FileWriteProgress += Connection_FileWriteProgress; - await AppManager.DeployApplication(_packageManager, Connection, targetDirectory, true, false, Logger, CancellationToken); + await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); Console?.Output.WriteAsync("\n"); Connection.FileWriteProgress -= Connection_FileWriteProgress; From 728a575fb47adaca8061f7f5f3ef07bfb5ea4339 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 6 Nov 2023 15:59:08 +0000 Subject: [PATCH 017/141] Intial pass are adding -f support to firmware write. More work needed. --- .../Current/Firmware/FirmwareWriteCommand.cs | 114 +++++++++++------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 0c2daa0e..35c15bcc 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -2,6 +2,7 @@ using CliFx.Attributes; using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Cloud; using Meadow.Hcom; using Meadow.LibUsb; using Meadow.Software; @@ -28,11 +29,16 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandParameter(0, Name = "Files to write", IsRequired = false)] public FirmwareType[]? Files { get; set; } = default!; + [CommandOption("file", 'f', IsRequired = false, Description = "Path to OS, Runtime or ESP file")] + public string? Path { get; set; } = default!; + private FileManager FileManager { get; } private ISettingsManager Settings { get; } private const string FileWriteComplete = "Firmware Write Complete!"; private ILibUsbDevice[]? _libUsbDevices; + private IMeadowConnection? connection; + // TODO private bool _fileWriteError = false; public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) @@ -68,21 +74,22 @@ protected override async ValueTask ExecuteCommand() bool deviceSupportsOta = false; // TODO: get this based on device OS version - if (package != null && package.OsWithoutBootloader == null + if ((Files != null && Files.Contains(FirmwareType.OS)) && (package != null && package.OsWithoutBootloader == null || !deviceSupportsOta - || UseDfu) + || UseDfu)) { UseDfu = true; } - IMeadowConnection? connection; - - if (UseDfu && Files != null && package != null) + if (Files != null && package != null) { // get the device's serial number via DFU - we'll need it to find the device after it resets try { - GetLibUsbDevicesInBootloaderModeForCurrentEnvironment(); + if (UseDfu) + { + GetLibUsbDevicesInBootloaderModeForCurrentEnvironment(); + } } catch (Exception ex) { @@ -90,6 +97,8 @@ protected override async ValueTask ExecuteCommand() return; } + var flashStatus = new ConcurrentDictionary(); + if (_libUsbDevices != null) { if (_libUsbDevices.Length > 1) @@ -106,8 +115,6 @@ protected override async ValueTask ExecuteCommand() } } - var flashStatus = new ConcurrentDictionary(); - foreach (var libUsbDevice in _libUsbDevices) { var serialNumber = libUsbDevice.GetDeviceSerialNumber(); @@ -145,37 +152,7 @@ protected override async ValueTask ExecuteCommand() // configure the route to that port for the user Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - // get the connection associated with that route - connection = await GetCurrentConnection(); - - try - { - if (connection != null && Files.Any(f => f != FirmwareType.OS)) - { - await connection.WaitForMeadowAttach(); - - if (CancellationToken.IsCancellationRequested) - { - continue; - } - - flashStatus[serialNumber] = "WriteFiles"; - await WriteFiles(package, connection); - } - } - catch (Exception ex) - { - flashStatus[serialNumber] = ex.Message; - // Log the exception but move onto the next device - Logger?.LogError($"{Environment.NewLine}Exception type: {ex.GetType().Name}", ex); - - continue; - } - finally - { - // Needed to avoid double messages - DetachMessageHandlers(connection); - } + await WriteNonOSToDevice(Files, package, flashStatus); flashStatus[serialNumber] = FileWriteComplete; } @@ -192,6 +169,49 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Serial Number: {item.Key} - {item.Value}".ColourConsoleText(textColour)); } } + else + { + await WriteNonOSToDevice(Files, package, flashStatus); + + if (connection != null) + flashStatus[connection.Name] = FileWriteComplete; + } + } + } + + private async Task WriteNonOSToDevice(FirmwareType[] files, FirmwarePackage? package, ConcurrentDictionary flashStatus) + { + // get the connection associated with that route + connection = await GetCurrentConnection(); + + try + { + if (connection != null && files.Any(f => f != FirmwareType.OS)) + { + await connection.WaitForMeadowAttach(); + + if (CancellationToken.IsCancellationRequested) + { + return; + } + + flashStatus[connection.Name] = "WriteFiles"; + await WriteFiles(package, connection); + } + } + catch (Exception ex) + { + if (connection != null) + flashStatus[connection.Name] = ex.Message; + // Log the exception but move onto the next device + Logger?.LogError($"{Environment.NewLine}Exception type: {ex.GetType().Name}", ex); + + return; + } + finally + { + // Needed to avoid double messages + DetachMessageHandlers(connection); } } @@ -299,10 +319,20 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c if (Files.Contains(FirmwareType.Runtime)) { - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + string? runtime; + if (!string.IsNullOrEmpty(Path)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {Path}..."); + runtime = Path; + } + else + { + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + + // get the path to the runtime file + runtime = package.Runtime; + } - // get the path to the runtime file - var runtime = package.Runtime; if (string.IsNullOrEmpty(runtime)) runtime = string.Empty; var rtpath = package.GetFullyQualifiedPath(runtime); From b02754c1b7f0d3326ea7a6f90a606af0e6071720 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 6 Nov 2023 16:04:25 +0000 Subject: [PATCH 018/141] Colour Code default firmware folder to green during firmware list. --- .../Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index ced95701..6aafe958 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -90,7 +90,7 @@ private async Task DisplayTerseResults(FileManager manager) { if (package == collection.DefaultPackage) { - Logger?.LogInformation($" * {package.Version} (default)"); + Logger?.LogInformation($" * {package.Version} (default)".ColourConsoleText(ExtensionMethods.ConsoleColourGreen)); } else { From 6ccc53494699261e3353419cfe87db67bab9ca96 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 6 Nov 2023 16:14:46 +0000 Subject: [PATCH 019/141] Add simple ConsoleColourText extension methods. --- .../Commands/Helper/ExtensionMethods.cs | 96 +++++++++++++++++++ .../Current/Firmware/FirmwareListCommand.cs | 2 +- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs index 738ed1ff..65275656 100644 --- a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs +++ b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs @@ -24,5 +24,101 @@ public static string ColourConsoleText(this string textToColour, string textColo return string.Empty; } } + + public static string ColourConsoleTextBlack(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourBlack + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextCyan(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourCyan + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextBlue(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourBlue + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextGreen(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourGreen + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextMagenta(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourMagenta + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextRed(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourRed + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextWhite(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourWhite + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } + + public static string ColourConsoleTextYellow(this string textToColour) + { + if (!string.IsNullOrEmpty(textToColour)) + { + return ConsoleColourYellow + textToColour + ConsoleColourReset; + } + else + { + return string.Empty; + } + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index 6aafe958..6b8ce660 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -90,7 +90,7 @@ private async Task DisplayTerseResults(FileManager manager) { if (package == collection.DefaultPackage) { - Logger?.LogInformation($" * {package.Version} (default)".ColourConsoleText(ExtensionMethods.ConsoleColourGreen)); + Logger?.LogInformation($" * {package.Version} (default)".ColourConsoleTextGreen()); } else { From a63e81a0831b2d142fda0aefa5cbac1791fd049b Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 6 Nov 2023 16:21:09 +0000 Subject: [PATCH 020/141] Add colour coding to verbose text too. --- .../Commands/Current/Firmware/FirmwareListCommand.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index 6b8ce660..4f328b6a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -49,14 +49,14 @@ private async Task DisplayVerboseResults(FileManager manager) { if (package == collection.DefaultPackage) { - Logger?.LogInformation( - $" * {package.Version?.PadRight(18)} " + + var detailedInformation = $" * {package.Version?.PadRight(18)} " + $"{(package.OSWithBootloader != null ? "X " : " ")}" + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + $"{(package.Runtime != null ? "X " : " ")}" + $"{(package.CoprocApplication != null ? "X " : " ")}" + - $"{(package.BclFolder != null ? "X " : " ")}" - ); + $"{(package.BclFolder != null ? "X " : " ")}" + + " (default)"; + Logger?.LogInformation(detailedInformation.ColourConsoleTextGreen()); } else { @@ -66,7 +66,7 @@ private async Task DisplayVerboseResults(FileManager manager) $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + $"{(package.Runtime != null ? "X " : " ")}" + $"{(package.CoprocApplication != null ? "X " : " ")}" + - $"{(package.BclFolder != null ? "X " : " ")}" + $"{(package.BclFolder != null ? "X " : " ")}" ); } } From fc2842567990b88eb6136f3923d2196259d6e09d Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 6 Nov 2023 17:01:30 +0000 Subject: [PATCH 021/141] Pass cancellation token to downloadFile. Also Check for default gateway. --- .github/workflows/dotnet.yml | 2 +- Meadow.CLI.Core/Constants.cs | 2 +- Meadow.CLI.Core/Firmware/FirmwareManager.cs | 4 +-- Meadow.CLI.Core/Managers/DownloadManager.cs | 34 +++++++++++++++---- Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.csproj | 2 +- Meadow.CLI/Commands/App/DeployAppCommand.cs | 2 +- Meadow.CLI/Meadow.CLI.Classic.csproj | 2 +- 9 files changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b63b3fa8..aff298d9 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,6 +1,6 @@ name: Meadow.CLI env: - CLI_RELEASE_VERSION: 1.4.0.0 + CLI_RELEASE_VERSION: 1.4.2.0 IDE_TOOLS_RELEASE_VERSION: 1.4.0 MEADOW_OS_VERSION: 1.4.0.3 VS_MAC_2019_VERSION: 8.10 diff --git a/Meadow.CLI.Core/Constants.cs b/Meadow.CLI.Core/Constants.cs index 44eb93d3..7b9a747f 100644 --- a/Meadow.CLI.Core/Constants.cs +++ b/Meadow.CLI.Core/Constants.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Core { public static class Constants { - public const string CLI_VERSION = "1.4.0.0"; + public const string CLI_VERSION = "1.4.2.0"; public const ushort HCOM_PROTOCOL_PREVIOUS_VERSION_NUMBER = 0x0006; public const ushort HCOM_PROTOCOL_CURRENT_VERSION_NUMBER = 0x0007; // Used for transmission public const string WILDERNESS_LABS_USB_VID = "2E6A"; diff --git a/Meadow.CLI.Core/Firmware/FirmwareManager.cs b/Meadow.CLI.Core/Firmware/FirmwareManager.cs index def243cb..9e1657cd 100644 --- a/Meadow.CLI.Core/Firmware/FirmwareManager.cs +++ b/Meadow.CLI.Core/Firmware/FirmwareManager.cs @@ -20,11 +20,11 @@ public static partial class JsonSerializerExtensions public static class FirmwareManager { - public static async Task GetRemoteFirmwareInfo(string versionNumber, ILogger logger) + public static async Task GetRemoteFirmwareInfo(string versionNumber, ILogger logger, CancellationToken cancellationToken) { var manager = new DownloadManager(logger); - return await manager.DownloadMeadowOSVersionFile(versionNumber); + return await manager.DownloadMeadowOSVersionFile(versionNumber, cancellationToken); } public static async Task GetRemoteFirmware(string versionNumber, ILogger logger) diff --git a/Meadow.CLI.Core/Managers/DownloadManager.cs b/Meadow.CLI.Core/Managers/DownloadManager.cs index 3674a5e6..318619b9 100644 --- a/Meadow.CLI.Core/Managers/DownloadManager.cs +++ b/Meadow.CLI.Core/Managers/DownloadManager.cs @@ -70,7 +70,7 @@ public DownloadManager(ILogger logger) _logger = logger; } - internal async Task DownloadMeadowOSVersionFile(string? version) + internal async Task DownloadMeadowOSVersionFile(string? version, CancellationToken cancellationToken) { string versionCheckUrl; if (version is null || string.IsNullOrWhiteSpace(version)) @@ -88,7 +88,7 @@ public DownloadManager(ILogger logger) try { - versionCheckFile = await DownloadFile(new Uri(versionCheckUrl)); + versionCheckFile = await DownloadFile(new Uri(versionCheckUrl), cancellationToken); } catch { @@ -118,17 +118,39 @@ bool CreateFolder(string path, bool eraseIfExists) return true; } + static bool IsPassingThroughDefaultGateway() + { + NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (NetworkInterface networkInterface in networkInterfaces) + { + if (networkInterface.OperationalStatus == OperationalStatus.Up) + { + GatewayIPAddressInformationCollection gateways = networkInterface.GetIPProperties().GatewayAddresses; + + if (gateways.Count > 0) + { + // If any network interface has a gateway, you are likely passing through a gateway. + return true; + } + } + } + + return false; + } + + //ToDo rename this method - DownloadOSAsync? - public async Task DownloadOsBinaries(string? version = null, bool force = false) + public async Task DownloadOsBinaries(string? version = null, bool force = false, CancellationToken cancellationToken = default) { // Check if there is an active internet connection - if (!NetworkInterface.GetIsNetworkAvailable()) + if (!NetworkInterface.GetIsNetworkAvailable() && IsPassingThroughDefaultGateway()) { _logger.LogError($"No internet connection! Cannot download Meadow OS version {version}. Please retry once an internet connection is available.{Environment.NewLine}"); return; } - var versionCheckFilePath = await DownloadMeadowOSVersionFile(version); + var versionCheckFilePath = await DownloadMeadowOSVersionFile(version, cancellationToken); if (versionCheckFilePath == null) { @@ -319,7 +341,7 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT downloadFileName = Path.GetTempFileName(); - using (var stream = await response.Content.ReadAsStreamAsync()) + using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken)) using (var downloadFileStream = new DownloadFileStream(stream, _logger)) { _logger.LogDebug($"Copying downloaded file to temp file {downloadFileName}"); diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj index b5602a9f..92915e2a 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.0.0 + 1.4.2.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj index 210e8c76..876bd2b1 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.0.0 + 1.4.2.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.csproj index f4e50d4c..21f66867 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.0.0 + 1.4.2.0 diff --git a/Meadow.CLI/Commands/App/DeployAppCommand.cs b/Meadow.CLI/Commands/App/DeployAppCommand.cs index 3c3fd3e7..a72da6ca 100644 --- a/Meadow.CLI/Commands/App/DeployAppCommand.cs +++ b/Meadow.CLI/Commands/App/DeployAppCommand.cs @@ -51,7 +51,7 @@ public override async ValueTask ExecuteAsync(IConsole console) try { // make sure we have the same locally because we will do linking/trimming against that runtime - await new DownloadManager(LoggerFactory).DownloadOsBinaries(osVersion); + await new DownloadManager(LoggerFactory).DownloadOsBinaries(osVersion, cancellationToken: cancellationToken); } catch { //OS binaries failed to download diff --git a/Meadow.CLI/Meadow.CLI.Classic.csproj b/Meadow.CLI/Meadow.CLI.Classic.csproj index 33812325..49f336de 100644 --- a/Meadow.CLI/Meadow.CLI.Classic.csproj +++ b/Meadow.CLI/Meadow.CLI.Classic.csproj @@ -10,7 +10,7 @@ Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 1.4.0.0 + 1.4.2.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From c456932e012f61e2181b61606eefd5be2256d3e0 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 7 Nov 2023 11:30:33 +0000 Subject: [PATCH 022/141] Don't pass cancellationTocken to ReadAsStreamAsync as it won't build on VS4Win :/ --- Meadow.CLI.Core/Managers/DownloadManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meadow.CLI.Core/Managers/DownloadManager.cs b/Meadow.CLI.Core/Managers/DownloadManager.cs index 318619b9..905e895f 100644 --- a/Meadow.CLI.Core/Managers/DownloadManager.cs +++ b/Meadow.CLI.Core/Managers/DownloadManager.cs @@ -341,7 +341,7 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT downloadFileName = Path.GetTempFileName(); - using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken)) + using (var stream = await response.Content.ReadAsStreamAsync()) using (var downloadFileStream = new DownloadFileStream(stream, _logger)) { _logger.LogDebug($"Copying downloaded file to temp file {downloadFileName}"); From eeaa17cff0b3b2ac5c73dafe2ffeed9c85f74c98 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 8 Nov 2023 00:26:11 +0000 Subject: [PATCH 023/141] Try to include more files in both dependencies and trimming. --- .../Devices/MeadowLocalDevice.FileManager.cs | 3 +- Source/v2/Meadow.Cli/AppManager.cs | 132 +++++++++++------- .../Commands/Current/App/AppBuildCommand.cs | 4 +- .../Commands/Current/App/AppDeployCommand.cs | 6 +- .../Commands/Current/App/AppTrimCommand.cs | 4 +- Source/v2/Meadow.Cli/IPackageManager.cs | 1 + .../PackageManager.AssemblyManager.cs | 4 +- Source/v2/Meadow.Cli/PackageManager.cs | 15 +- 8 files changed, 102 insertions(+), 67 deletions(-) diff --git a/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs b/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs index 4d681dff..b548789e 100644 --- a/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs +++ b/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs @@ -605,7 +605,8 @@ public async Task DeployApp(string applicationFilePath, var binaries = Directory.EnumerateFiles(directoryName, "*.*", SearchOption.TopDirectoryOnly) .Where(s => new FileInfo(s).Extension != ".dll") - .Where(s => new FileInfo(s).Extension != ".pdb"); + .Where(s => new FileInfo(s).Extension != ".pdb") + .Where(s => !s.Contains(".DS_Store")); // .Where(s => extensions.Contains(new FileInfo(s).Extension)); var files = new Dictionary(); diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 56393ba7..14069722 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -8,6 +8,9 @@ namespace Meadow.CLI; public static class AppManager { + static string[] dllLinkIngoreList = { "System.Threading.Tasks.Extensions.dll" };//, "Microsoft.Extensions.Primitives.dll" }; + static string[] pdbLinkIngoreList = { "System.Threading.Tasks.Extensions.pdb" };//, "Microsoft.Extensions.Primitives.pdb" }; + private static bool MatchingDllExists(string file) { var root = Path.GetFileNameWithoutExtension(file); @@ -42,22 +45,23 @@ public static async Task> GenerateDeployList(IPackageMa var localFiles = new Dictionary(); - var additionFilesList = new string[] - { - "app.config.json", - "meadow.config.yaml", - "wifi.config.yaml", - }; - - string[] dllLinkIngoreList = { "System.Threading.Tasks.Extensions.dll" };//, "Microsoft.Extensions.Primitives.dll" }; - string[] pdbLinkIngoreList = { "System.Threading.Tasks.Extensions.pdb" };//, "Microsoft.Extensions.Primitives.pdb" }; + var auxiliary = Directory.EnumerateFiles(localBinaryDirectory, "*.*", SearchOption.TopDirectoryOnly) + .Where(s => new FileInfo(s).Extension != ".dll") + .Where(s => new FileInfo(s).Extension != ".pdb") + .Where(s => !s.Contains(".DS_Store")); - // get a list of files to send - var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); + foreach (var item in auxiliary) + { + var file = Path.Combine(localBinaryDirectory, item); + if (File.Exists(file)) + { + await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); + } + } if (packageManager.Trimmed && packageManager.TrimmedDependencies != null) { - var trimmedDependencies = packageManager.TrimmedDependencies.Where(x => x.Contains("App.") == false) + var trimmedDependencies = packageManager.TrimmedDependencies .Where(x => dllLinkIngoreList.Any(f => x.Contains(f)) == false) .Where(x => pdbLinkIngoreList.Any(f => x.Contains(f)) == false) .ToList(); @@ -65,36 +69,47 @@ public static async Task> GenerateDeployList(IPackageMa //crawl trimmed dependencies foreach (var file in trimmedDependencies) { - if (!includePdbs && IsPdb(file)) - continue; - if (!includeXmlDocs && IsXmlDoc(file)) - continue; + await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); + } + + // Add the Dlls from the TrimmingIgnorelist + for (int i = 0; i < dllLinkIngoreList.Length; i++) + { + //add the files from the dll link ignore list + if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(dllLinkIngoreList[i]))) + { + var dllfound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(dllLinkIngoreList[i])); + if (!string.IsNullOrEmpty(dllfound)) + { + await AddToLocalFiles(localFiles, dllfound, includePdbs, includeXmlDocs, cancellationToken); + } + } + } - await AddToLocalFiles(localFiles, file, cancellationToken); + if (includePdbs) + { + for (int i = 0; i < pdbLinkIngoreList.Length; i++) + { + //add the files from the pdb link ignore list + if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(pdbLinkIngoreList[i]))) + { + var pdbFound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(pdbLinkIngoreList[i])); + if (!string.IsNullOrEmpty(pdbFound)) + { + await AddToLocalFiles(localFiles, pdbFound, includePdbs, includeXmlDocs, cancellationToken); + } + } + } } } else { - foreach (var file in dependencies) + foreach (var file in packageManager.AssemblyDependencies!) { // TODO: add any other filtering capability here - if (!includePdbs && IsPdb(file)) - continue; - if (!includeXmlDocs && IsXmlDoc(file)) - continue; - //Populate out LocalFile Dictionary with this entry - await AddToLocalFiles(localFiles, file, cancellationToken); - } - } - - foreach (var item in additionFilesList) - { - var file = Path.Combine(localBinaryDirectory, item); - if (File.Exists(file)) - { - await AddToLocalFiles(localFiles, file, cancellationToken); + await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); } } @@ -103,6 +118,17 @@ public static async Task> GenerateDeployList(IPackageMa logger.LogInformation($"No new files to deploy"); } + logger.LogInformation("Done."); + + return localFiles; + } + + public static async Task DeployApplication( + IMeadowConnection connection, + Dictionary localFiles, + ILogger logger, + CancellationToken cancellationToken) + { // get a list of files on-device, with CRCs var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); @@ -124,31 +150,24 @@ public static async Task> GenerateDeployList(IPackageMa await connection.DeleteFile(file, cancellationToken); } - foreach (var deviceFile in deviceFiles) + // now send all files with differing CRCs + foreach (var localFile in localFiles) { - var exists = localFiles.FirstOrDefault(l => Path.GetFileName(deviceFile.Name) == Path.GetFileName(l.Key)); - if (deviceFile.Crc != null) + // does the file name and CRC match? + var filename = Path.GetFileName(localFile.Key); + + if (!File.Exists(localFile.Key)) { - if (uint.Parse(deviceFile.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == exists.Value) - { - // exists and has a matching CRC, skip it - localFiles.Remove(exists.Key); - } + logger.LogInformation($"{filename} not found" + Environment.NewLine); + continue; } - } - return localFiles; - } + if (deviceFiles.Any(d => Path.GetFileName(d.Name) == filename && !string.IsNullOrEmpty(d.Crc) && uint.Parse(d.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value)) + { + logger.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); + continue; + } - public static async Task DeployApplication( - IMeadowConnection connection, - Dictionary localFiles, - ILogger logger, - CancellationToken cancellationToken) - { - // now send all files with differing CRCs - foreach (var localFile in localFiles) - { bool success; do @@ -177,8 +196,13 @@ public static async Task DeployApplication( } } - private static async Task AddToLocalFiles(Dictionary localFiles, string file, CancellationToken cancellationToken) + private static async Task AddToLocalFiles(Dictionary localFiles, string file, bool includePdbs, bool includeXmlDocs, CancellationToken cancellationToken) { + if (!includePdbs && IsPdb(file)) + return; + if (!includeXmlDocs && IsXmlDoc(file)) + return; + // read the file data so we can generate a CRC using FileStream fs = File.Open(file, FileMode.Open); var len = (int)fs.Length; diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index ef1479de..da9d7633 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -45,7 +45,7 @@ await Task.Run(async () => Logger?.LogInformation($"Building {Configuration} configuration of {path} (this may take a few seconds)..."); - // Get out spinner ready + // Get our spinner ready var spinnerCancellationTokenSource = new CancellationTokenSource(); var consoleSpinner = new ConsoleSpinner(Console!); Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); @@ -53,7 +53,7 @@ await Task.Run(async () => // TODO: enable cancellation of this call var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)); - // Cancel the spinner as soon as EraseFlash finishes + // Cancel the spinner as soon as BuildApplication finishes spinnerCancellationTokenSource.Cancel(); // Let's start spinning diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 886c0a52..2eadb083 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -83,15 +83,15 @@ protected override async ValueTask ExecuteCommand() }; await trimApplicationCommand.ExecuteAsync(Console!); - // Get out spinner ready + // Get our spinner ready var spinnerCancellationTokenSource = new CancellationTokenSource(); var consoleSpinner = new ConsoleSpinner(Console!); Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - var localFiles = await AppManager.GenerateDeployList(_packageManager, Connection, targetDirectory, true, false, Logger, CancellationToken); + var localFiles = await AppManager.GenerateDeployList(_packageManager, Connection, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken); Console?.Output.WriteAsync("\n"); - // Cancel the spinner as soon as EraseFlash finishes + // Cancel the spinner as soon as GenerateDeployList finishes spinnerCancellationTokenSource.Cancel(); // Let's start spinning diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 1e524d65..31bb0a64 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -56,7 +56,7 @@ protected override async ValueTask ExecuteCommand() file = new FileInfo(path); } - // Get out spinner ready + // Get our spinner ready var spinnerCancellationTokenSource = new CancellationTokenSource(); var consoleSpinner = new ConsoleSpinner(Console!); Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); @@ -64,7 +64,7 @@ protected override async ValueTask ExecuteCommand() // TODO: support `nolink` command line args await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken); - // Cancel the spinner as soon as EraseFlash finishes + // Cancel the spinner as soon as TrimApplication finishes spinnerCancellationTokenSource.Cancel(); // Let's start spinning diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 5960c5a7..57e3b474 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -29,6 +29,7 @@ Task AssemblePackage( ILogger? logger = null, CancellationToken? cancellationToken = null); + List? AssemblyDependencies { get; set; } IEnumerable? TrimmedDependencies { get; set; } bool Trimmed { get; set; } diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index d0fe4387..a93721d6 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -2,6 +2,7 @@ using Mono.Cecil; using Mono.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Reflection; namespace Meadow.CLI; @@ -46,6 +47,7 @@ private string? MeadowAssembliesPath } } + public List? AssemblyDependencies { get; set; } public IEnumerable? TrimmedDependencies { get; set; } public bool Trimmed { get; set; } = false; @@ -169,7 +171,7 @@ private string? MeadowAssembliesPath process.WaitForExit(60000); if (process.ExitCode != 0) { - Debug.WriteLine($"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + logger?.LogDebug($"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); throw new Exception("Trimming failed"); } } diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 49c1b1b5..5923edea 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -157,17 +157,24 @@ public async Task TrimApplication( } } - var dependencies = GetDependencies(applicationFilePath) - .Where(x => x.Contains("App.") == false) + AssemblyDependencies = GetDependencies(applicationFilePath) .ToList(); - TrimmedDependencies = await TrimDependencies( + try + { + TrimmedDependencies = await TrimDependencies( applicationFilePath, - dependencies, + AssemblyDependencies, noLink, logger, includePdbs, verbose: false); + } + catch (Exception) + { + logger?.LogError($"Trimming FAILED. Falling back to untrimmed dependencies"); + Trimmed = false; + } Trimmed = true; } From 270c65d3712bccc069c71bad4ee129ec992d09f6 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 8 Nov 2023 00:26:54 +0000 Subject: [PATCH 024/141] Display last part of path, so we know if it came from trimming or not. --- Source/v2/Meadow.Hcom/Connections/SerialConnection.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 28ed952d..f40a6317 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1048,8 +1048,10 @@ void OnFileRetry(object? sender, EventArgs e) var expected = fileBytes.Length; var fileName = Path.GetFileName(localFileName); + var directoryName = Path.GetDirectoryName(localFileName).Split(Path.DirectorySeparatorChar); + var displayedFileName = Path.Combine(directoryName[directoryName.Length - 1], fileName); - base.RaiseFileWriteProgress(fileName, progress, expected); + base.RaiseFileWriteProgress(displayedFileName, progress, expected); var oldTimeout = _port.ReadTimeout; _port.ReadTimeout = 60000; @@ -1081,7 +1083,7 @@ void OnFileRetry(object? sender, EventArgs e) } progress += toRead; - base.RaiseFileWriteProgress(fileName, progress, expected); + base.RaiseFileWriteProgress(displayedFileName, progress, expected); if (progress >= fileBytes.Length) break; } @@ -1089,7 +1091,7 @@ void OnFileRetry(object? sender, EventArgs e) { _port.ReadTimeout = oldTimeout; - base.RaiseFileWriteProgress(fileName, expected, expected); + base.RaiseFileWriteProgress(displayedFileName, expected, expected); // finish with an "end" message - not enqued because this is all a serial operation var request = RequestBuilder.Build(); From 29f90e6b9da14c44013cec00ec306263dea47b8f Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 9 Nov 2023 11:25:03 +0000 Subject: [PATCH 025/141] Make sure meadow is disabled before we try to delete files. Also add prompt for deleting all files. --- .../Current/File/FileDeleteCommand.cs | 87 +++++++++++++------ 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 7578932c..56f7a1f0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -20,53 +20,86 @@ protected override async ValueTask ExecuteCommand() if (Connection != null) { - var fileList = await Connection.GetFileList(false); + // in order to delete, the runtime must be disabled + var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); - if (MeadowFile == "all") + if (wasRuntimeEnabled) { - if (fileList != null) + Logger?.LogInformation("Disabling runtime..."); + + await Connection.RuntimeDisable(CancellationToken); + } + + try + { + var fileList = await Connection.GetFileList(false); + + if (MeadowFile == "all") { - foreach (var f in fileList) + if (Console != null) { - if (Connection.Device != null) + Logger?.LogInformation($"{Environment.NewLine}Are you sure you want to delete ALL files from this device (Y/N)?"); + + var reply = await Console.Input.ReadLineAsync(); + if ((!string.IsNullOrEmpty(reply) && reply.ToLower() != "y") || string.IsNullOrEmpty(reply)) { - var p = Path.GetFileName(f.Name); + return; + } + } - Logger?.LogInformation($"Deleting file '{p}' from device..."); - await Connection.Device.DeleteFile(p, CancellationToken); + if (fileList != null) + { + if (fileList.Length > 0) + { + foreach (var f in fileList) + { + if (Connection.Device != null) + { + var p = Path.GetFileName(f.Name); + + Logger?.LogInformation($"Deleting file '{p}' from device..."); + await Connection.Device.DeleteFile(p, CancellationToken); + } + else + { + Logger?.LogError($"No Device Found."); + } + } } else { - Logger?.LogError($"No Device Found."); + Logger?.LogInformation($"No files to delete."); } } } - } - else - { - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; - - if (!exists) - { - Logger?.LogError($"File '{MeadowFile}' not found on device."); - } else { - if (Connection.Device != null) - { - var wasRuntimeEnabled = await Connection.Device.IsRuntimeEnabled(CancellationToken); + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; - if (wasRuntimeEnabled) + if (!exists) + { + Logger?.LogError($"File '{MeadowFile}' not found on device."); + } + else + { + if (Connection.Device != null) { - Logger?.LogInformation("Disabling device runtime..."); - await Connection.Device.RuntimeDisable(); + Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await Connection.Device.DeleteFile(MeadowFile, CancellationToken); } - - Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); - await Connection.Device.DeleteFile(MeadowFile, CancellationToken); } } } + finally + { + if (wasRuntimeEnabled) + { + // restore runtime state + Logger?.LogInformation("Enabling runtime..."); + + await Connection.RuntimeEnable(CancellationToken); + } + } } } } \ No newline at end of file From b31e2c774d8ed54c4c33d4421522995b6d28e7b9 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 9 Nov 2023 18:47:43 +0000 Subject: [PATCH 026/141] Fix cloud package upload command. Add spinners for long running tasks. --- .../Commands/Current/BaseCloudCommand.cs | 22 ++++++++++++-- .../Package/CloudPackageUploadCommand.cs | 29 +++++++++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index da0140dd..f5c6e810 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -1,5 +1,7 @@ -using Meadow.Cloud; +using System.Configuration; +using Meadow.Cloud; using Meadow.Cloud.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -35,7 +37,21 @@ public BaseCloudCommand( { Logger?.LogInformation("Retrieving your user and organization information..."); + // Get our spinner ready + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(Console!); + Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); + var userOrgs = await UserService.GetUserOrgs(host, cancellationToken).ConfigureAwait(false); + + // Cancel the spinner as soon as GetUserOrgs finishes + spinnerCancellationTokenSource.Cancel(); + + // Let's start spinning + await consoleSpinnerTask; + + Logger?.LogInformation("Done."); + if (!userOrgs.Any()) { Logger?.LogInformation($"Please visit {host} to register your account."); @@ -46,7 +62,7 @@ public BaseCloudCommand( } else if (userOrgs.Count() == 1 && string.IsNullOrEmpty(orgNameOrId)) { - orgNameOrId = userOrgs.First().Id; + org = userOrgs.First(); } else { @@ -65,4 +81,4 @@ public BaseCloudCommand( return org; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 06c087b6..485124de 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -47,24 +47,35 @@ protected override async ValueTask ExecuteCommand() var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) + if (org == null || string.IsNullOrEmpty(org.Id)) + { + Logger?.LogError($"Invalid Org"); return; + } if (string.IsNullOrEmpty(Description)) { - Logger?.LogError($"Invalid Description"); - return; + Description = string.Empty; } try { Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); - if (!string.IsNullOrEmpty(org.Id)) - { - var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken); - Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); - } + // Get our spinner ready + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(Console!); + Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); + + var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken); + + // Cancel the spinner as soon as UploadPackage finishes + spinnerCancellationTokenSource.Cancel(); + + // Let's start spinning + await consoleSpinnerTask; + + Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); } catch (MeadowCloudException mex) { @@ -72,4 +83,4 @@ protected override async ValueTask ExecuteCommand() } } -} +} \ No newline at end of file From e29ead19634dec52c652fe3d7bf077aac3a8a3ed Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 9 Nov 2023 19:00:45 +0000 Subject: [PATCH 027/141] Massaged Chris' RuntimeVersion change so we check the device's runtime version to we match assemblies --- Source/v2/Meadow.Cli/AppManager.cs | 8 ++--- .../Commands/Current/App/AppDeployCommand.cs | 5 ++-- .../Commands/Current/App/AppRunCommand.cs | 1 - .../Commands/Current/App/AppTrimCommand.cs | 24 +++++++++++---- Source/v2/Meadow.Cli/IPackageManager.cs | 5 +++- .../PackageManager.AssemblyManager.cs | 29 ++++++++++++++----- 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 14069722..0ebffef8 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -36,12 +36,12 @@ public static async Task> GenerateDeployList(IPackageMa string localBinaryDirectory, bool includePdbs, bool includeXmlDocs, - ILogger logger, + ILogger? logger, CancellationToken cancellationToken) { // TODO: add sub-folder support when HCOM supports it - logger.LogInformation("Generating the list of files to deploy..."); + logger?.LogInformation($"Generating the list of files to deploy from {localBinaryDirectory}..."); var localFiles = new Dictionary(); @@ -115,10 +115,10 @@ public static async Task> GenerateDeployList(IPackageMa if (localFiles.Count() == 0) { - logger.LogInformation($"No new files to deploy"); + logger?.LogInformation($"No new files to deploy"); } - logger.LogInformation("Done."); + logger?.LogInformation("Done."); return localFiles; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 2eadb083..500fec2e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -77,9 +76,9 @@ protected override async ValueTask ExecuteCommand() if (Logger != null && !string.IsNullOrEmpty(targetDirectory)) { - var trimApplicationCommand = new AppTrimCommand(_packageManager, LoggerFactory!) + var trimApplicationCommand = new AppTrimCommand(_packageManager, ConnectionManager, LoggerFactory!) { - Path = path + Path = path, }; await trimApplicationCommand.ExecuteAsync(Console!); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 4e0cbe6d..9ebf65dd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Hcom; using Microsoft.Extensions.Logging; diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 31bb0a64..6474b2cd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -6,24 +6,23 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] -public class AppTrimCommand : BaseCommand +public class AppTrimCommand : BaseAppCommand { - private IPackageManager _packageManager; - [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] public string? Configuration { get; set; } [CommandParameter(0, Name = "Path to project file", IsRequired = false)] public string? Path { get; set; } = default!; - public AppTrimCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) - : base(loggerFactory) + public AppTrimCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(packageManager, connectionManager, loggerFactory) { - _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { + await base.ExecuteCommand(); + string path = Path == null ? Environment.CurrentDirectory : Path; @@ -56,6 +55,19 @@ protected override async ValueTask ExecuteCommand() file = new FileInfo(path); } + // Find RuntimeVersion + if (Connection != null) + { + var info = await Connection.GetDeviceInfo(CancellationToken); + + _packageManager.RuntimeVersion = info?.RuntimeVersion; + + Logger?.LogInformation($"Using runtime files from {_packageManager.MeadowAssembliesPath}"); + + // Avoid double reporting. + DetachMessageHandlers(Connection); + } + // Get our spinner ready var spinnerCancellationTokenSource = new CancellationTokenSource(); var consoleSpinner = new ConsoleSpinner(Console!); diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 57e3b474..05e37243 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -30,7 +30,10 @@ Task AssemblePackage( CancellationToken? cancellationToken = null); List? AssemblyDependencies { get; set; } - IEnumerable? TrimmedDependencies { get; set; } + IEnumerable? TrimmedDependencies { get; set; } bool Trimmed { get; set; } + + string? RuntimeVersion { get; set; } + string? MeadowAssembliesPath { get; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index a93721d6..fe959787 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -2,7 +2,6 @@ using Mono.Cecil; using Mono.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Reflection; namespace Meadow.CLI; @@ -19,7 +18,7 @@ public partial class PackageManager private string? _meadowAssembliesPath; - private string? MeadowAssembliesPath + public string? MeadowAssembliesPath { get { @@ -31,15 +30,27 @@ private string? MeadowAssembliesPath if (store != null) { store.Refresh(); - if (store.DefaultPackage != null) - { - var defaultPackage = store.DefaultPackage; - if (defaultPackage.BclFolder != null) + if (RuntimeVersion == null) + { + if (store.DefaultPackage != null) { - _meadowAssembliesPath = defaultPackage.GetFullyQualifiedPath(defaultPackage.BclFolder); + var defaultPackage = store.DefaultPackage; + + if (defaultPackage.BclFolder != null) + { + _meadowAssembliesPath = defaultPackage.GetFullyQualifiedPath(defaultPackage.BclFolder); + } } } + else + { + var existing = store.FirstOrDefault(p => p.Version == RuntimeVersion); + + if (existing == null || existing.BclFolder == null) return null; + + return Path.Combine(existing.GetFullyQualifiedPath(existing.BclFolder)); + } } } @@ -48,9 +59,12 @@ private string? MeadowAssembliesPath } public List? AssemblyDependencies { get; set; } + public IEnumerable? TrimmedDependencies { get; set; } public bool Trimmed { get; set; } = false; + public string? RuntimeVersion { get; set; } + public async Task?> TrimDependencies(FileInfo file, List dependencies, IList? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) { var directoryName = file.DirectoryName; @@ -221,7 +235,6 @@ public List GetDependencies(FileInfo file) return File.Exists(attempted_path) ? attempted_path : null; } - //ToDo - is it ever correct to fall back to the root path without a version? if (!string.IsNullOrEmpty(MeadowAssembliesPath)) { string? resolved_path = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); From d20f3d76fe25f38c666cc1b620518fde74f706d7 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 9 Nov 2023 19:36:09 +0000 Subject: [PATCH 028/141] If not Mpak path is passed in, in checked the current directory and sub-directories for the latest mpak to send. --- .../Package/CloudPackageCreateCommand.cs | 2 +- .../Package/CloudPackageUploadCommand.cs | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index 5b6e1245..7f27e138 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud package create", Description = "Create a Meadow Package (MPAK)")] +[Command("cloud package create", Description = "Builds, trims and creates a Meadow Package (MPAK)")] public class CloudPackageCreateCommand : BaseCloudCommand { [CommandParameter(0, Name = "Path to project file", IsRequired = false)] diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 485124de..a8c4dfab 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -1,6 +1,8 @@ -using CliFx.Attributes; +using System.Configuration; +using CliFx.Attributes; using Meadow.Cloud; using Meadow.Cloud.Identity; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,7 +11,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudPackageUploadCommand : BaseCloudCommand { [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] - public string? MpakPath { get; init; } + public string? MpakPath { get; set; } [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] public string? OrgId { get; set; } @@ -36,6 +38,29 @@ public CloudPackageUploadCommand( protected override async ValueTask ExecuteCommand() { + if (string.IsNullOrEmpty(MpakPath)) + { + var candidates = PackageManager.GetAvailableBuiltConfigurations(Environment.CurrentDirectory, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{Environment.CurrentDirectory}'"); + return; + } + + var appDll = candidates.OrderByDescending(c => c.LastWriteTime).First(); + var packageDir = Path.Combine(appDll.Directory?.FullName ?? string.Empty, PackageManager.PackageOutputDirectoryName); + var files = Directory.EnumerateFiles(packageDir, "*.*", SearchOption.TopDirectoryOnly); + + var fileInfoList = new List(); + foreach (var file in files) + { + fileInfoList.Add(new FileInfo(file)); + } + + MpakPath = fileInfoList.OrderByDescending(f => f.LastWriteTime).First().FullName; + } + if (!File.Exists(MpakPath)) { Logger?.LogError($"Package {MpakPath} does not exist"); From 62e0e041d3a12c3a0f1b75c3140c12752a92f76a Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 9 Nov 2023 23:05:16 +0000 Subject: [PATCH 029/141] Don't make mpak path mandatory --- .../Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index a8c4dfab..51710ef5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -10,7 +10,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package upload", Description = "Upload a Meadow Package (MPAK) to Meadow.Cloud")] public class CloudPackageUploadCommand : BaseCloudCommand { - [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] + [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = false)] public string? MpakPath { get; set; } [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] From 83dfcf91b2c99785945364da60db451519f6627c Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 10 Nov 2023 18:34:13 +0000 Subject: [PATCH 030/141] Added TaskExtensions methods for Console Spinner and refactored to simplify Tasks using the Spinner. --- .../Commands/Helper/TaskExtensions.cs | 50 +++++++++++++++++++ .../Commands/Current/App/AppBuildCommand.cs | 14 +----- .../Commands/Current/App/AppDeployCommand.cs | 14 +----- .../Commands/Current/App/AppTrimCommand.cs | 14 +----- .../Commands/Current/BaseCloudCommand.cs | 16 +----- .../Package/CloudPackageUploadCommand.cs | 14 +----- Source/v2/Meadow.Cli/PackageManager.cs | 12 ++--- 7 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs diff --git a/Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs b/Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs new file mode 100644 index 00000000..36724afd --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs @@ -0,0 +1,50 @@ +using CliFx.Infrastructure; + +namespace Meadow.CLI +{ + public static class TaskExtensions + { + public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) + { + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(console); + + var consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); + + try + { + await task; + } + finally + { + // Cancel the spinner when the original task completes + spinnerCancellationTokenSource.Cancel(); + + // Let's wait for the spinner to finish + await consoleSpinnerTask; + } + } + + public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) + { + // Get our spinner read + var spinnerCancellationTokenSource = new CancellationTokenSource(); + var consoleSpinner = new ConsoleSpinner(console); + + Task consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); + + try + { + return await task; + } + finally + { + // Cancel the spinner when the original task completes + spinnerCancellationTokenSource.Cancel(); + + // Let's wait for the spinner to finish + await consoleSpinnerTask; + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index da9d7633..b3cc8cce 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -45,19 +45,9 @@ await Task.Run(async () => Logger?.LogInformation($"Building {Configuration} configuration of {path} (this may take a few seconds)..."); - // Get our spinner ready - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(Console!); - Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - // TODO: enable cancellation of this call - var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)); - - // Cancel the spinner as soon as BuildApplication finishes - spinnerCancellationTokenSource.Cancel(); - - // Let's start spinning - await consoleSpinnerTask; + var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)) + .WithSpinner(Console!, 250); if (!success) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 500fec2e..438f97f7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -82,20 +82,10 @@ protected override async ValueTask ExecuteCommand() }; await trimApplicationCommand.ExecuteAsync(Console!); - // Get our spinner ready - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(Console!); - Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - - var localFiles = await AppManager.GenerateDeployList(_packageManager, Connection, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken); + var localFiles = await AppManager.GenerateDeployList(_packageManager, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken) + .WithSpinner(Console!, 250); Console?.Output.WriteAsync("\n"); - // Cancel the spinner as soon as GenerateDeployList finishes - spinnerCancellationTokenSource.Cancel(); - - // Let's start spinning - await consoleSpinnerTask; - Connection.FileWriteProgress += Connection_FileWriteProgress; await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 6474b2cd..917e24ca 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -68,18 +68,8 @@ protected override async ValueTask ExecuteCommand() DetachMessageHandlers(Connection); } - // Get our spinner ready - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(Console!); - Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - // TODO: support `nolink` command line args - await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken); - - // Cancel the spinner as soon as TrimApplication finishes - spinnerCancellationTokenSource.Cancel(); - - // Let's start spinning - await consoleSpinnerTask; + await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken) + .WithSpinner(Console!, 250); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index f5c6e810..3482fef1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -37,20 +37,8 @@ public BaseCloudCommand( { Logger?.LogInformation("Retrieving your user and organization information..."); - // Get our spinner ready - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(Console!); - Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - - var userOrgs = await UserService.GetUserOrgs(host, cancellationToken).ConfigureAwait(false); - - // Cancel the spinner as soon as GetUserOrgs finishes - spinnerCancellationTokenSource.Cancel(); - - // Let's start spinning - await consoleSpinnerTask; - - Logger?.LogInformation("Done."); + var userOrgs = await UserService.GetUserOrgs(host, cancellationToken) + .WithSpinner(Console!, 250); if (!userOrgs.Any()) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 51710ef5..13948a45 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -87,18 +87,8 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); - // Get our spinner ready - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(Console!); - Task consoleSpinnerTask = consoleSpinner.Turn(250, spinnerCancellationTokenSource.Token); - - var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken); - - // Cancel the spinner as soon as UploadPackage finishes - spinnerCancellationTokenSource.Cancel(); - - // Let's start spinning - await consoleSpinnerTask; + var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken) + .WithSpinner(Console!, 250); Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); } diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 5923edea..5423871f 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -163,12 +163,12 @@ public async Task TrimApplication( try { TrimmedDependencies = await TrimDependencies( - applicationFilePath, - AssemblyDependencies, - noLink, - logger, - includePdbs, - verbose: false); + applicationFilePath, + AssemblyDependencies, + noLink, + logger, + includePdbs, + verbose: false); } catch (Exception) { From 7b4616709cc60355a6b5e956838cc1d93398d224 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 10 Nov 2023 18:41:12 +0000 Subject: [PATCH 031/141] Slight refactor to make things a tad dev friendly. Re-implmented Skipped file, just for debugging. --- Source/v2/Meadow.Cli/AppManager.cs | 28 ++++++---- .../PackageManager.AssemblyManager.cs | 52 +++++++++++-------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 0ebffef8..d5168f49 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -32,7 +32,6 @@ private static bool IsXmlDoc(string file) } public static async Task> GenerateDeployList(IPackageManager packageManager, - IMeadowConnection connection, string localBinaryDirectory, bool includePdbs, bool includeXmlDocs, @@ -66,7 +65,7 @@ public static async Task> GenerateDeployList(IPackageMa .Where(x => pdbLinkIngoreList.Any(f => x.Contains(f)) == false) .ToList(); - //crawl trimmed dependencies + // Crawl trimmed dependencies foreach (var file in trimmedDependencies) { await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); @@ -153,20 +152,29 @@ public static async Task DeployApplication( // now send all files with differing CRCs foreach (var localFile in localFiles) { - // does the file name and CRC match? - var filename = Path.GetFileName(localFile.Key); - if (!File.Exists(localFile.Key)) { - logger.LogInformation($"{filename} not found" + Environment.NewLine); + logger.LogInformation($"{localFile.Key} not found" + Environment.NewLine); continue; } - if (deviceFiles.Any(d => Path.GetFileName(d.Name) == filename && !string.IsNullOrEmpty(d.Crc) && uint.Parse(d.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value)) + var filename = Path.GetFileName(localFile.Key); + + var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == filename); + + if (existing != null && existing.Crc != null) { - logger.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); - continue; - } + var remoteCrc = uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber); + var localCrc = localFile.Value; + + // do the file name and CRC match? + if (remoteCrc == localCrc) + { + // exists and has a matching CRC, skip it + logger.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); + continue; + } + } bool success; diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index fe959787..382eb216 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -49,7 +49,7 @@ public string? MeadowAssembliesPath if (existing == null || existing.BclFolder == null) return null; - return Path.Combine(existing.GetFullyQualifiedPath(existing.BclFolder)); + _meadowAssembliesPath = existing.GetFullyQualifiedPath(existing.BclFolder); } } } @@ -210,7 +210,7 @@ public List GetDependencies(FileInfo file) var directoryName = file.DirectoryName; if (!string.IsNullOrEmpty(directoryName)) { - var refs = GetAssemblyNameReferences(file.Name, directoryName); + var refs = GetAssemblyReferences(file.Name, directoryName); var dependencies = GetDependencies(refs, dependencyMap, directoryName); @@ -222,35 +222,45 @@ public List GetDependencies(FileInfo file) } } - private (Collection?, string?) GetAssemblyNameReferences(string fileName, string path) + private (Collection? References, string? ResolvedPath) GetAssemblyReferences(string fileName, string path) { static string? ResolvePath(string fileName, string path) { - string attempted_path = Path.Combine(path, fileName); - if (Path.GetExtension(fileName) != ".exe" && - Path.GetExtension(fileName) != ".dll") + string attemptedPath = Path.Combine(path, fileName); + if (Path.GetExtension(fileName) != ".exe" + && Path.GetExtension(fileName) != ".dll") { - attempted_path += ".dll"; + attemptedPath += ".dll"; } - return File.Exists(attempted_path) ? attempted_path : null; + return File.Exists(attemptedPath) ? attemptedPath : null; } if (!string.IsNullOrEmpty(MeadowAssembliesPath)) { - string? resolved_path = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); + string? resolvedPath = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); - if (resolved_path is null) + if (resolvedPath is null) { return (null, null); } Collection references; - using (var definition = Mono.Cecil.AssemblyDefinition.ReadAssembly(resolved_path)) + try { - references = definition.MainModule.AssemblyReferences; + using (var definition = Mono.Cecil.AssemblyDefinition.ReadAssembly(resolvedPath)) + { + references = definition.MainModule.AssemblyReferences; + } + } + catch (Exception ex) + { + // Handle or log the exception appropriately + Console.WriteLine($"Error reading assembly: {ex.Message}"); + return (null, null); } - return (references, resolved_path); + + return (references, resolvedPath); } else { @@ -258,20 +268,20 @@ public List GetDependencies(FileInfo file) } } - private List GetDependencies((Collection?, string?) references, List dependencyMap, string folderPath) + private List GetDependencies((Collection? References, string? ResolvedPath) references, List dependencyMap, string folderPath) { - if (references.Item2 == null || dependencyMap.Contains(references.Item2)) + if (references.ResolvedPath == null || dependencyMap.Contains(references.ResolvedPath)) return dependencyMap; - dependencyMap.Add(references.Item2); + dependencyMap.Add(references.ResolvedPath); - if (references.Item1 != null) + if (references.References != null) { - foreach (var ar in references.Item1) + foreach (var ar in references.References) { - var namedRefs = GetAssemblyNameReferences(ar.Name, folderPath); + var namedRefs = GetAssemblyReferences(ar.Name, folderPath); - if (namedRefs.Item1 == null) + if (namedRefs.References == null) continue; GetDependencies(namedRefs, dependencyMap, folderPath); @@ -280,4 +290,4 @@ private List GetDependencies((Collection?, string return dependencyMap; } -} +} \ No newline at end of file From 1eb6f9650fe8bc43b6766faa58ee658ab824ec07 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 10 Nov 2023 18:47:24 +0000 Subject: [PATCH 032/141] Reduce the amount of noise/duplication when deleting files. --- .../Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 56f7a1f0..d8d207c2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -1,4 +1,6 @@ -using CliFx.Attributes; +using System; +using System.Diagnostics.Metrics; +using CliFx.Attributes; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -57,7 +59,7 @@ protected override async ValueTask ExecuteCommand() { var p = Path.GetFileName(f.Name); - Logger?.LogInformation($"Deleting file '{p}' from device..."); + Console?.Output.WriteAsync($"Deleting file '{p}' from device... \r"); await Connection.Device.DeleteFile(p, CancellationToken); } else @@ -84,7 +86,7 @@ protected override async ValueTask ExecuteCommand() { if (Connection.Device != null) { - Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + Console?.Output.WriteAsync($"Deleting file '{MeadowFile}' from device... \r"); await Connection.Device.DeleteFile(MeadowFile, CancellationToken); } } From a791e3b91ee055a7a6ac543e4b6204f60170cb12 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Sat, 11 Nov 2023 11:03:51 +0000 Subject: [PATCH 033/141] Make our Console?.Output.Write Async calls. --- Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index 1d56cbd8..b9995901 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -43,7 +43,7 @@ protected override async ValueTask ExecuteCommand() var p = (e.completed / (double)e.total) * 100d; // Console instead of Logger due to line breaking for progress bar - Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); }; Logger?.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 35c15bcc..92c3a667 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -300,7 +300,7 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; - Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); }; if (Files.Contains(FirmwareType.OS)) diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index 297937a1..bd8cb1f5 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -259,7 +259,7 @@ private async ValueTask WriteFiles() Connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; - Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); }; From 9ee3210f32756cab3798cfc71982e3f8e1f69de9 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 14 Nov 2023 10:36:32 +0000 Subject: [PATCH 034/141] BumpYamlDotNet to latest nuget. --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 0d884816..0245f448 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -44,7 +44,7 @@ - + From 81d32694ffa0cefba6b9ad0ca79bd4976d020b5c Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 14 Nov 2023 10:38:57 +0000 Subject: [PATCH 035/141] Use logger rather than Console.WriteLine where possible. --- Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index 382eb216..f5f7c8e8 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -167,7 +167,7 @@ public string? MeadowAssembliesPath stdOutReaderResult = await stdOutReader.ReadToEndAsync(); if (verbose) { - Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + logger?.LogInformation("StandardOutput Contains: " + stdOutReaderResult); } } @@ -178,7 +178,7 @@ public string? MeadowAssembliesPath stdErrorReaderResult = await stdErrorReader.ReadToEndAsync(); if (!string.IsNullOrEmpty(stdErrorReaderResult)) { - Console.WriteLine("StandardError Contains: " + stdErrorReaderResult); + logger?.LogInformation("StandardError Contains: " + stdErrorReaderResult); } } @@ -248,10 +248,11 @@ public List GetDependencies(FileInfo file) try { - using (var definition = Mono.Cecil.AssemblyDefinition.ReadAssembly(resolvedPath)) + using (var definition = AssemblyDefinition.ReadAssembly(resolvedPath)) { references = definition.MainModule.AssemblyReferences; } + return (references, resolvedPath); } catch (Exception ex) { @@ -259,8 +260,6 @@ public List GetDependencies(FileInfo file) Console.WriteLine($"Error reading assembly: {ex.Message}"); return (null, null); } - - return (references, resolvedPath); } else { From b9101592dabe0e745a39eb8615ccae347cfcf15d Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 14 Nov 2023 10:40:13 +0000 Subject: [PATCH 036/141] Rename TaskExtensions to UITaskExtensions for clarity. --- .../Commands/Helper/{TaskExtensions.cs => UITaskExtensions.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Source/v2/Meadow.CLI/Commands/Helper/{TaskExtensions.cs => UITaskExtensions.cs} (100%) diff --git a/Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs b/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs similarity index 100% rename from Source/v2/Meadow.CLI/Commands/Helper/TaskExtensions.cs rename to Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs From 211b420e500897001e57ee12be1a2045ca5c5baf Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Tue, 14 Nov 2023 13:09:42 +0000 Subject: [PATCH 037/141] push rename changes. --- Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs b/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs index 36724afd..68fbca39 100644 --- a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs +++ b/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs @@ -2,7 +2,7 @@ namespace Meadow.CLI { - public static class TaskExtensions + public static class UITaskExtensions { public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) { From 4300eb3f5b9f6dc13bf7733c5ad16f7393cebe6e Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 14:12:12 +0000 Subject: [PATCH 038/141] Allow a file path to be recognised by the esp command too. --- .../Current/Firmware/FirmwareWriteCommand.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 92c3a667..99df3b25 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -32,6 +32,9 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandOption("file", 'f', IsRequired = false, Description = "Path to OS, Runtime or ESP file")] public string? Path { get; set; } = default!; + [CommandOption("address", 'a', IsRequired = false, Description = "Address location to write the file to")] + public int? Address { get; set; } = default!; + private FileManager FileManager { get; } private ISettingsManager Settings { get; } @@ -322,17 +325,16 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c string? runtime; if (!string.IsNullOrEmpty(Path)) { - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {Path}..."); runtime = Path; } else { - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); - - // get the path to the runtime file + // get the path to the runtime bin file runtime = package.Runtime; } + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {runtime}..."); + if (string.IsNullOrEmpty(runtime)) runtime = string.Empty; var rtpath = package.GetFullyQualifiedPath(runtime); @@ -352,16 +354,28 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c if (Files.Contains(FirmwareType.ESP)) { - Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + string? coProcessorFilePath; + if (!string.IsNullOrEmpty(Path)) + { + // use passed in path + coProcessorFilePath = Path; + } + else + { + // get the default path to the coprocessor bin file + coProcessorFilePath = package.CoprocApplication; + } + + Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor file {coProcessorFilePath}..."); string[]? fileList; - if (package.CoprocApplication != null + if (coProcessorFilePath != null && package.CoprocBootloader != null && package.CoprocPartitionTable != null) { fileList = new string[] { - package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(coProcessorFilePath), package.GetFullyQualifiedPath(package.CoprocBootloader), package.GetFullyQualifiedPath(package.CoprocPartitionTable), }; From 6276d5d91e4ed84097324794bcdf970b48fa9643 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 14:13:09 +0000 Subject: [PATCH 039/141] Make our port read async. --- .../v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index b2be8d3e..44e50c1d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -55,7 +55,7 @@ private async Task ListenerProc() read: try { - receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); + receivedLength = await _port.BaseStream.ReadAsync(readBuffer, 0, readBuffer.Length); } catch (OperationCanceledException) { From 9f0f931b70357ed4b448fe90bcacfd401cb21aa4 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 15:35:10 +0000 Subject: [PATCH 040/141] Fix overwrite issue. --- .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 99df3b25..7c906649 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -303,7 +303,14 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + if (p == 100.0) + { + Console?.Output.WriteAsync($"{Environment.NewLine}"); + } + else + { + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + } }; if (Files.Contains(FirmwareType.OS)) From 13fca60de066a354d1d7263ebabd9f2c7ed1384a Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 16:08:19 +0000 Subject: [PATCH 041/141] Fix deprecated text. --- Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs index b1804fcf..dd27dbc4 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs @@ -11,6 +11,6 @@ public class DownloadOsCommand : FirmwareDownloadCommand public DownloadOsCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime disable` instead"); + Logger?.LogWarning($"Deprecated command. Use `firmware download` instead"); } -} +} \ No newline at end of file From ccf3e221bf9a73a460caf2427dc7820ea61868a0 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 16:08:57 +0000 Subject: [PATCH 042/141] Refresh the local firmare list, incase this is a new download. --- .../v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 8dc984ec..9b7ac9c8 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -92,6 +92,9 @@ public Task DeletePackage(string version) public Task SetDefaultPackage(string version) { + // Refresh the list, in case we've just downloaded it. + Refresh(); + var existing = _f7Packages.FirstOrDefault(p => p.Version == version); if (existing == null) From bd7a2e1891b4e9c724ba0a1aa07118b21116a778 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 16 Nov 2023 18:05:17 +0000 Subject: [PATCH 043/141] Only flash the MeadowComms.bin file if path provided. --- .../Current/Firmware/FirmwareWriteCommand.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 7c906649..8f76945f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -375,21 +375,27 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor file {coProcessorFilePath}..."); - string[]? fileList; + string[]? fileList = Array.Empty(); if (coProcessorFilePath != null && package.CoprocBootloader != null && package.CoprocPartitionTable != null) { - fileList = new string[] + if (!string.IsNullOrEmpty(Path)) + { + fileList = new string[] { - package.GetFullyQualifiedPath(coProcessorFilePath), - package.GetFullyQualifiedPath(package.CoprocBootloader), - package.GetFullyQualifiedPath(package.CoprocPartitionTable), + package.GetFullyQualifiedPath(coProcessorFilePath), }; - } - else - { - fileList = Array.Empty(); + } + else + { + fileList = new string[] + { + package.GetFullyQualifiedPath(coProcessorFilePath), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; + } } await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); From c4f34c376325c86e39aee2f215937aa8b548fae7 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 17 Nov 2023 10:22:13 +0000 Subject: [PATCH 044/141] Setting Trimmed to false for now, so customers can at least deploy. --- Source/v2/Meadow.Cli/PackageManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 5423871f..4caaef23 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -176,7 +176,7 @@ public async Task TrimApplication( Trimmed = false; } - Trimmed = true; + Trimmed = false; } public const string PackageMetadataFileName = "info.json"; From 30a85cf99fb848acd21297790e39d1f7ffbf8d1a Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 17 Nov 2023 18:40:52 +0000 Subject: [PATCH 045/141] Bump to alpha 3, so we can get the Trimmed = false change out. --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 0245f448..275726c4 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Chris Tacke, Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 2.0.0-alpha.2 + 2.0.0-alpha.3 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From 9f1e490ae3a33229a9a590d9d4f92e7980f5fa11 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 22 Nov 2023 20:11:14 -0600 Subject: [PATCH 046/141] improve dependency filter for App.dll --- Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs b/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs index b548789e..91286ce7 100644 --- a/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs +++ b/Meadow.CLI.Core/Devices/MeadowLocalDevice.FileManager.cs @@ -651,9 +651,8 @@ async Task AddFile(string file, bool includePdbs) } var dependencies = AssemblyManager.GetDependencies(fileName, directoryName, osVersion) - .Where(x => x.Contains("App.") == false) - // .Where(x => dllLinkIngoreList.Any(f => x.Contains(f)) == false) - .ToList(); + .Except(new string[] { "App.dll", "App.exe" }) + .ToList(); var trimmedDependencies = await AssemblyManager.TrimDependencies(fileName, directoryName, dependencies, noLink, Logger, includePdbs: includePdbs, verbose: verbose); From 5af728253d51db069086fdc44cf45bf6f42b8f84 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 17 Nov 2023 16:04:06 +0000 Subject: [PATCH 047/141] Do a directory existing check, before we check for internet. --- Meadow.CLI.Core/Managers/DownloadManager.cs | 45 ++++++++------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/Meadow.CLI.Core/Managers/DownloadManager.cs b/Meadow.CLI.Core/Managers/DownloadManager.cs index 905e895f..01f609a5 100644 --- a/Meadow.CLI.Core/Managers/DownloadManager.cs +++ b/Meadow.CLI.Core/Managers/DownloadManager.cs @@ -118,33 +118,25 @@ bool CreateFolder(string path, bool eraseIfExists) return true; } - static bool IsPassingThroughDefaultGateway() + //ToDo rename this method - DownloadOSAsync? + public async Task DownloadOsBinaries(string? version = null, bool force = false, CancellationToken cancellationToken = default) { - NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); - - foreach (NetworkInterface networkInterface in networkInterfaces) + string local_path; + if (!string.IsNullOrEmpty(version)) { - if (networkInterface.OperationalStatus == OperationalStatus.Up) - { - GatewayIPAddressInformationCollection gateways = networkInterface.GetIPProperties().GatewayAddresses; + local_path = Path.Combine(FirmwareDownloadsFilePathRoot, version); - if (gateways.Count > 0) - { - // If any network interface has a gateway, you are likely passing through a gateway. - return true; - } + if (!force + && Directory.Exists(local_path) + && Directory.EnumerateFiles(local_path).Any()) + { + _logger.LogInformation($"Meadow OS version {version} was previously downloaded to: {local_path}.{Environment.NewLine}"); + return; } } - return false; - } - - - //ToDo rename this method - DownloadOSAsync? - public async Task DownloadOsBinaries(string? version = null, bool force = false, CancellationToken cancellationToken = default) - { // Check if there is an active internet connection - if (!NetworkInterface.GetIsNetworkAvailable() && IsPassingThroughDefaultGateway()) + if (!NetworkInterface.GetIsNetworkAvailable()) { _logger.LogError($"No internet connection! Cannot download Meadow OS version {version}. Please retry once an internet connection is available.{Environment.NewLine}"); return; @@ -171,19 +163,14 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false, //we'll write latest.txt regardless of version if it doesn't exist File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); - string local_path; - if (string.IsNullOrWhiteSpace(version)) { - local_path = Path.Combine(FirmwareDownloadsFilePathRoot, release.Version); version = release.Version; } - else - { - local_path = Path.Combine(FirmwareDownloadsFilePathRoot, version); - } - if (CreateFolder(local_path, force) == false) + local_path = Path.Combine(FirmwareDownloadsFilePathRoot, version); + + if (!CreateFolder(local_path, force) && Directory.EnumerateFiles(local_path).Any()) { _logger.LogInformation($"Meadow OS version {version} was previously downloaded to: {local_path}.{Environment.NewLine}"); return; @@ -412,4 +399,4 @@ private void CleanPath(string path) } } } -} +} \ No newline at end of file From e1b2f86acfbb78e926d142f31119c7ac9e91312d Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Sat, 18 Nov 2023 13:15:52 +0000 Subject: [PATCH 048/141] Bump version to 1.4.4 for public release. --- .github/workflows/dotnet.yml | 4 ++-- Meadow.CLI.Core/Constants.cs | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.csproj | 2 +- Meadow.CLI/Meadow.CLI.Classic.csproj | 2 +- Meadow.CLI/Meadow.CLI.csproj | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index aff298d9..0d07edb0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,8 +1,8 @@ name: Meadow.CLI env: - CLI_RELEASE_VERSION: 1.4.2.0 + CLI_RELEASE_VERSION: 1.4.4.0 IDE_TOOLS_RELEASE_VERSION: 1.4.0 - MEADOW_OS_VERSION: 1.4.0.3 + MEADOW_OS_VERSION: 1.5.0.4 VS_MAC_2019_VERSION: 8.10 VS_MAC_2022_VERSION: 17.5 diff --git a/Meadow.CLI.Core/Constants.cs b/Meadow.CLI.Core/Constants.cs index 7b9a747f..147b268b 100644 --- a/Meadow.CLI.Core/Constants.cs +++ b/Meadow.CLI.Core/Constants.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Core { public static class Constants { - public const string CLI_VERSION = "1.4.2.0"; + public const string CLI_VERSION = "1.4.4.0"; public const ushort HCOM_PROTOCOL_PREVIOUS_VERSION_NUMBER = 0x0006; public const ushort HCOM_PROTOCOL_CURRENT_VERSION_NUMBER = 0x0007; // Used for transmission public const string WILDERNESS_LABS_USB_VID = "2E6A"; diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj index 92915e2a..fbd93031 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.2.0 + 1.4.4.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj index 374db005..41a0ed98 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.0.0 + 1.4.4.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj index 876bd2b1..55ea0e3b 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.2.0 + 1.4.4.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.csproj index 21f66867..68e67a66 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.2.0 + 1.4.4.0 diff --git a/Meadow.CLI/Meadow.CLI.Classic.csproj b/Meadow.CLI/Meadow.CLI.Classic.csproj index 49f336de..5ad08c6d 100644 --- a/Meadow.CLI/Meadow.CLI.Classic.csproj +++ b/Meadow.CLI/Meadow.CLI.Classic.csproj @@ -10,7 +10,7 @@ Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 1.4.2.0 + 1.4.4.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png diff --git a/Meadow.CLI/Meadow.CLI.csproj b/Meadow.CLI/Meadow.CLI.csproj index 3d56a364..481f04f2 100644 --- a/Meadow.CLI/Meadow.CLI.csproj +++ b/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 1.4.0.0 + 1.4.4.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From b7205e333eeda903948a9a66688b7180d315f289 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 30 Nov 2023 14:56:58 +0000 Subject: [PATCH 049/141] bump to v1.5.0.x --- .github/workflows/dotnet.yml | 8 ++++---- Meadow.CLI.Core/Constants.cs | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj | 2 +- Meadow.CLI.Core/Meadow.CLI.Core.csproj | 2 +- Meadow.CLI/Meadow.CLI.Classic.csproj | 2 +- Meadow.CLI/Meadow.CLI.csproj | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 0d07edb0..93688725 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,10 +1,10 @@ name: Meadow.CLI env: - CLI_RELEASE_VERSION: 1.4.4.0 - IDE_TOOLS_RELEASE_VERSION: 1.4.0 - MEADOW_OS_VERSION: 1.5.0.4 + CLI_RELEASE_VERSION: 1.5.0.0 + IDE_TOOLS_RELEASE_VERSION: 1.5.0 + MEADOW_OS_VERSION: 1.5.0.0 VS_MAC_2019_VERSION: 8.10 - VS_MAC_2022_VERSION: 17.5 + VS_MAC_2022_VERSION: 17.6 on: push: diff --git a/Meadow.CLI.Core/Constants.cs b/Meadow.CLI.Core/Constants.cs index 147b268b..de655f1d 100644 --- a/Meadow.CLI.Core/Constants.cs +++ b/Meadow.CLI.Core/Constants.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Core { public static class Constants { - public const string CLI_VERSION = "1.4.4.0"; + public const string CLI_VERSION = "1.5.0.0"; public const ushort HCOM_PROTOCOL_PREVIOUS_VERSION_NUMBER = 0x0006; public const ushort HCOM_PROTOCOL_CURRENT_VERSION_NUMBER = 0x0007; // Used for transmission public const string WILDERNESS_LABS_USB_VID = "2E6A"; diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj index fbd93031..0f7f45dd 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.6.0.0.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.4.0 + 1.5.0.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj index 41a0ed98..144ac96f 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.Classic.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.4.0 + 1.5.0.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj index 55ea0e3b..f796bb81 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.4.0 + 1.5.0.0 diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.csproj index 68e67a66..5ae6c32e 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -11,7 +11,7 @@ preview enable True - 1.4.4.0 + 1.5.0.0 diff --git a/Meadow.CLI/Meadow.CLI.Classic.csproj b/Meadow.CLI/Meadow.CLI.Classic.csproj index 5ad08c6d..5815d5e1 100644 --- a/Meadow.CLI/Meadow.CLI.Classic.csproj +++ b/Meadow.CLI/Meadow.CLI.Classic.csproj @@ -10,7 +10,7 @@ Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 1.4.4.0 + 1.5.0.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png diff --git a/Meadow.CLI/Meadow.CLI.csproj b/Meadow.CLI/Meadow.CLI.csproj index 481f04f2..8e33bd81 100644 --- a/Meadow.CLI/Meadow.CLI.csproj +++ b/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 1.4.4.0 + 1.5.0.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From af2c57e2f9a27faeec59d39c8644647d1acb3b38 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 30 Nov 2023 18:39:24 +0000 Subject: [PATCH 050/141] Comment out v2 auto-publishing. We'll do it manually for now. --- .github/workflows/dotnet.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 93688725..300b7506 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -124,10 +124,10 @@ jobs: name: Meadow.CLI.V2.nuget.2.0.0 path: 'main\Source\v2\Meadow.CLI\bin\Release\*.nupkg' - - if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} - name: Publish Meadow.CLI v2.0 Nuget publically - run: | - nuget push main\Source\v2\Meadow.CLI\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} + #- if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} + # name: Publish Meadow.CLI v2.0 Nuget publically + # run: | + # nuget push main\Source\v2\Meadow.CLI\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} build-vswin-2019: runs-on: windows-2019 From b73757081781aa85bbdc3ac806b29404b3914f4e Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 1 Dec 2023 18:53:05 -0600 Subject: [PATCH 051/141] cleaning up local provisioning --- .../Current/Device/DeviceProvisionCommand.cs | 28 +- .../v2/Meadow.Cli/MeadowConnectionManager.cs | 7 +- .../Connections/SerialConnection.cs | 2546 ++++++++--------- 3 files changed, 1301 insertions(+), 1280 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 1e36a798..1dcf1810 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -82,6 +82,12 @@ protected override async ValueTask ExecuteCommand() var info = await connection.Device!.GetDeviceInfo(CancellationToken); + if (info == null) + { + Logger?.LogError($"Unable to get device info"); + return; + } + Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); @@ -103,7 +109,27 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); - var provisioningID = !string.IsNullOrWhiteSpace(info?.ProcessorId) ? info.ProcessorId : info?.SerialNumber; + string provisioningID; + + // prefer processorID (since the F7 works that way) + if (!string.IsNullOrEmpty(info.ProcessorId)) + { + provisioningID = info.ProcessorId; + } + else + { + if (info.SerialNumber == null) + { + Logger?.LogError($"Unable to get device serial number or processor ID"); + return; + } + + provisioningID = info.SerialNumber; + } + + // upper-case to prevent issues where clients and provisioning differ + provisioningID = provisioningID.ToUpper(); + var provisioningName = !string.IsNullOrWhiteSpace(Name) ? Name : info?.DeviceName; var result = await _deviceService.AddDevice(org.Id!, provisioningID!, publicKey, CollectionId, provisioningName, Host, CancellationToken); diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 80a33fba..d97a236c 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,9 +1,6 @@ -using Meadow.CLI; -using Meadow.Hcom; -using Meadow.LibUsb; +using Meadow.Hcom; using System.Diagnostics; using System.IO.Ports; -using System.Linq; using System.Management; using System.Net; using System.Runtime.InteropServices; @@ -41,7 +38,7 @@ public MeadowConnectionManager(ISettingsManager settingsManager) { _currentConnection = new LocalConnection(); } - if (route == "simulator") + else if (route == "simulator") { _currentConnection = new SimulatorConnection(); } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index f40a6317..117e0afd 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1,1275 +1,1273 @@ -using Meadow.Hardware; -using Microsoft.Extensions.Logging; -using System.Buffers; -using System.Diagnostics; -using System.IO.Ports; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace Meadow.Hcom; - -public partial class SerialConnection : ConnectionBase, IDisposable -{ - public const int DefaultBaudRate = 115200; - public const int ReadBufferSizeBytes = 0x2000; - private const int DefaultTimeout = 5000; - - private event EventHandler? FileReadCompleted = delegate { }; - private event EventHandler? FileWriteAccepted; - private event EventHandler? FileDataReceived; - - private SerialPort _port = default!; - private ILogger? _logger; - private bool _isDisposed; - private List _listeners = new List(); - private Queue _pendingCommands = new Queue(); - private bool _maintainConnection; - private Thread? _connectionManager = null; - private List _textList = new List(); - private int _messageCount = 0; - private ReadFileInfo? _readFileInfo = null; - private string? _lastError = null; - - public override string Name { get; } - - public SerialConnection(string port, ILogger? logger = default) - { - if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) - { - throw new ArgumentException($"Serial Port '{port}' not found."); - } - - Name = port; - State = ConnectionState.Disconnected; - _logger = logger; - - CreatePort(); - - new Task( - () => _ = ListenerProc(), - TaskCreationOptions.LongRunning) - .Start(); - - new Thread(CommandManager) - { - IsBackground = true, - Name = "HCOM Sender" - } - .Start(); - } - - private void CreatePort() - { - _port = new SerialPort(Name); - _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; - _port.Open(); - } - - private bool MaintainConnection - { - get => _maintainConnection; - set - { - if (value == MaintainConnection) return; - - _maintainConnection = value; - - if (value) - { - if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) - { - _connectionManager = new Thread(ConnectionManagerProc) - { - IsBackground = true, - Name = "HCOM Connection Manager" - }; - _connectionManager.Start(); - - } - } - } - } - - private void ConnectionManagerProc() - { - while (_maintainConnection) - { - Open(true); - } - } - - public void AddListener(IConnectionListener listener) - { - lock (_listeners) - { - _listeners.Add(listener); - } - - Open(); - - MaintainConnection = true; - } - - public void RemoveListener(IConnectionListener listener) - { - lock (_listeners) - { - _listeners.Remove(listener); - } - - // TODO: stop maintaining connection? - } - - private void Open(bool inLoop = false) - { - if (!_port.IsOpen) - { - try - { - Debug.WriteLine("Opening COM port..."); - _port.Open(); - } - catch (UnauthorizedAccessException ex) - { - // Handle unauthorized access (e.g., port in use by another application) - throw new Exception($"Serial port '{_port.PortName}' is in use by another application.", ex.InnerException); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - catch (TimeoutException ex) - { - // Handle timeout - throw new Exception($"Timeout occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - } - else if (inLoop) - { - Thread.Sleep(1000); - } - - State = ConnectionState.Connected; - - Debug.WriteLine("Opened COM port"); - } - - private void Close() - { - if (_port.IsOpen) - { - try - { - _port.Close(); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when attempting to close the serial port '{_port.PortName}'.", ex.InnerException); - } - } - - State = ConnectionState.Disconnected; - } - - public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) - { - try - { - // ensure the port is open - Open(); - - // search for the device via HCOM - we'll use a simple command since we don't have a "ping" - var command = RequestBuilder.Build(); - - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang - - _port.DiscardInBuffer(); - - // wait for a response - var timeout = timeoutSeconds * 2; - var dataReceived = false; - - // local function so we can unsubscribe - var count = _messageCount; - - _pendingCommands.Enqueue(command); - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); - - if (count != _messageCount) - { - dataReceived = true; - break; - } - - await Task.Delay(500); - } - - // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no - - // create the device instance - if (dataReceived) - { - Device = new MeadowDevice(this); - } - - return Device; - } - catch (Exception e) - { - _logger?.LogError(e, "Failed to connect"); - throw; - } - } - - private async void CommandManager() - { - await Task.Run(async () => - { - while (!_isDisposed) - { - while (_pendingCommands.Count > 0) - { - Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); - - var command = _pendingCommands.Dequeue() as Request; - - // if this is a file write, we need to packetize for progress - - if (command != null) - { - var payload = command.Serialize(); - await EncodeAndSendPacket(payload); - } - - // TODO: re-queue on fail? - } - - Thread.Sleep(1000); - } - }); - } - - private class ReadFileInfo - { - private string? _localFileName; - - public string MeadowFileName { get; set; } = default!; - public string? LocalFileName - { - get - { - if (_localFileName != null) return _localFileName; - - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); - } - set => _localFileName = value; - } - public FileStream FileStream { get; set; } = default!; - } - - public void EnqueueRequest(IRequest command) - { - // TODO: verify we're connected - - if (command is InitFileReadRequest sfr) - { - _readFileInfo = new ReadFileInfo - { - MeadowFileName = sfr.MeadowFileName, - LocalFileName = sfr.LocalFileName, - }; - } - - _pendingCommands.Enqueue(command); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = default) - { - await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = default) - { - if (messageBytes != null) - { - Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - - while (!_port.IsOpen) - { - State = ConnectionState.Disconnected; - Thread.Sleep(100); - // wait for the port to open - } - - State = ConnectionState.Connected; - - try - { - int encodedToSend; - byte[] encodedBytes; - - // For file download this is a LOT of messages - // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); - - // For testing calculate the crc including the sequence number - //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); - try - { - // The encoded size using COBS is just a bit more than the original size adding 1 byte - // every 254 bytes plus 1 and need room for beginning and ending delimiters. - var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; - encodedBytes = new byte[l + 2]; - - // Skip over first byte so it can be a start delimiter - encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); - - // DEBUG TESTING - if (encodedToSend == -1) - { - _logger?.LogError($"Error - encodedToSend == -1"); - return; - } - - if (_port == null) - { - _logger?.LogError($"Error - SerialPort == null"); - throw new Exception("Port is null"); - } - } - catch (Exception except) - { - string msg = string.Format("Send setup Exception: {0}", except); - _logger?.LogError(msg); - throw; - } - - // Add delimiters to packet boundaries - try - { - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; - } - catch (Exception encodedBytesEx) - { - // This should drop the connection and retry - Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); - Thread.Sleep(500); // Place for break point - throw; - } - - try - { - // Send the data to Meadow - await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken.HasValue ? cancellationToken.Value : default); - } - catch (InvalidOperationException ioe) // Port not opened - { - string msg = string.Format("Write but port not opened. Exception: {0}", ioe); - _logger?.LogError(msg); - throw; - } - catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer - { - string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); - _logger?.LogError(msg); - throw; - } - catch (ArgumentException ae) // offset plus count > buffer length - { - string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); - _logger?.LogError(msg); - throw; - } - catch (TimeoutException te) // Took too long to send - { - string msg = string.Format("Write took too long to send. Exception: {0}", te); - _logger?.LogError(msg); - throw; - } - } - catch (Exception except) - { - // DID YOU RESTART MEADOW? - // This should drop the connection and retry - _logger?.LogError($"EncodeAndSendPacket threw: {except}"); - throw; - } - } - } - - - private class SerialMessage - { - private readonly IList> _segments; - - public SerialMessage(Memory segment) - { - _segments = new List>(); - _segments.Add(segment); - } - - public void AddSegment(Memory segment) - { - _segments.Add(segment); - } - - public byte[] ToArray() - { - using var ms = new MemoryStream(); - foreach (var segment in _segments) - { - // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. - var tmp = ArrayPool.Shared.Rent(segment.Length); - segment.CopyTo(tmp); - ms.Write(tmp, 0, segment.Length); - ArrayPool.Shared.Return(tmp); - } - return ms.ToArray(); - } - } - - private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) - { - var decodedBuffer = ArrayPool.Shared.Rent(8192); - var packetLength = packetBuffer.Length; - // It's possible that we may find a series of 0x00 values in the buffer. - // This is because when the sender is blocked (because this code isn't - // running) it will attempt to send a single 0x00 before the full message. - // This allows it to test for a connection. When the connection is - // unblocked this 0x00 is sent and gets put into the buffer along with - // any others that were queued along the usb serial pipe line. - if (packetLength == 1) - { - //_logger?.LogTrace("Throwing out 0x00 from buffer"); - return false; - } - - var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); - - /* - // If a message is too short it is ignored - if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) - { - return false; - } - - Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); - - // Process the received packet - ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), - cancellationToken); - - */ - ArrayPool.Shared.Return(decodedBuffer); - return true; - } - - protected override void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - Close(); - _port.Dispose(); - } - - _isDisposed = true; - } - } - - // ---------------------------------------------- - // ---------------------------------------------- - // ---------------------------------------------- - - private Exception? _lastException; - private bool? _textListComplete; - private DeviceInfo? _deviceInfo; - private RequestType? _lastRequestConcluded = null; - private List StdOut { get; } = new List(); - private List StdErr { get; } = new List(); - private List InfoMessages { get; } = new List(); - - private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; - private const string RuntimeStateToken = "Mono is"; - private const string RuntimeIsEnabledToken = "Mono is enabled"; - private const string RuntimeIsDisabledToken = "Mono is disabled"; - private const string RuntimeHasBeenToken = "Mono has been"; - private const string RuntimeHasBeenEnabledToken = "Mono has been enabled"; - private const string RuntimeHasBeenDisabledToken = "Mono has been disabled"; - private const string RtcRetrievalToken = "UTC time:"; - - public int CommandTimeoutSeconds { get; set; } = 30; - - private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) - { - var timeout = CommandTimeoutSeconds * 2; - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return false; - if (_lastException != null) return false; - - if (timeout <= 0) throw new TimeoutException(); - - if (checkAction()) - { - break; - } - - await Task.Delay(500); - } - - return true; - } - - private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - if (requestType == null || requestType == _lastRequestConcluded) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.Time = dateTime; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - var success = await WaitForResult(() => - { - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) - { - return true; - } - - return false; - }, cancellationToken); - } - - public override async Task GetRtcTime(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - DateTimeOffset? now = null; - - var success = await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); - if (m != null) - { - var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); - now = DateTimeOffset.Parse(timeString); - return true; - } - } - - return false; - }, cancellationToken); - - return now; - } - - public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - return await WaitForInformationResponse(RuntimeStateToken, RuntimeIsEnabledToken, cancellationToken); - } - - private async Task WaitForInformationResponse(string[] textToWaitOn, CancellationToken? cancellationToken) - { - // wait for an information response - var timeout = CommandTimeoutSeconds * 2; - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return false; - if (timeout <= 0) - throw new TimeoutException(); - - foreach (var t in textToWaitOn) - { - if (InfoMessages.Any(m => m.Contains(t))) return true; - } - - await Task.Delay(500); - } - return false; - } - - private async Task WaitForInformationResponse(string textToContain, string textToVerify, CancellationToken? cancellationToken) - { - // wait for an information response - var timeout = CommandTimeoutSeconds * 2; - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return false; - if (timeout <= 0) - throw new TimeoutException(); - - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(textToContain)); - if (m != null) - { - return m == textToVerify; - } - } - - await Task.Delay(500); - } - return false; - } - - public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeHasBeenEnabledToken }, cancellationToken); - } - - public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeIsDisabledToken }, cancellationToken); - } - - public override async Task TraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task TraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.UserData = (uint)level; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.ExtraData = parameter; - command.UserData = value; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task ResetDevice(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - await WaitForMeadowAttach(cancellationToken); - } - - public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _deviceInfo = null; - - _lastException = null; - EnqueueRequest(command); - - if (!await WaitForResult( - () => _deviceInfo != null, - cancellationToken)) - { - return null; - } - - return _deviceInfo; - } - - public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.IncludeCrcs = includeCrcs; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => _textListComplete ?? false, - cancellationToken)) - { - _textListComplete = null; - return null; - } - - var list = new List(); - - foreach (var candidate in _textList) - { - var fi = MeadowFileInfo.Parse(candidate); - if (fi != null) - { - list.Add(fi); - } - } - - _textListComplete = null; - return list.ToArray(); - } - - public override async Task WriteFile( - string localFileName, - string? meadowFileName = null, - CancellationToken? cancellationToken = null) - { - return await WriteFile(localFileName, meadowFileName, - RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, - 0, - cancellationToken); - } - - public override async Task WriteRuntime( - string localFileName, - CancellationToken? cancellationToken = null) - { - var commandTimeout = CommandTimeoutSeconds; - - - CommandTimeoutSeconds = 120; - _lastRequestConcluded = null; - - try - { - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", - RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, - RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, - 0, - cancellationToken); - - - /* - RaiseConnectionMessage("\nErasing runtime flash blocks..."); - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Mono memory erase success"); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Moving runtime to flash..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Verifying runtime flash operation."); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Verifying..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - return true; - } - - return false; - }, - cancellationToken); - */ - - if (status) - { - await WaitForConcluded(null, cancellationToken); - } - - return status; - } - finally - { - CommandTimeoutSeconds = commandTimeout; - } - } - - public override async Task WriteCoprocessorFile( - string localFileName, - int destinationAddress, - CancellationToken? cancellationToken = null) - { - // make the timeouts much bigger, as the ESP flash takes a lot of time - var readTimeout = _port.ReadTimeout; - var commandTimeout = CommandTimeoutSeconds; - _lastRequestConcluded = null; - - _port.ReadTimeout = 60000; - CommandTimeoutSeconds = 180; - InfoMessages.Clear(); - - try - { - RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); - - // push the file to the device - if (!await WriteFile(localFileName, null, - RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, - destinationAddress, - cancellationToken)) - { - return false; - } - - - _lastRequestConcluded = null; - - // now wait for the STM32 to finish writing to the ESP32 - await WaitForConcluded(null, cancellationToken); - return true; - } - finally - { - _port.ReadTimeout = readTimeout; - CommandTimeoutSeconds = commandTimeout; - } - } - - private async Task WriteFile( - string localFileName, - string? meadowFileName, - RequestType initialRequestType, - RequestType endRequestType, - int writeAddress = 0, - CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - var fileBytes = File.ReadAllBytes(localFileName); - - var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes - if (writeAddress != 0) - { - // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. - using var md5 = MD5.Create(); - var hashBytes = md5.ComputeHash(fileBytes); - var hashString = BitConverter.ToString(hashBytes) - .Replace("-", "") - .ToLowerInvariant(); - fileHash = Encoding.UTF8.GetBytes(hashString); - } - var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); - - command.SetParameters( - localFileName, - meadowFileName ?? Path.GetFileName(localFileName), - fileCrc, - writeAddress, - fileHash, - initialRequestType); - - var accepted = false; - Exception? ex = null; - var needsRetry = false; - - void OnFileWriteAccepted(object? sender, EventArgs a) - { - accepted = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - void OnFileRetry(object? sender, EventArgs e) - { - needsRetry = true; - } - - FileWriteAccepted += OnFileWriteAccepted; - FileException += OnFileError; - FileWriteFailed += OnFileRetry; - - Debug.WriteLine($"Sending '{localFileName}'"); - - EnqueueRequest(command); - - // this will wait for a "file write accepted" from the target - if (!await WaitForResult( - () => - { - if (ex != null) - throw ex; - return accepted; - }, - cancellationToken)) - { - return false; - } - - // now send the file data - // The maximum data bytes is max packet size - 2 bytes for the sequence number - byte[] packet = new byte[Protocol.HCOM_PROTOCOL_PACKET_MAX_SIZE - 2]; - ushort sequenceNumber = 0; - - var progress = 0; - var expected = fileBytes.Length; - - var fileName = Path.GetFileName(localFileName); - var directoryName = Path.GetDirectoryName(localFileName).Split(Path.DirectorySeparatorChar); - var displayedFileName = Path.Combine(directoryName[directoryName.Length - 1], fileName); - - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - - var oldTimeout = _port.ReadTimeout; - _port.ReadTimeout = 60000; - - while (true && !needsRetry) - { - if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) - { - return false; - } - - sequenceNumber++; - - Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); - - var toRead = fileBytes.Length - progress; - if (toRead > packet.Length - 2) - { - toRead = packet.Length - 2; - } - Array.Copy(fileBytes, progress, packet, 2, toRead); - try - { - await EncodeAndSendPacket(packet, toRead + 2, cancellationToken); - } - catch (Exception) - { - break; - } - - progress += toRead; - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - if (progress >= fileBytes.Length) break; - } - - if (!needsRetry) - { - _port.ReadTimeout = oldTimeout; - - base.RaiseFileWriteProgress(displayedFileName, expected, expected); - - // finish with an "end" message - not enqued because this is all a serial operation - var request = RequestBuilder.Build(); - request.SetRequestType(endRequestType); - var p = request.Serialize(); - await EncodeAndSendPacket(p, cancellationToken); - } - - FileWriteAccepted -= OnFileWriteAccepted; - FileException -= OnFileError; - FileWriteFailed -= OnFileRetry; - - return !needsRetry; - } - - public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - command.LocalFileName = localFileName; - - var completed = false; - Exception? ex = null; - - void OnFileReadCompleted(object? sender, string filename) - { - completed = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - - try - { - FileReadCompleted += OnFileReadCompleted; - FileException += OnFileError; - ConnectionError += OnFileError; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => - { - return completed | ex != null; - }, - cancellationToken)) - { - return false; - } - - return ex == null; - } - finally - { - FileReadCompleted -= OnFileReadCompleted; - FileException -= OnFileError; - } - } - - public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = fileName; - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - return contents; - } - - public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task EraseFlash(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - } - - public override async Task GetPublicKey(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - - return contents!; - } - - public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) - { - if (Device != null) - { - logger?.LogDebug($"Start Debugging on port: {port}"); - await Device.StartDebugging(port, logger, cancellationToken); - - /* TODO logger?.LogDebug("Reinitialize the device"); - await ReInitializeMeadow(cancellationToken); */ - - var endpoint = new IPEndPoint(IPAddress.Loopback, port); - var debuggingServer = new DebuggingServer(Device, endpoint, logger!); - - logger?.LogDebug("Tell the Debugging Server to Start Listening"); - await debuggingServer.StartListening(cancellationToken); - return debuggingServer; - } - else - { - throw new DeviceNotFoundException(); - } - } - - public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) - { - var command = RequestBuilder.Build(); - - if (command != null) - { - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForMeadowAttach(cancellationToken); - } - else - { - new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); - } - } +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Diagnostics; +using System.IO.Ports; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Meadow.Hcom; + +public partial class SerialConnection : ConnectionBase, IDisposable +{ + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + private const int DefaultTimeout = 5000; + + private event EventHandler? FileReadCompleted = delegate { }; + private event EventHandler? FileWriteAccepted; + private event EventHandler? FileDataReceived; + + private SerialPort _port = default!; + private ILogger? _logger; + private bool _isDisposed; + private List _listeners = new List(); + private Queue _pendingCommands = new Queue(); + private bool _maintainConnection; + private Thread? _connectionManager = null; + private List _textList = new List(); + private int _messageCount = 0; + private ReadFileInfo? _readFileInfo = null; + private string? _lastError = null; + + public override string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) + { + if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) + { + throw new ArgumentException($"Serial Port '{port}' not found."); + } + + Name = port; + State = ConnectionState.Disconnected; + _logger = logger; + + CreatePort(); + + new Task( + () => _ = ListenerProc(), + TaskCreationOptions.LongRunning) + .Start(); + + new Thread(CommandManager) + { + IsBackground = true, + Name = "HCOM Sender" + } + .Start(); + } + + private void CreatePort() + { + _port = new SerialPort(Name); + _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; + _port.Open(); + } + + private bool MaintainConnection + { + get => _maintainConnection; + set + { + if (value == MaintainConnection) return; + + _maintainConnection = value; + + if (value) + { + if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) + { + _connectionManager = new Thread(ConnectionManagerProc) + { + IsBackground = true, + Name = "HCOM Connection Manager" + }; + _connectionManager.Start(); + + } + } + } + } + + private void ConnectionManagerProc() + { + while (_maintainConnection) + { + Open(true); + } + } + + public void AddListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Add(listener); + } + + Open(); + + MaintainConnection = true; + } + + public void RemoveListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Remove(listener); + } + + // TODO: stop maintaining connection? + } + + private void Open(bool inLoop = false) + { + if (!_port.IsOpen) + { + try + { + Debug.WriteLine("Opening COM port..."); + _port.Open(); + } + catch (UnauthorizedAccessException ex) + { + // Handle unauthorized access (e.g., port in use by another application) + throw new Exception($"Serial port '{_port.PortName}' is in use by another application.", ex.InnerException); + } + catch (IOException ex) + { + // Handle I/O errors + throw new Exception($"An I/O error occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); + } + catch (TimeoutException ex) + { + // Handle timeout + throw new Exception($"Timeout occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); + } + } + else if (inLoop) + { + Thread.Sleep(1000); + } + + State = ConnectionState.Connected; + + Debug.WriteLine("Opened COM port"); + } + + private void Close() + { + if (_port.IsOpen) + { + try + { + _port.Close(); + } + catch (IOException ex) + { + // Handle I/O errors + throw new Exception($"An I/O error occurred when attempting to close the serial port '{_port.PortName}'.", ex.InnerException); + } + } + + State = ConnectionState.Disconnected; + } + + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + try + { + // ensure the port is open + Open(); + + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" + var command = RequestBuilder.Build(); + + // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + + _port.DiscardInBuffer(); + + // wait for a response + var timeout = timeoutSeconds * 2; + var dataReceived = false; + + // local function so we can unsubscribe + var count = _messageCount; + + _pendingCommands.Enqueue(command); + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); + + if (count != _messageCount) + { + dataReceived = true; + break; + } + + await Task.Delay(500); + } + + // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no + + // create the device instance + if (dataReceived) + { + Device = new MeadowDevice(this); + } + + return Device; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to connect"); + throw; + } + } + + private async void CommandManager() + { + await Task.Run(async () => + { + while (!_isDisposed) + { + while (_pendingCommands.Count > 0) + { + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + + var command = _pendingCommands.Dequeue() as Request; + + // if this is a file write, we need to packetize for progress + + if (command != null) + { + var payload = command.Serialize(); + await EncodeAndSendPacket(payload); + } + + // TODO: re-queue on fail? + } + + Thread.Sleep(1000); + } + }); + } + + private class ReadFileInfo + { + private string? _localFileName; + + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get + { + if (_localFileName != null) return _localFileName; + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); + } + set => _localFileName = value; + } + public FileStream FileStream { get; set; } = default!; + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = default) + { + await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); + } + + private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = default) + { + if (messageBytes != null) + { + Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); + + while (!_port.IsOpen) + { + State = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } + + State = ConnectionState.Connected; + + try + { + int encodedToSend; + byte[] encodedBytes; + + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + + // For testing calculate the crc including the sequence number + //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + try + { + // The encoded size using COBS is just a bit more than the original size adding 1 byte + // every 254 bytes plus 1 and need room for beginning and ending delimiters. + var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; + encodedBytes = new byte[l + 2]; + + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + + // DEBUG TESTING + if (encodedToSend == -1) + { + _logger?.LogError($"Error - encodedToSend == -1"); + return; + } + + if (_port == null) + { + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); + } + } + catch (Exception except) + { + string msg = string.Format("Send setup Exception: {0}", except); + _logger?.LogError(msg); + throw; + } + + // Add delimiters to packet boundaries + try + { + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + } + catch (Exception encodedBytesEx) + { + // This should drop the connection and retry + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; + } + + try + { + // Send the data to Meadow + await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken.HasValue ? cancellationToken.Value : default); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); + throw; + } + } + catch (Exception except) + { + // DID YOU RESTART MEADOW? + // This should drop the connection and retry + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + throw; + } + } + } + + + private class SerialMessage + { + private readonly IList> _segments; + + public SerialMessage(Memory segment) + { + _segments = new List>(); + _segments.Add(segment); + } + + public void AddSegment(Memory segment) + { + _segments.Add(segment); + } + + public byte[] ToArray() + { + using var ms = new MemoryStream(); + foreach (var segment in _segments) + { + // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. + var tmp = ArrayPool.Shared.Rent(segment.Length); + segment.CopyTo(tmp); + ms.Write(tmp, 0, segment.Length); + ArrayPool.Shared.Return(tmp); + } + return ms.ToArray(); + } + } + + private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) + { + var decodedBuffer = ArrayPool.Shared.Rent(8192); + var packetLength = packetBuffer.Length; + // It's possible that we may find a series of 0x00 values in the buffer. + // This is because when the sender is blocked (because this code isn't + // running) it will attempt to send a single 0x00 before the full message. + // This allows it to test for a connection. When the connection is + // unblocked this 0x00 is sent and gets put into the buffer along with + // any others that were queued along the usb serial pipe line. + if (packetLength == 1) + { + //_logger?.LogTrace("Throwing out 0x00 from buffer"); + return false; + } + + var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); + + /* + // If a message is too short it is ignored + if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + { + return false; + } + + Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + + // Process the received packet + ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), + cancellationToken); + + */ + ArrayPool.Shared.Return(decodedBuffer); + return true; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Close(); + _port.Dispose(); + } + + _isDisposed = true; + } + } + + // ---------------------------------------------- + // ---------------------------------------------- + // ---------------------------------------------- + + private Exception? _lastException; + private bool? _textListComplete; + private DeviceInfo? _deviceInfo; + private RequestType? _lastRequestConcluded = null; + private List StdOut { get; } = new List(); + private List StdErr { get; } = new List(); + private List InfoMessages { get; } = new List(); + + private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; + private const string RuntimeStateToken = "Mono is"; + private const string RuntimeIsEnabledToken = "Mono is enabled"; + private const string RuntimeIsDisabledToken = "Mono is disabled"; + private const string RuntimeHasBeenToken = "Mono has been"; + private const string RuntimeHasBeenEnabledToken = "Mono has been enabled"; + private const string RuntimeHasBeenDisabledToken = "Mono has been disabled"; + private const string RtcRetrievalToken = "UTC time:"; + + public int CommandTimeoutSeconds { get; set; } = 30; + + private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) + { + var timeout = CommandTimeoutSeconds * 2; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (_lastException != null) return false; + + if (timeout <= 0) throw new TimeoutException(); + + if (checkAction()) + { + break; + } + + await Task.Delay(500); + } + + return true; + } + + private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + if (requestType == null || requestType == _lastRequestConcluded) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.Time = dateTime; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } + + public override async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + DateTimeOffset? now = null; + + var success = await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); + if (m != null) + { + var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); + now = DateTimeOffset.Parse(timeString); + return true; + } + } + + return false; + }, cancellationToken); + + return now; + } + + public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + return await WaitForInformationResponse(RuntimeStateToken, RuntimeIsEnabledToken, cancellationToken); + } + + private async Task WaitForInformationResponse(string[] textToWaitOn, CancellationToken? cancellationToken) + { + // wait for an information response + var timeout = CommandTimeoutSeconds * 2; + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) + return false; + if (timeout <= 0) + throw new TimeoutException(); + + foreach (var t in textToWaitOn) + { + if (InfoMessages.Any(m => m.Contains(t))) return true; + } + + await Task.Delay(500); + } + return false; + } + + private async Task WaitForInformationResponse(string textToContain, string textToVerify, CancellationToken? cancellationToken) + { + // wait for an information response + var timeout = CommandTimeoutSeconds * 2; + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) + return false; + if (timeout <= 0) + throw new TimeoutException(); + + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(textToContain)); + if (m != null) + { + return m == textToVerify; + } + } + + await Task.Delay(500); + } + return false; + } + + public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! + await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeHasBeenEnabledToken }, cancellationToken); + } + + public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! + await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeIsDisabledToken }, cancellationToken); + } + + public override async Task TraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.UserData = (uint)level; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.ExtraData = parameter; + command.UserData = value; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task ResetDevice(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + await WaitForMeadowAttach(cancellationToken); + } + + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _deviceInfo = null; + + _lastException = null; + EnqueueRequest(command); + + if (!await WaitForResult( + () => _deviceInfo != null, + cancellationToken)) + { + return null; + } + + return _deviceInfo; + } + + public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.IncludeCrcs = includeCrcs; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => _textListComplete ?? false, + cancellationToken)) + { + _textListComplete = null; + return null; + } + + var list = new List(); + + foreach (var candidate in _textList) + { + var fi = MeadowFileInfo.Parse(candidate); + if (fi != null) + { + list.Add(fi); + } + } + + _textListComplete = null; + return list.ToArray(); + } + + public override async Task WriteFile( + string localFileName, + string? meadowFileName = null, + CancellationToken? cancellationToken = null) + { + return await WriteFile(localFileName, meadowFileName, + RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, + 0, + cancellationToken); + } + + public override async Task WriteRuntime( + string localFileName, + CancellationToken? cancellationToken = null) + { + var commandTimeout = CommandTimeoutSeconds; + + + CommandTimeoutSeconds = 120; + _lastRequestConcluded = null; + + try + { + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, + 0, + cancellationToken); + + + /* + RaiseConnectionMessage("\nErasing runtime flash blocks..."); + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Moving runtime to flash..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Verifying..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + return true; + } + + return false; + }, + cancellationToken); + */ + + if (status) + { + await WaitForConcluded(null, cancellationToken); + } + + return status; + } + finally + { + CommandTimeoutSeconds = commandTimeout; + } + } + + public override async Task WriteCoprocessorFile( + string localFileName, + int destinationAddress, + CancellationToken? cancellationToken = null) + { + // make the timeouts much bigger, as the ESP flash takes a lot of time + var readTimeout = _port.ReadTimeout; + var commandTimeout = CommandTimeoutSeconds; + _lastRequestConcluded = null; + + _port.ReadTimeout = 60000; + CommandTimeoutSeconds = 180; + InfoMessages.Clear(); + + try + { + RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); + + // push the file to the device + if (!await WriteFile(localFileName, null, + RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, + destinationAddress, + cancellationToken)) + { + return false; + } + + + _lastRequestConcluded = null; + + // now wait for the STM32 to finish writing to the ESP32 + await WaitForConcluded(null, cancellationToken); + return true; + } + finally + { + _port.ReadTimeout = readTimeout; + CommandTimeoutSeconds = commandTimeout; + } + } + + private async Task WriteFile( + string localFileName, + string? meadowFileName, + RequestType initialRequestType, + RequestType endRequestType, + int writeAddress = 0, + CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + var fileBytes = File.ReadAllBytes(localFileName); + + var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes + if (writeAddress != 0) + { + // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. + using var md5 = MD5.Create(); + var hashBytes = md5.ComputeHash(fileBytes); + var hashString = BitConverter.ToString(hashBytes) + .Replace("-", "") + .ToLowerInvariant(); + fileHash = Encoding.UTF8.GetBytes(hashString); + } + var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); + + command.SetParameters( + localFileName, + meadowFileName ?? Path.GetFileName(localFileName), + fileCrc, + writeAddress, + fileHash, + initialRequestType); + + var accepted = false; + Exception? ex = null; + var needsRetry = false; + + void OnFileWriteAccepted(object? sender, EventArgs a) + { + accepted = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + void OnFileRetry(object? sender, EventArgs e) + { + needsRetry = true; + } + + FileWriteAccepted += OnFileWriteAccepted; + FileException += OnFileError; + FileWriteFailed += OnFileRetry; + + Debug.WriteLine($"Sending '{localFileName}'"); + + EnqueueRequest(command); + + // this will wait for a "file write accepted" from the target + if (!await WaitForResult( + () => + { + if (ex != null) + throw ex; + return accepted; + }, + cancellationToken)) + { + return false; + } + + // now send the file data + // The maximum data bytes is max packet size - 2 bytes for the sequence number + byte[] packet = new byte[Protocol.HCOM_PROTOCOL_PACKET_MAX_SIZE - 2]; + ushort sequenceNumber = 0; + + var progress = 0; + var expected = fileBytes.Length; + + var fileName = Path.GetFileName(localFileName); + var directoryName = Path.GetDirectoryName(localFileName).Split(Path.DirectorySeparatorChar); + var displayedFileName = Path.Combine(directoryName[directoryName.Length - 1], fileName); + + base.RaiseFileWriteProgress(displayedFileName, progress, expected); + + var oldTimeout = _port.ReadTimeout; + _port.ReadTimeout = 60000; + + while (true && !needsRetry) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) + { + return false; + } + + sequenceNumber++; + + Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); + + var toRead = fileBytes.Length - progress; + if (toRead > packet.Length - 2) + { + toRead = packet.Length - 2; + } + Array.Copy(fileBytes, progress, packet, 2, toRead); + try + { + await EncodeAndSendPacket(packet, toRead + 2, cancellationToken); + } + catch (Exception) + { + break; + } + + progress += toRead; + base.RaiseFileWriteProgress(displayedFileName, progress, expected); + if (progress >= fileBytes.Length) break; + } + + if (!needsRetry) + { + _port.ReadTimeout = oldTimeout; + + base.RaiseFileWriteProgress(displayedFileName, expected, expected); + + // finish with an "end" message - not enqued because this is all a serial operation + var request = RequestBuilder.Build(); + request.SetRequestType(endRequestType); + var p = request.Serialize(); + await EncodeAndSendPacket(p, cancellationToken); + } + + FileWriteAccepted -= OnFileWriteAccepted; + FileException -= OnFileError; + FileWriteFailed -= OnFileRetry; + + return !needsRetry; + } + + public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + command.LocalFileName = localFileName; + + var completed = false; + Exception? ex = null; + + void OnFileReadCompleted(object? sender, string filename) + { + completed = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + + try + { + FileReadCompleted += OnFileReadCompleted; + FileException += OnFileError; + ConnectionError += OnFileError; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + return completed | ex != null; + }, + cancellationToken)) + { + return false; + } + + return ex == null; + } + finally + { + FileReadCompleted -= OnFileReadCompleted; + FileException -= OnFileError; + } + } + + public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = fileName; + + string? contents = null; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + return contents; + } + + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task EraseFlash(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + CommandTimeoutSeconds = lastTimeout; + } + + public override async Task GetPublicKey(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + string? contents = null; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + CommandTimeoutSeconds = lastTimeout; + + return contents!; + } + + public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) + { + if (Device != null) + { + logger?.LogDebug($"Start Debugging on port: {port}"); + await Device.StartDebugging(port, logger, cancellationToken); + + /* TODO logger?.LogDebug("Reinitialize the device"); + await ReInitializeMeadow(cancellationToken); */ + + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + var debuggingServer = new DebuggingServer(Device, endpoint, logger!); + + logger?.LogDebug("Tell the Debugging Server to Start Listening"); + await debuggingServer.StartListening(cancellationToken); + return debuggingServer; + } + else + { + throw new DeviceNotFoundException(); + } + } + + public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) + { + var command = RequestBuilder.Build(); + + if (command != null) + { + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForMeadowAttach(cancellationToken); + } + else + { + new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); + } + } } \ No newline at end of file From 793baeb54cb6ffbf354ca6d6be9284f820cdc82d Mon Sep 17 00:00:00 2001 From: ctacke Date: Tue, 5 Dec 2023 17:12:38 -0600 Subject: [PATCH 052/141] updates for Mac PEM provisioning --- .../Current/Device/DeviceProvisionCommand.cs | 46 ++-------------- .../Connections/LocalConnection.cs | 55 +++++-------------- 2 files changed, 18 insertions(+), 83 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 1dcf1810..fd59ef20 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -21,7 +21,7 @@ public class DeviceProvisionCommand : BaseDeviceCommand [CommandOption("name", 'n', Description = "Device friendly name", IsRequired = false)] public string? Name { get; set; } - [CommandOption("host", 'e', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] + [CommandOption("host", 'h', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) @@ -82,54 +82,16 @@ protected override async ValueTask ExecuteCommand() var info = await connection.Device!.GetDeviceInfo(CancellationToken); - if (info == null) - { - Logger?.LogError($"Unable to get device info"); - return; - } - Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); - var delimList = new string[] - { - "-----END PUBLIC KEY-----", - "-----END RSA PUBLIC KEY-----" - }; + var delim = "-----END RSA PUBLIC KEY-----\n"; + publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); - foreach (var delim in delimList) - { - var i = publicKey.IndexOf(delim); - if (i >= 0) - { - publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); - break; - } - } Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); - string provisioningID; - - // prefer processorID (since the F7 works that way) - if (!string.IsNullOrEmpty(info.ProcessorId)) - { - provisioningID = info.ProcessorId; - } - else - { - if (info.SerialNumber == null) - { - Logger?.LogError($"Unable to get device serial number or processor ID"); - return; - } - - provisioningID = info.SerialNumber; - } - - // upper-case to prevent issues where clients and provisioning differ - provisioningID = provisioningID.ToUpper(); - + var provisioningID = !string.IsNullOrWhiteSpace(info?.ProcessorId) ? info.ProcessorId : info?.SerialNumber; var provisioningName = !string.IsNullOrWhiteSpace(Name) ? Name : info?.DeviceName; var result = await _deviceService.AddDevice(org.Id!, provisioningID!, publicKey, CollectionId, provisioningName, Host, CancellationToken); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index b8fe5204..f42afbb7 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -39,7 +39,7 @@ public LocalConnection() } using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography\")) { - info.Add("SerialNo", (key?.GetValue("MachineGuid")?.ToString() ?? "Unknown").Trim().ToUpper()); + info.Add("SerialNo", (key?.GetValue("MachineGuid")?.ToString() ?? "Unknown").Trim()); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -58,7 +58,7 @@ public LocalConnection() _deviceInfo = new DeviceInfo(info); } - return Task.FromResult(_deviceInfo); + return Task.FromResult< DeviceInfo?>(_deviceInfo); } private string ExecuteBashCommandLine(string command) @@ -68,6 +68,7 @@ private string ExecuteBashCommandLine(string command) FileName = "/bin/bash", Arguments = $"-c \"{command}\"", RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; @@ -76,30 +77,14 @@ private string ExecuteBashCommandLine(string command) process?.WaitForExit(); - return process?.StandardOutput.ReadToEnd() ?? string.Empty; - } - - private string ExecuteWindowsCommandLine(string command, string args) - { - var psi = new ProcessStartInfo() - { - FileName = command, - Arguments = args, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; + var stdout = process?.StandardOutput.ReadToEnd() ?? string.Empty; + var stderr = process?.StandardError.ReadToEnd() ?? string.Empty; - using var process = Process.Start(psi); - - process?.WaitForExit(); - - return process?.StandardOutput.ReadToEnd() ?? string.Empty; + return stdout; } public override Task GetPublicKey(CancellationToken? cancellationToken = null) { - // DEV NOTE: this *must* be in PEM format: i.e. -----BEGIN RSA PUBLIC KEY----- ... -----END RSA PUBLIC KEY---- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -117,37 +102,25 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = throw new Exception("Public key not found"); } - var pkFileContent = File.ReadAllText(pkFile); - if (!pkFileContent.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) - { - // need to convert - pkFileContent = ExecuteWindowsCommandLine("ssh-keygen", $"-e -m pem -f {pkFile}"); - } - - return Task.FromResult(pkFileContent); + return Task.FromResult(File.ReadAllText(pkFile)); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - // this will generate a PEM output *assuming* the key has already been created - var keygenOutput = ExecuteBashCommandLine("ssh-keygen -f id_rsa -e -m pem"); - if (!keygenOutput.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) - { - // probably no key generated - throw new Exception("Unable to retrieve a public key. Please run 'ssh-keygen -t rsa'"); - } - - return Task.FromResult(keygenOutput); + // ssh-agent sh -c 'ssh-add; ssh-add -L' + throw new PlatformNotSupportedException(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // ssh-agent sh -c 'ssh-add; ssh-add -L' var pubkey = this.ExecuteBashCommandLine("ssh-agent sh -c 'ssh-add; ssh-add -L'"); - if (!pubkey.Contains("BEGIN RSA PUBLIC KEY", StringComparison.OrdinalIgnoreCase)) + + if (pubkey.StartsWith("ssh-rsa")) { - var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh", "id_rsa.pub"); - pubkey = ExecuteBashCommandLine($"ssh-keygen -f {path} -e -m pem"); + // convert to PEM format + pubkey = this.ExecuteBashCommandLine("ssh-keygen -f ~/.ssh/id_rsa.pub -m 'PEM' -e"); } + return Task.FromResult(pubkey); } else From 64ae3660771e67c986ab890d74f28f37425885c6 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Sun, 26 Nov 2023 11:30:54 +0000 Subject: [PATCH 053/141] Add more *.IsCancellationRequested checks, so we can bail sooner in loops. --- Source/v2/Meadow.Cli/AppManager.cs | 46 +++++++++++++++---- .../Commands/Current/App/AppDeployCommand.cs | 11 +++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index d5168f49..7d45e605 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -31,13 +31,18 @@ private static bool IsXmlDoc(string file) return false; } - public static async Task> GenerateDeployList(IPackageManager packageManager, + public static async Task?> GenerateDeployList(IPackageManager packageManager, string localBinaryDirectory, bool includePdbs, bool includeXmlDocs, ILogger? logger, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + // TODO: add sub-folder support when HCOM supports it logger?.LogInformation($"Generating the list of files to deploy from {localBinaryDirectory}..."); @@ -68,6 +73,10 @@ public static async Task> GenerateDeployList(IPackageMa // Crawl trimmed dependencies foreach (var file in trimmedDependencies) { + if (cancellationToken.IsCancellationRequested) + { + return null; + } await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); } @@ -105,6 +114,11 @@ public static async Task> GenerateDeployList(IPackageMa { foreach (var file in packageManager.AssemblyDependencies!) { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + // TODO: add any other filtering capability here //Populate out LocalFile Dictionary with this entry @@ -112,7 +126,7 @@ public static async Task> GenerateDeployList(IPackageMa } } - if (localFiles.Count() == 0) + if (localFiles?.Count() == 0) { logger?.LogInformation($"No new files to deploy"); } @@ -125,9 +139,14 @@ public static async Task> GenerateDeployList(IPackageMa public static async Task DeployApplication( IMeadowConnection connection, Dictionary localFiles, - ILogger logger, + ILogger? logger, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + // get a list of files on-device, with CRCs var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); @@ -139,22 +158,31 @@ public static async Task DeployApplication( if (removeFiles.Count() == 0) { - logger.LogInformation($"No files to delete"); + logger?.LogInformation($"No files to delete"); } // delete those files foreach (var file in removeFiles) { - logger.LogInformation($"Deleting file '{file}'..."); + if (cancellationToken.IsCancellationRequested) + { + return; + } + logger?.LogInformation($"Deleting file '{file}'..."); await connection.DeleteFile(file, cancellationToken); } // now send all files with differing CRCs foreach (var localFile in localFiles) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + if (!File.Exists(localFile.Key)) { - logger.LogInformation($"{localFile.Key} not found" + Environment.NewLine); + logger?.LogInformation($"{localFile.Key} not found" + Environment.NewLine); continue; } @@ -171,7 +199,7 @@ public static async Task DeployApplication( if (remoteCrc == localCrc) { // exists and has a matching CRC, skip it - logger.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); + logger?.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); continue; } } @@ -184,7 +212,7 @@ public static async Task DeployApplication( { if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) { - logger.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}'. Retrying."); + logger?.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}'. Retrying."); await Task.Delay(100); success = false; } @@ -195,7 +223,7 @@ public static async Task DeployApplication( } catch (Exception ex) { - logger.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}' ({ex.Message}). Retrying."); + logger?.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}' ({ex.Message}). Retrying."); await Task.Delay(100); success = false; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 438f97f7..66c69e40 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -86,12 +86,15 @@ protected override async ValueTask ExecuteCommand() .WithSpinner(Console!, 250); Console?.Output.WriteAsync("\n"); - Connection.FileWriteProgress += Connection_FileWriteProgress; + if (localFiles != null && localFiles.Count > 0) + { + Connection.FileWriteProgress += Connection_FileWriteProgress; - await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); - Console?.Output.WriteAsync("\n"); + await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); + Console?.Output.WriteAsync("\n"); - Connection.FileWriteProgress -= Connection_FileWriteProgress; + Connection.FileWriteProgress -= Connection_FileWriteProgress; + } } if (wasRuntimeEnabled) From cb6920a24b19c616052a88f06fb003094a5bc0c5 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 27 Nov 2023 14:21:28 +0000 Subject: [PATCH 054/141] Make sure MeadowAssembliesPath is valid before proceeding. --- Source/v2/Meadow.Cli/AppManager.cs | 7 ++++++- .../Commands/Current/App/AppTrimCommand.cs | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 7d45e605..a859e2cc 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -38,7 +38,9 @@ private static bool IsXmlDoc(string file) ILogger? logger, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested + || string.IsNullOrWhiteSpace(packageManager.MeadowAssembliesPath) + || !Directory.Exists(packageManager.MeadowAssembliesPath)) { return null; } @@ -230,6 +232,9 @@ public static async Task DeployApplication( } while (!success); } + + // Delay to receive last successful write message. + await Task.Delay(330); } private static async Task AddToLocalFiles(Dictionary localFiles, string file, bool includePdbs, bool includeXmlDocs, CancellationToken cancellationToken) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 917e24ca..3eaabc6a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -62,10 +62,19 @@ protected override async ValueTask ExecuteCommand() _packageManager.RuntimeVersion = info?.RuntimeVersion; - Logger?.LogInformation($"Using runtime files from {_packageManager.MeadowAssembliesPath}"); + if (!string.IsNullOrWhiteSpace(_packageManager.MeadowAssembliesPath) && Directory.Exists(_packageManager.MeadowAssembliesPath)) + { + Logger?.LogInformation($"Using runtime files from {_packageManager.MeadowAssembliesPath}"); + + // Avoid double reporting. + DetachMessageHandlers(Connection); + } + else + { + Logger?.LogError($"Meadow Assemblies Path: '{_packageManager.MeadowAssembliesPath}' does NOT exist for Runtime: '{_packageManager.RuntimeVersion}'."); + return; + } - // Avoid double reporting. - DetachMessageHandlers(Connection); } // TODO: support `nolink` command line args From a0b890ed61c7435293ebefacf87032e184a26c8f Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 27 Nov 2023 14:25:55 +0000 Subject: [PATCH 055/141] Standardise how we report the file progress. --- .../Commands/Current/App/AppDeployCommand.cs | 2 +- .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 11 +++-------- .../v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs | 2 ++ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 66c69e40..b586b625 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -118,6 +118,6 @@ private void Connection_FileWriteProgress(object? sender, (string fileName, long } // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 8f76945f..68dbe8d3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -303,14 +303,9 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; - if (p == 100.0) - { - Console?.Output.WriteAsync($"{Environment.NewLine}"); - } - else - { - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - } + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); }; if (Files.Contains(FirmwareType.OS)) diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index bd8cb1f5..cfd82ca1 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -259,6 +259,8 @@ private async ValueTask WriteFiles() Connection.FileWriteProgress += (s, e) => { var p = (e.completed / (double)e.total) * 100d; + + // Console instead of Logger due to line breaking for progress bar Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); }; From a900924703d7371914df3a821ee71f08b0ca4be9 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 27 Nov 2023 14:54:12 +0000 Subject: [PATCH 056/141] Re-enable Trimming. --- Source/v2/Meadow.Cli/PackageManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 4caaef23..5423871f 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -176,7 +176,7 @@ public async Task TrimApplication( Trimmed = false; } - Trimmed = false; + Trimmed = true; } public const string PackageMetadataFileName = "info.json"; From 76adb73bf1233ef0a4e38cd87dc6bfd2a5f4876c Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 27 Nov 2023 14:56:35 +0000 Subject: [PATCH 057/141] Use default Spinner timer value for smoother spinning. --- Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs | 2 +- Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs | 2 +- .../Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index b3cc8cce..7464c009 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -47,7 +47,7 @@ await Task.Run(async () => // TODO: enable cancellation of this call var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)) - .WithSpinner(Console!, 250); + .WithSpinner(Console!); if (!success) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index b586b625..0418b47e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -83,7 +83,7 @@ protected override async ValueTask ExecuteCommand() await trimApplicationCommand.ExecuteAsync(Console!); var localFiles = await AppManager.GenerateDeployList(_packageManager, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken) - .WithSpinner(Console!, 250); + .WithSpinner(Console!); Console?.Output.WriteAsync("\n"); if (localFiles != null && localFiles.Count > 0) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 3eaabc6a..d136ece0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -79,6 +79,6 @@ protected override async ValueTask ExecuteCommand() // TODO: support `nolink` command line args await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken) - .WithSpinner(Console!, 250); + .WithSpinner(Console!); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index 3482fef1..cbdc3171 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -38,7 +38,7 @@ public BaseCloudCommand( Logger?.LogInformation("Retrieving your user and organization information..."); var userOrgs = await UserService.GetUserOrgs(host, cancellationToken) - .WithSpinner(Console!, 250); + .WithSpinner(Console!); if (!userOrgs.Any()) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 13948a45..f59f4227 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -88,7 +88,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken) - .WithSpinner(Console!, 250); + .WithSpinner(Console!); Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); } From 16099d241f036334a7e210db8d37ffe3dcb4f4f8 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Mon, 27 Nov 2023 15:16:36 +0000 Subject: [PATCH 058/141] Bump to alpha.4 --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 275726c4..c2c59fee 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Chris Tacke, Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis Wilderness Labs, Inc true - 2.0.0-alpha.3 + 2.0.0-alpha.4 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png From ead0ea4c3c5e0825b330853380cadc8e6f0288cb Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 29 Nov 2023 10:14:13 +0000 Subject: [PATCH 059/141] Add extra IsCancelationRequested check inside Retry loops. --- Source/v2/Meadow.Cli/AppManager.cs | 6 ++++++ .../Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs | 6 ++++++ .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 7 +++++++ Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index a859e2cc..91bb5951 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -212,6 +212,12 @@ public static async Task DeployApplication( { try { + // If we've cancelled bail + if (cancellationToken.IsCancellationRequested) + { + return; + } + if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) { logger?.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}'. Retrying."); diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index d8d207c2..659d0fda 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -55,6 +55,12 @@ protected override async ValueTask ExecuteCommand() { foreach (var f in fileList) { + // If we've cancelled bail + if (CancellationToken.IsCancellationRequested) + { + return; + } + if (Connection.Device != null) { var p = Path.GetFileName(f.Name); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 68dbe8d3..d509639f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Threading; using CliFx.Attributes; using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; @@ -342,6 +343,12 @@ private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection c var rtpath = package.GetFullyQualifiedPath(runtime); write_runtime: + // If we've cancelled bail + if (CancellationToken.IsCancellationRequested) + { + return; + } + if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) { Logger?.LogInformation($"Error writing runtime. Retrying."); diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index cfd82ca1..b97d2ec0 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -292,6 +292,12 @@ private async ValueTask WriteFiles() var rtpath = package.GetFullyQualifiedPath(runtime); write_runtime: + // If we've cancelled bail + if (CancellationToken.IsCancellationRequested) + { + return; + } + if (!await Connection.Device.WriteRuntime(rtpath, CancellationToken)) { Logger?.LogInformation($"Error writing runtime. Retrying."); From edd0f913e31aa716a9d185d5757bf2b0ad37a9b3 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Wed, 6 Dec 2023 14:26:29 +0000 Subject: [PATCH 060/141] Comment out VS2019 Mac/Win builds from CI --- .github/workflows/dotnet.yml | 256 +++++++++++++++++------------------ 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 300b7506..8dfb0e61 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -129,68 +129,68 @@ jobs: # run: | # nuget push main\Source\v2\Meadow.CLI\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} - build-vswin-2019: - runs-on: windows-2019 - needs: [build-and-optionally-publish-nuget] - name: Build Win 2019 Extension - - steps: - - name: Checkout Meadow.CLI.Core side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/Meadow.CLI - path: Meadow.CLI - - - if: ${{ github.ref == 'refs/heads/main' }} - name: Checkout Win Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Win_Meadow_Extension - path: vs-win - ref: main - - - if: ${{ github.ref != 'refs/heads/main' }} - name: Checkout Win Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Win_Meadow_Extension - path: vs-win - - - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 5.0.x - 6.0.x - 7.0.x - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Add MSBuild to Path - uses: microsoft/setup-msbuild@v1.1 - - - if: ${{ github.event_name == 'workflow_dispatch' }} - name: Update VS2019 Version Numbers - run: | - $content = Get-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest | Out-String - $newcontent = $content -replace 'Version="1.*" Language="en-US" Publisher="Wilderness Labs"', 'Version="${{ ENV.IDE_TOOLS_RELEASE_VERSION }}" Language="en-US" Publisher="Wilderness Labs"' - $newcontent | Set-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest - - - name: Restore VS2019 dependencies - run: dotnet restore vs-win/VS_Meadow_Extension.2019.sln /p:Configuration=Release - - - name: Build VS2019 Extension - id: VS2019-Extension - run: msbuild vs-win/VS_Meadow_Extension.2019.sln /t:Rebuild /p:Configuration=Release - env: - DevEnvDir: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE' - - - name: Upload VS2019 VSIX Artifacts - uses: actions/upload-artifact@v2 - with: - name: Meadow.Win.VS2019.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} - path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2019\bin\Release\*.vsix' + # build-vswin-2019: + # runs-on: windows-2019 + # needs: [build-and-optionally-publish-nuget] + # name: Build Win 2019 Extension + + # steps: + # - name: Checkout Meadow.CLI.Core side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/Meadow.CLI + # path: Meadow.CLI + + # - if: ${{ github.ref == 'refs/heads/main' }} + # name: Checkout Win Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Win_Meadow_Extension + # path: vs-win + # ref: main + + # - if: ${{ github.ref != 'refs/heads/main' }} + # name: Checkout Win Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Win_Meadow_Extension + # path: vs-win + + # - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: | + # 5.0.x + # 6.0.x + # 7.0.x + + # - name: Setup NuGet + # uses: NuGet/setup-nuget@v1.0.5 + + # - name: Add MSBuild to Path + # uses: microsoft/setup-msbuild@v1.1 + + # - if: ${{ github.event_name == 'workflow_dispatch' }} + # name: Update VS2019 Version Numbers + # run: | + # $content = Get-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest | Out-String + # $newcontent = $content -replace 'Version="1.*" Language="en-US" Publisher="Wilderness Labs"', 'Version="${{ ENV.IDE_TOOLS_RELEASE_VERSION }}" Language="en-US" Publisher="Wilderness Labs"' + # $newcontent | Set-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest + + # - name: Restore VS2019 dependencies + # run: dotnet restore vs-win/VS_Meadow_Extension.2019.sln /p:Configuration=Release + + # - name: Build VS2019 Extension + # id: VS2019-Extension + # run: msbuild vs-win/VS_Meadow_Extension.2019.sln /t:Rebuild /p:Configuration=Release + # env: + # DevEnvDir: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE' + + # - name: Upload VS2019 VSIX Artifacts + # uses: actions/upload-artifact@v2 + # with: + # name: Meadow.Win.VS2019.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} + # path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2019\bin\Release\*.vsix' #- if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} # name: Publish VS2019 Extension @@ -265,72 +265,72 @@ jobs: run: | & "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\VSSDK\VisualStudioIntegration\Tools\Bin\VsixPublisher.exe" publish -payload "vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2022\bin\Release\Meadow.2022.vsix" -publishManifest "vs-win\publishManifest.2022.json" -ignoreWarnings "None" -personalAccessToken "${{secrets.MARKETPLACE_PUBLISH_PAT}}" - build-vsmac-2019: - name: Build Mac 2019 Extension - needs: [build-and-optionally-publish-nuget] - runs-on: macos-11 - - steps: - - name: Checkout Meadow.CLI.Core side-by-side - uses: actions/checkout@v2 - with: - path: Meadow.CLI - - - if: ${{ github.ref == 'refs/heads/main' }} - name: Checkout Mac Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Mac_Meadow_Extension - path: vs-mac - ref: main - - - if: ${{ github.ref != 'refs/heads/main' }} - name: Checkout Mac Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Mac_Meadow_Extension - path: vs-mac - - - name: Set default Xcode 13.0 - run: | - XCODE_ROOT=/Applications/Xcode_13.0.0.app - echo "MD_APPLE_SDK_ROOT=$XCODE_ROOT" >> $GITHUB_ENV - sudo xcode-select -s $XCODE_ROOT - - - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 5.0.x - 6.0.x - 7.0.x - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Work around so that VS2019 is picked up. - run: | - mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" - mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" - - - if: ${{ github.event_name == 'workflow_dispatch' }} - name: Update VS2019 Version Numbers - run: | - sed -i "" "s/Version = \"1.*\"/Version = \"${{ENV.IDE_TOOLS_RELEASE_VERSION}}\"/" vs-mac/VS4Mac_Meadow_Extension/Properties/AddinInfo.cs - - - name: Restore our VS2019 project - run: | - msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Restore /p:Configuration=Release - - - name: Build and Package the VS2019 Extension - run: | - msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Build /p:Configuration=Release /p:CreatePackage=true - - - name: Upload Mac VS2019 mpack Artifacts - uses: actions/upload-artifact@v2 - with: - name: Meadow.Mac.2019.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} - path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net472/*.mpack' + # build-vsmac-2019: + # name: Build Mac 2019 Extension + # needs: [build-and-optionally-publish-nuget] + # runs-on: macos-11 + + # steps: + # - name: Checkout Meadow.CLI.Core side-by-side + # uses: actions/checkout@v2 + # with: + # path: Meadow.CLI + + # - if: ${{ github.ref == 'refs/heads/main' }} + # name: Checkout Mac Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Mac_Meadow_Extension + # path: vs-mac + # ref: main + + # - if: ${{ github.ref != 'refs/heads/main' }} + # name: Checkout Mac Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Mac_Meadow_Extension + # path: vs-mac + + # - name: Set default Xcode 13.0 + # run: | + # XCODE_ROOT=/Applications/Xcode_13.0.0.app + # echo "MD_APPLE_SDK_ROOT=$XCODE_ROOT" >> $GITHUB_ENV + # sudo xcode-select -s $XCODE_ROOT + + # - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: | + # 5.0.x + # 6.0.x + # 7.0.x + + # - name: Setup NuGet + # uses: NuGet/setup-nuget@v1.0.5 + + # - name: Work around so that VS2019 is picked up. + # run: | + # mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" + # mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" + + # - if: ${{ github.event_name == 'workflow_dispatch' }} + # name: Update VS2019 Version Numbers + # run: | + # sed -i "" "s/Version = \"1.*\"/Version = \"${{ENV.IDE_TOOLS_RELEASE_VERSION}}\"/" vs-mac/VS4Mac_Meadow_Extension/Properties/AddinInfo.cs + + # - name: Restore our VS2019 project + # run: | + # msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Restore /p:Configuration=Release + + # - name: Build and Package the VS2019 Extension + # run: | + # msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Build /p:Configuration=Release /p:CreatePackage=true + + # - name: Upload Mac VS2019 mpack Artifacts + # uses: actions/upload-artifact@v2 + # with: + # name: Meadow.Mac.2019.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} + # path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net472/*.mpack' #- if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} # name: Get Commit Messages From 591a6777f35dfcd8b60a35b819121636fae118ea Mon Sep 17 00:00:00 2001 From: Eduardo Menezes Date: Mon, 11 Dec 2023 11:38:26 -0300 Subject: [PATCH 061/141] Change pdbs inclusion default to false --- Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs | 2 +- Meadow.CLI/Commands/App/DeployAppCommand.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs b/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs index 434515da..cfda7164 100644 --- a/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs +++ b/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs @@ -243,7 +243,7 @@ public Task QspiInit(int value, CancellationToken cancellationToken = default) return _meadowDevice.QspiInit(value, cancellationToken); } - public async Task DeployApp(string fileName, bool includePdbs = true, CancellationToken cancellationToken = default, bool verbose = false, IList? noLink = null) + public async Task DeployApp(string fileName, bool includePdbs = false, CancellationToken cancellationToken = default, bool verbose = false, IList? noLink = null) { try { diff --git a/Meadow.CLI/Commands/App/DeployAppCommand.cs b/Meadow.CLI/Commands/App/DeployAppCommand.cs index a72da6ca..d15a2423 100644 --- a/Meadow.CLI/Commands/App/DeployAppCommand.cs +++ b/Meadow.CLI/Commands/App/DeployAppCommand.cs @@ -27,7 +27,7 @@ public class DeployAppCommand : MeadowSerialCommand public IList NoLink { get; init; } = null; [CommandOption("includePdbs", 'i', Description = "Include the PDB files on deploy to enable debugging", IsRequired = false)] - public bool IncludePdbs { get; init; } = true; + public bool IncludePdbs { get; init; } = false; public DeployAppCommand(DownloadManager downloadManager, ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) : base(downloadManager, loggerFactory, meadowDeviceManager) From 2500fd981511e9aaacabbdcb79be3fa09ea19ccb Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 19 Dec 2023 17:41:06 -0800 Subject: [PATCH 062/141] Linker refactor and v2 refresh --- Source/TestApps/LinkerTest/LinkerTest.sln | 25 + .../LinkerTest/LinkerTest/ILLinker.cs | 86 + .../LinkerTest/LinkerTest/LinkerTest.csproj | 47 + .../LinkerTest/LinkerTest/MeadowLinker.cs | 175 ++ .../TestApps/LinkerTest/LinkerTest/Program.cs | 50 + .../TestApps/LinkerTest/LinkerTest/Readme.md | 41 + .../LinkerTest/lib/Mono.Cecil.Pdb.dll | Bin 0 -> 114176 bytes .../LinkerTest/LinkerTest/lib/Mono.Cecil.dll | Bin 0 -> 499200 bytes .../LinkerTest/lib/illink.deps.json | 112 + .../LinkerTest/LinkerTest/lib/illink.dll | Bin 0 -> 466944 bytes .../LinkerTest/lib/illink.runtimeconfig.json | 10 + .../LinkerTest/LinkerTest/lib/meadow_link.xml | 11 + .../v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj | 6 +- .../Commands/Current/App/BaseAppCommand.cs | 14 - .../Commands/Helper/ConsoleSpinner.cs | 27 - .../Commands/Helper/ExtensionMethods.cs | 124 - .../Commands/Helper/UITaskExtensions.cs | 50 - Source/v2/Meadow.CLI/Linker/ILLinker.cs | 86 + Source/v2/Meadow.CLI/Linker/MeadowLinker.cs | 175 ++ Source/v2/Meadow.CLI/Meadow.CLI.csproj | 13 +- Source/v2/Meadow.Cli/AppManager.cs | 381 +-- .../Commands/Current/App/AppBuildCommand.cs | 51 +- .../Commands/Current/App/AppDebugCommand.cs | 17 +- .../Commands/Current/App/AppDeployCommand.cs | 80 +- .../Commands/Current/App/AppRunCommand.cs | 138 +- .../Commands/Current/App/AppTrimCommand.cs | 148 +- .../Commands/Current/BaseCloudCommand.cs | 140 +- .../Commands/Current/BaseCommand.cs | 5 - .../Commands/Current/BaseDeviceCommand.cs | 65 +- .../Commands/Current/BaseFileCommand.cs | 12 +- .../Current/Cloud/CloudLogoutCommand.cs | 7 +- .../Collection/CloudCollectionListCommand.cs | 28 +- .../JsonDocumentBindingConverter.cs | 11 +- .../Command/CloudCommandPublishCommand.cs | 32 +- .../Package/CloudPackageCreateCommand.cs | 14 +- .../Cloud/Package/CloudPackageListCommand.cs | 32 +- .../Package/CloudPackagePublishCommand.cs | 24 +- .../Package/CloudPackageUploadCommand.cs | 164 +- .../Commands/Current/Config/ConfigCommand.cs | 67 +- .../Commands/Current/DeveloperCommand.cs | 27 +- .../Current/Device/DeviceClockCommand.cs | 46 +- .../Current/Device/DeviceInfoCommand.cs | 20 +- .../Current/Device/DeviceProvisionCommand.cs | 6 +- .../Current/Device/DeviceResetCommand.cs | 10 +- .../Current/File/FileDeleteCommand.cs | 176 +- .../Current/File/FileInitialCommand.cs | 19 +- .../Commands/Current/File/FileListCommand.cs | 18 +- .../Commands/Current/File/FileReadCommand.cs | 30 +- .../Commands/Current/File/FileWriteCommand.cs | 72 +- .../Firmware/FirmwareDefaultCommand.cs | 29 +- .../Current/Firmware/FirmwareDeleteCommand.cs | 9 +- .../Firmware/FirmwareDownloadCommand.cs | 92 +- .../Current/Firmware/FirmwareListCommand.cs | 218 +- .../Current/Firmware/FirmwareWriteCommand.cs | 414 ++- .../Current/Flash/FlashEraseCommand.cs | 22 +- .../Commands/Current/ListenCommand.cs | 44 +- .../Commands/Current/Port/PortListCommand.cs | 4 +- .../Current/Port/PortSelectCommand.cs | 9 +- .../Current/Runtime/RuntimeDisableCommand.cs | 27 +- .../Current/Runtime/RuntimeEnableCommand.cs | 27 +- .../Current/Runtime/RuntimeStateCommand.cs | 25 +- .../Current/Trace/BaseTraceCommand.cs | 12 - .../Current/Trace/TraceDisableCommand.cs | 22 +- .../Current/Trace/TraceEnableCommand.cs | 84 +- .../Current/Trace/TraceLevelCommand.cs | 43 +- .../Current/Uart/UartTraceDisableCommand.cs | 17 +- .../Current/Uart/UartTraceEnableCommand.cs | 17 +- .../Commands/Legacy/FlashOsCommand.cs | 385 ++- .../Commands/Legacy/MonoEnableCommand.cs | 2 +- .../Commands/Legacy/MonoStateCommand.cs | 4 +- Source/v2/Meadow.Cli/DFU/DfuSharp.cs | 6 +- Source/v2/Meadow.Cli/IPackageManager.cs | 17 +- .../v2/Meadow.Cli/MeadowConnectionManager.cs | 210 +- .../PackageManager.AssemblyManager.cs | 270 +- .../Meadow.Cli/PackageManager.BuildOptions.cs | 4 +- Source/v2/Meadow.Cli/PackageManager.cs | 45 +- Source/v2/Meadow.Cli/Program.cs | 14 +- .../Meadow.Cli/Properties/launchSettings.json | 4 +- .../Meadow.Cloud.Client/Identity/LibSecret.cs | 4 +- .../Meadow.Cloud.Client.csproj | 4 - .../Messages/PackageInfo.cs | 4 +- .../v2/Meadow.Cloud.Client/Messages/User.cs | 10 +- .../Meadow.Cloud.Client/Messages/UserOrg.cs | 6 +- .../Services/PackageService.cs | 7 +- .../Meadow.HCom.Integration.Tests.csproj | 4 - .../SerialConnectionTests.cs | 6 +- Source/v2/Meadow.HCom/Meadow.HCom.csproj | 13 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 19 +- .../SerialConnection.ListenerProc.cs | 43 +- .../Connections/SerialConnection.cs | 2504 ++++++++--------- .../Meadow.Hcom/Connections/TcpConnection.cs | 52 +- .../Meadow.Hcom/Debugging/DebuggingServer.cs | 70 +- .../v2/Meadow.Hcom/DeviceNotFoundException.cs | 6 +- .../Meadow.Hcom/Firmware/DownloadManager.cs | 48 +- .../Meadow.Hcom/Firmware/FirmwareManager.cs | 6 +- .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 100 +- .../Meadow.Hcom/Firmware/PackageVersions.cs | 2 +- .../Meadow.Hcom/Firmware/ReleaseMetadata.cs | 8 +- Source/v2/Meadow.Hcom/IMeadowDevice.cs | 3 - Source/v2/Meadow.Hcom/Meadow.Hcom.sln | 2 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 7 - .../SerialRequests/InitFileWriteRequest.cs | 2 +- .../SerialRequests/RequestBuilder.cs | 2 +- .../SerialRequests/SetRtcTimeRequest.cs | 8 +- .../Meadow.SoftwareManager.Unit.Tests.csproj | 4 - .../DownloadFileStream.cs | 2 +- .../F7FirmwarePackageCollection.cs | 20 +- .../Meadow.SoftwareManager/FirmwarePackage.cs | 4 +- .../Meadow.SoftwareManager.csproj | 3 - Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj | 4 - .../Meadow.UsbLibClassic.csproj | 4 - 111 files changed, 3820 insertions(+), 4129 deletions(-) create mode 100644 Source/TestApps/LinkerTest/LinkerTest.sln create mode 100644 Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs create mode 100644 Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj create mode 100644 Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs create mode 100644 Source/TestApps/LinkerTest/LinkerTest/Program.cs create mode 100644 Source/TestApps/LinkerTest/LinkerTest/Readme.md create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.Pdb.dll create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.dll create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/illink.dll create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json create mode 100644 Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml delete mode 100644 Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs delete mode 100644 Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs delete mode 100644 Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs delete mode 100644 Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs create mode 100644 Source/v2/Meadow.CLI/Linker/ILLinker.cs create mode 100644 Source/v2/Meadow.CLI/Linker/MeadowLinker.cs delete mode 100644 Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs diff --git a/Source/TestApps/LinkerTest/LinkerTest.sln b/Source/TestApps/LinkerTest/LinkerTest.sln new file mode 100644 index 00000000..6d8b8ee0 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkerTest", "LinkerTest\LinkerTest.csproj", "{F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8AF82077-F626-42F0-81B2-B92439C770DA} + EndGlobalSection +EndGlobal diff --git a/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs b/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs new file mode 100644 index 00000000..825aee14 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace LinkerTest +{ + internal class ILLinker + { + readonly ILogger? _logger; + + public ILLinker(ILogger? logger = null) + { + _logger = logger; + } + + public async Task RunILLink( + string illinkerDllPath, + string descriptorXmlPath, + string noLinkArgs, + string prelinkAppPath, + string prelinkDir, + string postlinkDir) + { + if (!File.Exists(illinkerDllPath)) + { + throw new FileNotFoundException("Cannot run trimming operation, illink.dll not found"); + } + + //original + //var monolinker_args = $"\"{illinkerDllPath}\" -x \"{descriptorXmlPath}\" {noLinkArgs} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlinkDir}\" -r \"{prelinkAppPath}\" -a \"{prelink_os}\" -d \"{prelinkDir}\""; + + var monolinker_args = $"\"{illinkerDllPath}\"" + + $" -x \"{descriptorXmlPath}\" " + //link files in the descriptor file (needed) + $"{noLinkArgs} " + //arguments to skip linking - will be blank if we are linking + $"-r \"{prelinkAppPath}\" " + //link the app in the prelink folder (needed) + $"--skip-unresolved true " + //skip unresolved references (needed -hangs without) + $"--deterministic true " + //make deterministic (to avoid pushing unchanged files to the device) + $"--keep-facades true " + //keep facades (needed - will skip key libs without) + $"-b true " + //Update debug symbols for each linked module (needed - will skip key libs without) + $"-o \"{postlinkDir}\" " + //output directory + + + //old + //$"--ignore-descriptors false " + //ignore descriptors (doesn't appear to impact behavior) + //$"-c link " + //link framework assemblies + //$"-d \"{prelinkDir}\"" //additional folder to link (not needed) + + //experimental + //$"--explicit-reflection true " + //enable explicit reflection (throws an exception with it) + //$"--keep-dep-attributes true " + //keep dependency attributes (files are slightly larger with, doesn't fix dependency issue) + ""; + + _logger?.Log(LogLevel.Information, "Trimming assemblies"); + + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Directory.GetDirectoryRoot(illinkerDllPath); + process.StartInfo.FileName = "dotnet"; + process.StartInfo.Arguments = monolinker_args; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + + // To avoid deadlocks, read the output stream first and then wait + string stdOutReaderResult; + using (StreamReader stdOutReader = process.StandardOutput) + { + stdOutReaderResult = await stdOutReader.ReadToEndAsync(); + + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + + _logger?.Log(LogLevel.Debug, "StandardOutput Contains: " + stdOutReaderResult); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + _logger?.Log(LogLevel.Debug, $"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + throw new Exception("Trimming failed"); + } + } + } + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj b/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj new file mode 100644 index 00000000..5f9d9e86 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj @@ -0,0 +1,47 @@ + + + + Exe + net6.0 + enable + enable + preview + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs b/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs new file mode 100644 index 00000000..8ebcf4a3 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs @@ -0,0 +1,175 @@ +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; +using System.Reflection; + +namespace LinkerTest; + +public class MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) +{ + private const string IL_LINKER_DIR = "lib"; + private const string IL_LINKER_DLL = "illink.dll"; + private const string MEADOW_LINK_XML = "meadow_link.xml"; + + public const string PostLinkDirectoryName = "postlink_bin"; + public const string PreLinkDirectoryName = "prelink_bin"; + + readonly ILLinker _linker = new ILLinker(logger); + readonly ILogger? _logger = logger; + + private readonly string _meadowAssembliesPath = meadowAssembliesPath; + + public async Task Trim( + FileInfo meadowAppFile, + bool includePdbs = false, + IList? noLink = null) + { + var dependencies = MapDependencies(meadowAppFile); + + CopyDependenciesToPreLinkFolder(meadowAppFile, dependencies, includePdbs); + + //run the _linker against the dependencies + await TrimMeadowApp(meadowAppFile, noLink); + } + + List MapDependencies(FileInfo meadowAppFile) + { + //get all dependencies in meadowAppFile and exclude the Meadow App + var dependencyMap = new List(); + + var appRefs = GetAssemblyReferences(meadowAppFile.FullName); + return GetDependencies(meadowAppFile.FullName, appRefs, dependencyMap, meadowAppFile.DirectoryName); + } + + public void CopyDependenciesToPreLinkFolder( + FileInfo meadowApp, + List dependencies, + bool includePdbs) + { + //set up the paths + var prelinkDir = Path.Combine(meadowApp.DirectoryName!, PreLinkDirectoryName); + var postlinkDir = Path.Combine(meadowApp.DirectoryName!, PostLinkDirectoryName); + + //create output directories + CreateEmptyDirectory(prelinkDir); + CreateEmptyDirectory(postlinkDir); + + //copy meadow app + File.Copy(meadowApp.FullName, Path.Combine(prelinkDir, meadowApp.Name), overwrite: true); + + //copy dependencies and optional pdbs from the local folder and the meadow assemblies folder + foreach (var dependency in dependencies) + { + var destination = Path.Combine(prelinkDir, Path.GetFileName(dependency)); + File.Copy(dependency, destination, overwrite: true); + + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(dependency, "pdb"); + if (File.Exists(pdbFile)) + { + destination = Path.ChangeExtension(destination, "pdb"); + File.Copy(pdbFile, destination, overwrite: true); + } + } + } + } + + public async Task?> TrimMeadowApp( + FileInfo file, + IList? noLink) + { + //set up the paths + var prelink_dir = Path.Combine(file.DirectoryName!, PreLinkDirectoryName); + var postlink_dir = Path.Combine(file.DirectoryName!, PostLinkDirectoryName); + var prelink_app = Path.Combine(prelink_dir, file.Name); + var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var illinker_path = Path.Combine(base_path!, IL_LINKER_DIR, IL_LINKER_DLL); + var descriptor_path = Path.Combine(base_path!, IL_LINKER_DIR, MEADOW_LINK_XML); + + //prepare _linker arguments + var no_link_args = noLink != null ? string.Join(" ", noLink.Select(o => $"-p copy \"{o}\"")) : string.Empty; + + try + { + //link the apps + await _linker.RunILLink(illinker_path, descriptor_path, no_link_args, prelink_app, prelink_dir, postlink_dir); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error trimming Meadow app"); + } + + return Directory.EnumerateFiles(postlink_dir); + } + + /// + /// This method recursively gets all dependencies for the given assembly + /// + private List GetDependencies(string assemblyPath, Collection assemblyReferences, List dependencyMap, string appDir) + { + if (dependencyMap.Contains(assemblyPath)) + { //already have this assembly mapped + return dependencyMap; + } + + dependencyMap.Add(assemblyPath); + + foreach (var reference in assemblyReferences) + { + var fullPath = FindAssemblyFullPath(reference.Name, appDir, _meadowAssembliesPath); + + Collection namedRefs = default!; + + if (fullPath == null) + { + continue; + } + namedRefs = GetAssemblyReferences(fullPath); + + //recursive! + dependencyMap = GetDependencies(fullPath!, namedRefs!, dependencyMap, appDir); + } + + return dependencyMap.Where(x => x.Contains("App.") == false).ToList(); + } + + static string? FindAssemblyFullPath(string fileName, string localPath, string meadowAssembliesPath) + { + //Assembly may not have a file extension, add .dll if it doesn't + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") + { + fileName += ".dll"; + } + + //meadow assemblies path + if (File.Exists(Path.Combine(meadowAssembliesPath, fileName))) + { + return Path.Combine(meadowAssembliesPath, fileName); + } + + //localPath + if (File.Exists(Path.Combine(localPath, fileName))) + { + return Path.Combine(localPath, fileName); + } + + return null; + } + + private Collection GetAssemblyReferences(string assemblyPath) + { + using var definition = AssemblyDefinition.ReadAssembly(assemblyPath); + return definition.MainModule.AssemblyReferences; + } + + private void CreateEmptyDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, recursive: true); + } + Directory.CreateDirectory(directoryPath); + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/Program.cs b/Source/TestApps/LinkerTest/LinkerTest/Program.cs new file mode 100644 index 00000000..f0f60768 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/Program.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; + +namespace LinkerTest +{ + internal class Program + { + private static readonly string _meadowAssembliesPath = @"C:\Users\adria\AppData\Local\WildernessLabs\Firmware\1.6.0.1\meadow_assemblies\"; + + static async Task Main(string[] args) + { + Console.WriteLine("Hello, World!"); + + // await OtherLink(); + + // return; + + var linker = new MeadowLinker(_meadowAssembliesPath); + + string fileToLink = @"H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\App.dll"; + + await linker.Trim(new FileInfo(fileToLink), true); + } + + static async Task OtherLink() + { + var monolinker_args = @"""H:\WL\Meadow.CLI\Meadow.CLI.Classic\bin\Debug\lib\illink.dll"" -x ""H:\WL\Meadow.CLI\Meadow.CLI.Classic\bin\Debug\lib\meadow_link.xml"" --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\postlink_bin"" -r ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin\App.dll"" -a ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin\Meadow.dll"" -d ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin"""; + + Console.WriteLine("Trimming assemblies to reduce size (may take several seconds)..."); + + using (var process = new Process()) + { + process.StartInfo.FileName = "dotnet"; + process.StartInfo.Arguments = monolinker_args; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + + // To avoid deadlocks, read the output stream first and then wait + string stdOutReaderResult; + using (StreamReader stdOutReader = process.StandardOutput) + { + stdOutReaderResult = await stdOutReader.ReadToEndAsync(); + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + } + } + } + } +} diff --git a/Source/TestApps/LinkerTest/LinkerTest/Readme.md b/Source/TestApps/LinkerTest/LinkerTest/Readme.md new file mode 100644 index 00000000..52c7eaf0 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/Readme.md @@ -0,0 +1,41 @@ +Attempting to diagnose why adding nuget packages is changing the behavior of illink when trimming libraries. + +Notes: +It appears the Mono.Cecil libraries are version 0.11.2 +We do need the nugets to access the Cecil APIs (could dynamically load the lib), I vote we keep the nuget and the libs in sync + +Safe nugets: +Mono.Cecil 0.11.2 +Newtonsoft.Json 13.0.3 +System.Runtime.CompilerServices.Unsafe 6.0.0 +Microsoft.Extensions.DependencyInjection +System.Management + + +Unsafe nugets: +Microsoft.Extensions.Configuration.Json + +Impacts all extension libraries, netstandard.dll, system.core.dll, system.dll +This is probably the one that breaks the world + + +Serilog - impacts Microsoft.Extensions.Primitives + + +Microsoft.Extensions.Logging 7.0.0 +Microsoft.Extensions.Logging.Abstractions 8.0.0 + +Together change the linking behavior of: +Microsoft.Extensions.Configuration +Microsoft.Extensions.Configuration.Abstractions +Microsoft.Extensions.Configuration.FileExtensions +Microsoft.Extensions.Configuration.Primitives + + +Adding a reference to Meadow.CLI.Core also impacts the linking behavior of the above libraries and causes three libraries to be linked out completely + +Microsoft.Bcl.AsyncInterfaces +System.Buffers +System.Threading.Tasks.Extensions + + diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.Pdb.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.Pdb.dll new file mode 100644 index 0000000000000000000000000000000000000000..d6f5ee58c08204959957f7168aefc85fea9769cb GIT binary patch literal 114176 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 RWV^t>eWm|j;QvAg{sSvy`wRd8 literal 0 HcmV?d00001 diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.dll new file mode 100644 index 0000000000000000000000000000000000000000..682aef81ff4b3bab63113e63c8ae9e53b8077d19 GIT binary patch literal 499200 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%0APFdTIg%$_o8)Vy*rl#4%(MJpX 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 literal 0 HcmV?d00001 diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json new file mode 100644 index 00000000..d7b91143 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json @@ -0,0 +1,112 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v3.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v3.0": { + "illink/5.0.0-dev": { + "dependencies": { + "Microsoft.Net.Compilers.Toolset": "3.8.0-4.20503.2", + "Microsoft.SourceLink.AzureRepos.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.GitHub": "1.1.0-beta-20206-02", + "Mono.Cecil": "0.11.2", + "Mono.Cecil.Pdb": "0.11.2", + "XliffTasks": "1.0.0-beta.20502.2" + }, + "runtime": { + "illink.dll": {} + } + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": {}, + "Microsoft.Net.Compilers.Toolset/3.8.0-4.20503.2": {}, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": {}, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "XliffTasks/1.0.0-beta.20502.2": {}, + "Mono.Cecil/0.11.2": { + "runtime": { + "Mono.Cecil.dll": {} + } + }, + "Mono.Cecil.Pdb/0.11.2": { + "dependencies": { + "Mono.Cecil": "0.11.2" + }, + "runtime": { + "Mono.Cecil.Pdb.dll": {} + } + } + } + }, + "libraries": { + "illink/5.0.0-dev": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hZ9leS9Yd9MHpqvviMftSJFDcLYu2h1DrapW1TDm1s1fgOy71c8HvArNMd3fseVkXmp3VTfGnkgcw0FR+TI6xw==", + "path": "microsoft.build.tasks.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.build.tasks.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.Net.Compilers.Toolset/3.8.0-4.20503.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-jfscID/5IHHPVVEbFCAJEUEWCeWNZCLwyBcUFG3/u44oiRd/aseDOYRzl3OnIIvcwzi0U2lSAs6Lt2+rdRIDMg==", + "path": "microsoft.net.compilers.toolset/3.8.0-4.20503.2", + "hashPath": "microsoft.net.compilers.toolset.3.8.0-4.20503.2.nupkg.sha512" + }, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-vVYhSds9TfraTQkGHHMDMVWnr3kCkTZ7vmqUmrXQBDJFXiWTuMoP5RRa9s1M/KmgB4szi5TOb7sOaHWKDT9qDA==", + "path": "microsoft.sourcelink.azurerepos.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.azurerepos.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aek0RTQ+4Bf11WvqaXajwYoaBWkX2edBjAr5XJOvhAsHX6/9vPOb7IpHAiE/NyCse7IcpGWslJZHNkv4UBEFqw==", + "path": "microsoft.sourcelink.common/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.common.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-7A7P0EwL+lypaI/CEvG4IcpAlQeAt04uPPw1SO6Q9Jwz2nE9309pQXJ4TfP/RLL8IOObACidN66+gVR+bJDZHw==", + "path": "microsoft.sourcelink.github/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.github.1.1.0-beta-20206-02.nupkg.sha512" + }, + "XliffTasks/1.0.0-beta.20502.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fnLroyas9Lfo7+YWFHjfAALbTODgNDY4z8GB4uT9OKiqWwYje/bcW5QJuRCWCkGtC1uuAx9oxNYH/MZ9G9/fmw==", + "path": "xlifftasks/1.0.0-beta.20502.2", + "hashPath": "xlifftasks.1.0.0-beta.20502.2.nupkg.sha512" + }, + "Mono.Cecil/0.11.2": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Mono.Cecil.Pdb/0.11.2": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/illink.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.dll new file mode 100644 index 0000000000000000000000000000000000000000..f392213b13c7b453f5b1671a037526ea7363c162 GIT binary patch literal 466944 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 + /// This method recursively gets all dependencies for the given assembly + /// + private List GetDependencies(string assemblyPath, Collection assemblyReferences, List dependencyMap, string appDir) + { + if (dependencyMap.Contains(assemblyPath)) + { //already have this assembly mapped + return dependencyMap; + } + + dependencyMap.Add(assemblyPath); + + foreach (var reference in assemblyReferences) + { + var fullPath = FindAssemblyFullPath(reference.Name, appDir, _meadowAssembliesPath); + + Collection namedRefs = default!; + + if (fullPath == null) + { + continue; + } + namedRefs = GetAssemblyReferences(fullPath); + + //recursive! + dependencyMap = GetDependencies(fullPath!, namedRefs!, dependencyMap, appDir); + } + + return dependencyMap.Where(x => x.Contains("App.") == false).ToList(); + } + + static string? FindAssemblyFullPath(string fileName, string localPath, string meadowAssembliesPath) + { + //Assembly may not have a file extension, add .dll if it doesn't + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") + { + fileName += ".dll"; + } + + //meadow assemblies path + if (File.Exists(Path.Combine(meadowAssembliesPath, fileName))) + { + return Path.Combine(meadowAssembliesPath, fileName); + } + + //local path + if (File.Exists(Path.Combine(localPath, fileName))) + { + return Path.Combine(localPath, fileName); + } + + return null; + } + + private Collection GetAssemblyReferences(string assemblyPath) + { + using var definition = AssemblyDefinition.ReadAssembly(assemblyPath); + return definition.MainModule.AssemblyReferences; + } + + private void CreateEmptyDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, recursive: true); + } + Directory.CreateDirectory(directoryPath); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index c2c59fee..c6fc59f0 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -8,10 +8,10 @@ Wilderness Labs, Inc meadow WildernessLabs.Meadow.CLI - Chris Tacke, Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis + Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0-alpha.4 + 2.0.0.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png @@ -30,11 +30,6 @@ README.md

oWoE0{;WQWKS2#dqHO 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%0DbE7Pb(JM;gls>Rd~_TL`* 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 literal 0 HcmV?d00001 diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json new file mode 100644 index 00000000..617ab505 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json @@ -0,0 +1,10 @@ +{ + "runtimeOptions": { + "tfm": "netcoreapp3.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "3.0.0" + }, + "rollForwardOnNoCandidateFx": 2 + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml b/Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml new file mode 100644 index 00000000..0fe29752 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj index 8e75c458..b5cbc219 100644 --- a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -6,12 +6,8 @@ enable - - 4 - true - - + diff --git a/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs deleted file mode 100644 index 2c0f9d66..00000000 --- a/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseAppCommand : BaseDeviceCommand -{ - protected IPackageManager _packageManager; - - public BaseAppCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - _packageManager = packageManager; - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs b/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs deleted file mode 100644 index 744a62e3..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs +++ /dev/null @@ -1,27 +0,0 @@ - -using CliFx.Infrastructure; - -namespace Meadow.CLI -{ - public class ConsoleSpinner - { - private int counter = 0; - private char[] sequence = { '|', '/', '-', '\\' }; - private IConsole console; - - public ConsoleSpinner(IConsole console) - { - this.console = console; - } - - public async Task Turn(int delay = 100, CancellationToken cancellationToken = default) - { - while (!cancellationToken.IsCancellationRequested) - { - counter++; - console?.Output.WriteAsync($"{sequence[counter % 4]} \r"); - await Task.Delay(delay, CancellationToken.None); // Not propogating the token intentionally. - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs deleted file mode 100644 index 65275656..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs +++ /dev/null @@ -1,124 +0,0 @@ - -namespace Meadow.CLI -{ - public static class ExtensionMethods - { - public const string ConsoleColourBlack = "\u001b[30m"; - public const string ConsoleColourBlue = "\u001b[34m"; - public const string ConsoleColourCyan = "\u001b[36m"; - public const string ConsoleColourGreen = "\u001b[32m"; - public const string ConsoleColourMagenta = "\u001b[35m"; - public const string ConsoleColourRed = "\u001b[31m"; - public const string ConsoleColourReset = "\u001b[0m"; - public const string ConsoleColourWhite = "\u001b[37m"; - public const string ConsoleColourYellow = "\u001b[33m"; - - public static string ColourConsoleText(this string textToColour, string textColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return textColour + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextBlack(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourBlack + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextCyan(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourCyan + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextBlue(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourBlue + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextGreen(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourGreen + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextMagenta(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourMagenta + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextRed(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourRed + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextWhite(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourWhite + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextYellow(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourYellow + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs b/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs deleted file mode 100644 index 68fbca39..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CliFx.Infrastructure; - -namespace Meadow.CLI -{ - public static class UITaskExtensions - { - public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) - { - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(console); - - var consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); - - try - { - await task; - } - finally - { - // Cancel the spinner when the original task completes - spinnerCancellationTokenSource.Cancel(); - - // Let's wait for the spinner to finish - await consoleSpinnerTask; - } - } - - public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) - { - // Get our spinner read - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(console); - - Task consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); - - try - { - return await task; - } - finally - { - // Cancel the spinner when the original task completes - spinnerCancellationTokenSource.Cancel(); - - // Let's wait for the spinner to finish - await consoleSpinnerTask; - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Linker/ILLinker.cs b/Source/v2/Meadow.CLI/Linker/ILLinker.cs new file mode 100644 index 00000000..825aee14 --- /dev/null +++ b/Source/v2/Meadow.CLI/Linker/ILLinker.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace LinkerTest +{ + internal class ILLinker + { + readonly ILogger? _logger; + + public ILLinker(ILogger? logger = null) + { + _logger = logger; + } + + public async Task RunILLink( + string illinkerDllPath, + string descriptorXmlPath, + string noLinkArgs, + string prelinkAppPath, + string prelinkDir, + string postlinkDir) + { + if (!File.Exists(illinkerDllPath)) + { + throw new FileNotFoundException("Cannot run trimming operation, illink.dll not found"); + } + + //original + //var monolinker_args = $"\"{illinkerDllPath}\" -x \"{descriptorXmlPath}\" {noLinkArgs} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlinkDir}\" -r \"{prelinkAppPath}\" -a \"{prelink_os}\" -d \"{prelinkDir}\""; + + var monolinker_args = $"\"{illinkerDllPath}\"" + + $" -x \"{descriptorXmlPath}\" " + //link files in the descriptor file (needed) + $"{noLinkArgs} " + //arguments to skip linking - will be blank if we are linking + $"-r \"{prelinkAppPath}\" " + //link the app in the prelink folder (needed) + $"--skip-unresolved true " + //skip unresolved references (needed -hangs without) + $"--deterministic true " + //make deterministic (to avoid pushing unchanged files to the device) + $"--keep-facades true " + //keep facades (needed - will skip key libs without) + $"-b true " + //Update debug symbols for each linked module (needed - will skip key libs without) + $"-o \"{postlinkDir}\" " + //output directory + + + //old + //$"--ignore-descriptors false " + //ignore descriptors (doesn't appear to impact behavior) + //$"-c link " + //link framework assemblies + //$"-d \"{prelinkDir}\"" //additional folder to link (not needed) + + //experimental + //$"--explicit-reflection true " + //enable explicit reflection (throws an exception with it) + //$"--keep-dep-attributes true " + //keep dependency attributes (files are slightly larger with, doesn't fix dependency issue) + ""; + + _logger?.Log(LogLevel.Information, "Trimming assemblies"); + + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Directory.GetDirectoryRoot(illinkerDllPath); + process.StartInfo.FileName = "dotnet"; + process.StartInfo.Arguments = monolinker_args; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + + // To avoid deadlocks, read the output stream first and then wait + string stdOutReaderResult; + using (StreamReader stdOutReader = process.StandardOutput) + { + stdOutReaderResult = await stdOutReader.ReadToEndAsync(); + + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + + _logger?.Log(LogLevel.Debug, "StandardOutput Contains: " + stdOutReaderResult); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + _logger?.Log(LogLevel.Debug, $"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + throw new Exception("Trimming failed"); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs b/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs new file mode 100644 index 00000000..68e64d0c --- /dev/null +++ b/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs @@ -0,0 +1,175 @@ +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; +using System.Reflection; + +namespace LinkerTest; + +public class MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) +{ + private const string IL_LINKER_DIR = "lib"; + private const string IL_LINKER_DLL = "illink.dll"; + private const string MEADOW_LINK_XML = "meadow_link.xml"; + + private const string PostLinkDirectoryName = "postlink_bin"; + private const string PreLinkDirectoryName = "prelink_bin"; + + readonly ILLinker _linker = new ILLinker(logger); + readonly ILogger? _logger = logger; + + //ToDo ... might need to make this a property or pass it in when used + private readonly string _meadowAssembliesPath = meadowAssembliesPath; + + public async Task Trim( + FileInfo meadowAppFile, + bool includePdbs = false, + IList? noLink = null) + { + var dependencies = MapDependencies(meadowAppFile); + + CopyDependenciesToPreLinkFolder(meadowAppFile, dependencies, includePdbs); + + await TrimMeadowApp(meadowAppFile, noLink); + } + + public List MapDependencies(FileInfo meadowAppFile) + { + //get all dependencies in meadowAppFile and exclude the Meadow App + var dependencyMap = new List(); + + var appRefs = GetAssemblyReferences(meadowAppFile.FullName); + return GetDependencies(meadowAppFile.FullName, appRefs, dependencyMap, meadowAppFile.DirectoryName); + } + + public void CopyDependenciesToPreLinkFolder( + FileInfo meadowApp, + List dependencies, + bool includePdbs) + { + //set up the paths + var prelinkDir = Path.Combine(meadowApp.DirectoryName!, PreLinkDirectoryName); + var postlinkDir = Path.Combine(meadowApp.DirectoryName!, PostLinkDirectoryName); + + //create output directories + CreateEmptyDirectory(prelinkDir); + CreateEmptyDirectory(postlinkDir); + + //copy meadow app + File.Copy(meadowApp.FullName, Path.Combine(prelinkDir, meadowApp.Name), overwrite: true); + + //copy dependencies and optional pdbs from the local folder and the meadow assemblies folder + foreach (var dependency in dependencies) + { + var destination = Path.Combine(prelinkDir, Path.GetFileName(dependency)); + File.Copy(dependency, destination, overwrite: true); + + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(dependency, "pdb"); + if (File.Exists(pdbFile)) + { + destination = Path.ChangeExtension(destination, "pdb"); + File.Copy(pdbFile, destination, overwrite: true); + } + } + } + } + + private async Task> TrimMeadowApp( + FileInfo file, + IList? noLink) + { + //set up the paths + var prelink_dir = Path.Combine(file.DirectoryName!, PreLinkDirectoryName); + var postlink_dir = Path.Combine(file.DirectoryName!, PostLinkDirectoryName); + var prelink_app = Path.Combine(prelink_dir, file.Name); + var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var illinker_path = Path.Combine(base_path!, IL_LINKER_DIR, IL_LINKER_DLL); + var descriptor_path = Path.Combine(base_path!, IL_LINKER_DIR, MEADOW_LINK_XML); + + //prepare _linker arguments + var no_link_args = noLink != null ? string.Join(" ", noLink.Select(o => $"-p copy \"{o}\"")) : string.Empty; + + try + { + //link the apps + await _linker.RunILLink(illinker_path, descriptor_path, no_link_args, prelink_app, prelink_dir, postlink_dir); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error trimming Meadow app"); + } + + return Directory.EnumerateFiles(postlink_dir); + } + + ///

- - 4 - true - 1701;1702 - @@ -51,12 +46,12 @@ + + - - diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 91bb5951..e587e6bf 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -1,265 +1,116 @@ -using System.Drawing; -using System.Threading; -using Meadow.Hcom; -using Meadow.Software; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI; - -public static class AppManager -{ - static string[] dllLinkIngoreList = { "System.Threading.Tasks.Extensions.dll" };//, "Microsoft.Extensions.Primitives.dll" }; - static string[] pdbLinkIngoreList = { "System.Threading.Tasks.Extensions.pdb" };//, "Microsoft.Extensions.Primitives.pdb" }; - - private static bool MatchingDllExists(string file) - { - var root = Path.GetFileNameWithoutExtension(file); - return File.Exists($"{root}.dll"); - } - - private static bool IsPdb(string file) - { - return string.Compare(Path.GetExtension(file), ".pdb", true) == 0; - } - - private static bool IsXmlDoc(string file) - { - if (string.Compare(Path.GetExtension(file), ".xml", true) == 0) - { - return MatchingDllExists(file); - } - return false; - } - - public static async Task?> GenerateDeployList(IPackageManager packageManager, - string localBinaryDirectory, - bool includePdbs, - bool includeXmlDocs, - ILogger? logger, - CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested - || string.IsNullOrWhiteSpace(packageManager.MeadowAssembliesPath) - || !Directory.Exists(packageManager.MeadowAssembliesPath)) - { - return null; - } - - // TODO: add sub-folder support when HCOM supports it - - logger?.LogInformation($"Generating the list of files to deploy from {localBinaryDirectory}..."); - - var localFiles = new Dictionary(); - - var auxiliary = Directory.EnumerateFiles(localBinaryDirectory, "*.*", SearchOption.TopDirectoryOnly) - .Where(s => new FileInfo(s).Extension != ".dll") - .Where(s => new FileInfo(s).Extension != ".pdb") - .Where(s => !s.Contains(".DS_Store")); - - foreach (var item in auxiliary) - { - var file = Path.Combine(localBinaryDirectory, item); - if (File.Exists(file)) - { - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - } - - if (packageManager.Trimmed && packageManager.TrimmedDependencies != null) - { - var trimmedDependencies = packageManager.TrimmedDependencies - .Where(x => dllLinkIngoreList.Any(f => x.Contains(f)) == false) - .Where(x => pdbLinkIngoreList.Any(f => x.Contains(f)) == false) - .ToList(); - - // Crawl trimmed dependencies - foreach (var file in trimmedDependencies) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - - // Add the Dlls from the TrimmingIgnorelist - for (int i = 0; i < dllLinkIngoreList.Length; i++) - { - //add the files from the dll link ignore list - if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(dllLinkIngoreList[i]))) - { - var dllfound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(dllLinkIngoreList[i])); - if (!string.IsNullOrEmpty(dllfound)) - { - await AddToLocalFiles(localFiles, dllfound, includePdbs, includeXmlDocs, cancellationToken); - } - } - } - - if (includePdbs) - { - for (int i = 0; i < pdbLinkIngoreList.Length; i++) - { - //add the files from the pdb link ignore list - if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(pdbLinkIngoreList[i]))) - { - var pdbFound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(pdbLinkIngoreList[i])); - if (!string.IsNullOrEmpty(pdbFound)) - { - await AddToLocalFiles(localFiles, pdbFound, includePdbs, includeXmlDocs, cancellationToken); - } - } - } - } - } - else - { - foreach (var file in packageManager.AssemblyDependencies!) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - // TODO: add any other filtering capability here - - //Populate out LocalFile Dictionary with this entry - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - } - - if (localFiles?.Count() == 0) - { - logger?.LogInformation($"No new files to deploy"); - } - - logger?.LogInformation("Done."); - - return localFiles; - } - - public static async Task DeployApplication( - IMeadowConnection connection, - Dictionary localFiles, - ILogger? logger, - CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - // get a list of files on-device, with CRCs - var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); - - // get a list of files of the device files that are not in the list we intend to deploy - var removeFiles = deviceFiles - .Select(f => Path.GetFileName(f.Name)) - .Except(localFiles.Keys - .Select(f => Path.GetFileName(f))); - - if (removeFiles.Count() == 0) - { - logger?.LogInformation($"No files to delete"); - } - - // delete those files - foreach (var file in removeFiles) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - logger?.LogInformation($"Deleting file '{file}'..."); - await connection.DeleteFile(file, cancellationToken); - } - - // now send all files with differing CRCs - foreach (var localFile in localFiles) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - if (!File.Exists(localFile.Key)) - { - logger?.LogInformation($"{localFile.Key} not found" + Environment.NewLine); - continue; - } - - var filename = Path.GetFileName(localFile.Key); - - var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == filename); - - if (existing != null && existing.Crc != null) - { - var remoteCrc = uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber); - var localCrc = localFile.Value; - - // do the file name and CRC match? - if (remoteCrc == localCrc) - { - // exists and has a matching CRC, skip it - logger?.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); - continue; - } - } - - bool success; - - do - { - try - { - // If we've cancelled bail - if (cancellationToken.IsCancellationRequested) - { - return; - } - - if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) - { - logger?.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}'. Retrying."); - await Task.Delay(100); - success = false; - } - else - { - success = true; - } - } - catch (Exception ex) - { - logger?.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}' ({ex.Message}). Retrying."); - await Task.Delay(100); - success = false; - } - - } while (!success); - } - - // Delay to receive last successful write message. - await Task.Delay(330); - } - - private static async Task AddToLocalFiles(Dictionary localFiles, string file, bool includePdbs, bool includeXmlDocs, CancellationToken cancellationToken) - { - if (!includePdbs && IsPdb(file)) - return; - if (!includeXmlDocs && IsXmlDoc(file)) - return; - - // read the file data so we can generate a CRC - using FileStream fs = File.Open(file, FileMode.Open); - var len = (int)fs.Length; - var bytes = new byte[len]; - - await fs.ReadAsync(bytes, 0, len, cancellationToken); - - var crc = CrcTools.Crc32part(bytes, len, 0); - - if (!localFiles.ContainsKey(file)) - localFiles.Add(file, crc); - } -} \ No newline at end of file +using Meadow.Hcom; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI; + +public static class AppManager +{ + private static bool MatchingDllExists(string file) + { + var root = Path.GetFileNameWithoutExtension(file); + return File.Exists($"{root}.dll"); + } + + private static bool IsPdb(string file) + { + return string.Compare(Path.GetExtension(file), ".pdb", true) == 0; + } + + private static bool IsXmlDoc(string file) + { + if (string.Compare(Path.GetExtension(file), ".xml", true) == 0) + { + return MatchingDllExists(file); + } + return false; + } + + public static async Task DeployApplication( + IPackageManager packageManager, + IMeadowConnection connection, + string localBinaryDirectory, + bool includePdbs, + bool includeXmlDocs, + ILogger logger, + CancellationToken cancellationToken) + { + // TODO: add sub-folder support when HCOM supports it + + var localFiles = new Dictionary(); + + // get a list of files to send + var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); + dependencies.Add(Path.Combine(localBinaryDirectory, "App.dll")); + + logger.LogInformation("Generating the list of files to deploy..."); + foreach (var file in dependencies) + { + // TODO: add any other filtering capability here + + if (!includePdbs && IsPdb(file)) continue; + if (!includeXmlDocs && IsXmlDoc(file)) continue; + + // read the file data so we can generate a CRC + using FileStream fs = File.Open(file, FileMode.Open); + var len = (int)fs.Length; + var bytes = new byte[len]; + + await fs.ReadAsync(bytes, 0, len, cancellationToken); + + var crc = CrcTools.Crc32part(bytes, len, 0); + + localFiles.Add(file, crc); + } + + if (localFiles.Count() == 0) + { + logger.LogInformation($"No new files to deploy"); + } + + // get a list of files on-device, with CRCs + var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); + + // get a list of files of the device files that are not in the list we intend to deploy + var removeFiles = deviceFiles + .Select(f => Path.GetFileName(f.Name)) + .Except(localFiles.Keys + .Select(f => Path.GetFileName(f))); + + if (removeFiles.Count() == 0) + { + logger.LogInformation($"No files to delete"); + } + + // delete those files + foreach (var file in removeFiles) + { + logger.LogInformation($"Deleting file '{file}'..."); + await connection.DeleteFile(file, cancellationToken); + } + + // now send all files with differing CRCs + foreach (var localFile in localFiles) + { + var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == Path.GetFileName(localFile.Key)); + + if (existing != null) + { + if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) + { + // exists and has a matching CRC, skip it + continue; + } + } + + send_file: + + if (!await connection?.WriteFile(localFile.Key, null, cancellationToken)) + { + logger.LogWarning($"Error sending'{Path.GetFileName(localFile.Key)}'. Retrying."); + await Task.Delay(100); + goto send_file; + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 7464c009..711f07ab 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -1,5 +1,7 @@ using CliFx.Attributes; +using CliFx.Infrastructure; using Meadow.CLI; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -23,40 +25,35 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact protected override async ValueTask ExecuteCommand() { - await Task.Run(async () => - { - string path = Path == null - ? Environment.CurrentDirectory + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory : Path; - // is the path a file? - if (!File.Exists(path)) + // is the path a file? + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) { - // is it a valid directory? - if (!Directory.Exists(path)) - { - Logger?.LogError($"Invalid application path '{path}'"); - return; - } + Logger?.LogError($"Invalid application path '{path}'"); + return; } + } - if (Configuration == null) - Configuration = "Release"; + if (Configuration == null) Configuration = "Release"; - Logger?.LogInformation($"Building {Configuration} configuration of {path} (this may take a few seconds)..."); + Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); - // TODO: enable cancellation of this call - var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)) - .WithSpinner(Console!); + // TODO: enable cancellation of this call + var success = _packageManager.BuildApplication(path, Configuration); - if (!success) - { - Logger?.LogError($"Build failed!"); - } - else - { - Logger?.LogError($"Build success."); - } - }); + if (!success) + { + Logger?.LogError($"Build failed!"); + } + else + { + Logger?.LogError($"Build success."); + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index 6ec201cf..a20fcb83 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -19,11 +19,22 @@ public AppDebugCommand(MeadowConnectionManager connectionManager, ILoggerFactory protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - using (var server = await Connection.StartDebuggingSession(Port, Logger, CancellationToken)) + Logger?.LogError($"No connection path is defined"); + return; + } + + if (connection != null) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) { if (Console != null) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 0418b47e..a8125b30 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,26 +1,34 @@ using CliFx.Attributes; +using Meadow.CLI; + using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; [Command("app deploy", Description = "Deploys a built Meadow application to a target device")] -public class AppDeployCommand : BaseAppCommand +public class AppDeployCommand : BaseDeviceCommand { - private string lastFile = string.Empty; + private readonly IPackageManager _packageManager; [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] public string? Path { get; set; } = default!; public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) + : base(connectionManager, loggerFactory) { + _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } - if (Connection != null) + if (connection != null) { string path = Path == null ? Environment.CurrentDirectory @@ -29,18 +37,31 @@ protected override async ValueTask ExecuteCommand() // is the path a file? FileInfo file; - lastFile = string.Empty; + var lastFile = string.Empty; // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); - + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); if (wasRuntimeEnabled) { Logger?.LogInformation("Disabling runtime..."); - await Connection.RuntimeDisable(CancellationToken); + await connection.RuntimeDisable(CancellationToken); } + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + + if (e.fileName != lastFile) + { + Console?.Output.WriteAsync("\n"); + lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + }; + if (!File.Exists(path)) { // is it a valid directory? @@ -74,50 +95,15 @@ protected override async ValueTask ExecuteCommand() var targetDirectory = file.DirectoryName; - if (Logger != null && !string.IsNullOrEmpty(targetDirectory)) - { - var trimApplicationCommand = new AppTrimCommand(_packageManager, ConnectionManager, LoggerFactory!) - { - Path = path, - }; - await trimApplicationCommand.ExecuteAsync(Console!); - - var localFiles = await AppManager.GenerateDeployList(_packageManager, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken) - .WithSpinner(Console!); - Console?.Output.WriteAsync("\n"); - - if (localFiles != null && localFiles.Count > 0) - { - Connection.FileWriteProgress += Connection_FileWriteProgress; - - await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); - Console?.Output.WriteAsync("\n"); - - Connection.FileWriteProgress -= Connection_FileWriteProgress; - } - } + await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, CancellationToken); if (wasRuntimeEnabled) { // restore runtime state - Logger?.LogInformation("Enabling runtime..."); + Logger.LogInformation("Enabling runtime..."); - await Connection.RuntimeEnable(CancellationToken); + await connection.RuntimeEnable(CancellationToken); } } } - - private void Connection_FileWriteProgress(object? sender, (string fileName, long completed, long total) e) - { - var p = (e.completed / (double)e.total) * 100d; - - if (e.fileName != lastFile) - { - Console?.Output.WriteAsync("\n"); - lastFile = e.fileName; - } - - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 9ebf65dd..d5a3ba85 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -1,12 +1,16 @@ using CliFx.Attributes; +using Meadow.CLI; using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; [Command("app run", Description = "Builds, trims and deploys a Meadow application to a target device")] -public class AppRunCommand : BaseAppCommand +public class AppRunCommand : BaseDeviceCommand { + private readonly IPackageManager _packageManager; + private string _lastFile; + [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] public bool NoPrefix { get; set; } @@ -17,61 +21,67 @@ public class AppRunCommand : BaseAppCommand public string? Path { get; set; } = default!; public AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) + : base(connectionManager, loggerFactory) { + _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); - - string path = Path == null - ? Environment.CurrentDirectory - : Path; + var connection = await GetCurrentConnection(); - if (!Directory.Exists(path)) + if (connection == null) { - Logger?.LogError($"Target directory '{path}' not found."); + Logger?.LogError($"No connection path is defined"); return; } - var lastFile = string.Empty; - - var buildmApplicationCommand = new AppBuildCommand(_packageManager, LoggerFactory!) + if (connection != null) { - Path = path - }; - await buildmApplicationCommand.ExecuteAsync(Console!); + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + if (!Directory.Exists(path)) + { + Logger?.LogError($"Target directory '{path}' not found."); + return; + } - if (Connection != null) - { - // illink returns before all files are actually written. That's not fun, but we must just wait a little while. - // disabling the runtime provides us that time + var lastFile = string.Empty; // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling runtime..."); - Logger?.LogInformation("Disabling runtime..."); + await connection.RuntimeDisable(CancellationToken); + } - await Connection.RuntimeDisable(CancellationToken); + if (!await BuildApplication(path, CancellationToken)) + { + return; + } - if (Connection is SerialConnection s) + if (!await TrimApplication(path, CancellationToken)) { - s.CommandTimeoutSeconds = 60; + return; } - var deployApplication = new AppDeployCommand(_packageManager, ConnectionManager, LoggerFactory!) + // illink returns before all files are actually written. That's not fun, but we must just wait a little while. + await Task.Delay(1000); + + if (!await DeployApplication(connection, path, CancellationToken)) { - Path = path - }; - await deployApplication.ExecuteAsync(Console!); + return; + } Logger?.LogInformation("Enabling the runtime..."); - await Connection.RuntimeEnable(CancellationToken); + await connection.RuntimeEnable(CancellationToken); Logger?.LogInformation("Listening for messages from Meadow...\n"); - Connection.DeviceMessageReceived += OnDeviceMessageReceived; + connection.DeviceMessageReceived += OnDeviceMessageReceived; while (!CancellationToken.IsCancellationRequested) { @@ -82,6 +92,74 @@ protected override async ValueTask ExecuteCommand() } } + private Task BuildApplication(string path, CancellationToken cancellationToken) + { + if (Configuration == null) Configuration = "Debug"; + + Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); + + // TODO: enable cancellation of this call + return Task.FromResult(_packageManager.BuildApplication(path, Configuration)); + } + + private async Task TrimApplication(string path, CancellationToken cancellationToken) + { + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return false; + } + + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + + // if no configuration was provided, find the most recently built + Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + + await _packageManager.TrimApplication(file, false, null, cancellationToken); + + return true; + } + + private async Task DeployApplication(IMeadowConnection connection, string path, CancellationToken cancellationToken) + { + connection.FileWriteProgress += OnFileWriteProgress; + + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return false; + } + + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + + Logger?.LogInformation($"Deploying app from {file.DirectoryName}..."); + + await AppManager.DeployApplication(_packageManager, connection, file.DirectoryName, true, false, Logger, CancellationToken); + + connection.FileWriteProgress -= OnFileWriteProgress; + + return true; + } + + private void OnFileWriteProgress(object? sender, (string fileName, long completed, long total) e) + { + var p = (e.completed / (double)e.total) * 100d; + + if (e.fileName != _lastFile) + { + Console?.Output.Write("\n"); + _lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + } + private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) { if (NoPrefix) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index d136ece0..5d35d922 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,84 +1,64 @@ -using System.Threading; -using CliFx.Attributes; -using Meadow.CLI; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] -public class AppTrimCommand : BaseAppCommand -{ - [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] - public string? Configuration { get; set; } - - [CommandParameter(0, Name = "Path to project file", IsRequired = false)] - public string? Path { get; set; } = default!; - - public AppTrimCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) - { - } - - protected override async ValueTask ExecuteCommand() - { - await base.ExecuteCommand(); - - string path = Path == null - ? Environment.CurrentDirectory - : Path; - - // is the path a file? - FileInfo file; - - if (!File.Exists(path)) - { - // is it a valid directory? - if (!Directory.Exists(path)) - { - Logger?.LogError($"Invalid application path '{path}'"); - return; - } - - // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{path}'"); - return; - } - - file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - } - else - { - file = new FileInfo(path); - } - - // Find RuntimeVersion - if (Connection != null) - { - var info = await Connection.GetDeviceInfo(CancellationToken); - - _packageManager.RuntimeVersion = info?.RuntimeVersion; - - if (!string.IsNullOrWhiteSpace(_packageManager.MeadowAssembliesPath) && Directory.Exists(_packageManager.MeadowAssembliesPath)) - { - Logger?.LogInformation($"Using runtime files from {_packageManager.MeadowAssembliesPath}"); - - // Avoid double reporting. - DetachMessageHandlers(Connection); - } - else - { - Logger?.LogError($"Meadow Assemblies Path: '{_packageManager.MeadowAssembliesPath}' does NOT exist for Runtime: '{_packageManager.RuntimeVersion}'."); - return; - } - - } - - // TODO: support `nolink` command line args - await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken) - .WithSpinner(Console!); - } -} \ No newline at end of file +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] +public class AppTrimCommand : BaseCommand +{ + private IPackageManager _packageManager; + + [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] + public string? Configuration { get; set; } + + [CommandParameter(0, Name = "Path to project file", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppTrimCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand() + { + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + // is the path a file? + FileInfo file; + + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) + { + Logger?.LogError($"Invalid application path '{path}'"); + return; + } + + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return; + } + + file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + } + else + { + file = new FileInfo(path); + } + + // if no configuration was provided, find the most recently built + Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + + await _packageManager.TrimApplication(file, false, null, CancellationToken); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index cbdc3171..daccc131 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -1,72 +1,68 @@ -using System.Configuration; -using Meadow.Cloud; -using Meadow.Cloud.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseCloudCommand : BaseCommand -{ - public const string DefaultHost = "https://www.meadowcloud.co"; - - protected IdentityManager IdentityManager { get; } - protected UserService UserService { get; } - protected DeviceService DeviceService { get; } - protected CollectionService CollectionService { get; } - - public BaseCloudCommand( - IdentityManager identityManager, - UserService userService, - DeviceService deviceService, - CollectionService collectionService, - ILoggerFactory? loggerFactory) - : base(loggerFactory) - { - IdentityManager = identityManager; - UserService = userService; - DeviceService = deviceService; - CollectionService = collectionService; - } - - protected async Task ValidateOrg(string host, string? orgNameOrId = null, CancellationToken? cancellationToken = null) - { - UserOrg? org = null; - - try - { - Logger?.LogInformation("Retrieving your user and organization information..."); - - var userOrgs = await UserService.GetUserOrgs(host, cancellationToken) - .WithSpinner(Console!); - - if (!userOrgs.Any()) - { - Logger?.LogInformation($"Please visit {host} to register your account."); - } - else if (userOrgs.Count() > 1 && string.IsNullOrEmpty(orgNameOrId)) - { - Logger?.LogInformation($"You are a member of more than 1 organization. Please specify the desired orgId for this device provisioning."); - } - else if (userOrgs.Count() == 1 && string.IsNullOrEmpty(orgNameOrId)) - { - org = userOrgs.First(); - } - else - { - org = userOrgs.FirstOrDefault(o => o.Id == orgNameOrId || o.Name == orgNameOrId); - if (org == null) - { - Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{orgNameOrId}'"); - } - } - } - catch (MeadowCloudAuthException) - { - Logger?.LogError($"You must be signed in to execute this command."); - Logger?.LogError($"Please run \"meadow cloud login\" to sign in to Meadow.Cloud."); - } - - return org; - } -} \ No newline at end of file +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseCloudCommand : BaseCommand +{ + public const string DefaultHost = "https://www.meadowcloud.co"; + + protected IdentityManager IdentityManager { get; } + protected UserService UserService { get; } + protected DeviceService DeviceService { get; } + protected CollectionService CollectionService { get; } + + public BaseCloudCommand( + IdentityManager identityManager, + UserService userService, + DeviceService deviceService, + CollectionService collectionService, + ILoggerFactory? loggerFactory) + : base(loggerFactory) + { + IdentityManager = identityManager; + UserService = userService; + DeviceService = deviceService; + CollectionService = collectionService; + } + + protected async Task ValidateOrg(string host, string? orgNameOrId = null, CancellationToken? cancellationToken = null) + { + UserOrg? org = null; + + try + { + Logger?.LogInformation("Retrieving your user and organization information..."); + + var userOrgs = await UserService.GetUserOrgs(host, cancellationToken).ConfigureAwait(false); + if (!userOrgs.Any()) + { + Logger?.LogInformation($"Please visit {host} to register your account."); + } + else if (userOrgs.Count() > 1 && string.IsNullOrEmpty(orgNameOrId)) + { + Logger?.LogInformation($"You are a member of more than 1 organization. Please specify the desired orgId for this device provisioning."); + } + else if (userOrgs.Count() == 1 && string.IsNullOrEmpty(orgNameOrId)) + { + orgNameOrId = userOrgs.First().Id; + } + else + { + org = userOrgs.FirstOrDefault(o => o.Id == orgNameOrId || o.Name == orgNameOrId); + if (org == null) + { + Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{orgNameOrId}'"); + } + } + } + catch (MeadowCloudAuthException) + { + Logger?.LogError($"You must be signed in to execute this command."); + Logger?.LogError($"Please run \"meadow cloud login\" to sign in to Meadow.Cloud."); + } + + return org; + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index 076709b7..be02b31a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -1,8 +1,6 @@ using CliFx; -using CliFx.Attributes; using CliFx.Infrastructure; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace Meadow.CLI.Commands.DeviceManagement; @@ -13,9 +11,6 @@ public abstract class BaseCommand : ICommand protected IConsole? Console { get; private set; } protected CancellationToken CancellationToken { get; private set; } - [CommandOption("verbose", IsRequired = false)] - public bool Verbose { get; set; } - public BaseCommand(ILoggerFactory? loggerFactory) { LoggerFactory = loggerFactory; diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index b343f2f3..6645588b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -6,25 +6,22 @@ namespace Meadow.CLI.Commands.DeviceManagement; public abstract class BaseDeviceCommand : BaseCommand { protected MeadowConnectionManager ConnectionManager { get; } - public IMeadowConnection? Connection { get; private set; } public BaseDeviceCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(loggerFactory) { ConnectionManager = connectionManager; } - protected override async ValueTask ExecuteCommand() - { - Connection = await GetCurrentConnection(); - } - protected async Task GetCurrentConnection() { var connection = ConnectionManager.GetCurrentConnection(); if (connection != null) { - AttachMessageHandlers(connection); + connection.ConnectionError += (s, e) => + { + Logger?.LogError(e.Message); + }; try { @@ -54,61 +51,9 @@ protected override async ValueTask ExecuteCommand() } else { - Logger?.LogError("Current Connnection Unavailable"); // No connection path is defined ?? + Logger?.LogError("Current Connnection Unavailable"); } return null; } - - private void AttachMessageHandlers(IMeadowConnection? connection) - { - if (connection != null) - { - connection.ConnectionError += Connection_ConnectionError; - - connection.ConnectionMessage += Connection_ConnectionMessage; - - // the connection passes messages back to us (info about actions happening on-device) - connection.DeviceMessageReceived += Connection_DeviceMessageReceived; - } - } - - private void Connection_DeviceMessageReceived(object? sender, (string message, string? source) e) - { - if (e.message.Contains("% downloaded")) - { - // don't echo this, as we're already reporting % written - } - else if(e.source != null && e.source.Contains("stdout")) - { - // don't echo this, as we're already reporting it higher up - } - else - { - Logger?.LogInformation(e.message); - } - } - - private void Connection_ConnectionMessage(object? sender, string message) - { - Logger?.LogInformation(message); - } - - private void Connection_ConnectionError(object? sender, Exception e) - { - Logger?.LogError(e.Message); - } - - public void DetachMessageHandlers(IMeadowConnection? connection) - { - if (connection != null) - { - connection.ConnectionError -= Connection_ConnectionError; - - connection.ConnectionMessage -= Connection_ConnectionMessage; - - // the connection passes messages back to us (info about actions happening on-device) - connection.DeviceMessageReceived -= Connection_DeviceMessageReceived; - } - } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs index e995f156..09e0d728 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -7,20 +7,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public abstract class BaseFileCommand : BaseSettingsCommand { protected FileManager FileManager { get; } - protected IFirmwarePackageCollection? Collection { get; private set; } public BaseFileCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory) { FileManager = fileManager; } - - protected override async ValueTask ExecuteCommand() - { - await FileManager.Refresh(); - - // for now we only support F7 - // TODO: add switch and support for other platforms - Collection = FileManager.Firmware["Meadow F7"]; - } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index bea4c3ca..733a4f10 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -20,11 +20,8 @@ public CloudLogoutCommand( protected override async ValueTask ExecuteCommand() { - await Task.Run(() => - { - Logger?.LogInformation($"Logging out of Meadow.Cloud..."); + Logger?.LogInformation($"Logging out of Meadow.Cloud..."); - IdentityManager.Logout(); - }); + IdentityManager.Logout(); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs index f690b8c7..25127cdd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs @@ -25,27 +25,23 @@ public CloudCollectionListCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) - Host = DefaultHost; + if (Host == null) Host = DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) - return; + if (org == null) return; - if (!string.IsNullOrEmpty(org.Id)) { - var collections = await CollectionService.GetOrgCollections(org.Id, Host, CancellationToken); + var collections = await CollectionService.GetOrgCollections(org.Id, Host, CancellationToken); - if (collections == null || collections.Count == 0) - { - Logger?.LogInformation("No collections found."); - } - else + if (collections == null || collections.Count == 0) + { + Logger?.LogInformation("No collections found."); + } + else + { + Logger?.LogInformation("Collections:"); + foreach (var collection in collections) { - Logger?.LogInformation("Collections:"); - foreach (var collection in collections) - { - Logger?.LogInformation($" {collection.Id} | {collection.Name}"); - } + Logger?.LogInformation($" {collection.Id} | {collection.Name}"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs index fbe2eb63..aa8f4bf2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs @@ -6,20 +6,15 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class JsonDocumentBindingConverter : BindingConverter { - private const string InvalidArg = "Provided argument is not valid JSON:"; - - public override JsonDocument Convert(string? rawValue) + public override JsonDocument Convert(string rawValue) { try { - if (rawValue != null) - return JsonDocument.Parse(rawValue); - else - throw new CommandException($"{InvalidArg}"); + return JsonDocument.Parse(rawValue); } catch (JsonException ex) { - throw new CommandException($"{InvalidArg} {ex.Message}", showHelp: false, innerException: ex); + throw new CommandException($"Provided argument is not valid JSON: {ex.Message}", showHelp: false, innerException: ex); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index 1a445dd0..ba8c9694 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -11,7 +11,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudCommandPublishCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string? CommandName { get; set; } + public string CommandName { get; set; } [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] public string? CollectionId { get; set; } @@ -54,8 +54,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed.", showHelp: true); } - if (Host == null) - Host = DefaultHost; + if (Host == null) Host = DefaultHost; var token = await IdentityManager.GetAccessToken(CancellationToken); if (string.IsNullOrWhiteSpace(token)) @@ -65,23 +64,20 @@ protected override async ValueTask ExecuteCommand() try { - if (!string.IsNullOrEmpty(CommandName)) + Logger?.LogInformation($"Publishing '{CommandName}' command to Meadow.Cloud. Please wait..."); + if (!string.IsNullOrWhiteSpace(CollectionId)) { - Logger?.LogInformation($"Publishing '{CommandName}' command to Meadow.Cloud. Please wait..."); - if (!string.IsNullOrWhiteSpace(CollectionId)) - { - await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); - } - else if (DeviceIds != null && DeviceIds.Any()) - { - await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); - } - else - { - throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed."); - } - Logger?.LogInformation("Publish command successful."); + await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } + else if (DeviceIds.Any()) + { + await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); + } + else + { + throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed."); + } + Logger?.LogInformation("Publish command successful."); } catch (MeadowCloudAuthException ex) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index 7f27e138..f9a01c73 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Cloud; using Meadow.Cloud.Identity; using Meadow.Software; @@ -7,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud package create", Description = "Builds, trims and creates a Meadow Package (MPAK)")] +[Command("cloud package create", Description = "Create a Meadow Package (MPAK)")] public class CloudPackageCreateCommand : BaseCloudCommand { [CommandParameter(0, Name = "Path to project file", IsRequired = false)] @@ -23,8 +22,8 @@ public class CloudPackageCreateCommand : BaseCloudCommand public string? OrgId { get; set; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] - public string? Host { get; set; } + public string Host { get; set; } public CloudPackageListCommand( IdentityManager identityManager, @@ -30,29 +30,23 @@ public CloudPackageListCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) - Host = DefaultHost; - + if (Host == null) Host = DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) - return; + if (org == null) return; - if (!string.IsNullOrEmpty(org.Id)) - { - var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); + var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); - if (packages == null || packages.Count == 0) - { - Logger?.LogInformation("No packages found."); - } - else + if (packages == null || packages.Count == 0) + { + Logger?.LogInformation("No packages found."); + } + else + { + Logger?.LogInformation("packages:"); + foreach (var package in packages) { - Logger?.LogInformation("packages:"); - foreach (var package in packages) - { - Logger?.LogInformation($" {package.Id} | {package.Name}"); - } + Logger?.LogInformation($" {package.Id} | {package.Name}"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index 931141c1..9f0e440e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -11,16 +11,16 @@ public class CloudPackagePublishCommand : BaseCloudCommand -{ - [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = false)] - public string? MpakPath { get; set; } - - [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] - public string? OrgId { get; set; } - - [CommandOption("description", 'd', Description = "Description of the package", IsRequired = false)] - public string? Description { get; set; } - - [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] - public string? Host { get; set; } - - private PackageService _packageService; - - public CloudPackageUploadCommand( - IdentityManager identityManager, - UserService userService, - DeviceService deviceService, - CollectionService collectionService, - PackageService packageService, - ILoggerFactory? loggerFactory) - : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - _packageService = packageService; - } - - protected override async ValueTask ExecuteCommand() - { - if (string.IsNullOrEmpty(MpakPath)) - { - var candidates = PackageManager.GetAvailableBuiltConfigurations(Environment.CurrentDirectory, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{Environment.CurrentDirectory}'"); - return; - } - - var appDll = candidates.OrderByDescending(c => c.LastWriteTime).First(); - var packageDir = Path.Combine(appDll.Directory?.FullName ?? string.Empty, PackageManager.PackageOutputDirectoryName); - var files = Directory.EnumerateFiles(packageDir, "*.*", SearchOption.TopDirectoryOnly); - - var fileInfoList = new List(); - foreach (var file in files) - { - fileInfoList.Add(new FileInfo(file)); - } - - MpakPath = fileInfoList.OrderByDescending(f => f.LastWriteTime).First().FullName; - } - - if (!File.Exists(MpakPath)) - { - Logger?.LogError($"Package {MpakPath} does not exist"); - return; - } - - if (Host == null) - Host = DefaultHost; - - var org = await ValidateOrg(Host, OrgId, CancellationToken); - - if (org == null || string.IsNullOrEmpty(org.Id)) - { - Logger?.LogError($"Invalid Org"); - return; - } - - if (string.IsNullOrEmpty(Description)) - { - Description = string.Empty; - } - - try - { - Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); - - var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken) - .WithSpinner(Console!); - - Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); - } - catch (MeadowCloudException mex) - { - Logger?.LogError($"Upload failed: {mex.Message}"); - } - - } -} \ No newline at end of file +using CliFx.Attributes; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud package upload", Description = "Upload a Meadow Package (MPAK) to Meadow.Cloud")] +public class CloudPackageUploadCommand : BaseCloudCommand +{ + [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] + public string MpakPath { get; init; } + + [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] + public string? OrgId { get; set; } + + [CommandOption("description", 'd', Description = "Description of the package", IsRequired = false)] + public string? Description { get; set; } + + [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] + public string? Host { get; set; } + + private PackageService _packageService; + + public CloudPackageUploadCommand( + IdentityManager identityManager, + UserService userService, + DeviceService deviceService, + CollectionService collectionService, + PackageService packageService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + _packageService = packageService; + } + + protected override async ValueTask ExecuteCommand() + { + if (!File.Exists(MpakPath)) + { + Logger?.LogError($"Package {MpakPath} does not exist"); + return; + } + + if (Host == null) Host = DefaultHost; + var org = await ValidateOrg(Host, OrgId, CancellationToken); + + if (org == null) return; + + try + { + Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); + + var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken); + Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); + } + catch (MeadowCloudException mex) + { + Logger?.LogError($"Upload failed: {mex.Message}"); + } + + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs index 589c4c4e..19f5c84a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs @@ -24,48 +24,45 @@ public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFac protected override async ValueTask ExecuteCommand() { - await Task.Run(() => + if (List) { - if (List) - { - Logger?.LogInformation($"Current CLI configuration"); + Logger?.LogInformation($"Current CLI configuration"); - // display all current config - var settings = SettingsManager.GetPublicSettings(); - if (settings.Count == 0) - { - Logger?.LogInformation($" "); - } - else - { - foreach (var kvp in SettingsManager.GetPublicSettings()) - { - Logger?.LogInformation($" {kvp.Key} = {kvp.Value}"); - } - } + // display all current config + var settings = SettingsManager.GetPublicSettings(); + if (settings.Count == 0) + { + Logger?.LogInformation($" "); } else { - switch (Settings?.Length) + foreach (var kvp in SettingsManager.GetPublicSettings()) { - case 0: - // not valid - throw new CommandException($"No setting provided"); - case 1: - // erase a setting - Logger?.LogInformation($"{Environment.NewLine}Deleting Setting {Settings[0]}"); - SettingsManager.DeleteSetting(Settings[0]); - break; - case 2: - // set a setting - Logger?.LogInformation($"{Environment.NewLine}Setting {Settings[0]}={Settings[1]}"); - SettingsManager.SaveSetting(Settings[0], Settings[1]); - break; - default: - // not valid; - throw new CommandException($"Too many parameters provided"); + Logger?.LogInformation($" {kvp.Key} = {kvp.Value}"); } } - }); + } + else + { + switch (Settings?.Length) + { + case 0: + // not valid + throw new CommandException($"No setting provided"); + case 1: + // erase a setting + Logger?.LogInformation($"{Environment.NewLine}Deleting Setting {Settings[0]}"); + SettingsManager.DeleteSetting(Settings[0]); + break; + case 2: + // set a setting + Logger?.LogInformation($"{Environment.NewLine}Setting {Settings[0]}={Settings[1]}"); + SettingsManager.SaveSetting(Settings[0], Settings[1]); + break; + default: + // not valid; + throw new CommandException($"Too many parameters provided"); + } + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index f94910f2..af1263f8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -19,15 +19,28 @@ public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactor protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) + return; + } + + Logger?.LogInformation($"Setting developer parameter {Parameter} to {Value}"); + + if (connection != null) + { + connection.DeviceMessageReceived += (s, e) => { - Logger?.LogInformation($"Setting developer parameter {Parameter} to {Value}"); - await Connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); - } + Logger?.LogInformation(e.message); + }; + connection.ConnectionError += (s, e) => + { + Logger?.LogError(e.Message); + }; + + await connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); } } -} \ No newline at end of file +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index 7e657a44..dbf5fc88 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -16,33 +16,37 @@ public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFact protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - if (Time == null) + + return; + } + + if (Time == null) + { + Logger?.LogInformation($"Getting device clock..."); + var deviceTime = await connection.Device.GetRtcTime(CancellationToken); + Logger?.LogInformation($"{deviceTime.Value:s}Z"); + } + else + { + if (Time == "now") + { + Logger?.LogInformation($"Setting device clock..."); + await connection.Device.SetRtcTime(DateTimeOffset.UtcNow, CancellationToken); + } + else if (DateTimeOffset.TryParse(Time, out DateTimeOffset dto)) { - Logger?.LogInformation($"Getting device clock..."); - var deviceTime = await Connection.Device.GetRtcTime(CancellationToken); - Logger?.LogInformation($"{deviceTime.Value:s}Z"); + Logger?.LogInformation($"Setting device clock..."); + await connection.Device.SetRtcTime(dto, CancellationToken); } else { - if (Time == "now") - { - Logger?.LogInformation($"Setting device clock..."); - await Connection.Device.SetRtcTime(DateTimeOffset.UtcNow, CancellationToken); - } - else if (DateTimeOffset.TryParse(Time, out DateTimeOffset dto)) - { - Logger?.LogInformation($"Setting device clock..."); - await Connection.Device.SetRtcTime(dto, CancellationToken); - } - else - { - Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); - } + Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); } } + } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs index a1b55bd8..2c4c6265 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -9,20 +9,22 @@ public class DeviceInfoCommand : BaseDeviceCommand public DeviceInfoCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Getting device info..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - Logger?.LogInformation($"Getting device info..."); - var deviceInfo = await Connection.Device.GetDeviceInfo(CancellationToken); - if (deviceInfo != null) - { - Logger?.LogInformation(deviceInfo.ToString()); - } + return; + } + + var deviceInfo = await connection.Device.GetDeviceInfo(CancellationToken); + if (deviceInfo != null) + { + Logger?.LogInformation(deviceInfo.ToString()); } } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index fd59ef20..23d16c6f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -32,7 +32,8 @@ public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManag protected override async ValueTask ExecuteCommand() { - UserOrg? org; + UserOrg org; + try { if (Host == null) Host = DefaultHost; @@ -80,7 +81,7 @@ protected override async ValueTask ExecuteCommand() return; } - var info = await connection.Device!.GetDeviceInfo(CancellationToken); + var info = await connection.Device.GetDeviceInfo(CancellationToken); Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); @@ -90,7 +91,6 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); - var provisioningID = !string.IsNullOrWhiteSpace(info?.ProcessorId) ? info.ProcessorId : info?.SerialNumber; var provisioningName = !string.IsNullOrWhiteSpace(Name) ? Name : info?.DeviceName; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs index dadea321..833809a2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -9,16 +9,18 @@ public class DeviceResetCommand : BaseDeviceCommand public DeviceResetCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Resetting the device..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - Logger?.LogInformation($"Resetting the device..."); - await Connection.Device.Reset(); + return; } + + await connection.Device.Reset(); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 659d0fda..221db0af 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -1,113 +1,63 @@ -using System; -using System.Diagnostics.Metrics; -using CliFx.Attributes; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("file delete", Description = "Deletes a file from the device")] -public class FileDeleteCommand : BaseDeviceCommand -{ - [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; - - public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - } - - protected override async ValueTask ExecuteCommand() - { - await base.ExecuteCommand(); - - if (Connection != null) - { - // in order to delete, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); - - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); - - await Connection.RuntimeDisable(CancellationToken); - } - - try - { - var fileList = await Connection.GetFileList(false); - - if (MeadowFile == "all") - { - if (Console != null) - { - Logger?.LogInformation($"{Environment.NewLine}Are you sure you want to delete ALL files from this device (Y/N)?"); - - var reply = await Console.Input.ReadLineAsync(); - if ((!string.IsNullOrEmpty(reply) && reply.ToLower() != "y") || string.IsNullOrEmpty(reply)) - { - return; - } - } - - if (fileList != null) - { - if (fileList.Length > 0) - { - foreach (var f in fileList) - { - // If we've cancelled bail - if (CancellationToken.IsCancellationRequested) - { - return; - } - - if (Connection.Device != null) - { - var p = Path.GetFileName(f.Name); - - Console?.Output.WriteAsync($"Deleting file '{p}' from device... \r"); - await Connection.Device.DeleteFile(p, CancellationToken); - } - else - { - Logger?.LogError($"No Device Found."); - } - } - } - else - { - Logger?.LogInformation($"No files to delete."); - } - } - } - else - { - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; - - if (!exists) - { - Logger?.LogError($"File '{MeadowFile}' not found on device."); - } - else - { - if (Connection.Device != null) - { - Console?.Output.WriteAsync($"Deleting file '{MeadowFile}' from device... \r"); - await Connection.Device.DeleteFile(MeadowFile, CancellationToken); - } - } - } - } - finally - { - if (wasRuntimeEnabled) - { - // restore runtime state - Logger?.LogInformation("Enabling runtime..."); - - await Connection.RuntimeEnable(CancellationToken); - } - } - } - } -} \ No newline at end of file +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file delete", Description = "Deletes a file from the device")] +public class FileDeleteCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] + public string MeadowFile { get; set; } = default!; + + public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand() + { + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } + + if (connection != null) + { + var fileList = await connection.GetFileList(false); + + if (MeadowFile == "all") + { + foreach (var f in fileList) + { + var p = Path.GetFileName(f.Name); + Logger?.LogInformation($"Deleting file '{p}' from device..."); + await connection.Device.DeleteFile(p, CancellationToken); + } + } + else + { + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; + + if (!exists) + { + Logger?.LogError($"File '{MeadowFile}' not found on device."); + } + else + { + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + + if (wasRuntimeEnabled) + { + Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); + return; + } + + Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await connection.Device.DeleteFile(MeadowFile, CancellationToken); + } + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index 44115518..813a0e94 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -16,18 +16,17 @@ public FileInitialCommand(MeadowConnectionManager connectionManager, ILoggerFact protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) - { - Logger?.LogInformation($"Reading file '{MeadowFile}' from device...\n"); + return; + } - var data = await Connection.Device.ReadFileString(MeadowFile, CancellationToken); + Logger?.LogInformation($"Reading file '{MeadowFile}' from device...\n"); - Logger?.LogInformation(data); - } - } + var data = await connection.Device.ReadFileString(MeadowFile, CancellationToken); + + Logger.LogInformation(data); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 9325db79..ce4c9e69 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -8,19 +8,27 @@ public class FileListCommand : BaseDeviceCommand { public const int FileSystemBlockSize = 4096; + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; set; } + public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Getting file list..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } - if (Connection != null && Connection.Device != null) + if (connection != null) { - Logger?.LogInformation($"Getting file list..."); - var files = await Connection.Device.GetFileList(Verbose, CancellationToken); + var files = await connection.Device.GetFileList(Verbose, CancellationToken); if (files == null || files.Length == 0) { @@ -82,4 +90,4 @@ protected override async ValueTask ExecuteCommand() } } } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index 7538cef5..f17195be 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -19,22 +19,24 @@ public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - Logger?.LogInformation($"Getting file '{MeadowFile}' from device..."); - - var success = await Connection.Device.ReadFile(MeadowFile, LocalFile, CancellationToken); - - if (success) - { - Logger?.LogInformation($"Success"); - } - else - { - Logger?.LogInformation($"Failed to retrieve file"); - } + return; + } + + Logger?.LogInformation($"Getting file '{MeadowFile}' from device..."); + + var success = await connection.Device.ReadFile(MeadowFile, LocalFile, CancellationToken); + + if (success) + { + Logger?.LogInformation($"Success"); + } + else + { + Logger?.LogInformation($"Failed to retrieve file"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index b9995901..ad74832d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -11,7 +11,7 @@ public class FileWriteCommand : BaseDeviceCommand 'f', Description = "The file(s) to write to the Meadow Files System", IsRequired = true)] - public IList Files { get; init; } = Array.Empty(); + public IList Files { get; init; } [CommandOption( "targetFiles", @@ -26,51 +26,51 @@ public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactor protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) - { - Logger?.LogError( - $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); + return; + } - return; - } + if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) + { + Logger?.LogError( + $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); - Connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; + return; + } - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; - Logger?.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); + // Console instead of Logger due to line breaking for progress bar + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; - for (var i = 0; i < Files.Count; i++) + Logger?.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); + + for (var i = 0; i < Files.Count; i++) + { + if (!File.Exists(Files[i])) + { + Logger?.LogError($"Cannot find file '{Files[i]}'. Skippping"); + } + else { - if (!File.Exists(Files[i])) + var targetFileName = GetTargetFileName(i); + + Logger?.LogInformation( + $"Writing '{Files[i]}' as '{targetFileName}' to device"); + + try { - Logger?.LogError($"Cannot find file '{Files[i]}'. Skippping"); + await connection.Device.WriteFile(Files[i], targetFileName, CancellationToken); } - else + catch (Exception ex) { - try - { - if (Connection.Device != null) - { - var targetFileName = GetTargetFileName(i); - - Logger?.LogInformation($"Writing '{Files[i]}' as '{targetFileName}' to device"); - - await Connection.Device.WriteFile(Files[i], targetFileName, CancellationToken); - } - } - catch (Exception ex) - { - Logger?.LogError($"Error writing file: {ex.Message}"); - } + Logger?.LogError($"Error writing file: {ex.Message}"); } } } @@ -87,4 +87,4 @@ private string GetTargetFileName(int i) return new FileInfo(Files[i]).Name; } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 4de18847..7fbfeac1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -18,22 +18,23 @@ public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settings protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); - if (Collection != null) + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; + + if (Version == null) + { + Logger?.LogInformation($"Default firmware is '{collection.DefaultPackage.Version}'."); + } + else { - if (Version == null) - { - Logger?.LogInformation($"Default firmware is '{Collection.DefaultPackage?.Version}'."); - } - else - { - var existing = Collection.FirstOrDefault(p => p.Version == Version); - - Logger?.LogInformation($"Setting default firmware to '{Version}'..."); - - await Collection.SetDefaultPackage(Version); - } + var existing = collection.FirstOrDefault(p => p.Version == Version); + + Logger?.LogInformation($"Setting default firmware to '{Version}'..."); + + await collection.SetDefaultPackage(Version); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 77059364..30dbc2f1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -18,11 +18,14 @@ public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsM protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; Logger?.LogInformation($"Deleting firmware '{Version}'..."); - if (Collection != null) - await Collection.DeletePackage(Version); + await collection.DeletePackage(Version); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index d959fd0d..4c931efb 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -21,70 +21,72 @@ public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager setting protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); - if (Collection != null) - { - bool explicitVersion; + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; - if (Version == null) - { - explicitVersion = false; - var latest = await Collection.GetLatestAvailableVersion(); + bool explicitVersion; - if (latest == null) - { - Logger?.LogError($"Unable to get latest version information."); - return; - } + if (Version == null) + { + explicitVersion = false; + var latest = await collection.GetLatestAvailableVersion(); - Logger?.LogInformation($"Latest available version is '{latest}'..."); - Version = latest; - } - else + if (latest == null) { - explicitVersion = true; - Logger?.LogInformation($"Checking for firmware package '{Version}'..."); + Logger?.LogError($"Unable to get latest version information."); + return; } - var isAvailable = await Collection.IsVersionAvailableForDownload(Version); + Logger?.LogInformation($"Latest available version is '{latest}'..."); + Version = latest; + } + else + { + explicitVersion = true; + Logger?.LogInformation($"Checking for firmware package '{Version}'..."); + } - if (!isAvailable) - { - Logger?.LogError($"Requested package version '{Version}' is not available."); - return; - } + var isAvailable = await collection.IsVersionAvailableForDownload(Version); + + if (!isAvailable) + { + Logger?.LogError($"Requested package version '{Version}' is not available."); + return; + } - Logger?.LogInformation($"Downloading firmware package '{Version}'..."); + Logger?.LogInformation($"Downloading firmware package '{Version}'..."); - try - { - Collection.DownloadProgress += OnDownloadProgress; - var result = await Collection.RetrievePackage(Version, Force); + try + { + collection.DownloadProgress += OnDownloadProgress; - if (!result) - { - Logger?.LogError($"Unable to download package '{Version}'."); - } - else - { - Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + var result = await collection.RetrievePackage(Version, Force); - if (!explicitVersion) - { - await Collection.SetDefaultPackage(Version); - } - } + if (!result) + { + Logger?.LogError($"Unable to download package '{Version}'."); } - catch (Exception ex) + else { - Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + + if (explicitVersion) + { + await collection.SetDefaultPackage(Version); + } } } + catch (Exception ex) + { + Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + } } - // TODO private long _lastProgress = 0; + private long _lastProgress = 0; private void OnDownloadProgress(object? sender, long e) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index 4f328b6a..ce9d6c6b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,108 +1,110 @@ -using System.Linq; -using CliFx; -using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; -using Meadow.Software; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("firmware list", Description = "List locally available firmware")] -public class FirmwareListCommand : BaseCommand -{ - private FileManager FileManager { get; } - - public FirmwareListCommand(FileManager fileManager, ILoggerFactory? loggerFactory) - : base(loggerFactory) - { - FileManager = fileManager; - } - - protected override async ValueTask ExecuteCommand() - { - await FileManager.Refresh(); - - if (Verbose) - { - await DisplayVerboseResults(FileManager); - } - else - { - await DisplayTerseResults(FileManager); - } - } - - private async Task DisplayVerboseResults(FileManager manager) - { - Logger?.LogInformation($" (D== Default, OSB==OS without bootloader, RT==Runtime, CP==Coprocessor){Environment.NewLine}"); - Logger?.LogInformation($" D VERSION OS OSB RT CP BCL"); - - Logger?.LogInformation($"------------------------------------------"); - - foreach (var name in manager.Firmware.CollectionNames) - { - Logger?.LogInformation($" {name}"); - var collection = manager.Firmware[name.ToString()]; - - foreach (var package in collection.OrderByDescending(s=> s.Version)) - { - if (package == collection.DefaultPackage) - { - var detailedInformation = $" * {package.Version?.PadRight(18)} " + - $"{(package.OSWithBootloader != null ? "X " : " ")}" + - $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + - $"{(package.Runtime != null ? "X " : " ")}" + - $"{(package.CoprocApplication != null ? "X " : " ")}" + - $"{(package.BclFolder != null ? "X " : " ")}" + - " (default)"; - Logger?.LogInformation(detailedInformation.ColourConsoleTextGreen()); - } - else - { - Logger?.LogInformation( - $" {package.Version?.PadRight(18)} " + - $"{(package.OSWithBootloader != null ? "X " : " ")}" + - $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + - $"{(package.Runtime != null ? "X " : " ")}" + - $"{(package.CoprocApplication != null ? "X " : " ")}" + - $"{(package.BclFolder != null ? "X " : " ")}" - ); - } - } - - var update = await collection.UpdateAvailable(); - if (update != null) - { - Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); - } - } - } - - private async Task DisplayTerseResults(FileManager manager) - { - foreach (var name in manager.Firmware.CollectionNames) - { - Logger?.LogInformation($" {name}"); - var collection = manager.Firmware[name.ToString()]; - - foreach (var package in collection.OrderByDescending(s => s.Version)) - { - if (package == collection.DefaultPackage) - { - Logger?.LogInformation($" * {package.Version} (default)".ColourConsoleTextGreen()); - } - else - { - Logger?.LogInformation($" {package.Version}"); - } - } - - var update = await collection.UpdateAvailable(); - if (update != null) - { - Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); - } - } - } -} +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware list", Description = "List locally available firmware")] +public class FirmwareListCommand : BaseCommand +{ + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; set; } + + private FileManager FileManager { get; } + + public FirmwareListCommand(FileManager fileManager, ILoggerFactory? loggerFactory) + : base(loggerFactory) + { + FileManager = fileManager; + } + + protected override async ValueTask ExecuteCommand() + { + await FileManager.Refresh(); + + if (Verbose) + { + await DisplayVerboseResults(FileManager); + } + else + { + await DisplayTerseResults(FileManager); + } + } + + private async Task DisplayVerboseResults(FileManager manager) + { + Logger?.LogInformation($" (D== Default, OSB==OS without bootloader, RT==Runtime, CP==Coprocessor){Environment.NewLine}"); + Logger?.LogInformation($" D VERSION OS OSB RT CP BCL"); + + Logger?.LogInformation($"------------------------------------------"); + + foreach (var name in manager.Firmware.CollectionNames) + { + Logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + Logger?.LogInformation( + $" * {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + else + { + Logger?.LogInformation( + $" {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } + + private async Task DisplayTerseResults(FileManager manager) + { + foreach (var name in manager.Firmware.CollectionNames) + { + Logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + Logger?.LogInformation($" * {package.Version} (default)"); + } + else + { + Logger?.LogInformation($" {package.Version}"); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index d509639f..942f1577 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,9 +1,6 @@ -using System.Collections.Concurrent; -using System.Threading; -using CliFx.Attributes; +using CliFx.Attributes; using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; -using Meadow.Cloud; using Meadow.Hcom; using Meadow.LibUsb; using Meadow.Software; @@ -30,20 +27,11 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandParameter(0, Name = "Files to write", IsRequired = false)] public FirmwareType[]? Files { get; set; } = default!; - [CommandOption("file", 'f', IsRequired = false, Description = "Path to OS, Runtime or ESP file")] - public string? Path { get; set; } = default!; - - [CommandOption("address", 'a', IsRequired = false, Description = "Address location to write the file to")] - public int? Address { get; set; } = default!; - private FileManager FileManager { get; } private ISettingsManager Settings { get; } - private const string FileWriteComplete = "Firmware Write Complete!"; - private ILibUsbDevice[]? _libUsbDevices; - private IMeadowConnection? connection; - - // TODO private bool _fileWriteError = false; + private ILibUsbDevice? _libUsbDevice; + private bool _fileWriteError = false; public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -54,11 +42,9 @@ public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileMa protected override async ValueTask ExecuteCommand() { - // do not call the base implementation - it will look for a route, and with DFU we don't have (or need) one - var package = await GetSelectedPackage(); - if (Files == null && package != null) + if (Files == null) { Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); @@ -70,7 +56,7 @@ protected override async ValueTask ExecuteCommand() }; } - if (Files != null && !Files.Contains(FirmwareType.OS) && UseDfu) + if (!Files.Contains(FirmwareType.OS) && UseDfu) { Logger?.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); return; @@ -78,22 +64,25 @@ protected override async ValueTask ExecuteCommand() bool deviceSupportsOta = false; // TODO: get this based on device OS version - if ((Files != null && Files.Contains(FirmwareType.OS)) && (package != null && package.OsWithoutBootloader == null + if (package.OsWithoutBootloader == null || !deviceSupportsOta - || UseDfu)) + || UseDfu) { UseDfu = true; } - if (Files != null && package != null) + IMeadowConnection connection; + + if (UseDfu && Files.Contains(FirmwareType.OS)) { + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + // get the device's serial number via DFU - we'll need it to find the device after it resets + ILibUsbDevice libUsbDevice; try { - if (UseDfu) - { - GetLibUsbDevicesInBootloaderModeForCurrentEnvironment(); - } + libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); } catch (Exception ex) { @@ -101,96 +90,56 @@ protected override async ValueTask ExecuteCommand() return; } - var flashStatus = new ConcurrentDictionary(); + var serial = libUsbDevice.GetDeviceSerialNumber(); - if (_libUsbDevices != null) - { - if (_libUsbDevices.Length > 1) - { - await Console!.Output.WriteLineAsync($"Found {_libUsbDevices.Length} devices in bootloader mode.{Environment.NewLine}Would you like to flash them all (Y/N)?"); - var yesOrNo = await Console!.Input.ReadLineAsync(); - if (!string.IsNullOrEmpty(yesOrNo)) - { - if (yesOrNo.ToLower() == "n") - { - Logger?.LogInformation("User elected not to proceed."); - return; - } - } - } - - foreach (var libUsbDevice in _libUsbDevices) - { - var serialNumber = libUsbDevice.GetDeviceSerialNumber(); + // no connection is required here - in fact one won't exist + // unless maybe we add a "DFUConnection"? - // no connection is required here - in fact one won't exist - // unless maybe we add a "DFUConnection"? - - try - { - if (package != null && package.OSWithBootloader != null && Files.Contains(FirmwareType.OS)) - { - flashStatus[serialNumber] = "WritingOS"; - await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serialNumber); - } - } - catch (Exception ex) - { - flashStatus[serialNumber] = ex.Message; - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); + try + { + await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); + } + catch (Exception ex) + { + Logger?.LogError($"Exception type: {ex.GetType().Name}"); - Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); - continue; - } + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - var newPort = await MeadowConnectionManager.GetPortFromSerialNumber(serialNumber); + Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); + return; + } - if (!string.IsNullOrEmpty(newPort)) - { - Logger?.LogInformation($"Meadow found at {newPort}"); + // now wait for a new serial port to appear + var ports = await MeadowConnectionManager.GetSerialPorts(); + var retryCount = 0; - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + var newPort = ports.Except(initialPorts).FirstOrDefault(); + while (newPort == null) + { + if (retryCount++ > 10) + { + throw new Exception("New meadow device not found"); + } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + newPort = ports.Except(initialPorts).FirstOrDefault(); + } - await WriteNonOSToDevice(Files, package, flashStatus); + Logger?.LogInformation($"Meadow found at {newPort}"); - flashStatus[serialNumber] = FileWriteComplete; - } + // configure the route to that port for the user + Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - } + connection = await GetCurrentConnection(); - Logger?.LogInformation($"{Environment.NewLine}Firmware Write Status:"); - foreach (var item in flashStatus) - { - var textColour = ExtensionMethods.ConsoleColourRed; - if (item.Value.Contains(FileWriteComplete)) { - textColour = ExtensionMethods.ConsoleColourGreen; - } - Logger?.LogInformation($"Serial Number: {item.Key} - {item.Value}".ColourConsoleText(textColour)); - } - } - else + if (connection == null) { - await WriteNonOSToDevice(Files, package, flashStatus); - - if (connection != null) - flashStatus[connection.Name] = FileWriteComplete; + return; } - } - } - private async Task WriteNonOSToDevice(FirmwareType[] files, FirmwarePackage? package, ConcurrentDictionary flashStatus) - { - // get the connection associated with that route - connection = await GetCurrentConnection(); - - try - { - if (connection != null && files.Any(f => f != FirmwareType.OS)) + if (Files.Any(f => f != FirmwareType.OS)) { await connection.WaitForMeadowAttach(); @@ -199,56 +148,62 @@ private async Task WriteNonOSToDevice(FirmwareType[] files, FirmwarePackage? pac return; } - flashStatus[connection.Name] = "WriteFiles"; - await WriteFiles(package, connection); + await WriteFiles(connection); } } - catch (Exception ex) + else { - if (connection != null) - flashStatus[connection.Name] = ex.Message; - // Log the exception but move onto the next device - Logger?.LogError($"{Environment.NewLine}Exception type: {ex.GetType().Name}", ex); - - return; + connection = await GetCurrentConnection(); + if (connection == null) + { + return; + } + await WriteFiles(connection); } - finally + + await connection.ResetDevice(CancellationToken); + await connection.WaitForMeadowAttach(); + + var deviceInfo = await connection.Device.GetDeviceInfo(CancellationToken); + + if (deviceInfo != null) { - // Needed to avoid double messages - DetachMessageHandlers(connection); + Logger?.LogInformation(deviceInfo.ToString()); } } - private void GetLibUsbDevicesInBootloaderModeForCurrentEnvironment() + private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() { - // Clear it out each - if (_libUsbDevices != null && _libUsbDevices.Length > 0) + if (_libUsbDevice == null) { - _libUsbDevices = Array.Empty(); - } + ILibUsbProvider provider; - ILibUsbProvider provider; - - // TODO: read the settings manager to decide which provider to use (default to non-classic) - var setting = Settings.GetAppSetting(SettingsManager.PublicSettings.LibUsb); - if (setting == "classic") - { - provider = new ClassicLibUsbProvider(); - } - else - { - provider = new LibUsbProvider(); - } + // TODO: read the settings manager to decide which provider to use (default to non-classic) + var setting = Settings.GetAppSetting(SettingsManager.PublicSettings.LibUsb); + if (setting == "classic") + { + provider = new ClassicLibUsbProvider(); + } + else + { + provider = new LibUsbProvider(); + } - var devices = provider.GetDevicesInBootloaderMode(); + var devices = provider.GetDevicesInBootloaderMode(); - switch (devices.Count) - { - case 0: - throw new Exception("No devices found in bootloader mode."); + switch (devices.Count) + { + case 0: + throw new Exception("No device found in bootloader mode"); + case 1: + _libUsbDevice = devices[0]; + break; + default: + throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + } } - _libUsbDevices = devices.ToArray(); + return _libUsbDevice; } private async Task GetSelectedPackage() @@ -281,141 +236,105 @@ private void GetLibUsbDevicesInBootloaderModeForCurrentEnvironment() return package; } - private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection connection) + private async ValueTask WriteFiles(IMeadowConnection connection) { + // the connection passes messages back to us (info about actions happening on-device + connection.DeviceMessageReceived += (s, e) => + { + if (e.message.Contains("% downloaded")) + { + // don't echo this, as we're already reporting % written + } + else + { + Logger?.LogInformation(e.message); + } + }; + connection.ConnectionMessage += (s, message) => + { + Logger?.LogInformation(message); + }; connection.FileWriteFailed += (s, e) => { - Logger?.LogError($"WriteFiles FAILED!!"); - // TODO _fileWriteError = true; + _fileWriteError = true; }; - if (Files != null - && connection.Device != null - && package != null) - { - var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + var package = await GetSelectedPackage(); - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling device runtime..."); - await connection.Device.RuntimeDisable(); - } + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling device runtime..."); + await connection.Device.RuntimeDisable(); + } - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; - if (Files.Contains(FirmwareType.OS)) + if (Files.Contains(FirmwareType.OS)) + { + if (UseDfu) { - if (UseDfu) - { - // this would have already happened before now (in ExecuteAsync) so ignore - } - else - { - Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - - throw new NotSupportedException("OtA writes for the OS are not yet supported"); - } + // this would have already happened before now (in ExecuteAsync) so ignore } - - if (Files.Contains(FirmwareType.Runtime)) + else { - string? runtime; - if (!string.IsNullOrEmpty(Path)) - { - runtime = Path; - } - else - { - // get the path to the runtime bin file - runtime = package.Runtime; - } - - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {runtime}..."); - - if (string.IsNullOrEmpty(runtime)) - runtime = string.Empty; - var rtpath = package.GetFullyQualifiedPath(runtime); - - write_runtime: - // If we've cancelled bail - if (CancellationToken.IsCancellationRequested) - { - return; - } + Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) - { - Logger?.LogInformation($"Error writing runtime. Retrying."); - goto write_runtime; - } + throw new NotSupportedException("OtA writes for the OS are not yet supported"); } + } + if (Files.Contains(FirmwareType.Runtime)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); - if (CancellationToken.IsCancellationRequested) + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + write_runtime: + if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) { - return; + Logger?.LogInformation($"Error writing runtime. Retrying."); + goto write_runtime; } + } - if (Files.Contains(FirmwareType.ESP)) - { - string? coProcessorFilePath; - if (!string.IsNullOrEmpty(Path)) - { - // use passed in path - coProcessorFilePath = Path; - } - else - { - // get the default path to the coprocessor bin file - coProcessorFilePath = package.CoprocApplication; - } + if (CancellationToken.IsCancellationRequested) + { + return; + } - Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor file {coProcessorFilePath}..."); + if (Files.Contains(FirmwareType.ESP)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); - string[]? fileList = Array.Empty(); - if (coProcessorFilePath != null - && package.CoprocBootloader != null - && package.CoprocPartitionTable != null) + var fileList = new string[] { - if (!string.IsNullOrEmpty(Path)) - { - fileList = new string[] - { - package.GetFullyQualifiedPath(coProcessorFilePath), - }; - } - else - { - fileList = new string[] - { - package.GetFullyQualifiedPath(coProcessorFilePath), - package.GetFullyQualifiedPath(package.CoprocBootloader), - package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - } + package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; - await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); + await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); - if (CancellationToken.IsCancellationRequested) - { - return; - } + if (CancellationToken.IsCancellationRequested) + { + return; } + } - Logger?.LogInformation($"{Environment.NewLine}"); + Logger?.LogInformation($"{Environment.NewLine}"); - if (wasRuntimeEnabled) - { - await connection.Device.RuntimeEnable(CancellationToken); - } - // TODO: if we're an F7 device, we need to reset + if (wasRuntimeEnabled) + { + await connection.Device.RuntimeEnable(CancellationToken); } + + // TODO: if we're an F7 device, we need to reset } private async Task WriteOsWithDfu(string osFile, string serialNumber) @@ -426,4 +345,5 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} \ No newline at end of file +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index c6b9433c..3f3bbc7e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -13,16 +13,20 @@ public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFacto protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) - { - Logger?.LogInformation($"Erasing flash..."); - - await Connection.Device.EraseFlash(CancellationToken); - } + return; } + + Logger?.LogInformation($"Erasing flash..."); + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + await connection.Device.EraseFlash(CancellationToken); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index b0a6d097..5834937e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -20,49 +20,33 @@ private void Connection_ConnectionMessage(object? sender, string e) private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) { - string textColour; - switch (e.source) - { - case "stdout": - textColour = ExtensionMethods.ConsoleColourBlue; - break; - case "info": - textColour = ExtensionMethods.ConsoleColourGreen; - break; - case "stderr": - textColour = ExtensionMethods.ConsoleColourRed; - break; - default: - textColour = ExtensionMethods.ConsoleColourReset; - break; - } - if (NoPrefix) { - Logger?.LogInformation($"{e.message.TrimEnd('\n', '\r').ColourConsoleText(textColour)}"); + Logger?.LogInformation($"{e.message.TrimEnd('\n', '\r')}"); } else { - - Logger?.LogInformation($"{e.source?.ColourConsoleText(textColour)}> {e.message.TrimEnd('\n', '\r')}"); + Logger?.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); } } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - Connection.DeviceMessageReceived += OnDeviceMessageReceived; - Connection.ConnectionMessage += Connection_ConnectionMessage; + return; + } + + connection.DeviceMessageReceived += OnDeviceMessageReceived; + connection.ConnectionMessage += Connection_ConnectionMessage; - Logger?.LogInformation($"Listening for Meadow Console output on '{Connection.Name}'. Press Ctrl+C to exit..."); + Logger?.LogInformation($"Listening for Meadow Console output on '{connection.Name}'. Press Ctrl+C to exit..."); - while (!CancellationToken.IsCancellationRequested) - { - await Task.Delay(1000); - } + while (!CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); } } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs index fac6d79a..4b959b55 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs @@ -6,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("port list", Description = "List available local serial ports")] public class PortListCommand : BaseCommand { - public IList? Portlist; + public IList? Portlist; public PortListCommand(ILoggerFactory loggerFactory) : base(loggerFactory) @@ -22,7 +22,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Found the following device{plural}:"); for (int i = 0; i < Portlist.Count; i++) { - Logger?.LogInformation($" {i + 1}: {Portlist[i].Name}"); + Logger?.LogInformation($" {i + 1}: {Portlist[i]}"); } } else diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index d324d8e4..a5b43034 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -33,7 +33,7 @@ protected override async ValueTask ExecuteCommand() { if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) { - await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1].Name!); + await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); } } } @@ -41,7 +41,7 @@ protected override async ValueTask ExecuteCommand() { // Only 1 device attached, let's auto select it if (portListCommand.Portlist != null) - await CallConfigCommand(portListCommand.Portlist[0].Name!); + await CallConfigCommand(portListCommand.Portlist[0]); } } } @@ -55,9 +55,6 @@ private async Task CallConfigCommand(string selectedPort) Settings = new string[] { "route", selectedPort } }; - if (Console != null) - { - await setCommand.ExecuteAsync(Console); - } + await setCommand.ExecuteAsync(Console); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs index a3d1074d..e1fc867e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -9,27 +9,22 @@ public class RuntimeDisableCommand : BaseDeviceCommand public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Disabling runtime..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Disabling runtime..."); - - await Connection.Device.RuntimeDisable(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Failed to disable runtime."); - } - } + return; } + + await connection.Device.RuntimeDisable(CancellationToken); + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs index c073c96b..8061a64b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -9,27 +9,22 @@ public class RuntimeEnableCommand : BaseDeviceCommand public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Enabling runtime..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Enabling runtime..."); - - await Connection.Device.RuntimeEnable(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Failed to enable runtime."); - } - } + return; } + + await connection.Device.RuntimeEnable(CancellationToken); + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs index 6ae4095b..19569c1c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -9,27 +9,20 @@ public class RuntimeStateCommand : BaseDeviceCommand public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Querying runtime state..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Querying runtime state..."); - - await Connection.Device.IsRuntimeEnabled(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Unable to determine the runtime state."); - } - } + return; } + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs deleted file mode 100644 index 8bf759a6..00000000 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Meadow.Hcom; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseTraceCommand : BaseDeviceCommand -{ - public BaseTraceCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs index 7958a294..c3aa27f5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -4,7 +4,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("trace disable", Description = "Disable trace logging on the Meadow")] -public class TraceDisableCommand : BaseTraceCommand +public class TraceDisableCommand : BaseDeviceCommand { public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -13,13 +13,21 @@ public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFac protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - Logger?.LogInformation("Disabling tracing..."); - - await Connection.Device.TraceDisable(CancellationToken); + return; } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + Logger?.LogInformation("Disabling tracing..."); + + await connection.Device.TraceDisable(CancellationToken); } -} \ No newline at end of file +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs index 98c70969..faafb992 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -1,42 +1,42 @@ -using System.Reflection.Emit; -using CliFx.Attributes; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("trace enable", Description = "Enable trace logging on the Meadow")] -public class TraceEnableCommand : BaseTraceCommand -{ - [CommandOption("level", 'l', Description = "The desired trace level", IsRequired = false)] - public int? Level { get; init; } - - public TraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - } - - protected override async ValueTask ExecuteCommand() - { - await base.ExecuteCommand(); - - if (Connection != null) - { - if (Connection.Device != null) - { - if (Level != null) - { - Logger?.LogInformation($"Setting trace level to {Level}..."); - await Connection.Device.SetTraceLevel(Level.Value, CancellationToken); - } - - Logger?.LogInformation("Enabling tracing..."); - - await Connection.Device.TraceEnable(CancellationToken); - } - else - { - Logger?.LogError("Trace Error: No Device found..."); - } - } - } -} \ No newline at end of file +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("trace enable", Description = "Enable trace logging on the Meadow")] +public class TraceEnableCommand : BaseDeviceCommand +{ + [CommandOption("level", 'l', Description = "The desired trace level", IsRequired = false)] + public int? Level { get; init; } + + public TraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand() + { + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + if (Level != null) + { + Logger?.LogInformation($"Setting trace level to {Level}..."); + await connection.Device.SetTraceLevel(Level.Value, CancellationToken); + } + + Logger?.LogInformation("Enabling tracing..."); + + await connection.Device.TraceEnable(CancellationToken); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index ca711360..73309878 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -4,7 +4,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("trace level", Description = "Sets the trace logging level on the Meadow")] -public class TraceLevelCommand : BaseTraceCommand +public class TraceLevelCommand : BaseDeviceCommand { [CommandParameter(0, Name = "Level", IsRequired = true)] public int Level { get; set; } @@ -16,24 +16,31 @@ public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFacto protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null) { - if (Level <= 0) - { - Logger?.LogInformation("Disabling tracing..."); - - await Connection.Device.TraceDisable(CancellationToken); - } - else - { - Logger?.LogInformation($"Setting trace level to {Level}..."); - await Connection.Device.SetTraceLevel(Level, CancellationToken); - - Logger?.LogInformation("Enabling tracing..."); - await Connection.Device.TraceEnable(CancellationToken); - } + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + if (Level <= 0) + { + Logger?.LogInformation("Disabling tracing..."); + + await connection.Device.SetTraceLevel(Level, CancellationToken); + } + else + { + Logger?.LogInformation($"Setting trace level to {Level}..."); + await connection.Device.SetTraceLevel(Level, CancellationToken); + + Logger?.LogInformation("Enabling tracing..."); + await connection.Device.TraceEnable(CancellationToken); } } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs index 139ed92a..646492d4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs @@ -4,7 +4,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("uart trace disable", Description = "Disables trace log output to UART")] -public class UartTraceDisableCommand : BaseTraceCommand +public class UartTraceDisableCommand : BaseDeviceCommand { public UartTraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -13,11 +13,20 @@ public UartTraceDisableCommand(MeadowConnectionManager connectionManager, ILogge protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; Logger?.LogInformation("Setting UART to application use..."); - if (Connection != null && Connection.Device != null) - await Connection.Device.UartTraceDisable(CancellationToken); + await connection.Device.UartTraceDisable(CancellationToken); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs index 587362d6..d2e8b4bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs @@ -4,7 +4,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("uart trace enable", Description = "Enables trace log output to UART")] -public class UartTraceEnableCommand : BaseTraceCommand +public class UartTraceEnableCommand : BaseDeviceCommand { public UartTraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -13,11 +13,20 @@ public UartTraceEnableCommand(MeadowConnectionManager connectionManager, ILogger protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; Logger?.LogInformation("Setting UART to output trace messages..."); - if (Connection != null && Connection.Device != null) - await Connection.Device.UartTraceEnable(CancellationToken); + await connection.Device.UartTraceEnable(CancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index b97d2ec0..b81f43aa 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -11,10 +11,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FlashOsCommand : BaseDeviceCommand { [CommandOption("osFile", 'o', Description = "Path to the Meadow OS binary")] - public string? OSFile { get; init; } + public string OSFile { get; init; } [CommandOption("runtimeFile", 'r', Description = "Path to the Meadow Runtime binary")] - public string? RuntimeFile { get; init; } + public string RuntimeFile { get; init; } [CommandOption("skipDfu", 'd', Description = "Skip DFU flash")] public bool SkipOS { get; init; } @@ -29,7 +29,7 @@ public class FlashOsCommand : BaseDeviceCommand public bool DontPrompt { get; init; } [CommandOption("osVersion", 'v', Description = "Flash a specific downloaded OS version - x.x.x.x")] - public string? Version { get; private set; } + public string Version { get; private set; } private FirmwareType[]? Files { get; set; } = default!; private bool UseDfu = true; @@ -50,135 +50,126 @@ public FlashOsCommand(ISettingsManager settingsManager, FileManager fileManager, protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var package = await GetSelectedPackage(); - if (Connection != null) - { - var package = await GetSelectedPackage(); - - var files = new List(); - if (!SkipOS) files.Add(FirmwareType.OS); - if (!SkipEsp) files.Add(FirmwareType.ESP); - if (!SkipRuntime) files.Add(FirmwareType.Runtime); - Files = files.ToArray(); + var files = new List(); + if (!SkipOS) files.Add(FirmwareType.OS); + if (!SkipEsp) files.Add(FirmwareType.ESP); + if (!SkipRuntime) files.Add(FirmwareType.Runtime); + Files = files.ToArray(); - if (Files == null && package != null) - { - Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); + if (Files == null) + { + Logger.LogInformation($"Writing all firmware for version '{package.Version}'..."); - Files = new FirmwareType[] - { + Files = new FirmwareType[] + { FirmwareType.OS, FirmwareType.Runtime, FirmwareType.ESP - }; + }; + } + + if (!Files.Contains(FirmwareType.OS) && UseDfu) + { + Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + return; + } + + bool deviceSupportsOta = false; // TODO: get this based on device OS version + + if (package.OsWithoutBootloader == null + || !deviceSupportsOta + || UseDfu) + { + UseDfu = true; + } + + + if (UseDfu && Files.Contains(FirmwareType.OS)) + { + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + // get the device's serial number via DFU - we'll need it to find the device after it resets + try + { + _libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); } + catch (Exception ex) + { + Logger.LogError(ex.Message); + return; + } + + var serial = _libUsbDevice.GetDeviceSerialNumber(); - if (Files != null) + // no connection is required here - in fact one won't exist + // unless maybe we add a "DFUConnection"? + + try { - if (!Files.Contains(FirmwareType.OS) && UseDfu) - { - Logger?.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); - return; - } + await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); + } + catch (Exception ex) + { + Logger.LogError($"Exception type: {ex.GetType().Name}"); - bool deviceSupportsOta = false; // TODO: get this based on device OS version + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - if (package != null && package.OsWithoutBootloader == null - || !deviceSupportsOta - || UseDfu) - { - UseDfu = true; - } + Logger.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); + return; + } - if (UseDfu && Files.Contains(FirmwareType.OS)) - { - // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - - // get the device's serial number via DFU - we'll need it to find the device after it resets - try - { - _libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); - } - catch (Exception ex) - { - Logger?.LogError(ex.Message); - return; - } - - var serial = _libUsbDevice.GetDeviceSerialNumber(); - - // no connection is required here - in fact one won't exist - // unless maybe we add a "DFUConnection"? - - try - { - if (package != null && package.OSWithBootloader != null) - { - await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); - } - } - catch (Exception ex) - { - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - - Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); - return; - } - - // now wait for a new serial port to appear - var ports = await MeadowConnectionManager.GetSerialPorts(); - var retryCount = 0; - - var newPort = ports.Except(initialPorts).FirstOrDefault(); - while (newPort == null) - { - if (retryCount++ > 10) - { - throw new Exception("New meadow device not found"); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); - newPort = ports.Except(initialPorts).FirstOrDefault(); - } - - // configure the route to that port for the user - if (newPort != null) - { - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort.Name!); - - var cancellationToken = Console?.RegisterCancellationHandler(); - - if (Files.Any(f => f != FirmwareType.OS)) - { - await Connection.WaitForMeadowAttach(); - - await WriteFiles(); - } - - if (Connection.Device != null) - { - var deviceInfo = await Connection.Device.GetDeviceInfo(cancellationToken); - - if (deviceInfo != null) - { - Logger?.LogInformation($"Done."); - Logger?.LogInformation(deviceInfo.ToString()); - } - } - } - } - else + // now wait for a new serial port to appear + var ports = await MeadowConnectionManager.GetSerialPorts(); + var retryCount = 0; + + var newPort = ports.Except(initialPorts).FirstOrDefault(); + while (newPort == null) + { + if (retryCount++ > 10) { - await WriteFiles(); + throw new Exception("New meadow device not found"); } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + newPort = ports.Except(initialPorts).FirstOrDefault(); + } + + // configure the route to that port for the user + Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + var connection = ConnectionManager.GetCurrentConnection(); + if (connection == null) + { + Logger.LogError($"No connection path is defined"); + return; + } + + var cancellationToken = Console.RegisterCancellationHandler(); + + if (Files.Any(f => f != FirmwareType.OS)) + { + await connection.WaitForMeadowAttach(); + + await WriteFiles(); + } + + var deviceInfo = await connection.Device.GetDeviceInfo(cancellationToken); + + if (deviceInfo != null) + { + Logger.LogInformation($"Done."); + Logger.LogInformation(deviceInfo.ToString()); } } + else + { + await WriteFiles(); + } } private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() @@ -223,7 +214,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() if (existing == null) { - Logger?.LogError($"Requested version '{Version}' not found."); + Logger.LogError($"Requested version '{Version}' not found."); return null; } package = existing; @@ -241,106 +232,92 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() private async ValueTask WriteFiles() { - if (Connection != null) + var connection = await GetCurrentConnection(); + + if (connection == null) { - var package = await GetSelectedPackage(); + return; + } - if (Connection.Device != null - && package != null) + // the connection passes messages back to us (info about actions happening on-device + connection.DeviceMessageReceived += (s, e) => + { + if (e.message.Contains("% downloaded")) + { + // don't echo this, as we're already reporting % written + } + else { - var wasRuntimeEnabled = await Connection.Device.IsRuntimeEnabled(CancellationToken); + Logger?.LogInformation(e.message); + } + }; + connection.ConnectionMessage += (s, message) => + { + Logger.LogInformation(message); + }; - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling device runtime..."); - await Connection.Device.RuntimeDisable(); - } + var package = await GetSelectedPackage(); - Connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling device runtime..."); + await connection.Device.RuntimeDisable(); + } + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; - if (Files != null) + if (Files.Contains(FirmwareType.OS)) + { + if (UseDfu) + { + // this would have already happened before now (in ExecuteAsync) so ignore + } + else + { + Logger.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + + throw new NotSupportedException("OtA writes for the OS are not yet supported"); + } + } + if (Files.Contains(FirmwareType.Runtime)) + { + Logger.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + // TODO: for serial, we must wait for the flash to complete + + await connection.Device.WriteRuntime(rtpath, CancellationToken); + } + if (Files.Contains(FirmwareType.ESP)) + { + Logger.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + + var fileList = new string[] { - if (Files.Contains(FirmwareType.OS)) - { - if (UseDfu) - { - // this would have already happened before now (in ExecuteAsync) so ignore - } - else - { - Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - - throw new NotSupportedException("OtA writes for the OS are not yet supported"); - } - } - - if (Files.Contains(FirmwareType.Runtime)) - { - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); - - // get the path to the runtime file - var runtime = package.Runtime; - if (string.IsNullOrEmpty(runtime)) - runtime = string.Empty; - var rtpath = package.GetFullyQualifiedPath(runtime); - - write_runtime: - // If we've cancelled bail - if (CancellationToken.IsCancellationRequested) - { - return; - } - - if (!await Connection.Device.WriteRuntime(rtpath, CancellationToken)) - { - Logger?.LogInformation($"Error writing runtime. Retrying."); - goto write_runtime; - } - } - - - if (Files.Contains(FirmwareType.ESP)) - { - Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); - - string[]? fileList; - if (package.CoprocApplication != null - && package.CoprocBootloader != null - && package.CoprocPartitionTable != null) - { - fileList = new string[] - { package.GetFullyQualifiedPath(package.CoprocApplication), package.GetFullyQualifiedPath(package.CoprocBootloader), package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - else - { - fileList = Array.Empty(); - } - - await Connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); - } - - Logger?.LogInformation($"{Environment.NewLine}"); - - if (wasRuntimeEnabled) - { - await Connection.Device.RuntimeEnable(); - } - } + }; - // TODO: if we're an F7 device, we need to reset - } + await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); } + + Logger.LogInformation($"{Environment.NewLine}"); + + if (wasRuntimeEnabled) + { + await connection.Device.RuntimeEnable(); + } + + // TODO: if we're an F7 device, we need to reset } private async Task WriteOsWithDfu(string osFile, string serialNumber) @@ -351,4 +328,4 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs index d17c8c99..f5bdb9f0 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -9,6 +9,6 @@ public class MonoEnableCommand : RuntimeEnableCommand public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime enable` instead"); + Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs index 9514b87e..02b08da3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs @@ -9,6 +9,6 @@ public class MonoStateCommand : RuntimeStateCommand public MonoStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime state` instead"); + Logger.LogWarning($"Deprecated command. Use `runtime state` instead"); } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs b/Source/v2/Meadow.Cli/DFU/DfuSharp.cs index 62f502f0..04888f92 100644 --- a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs +++ b/Source/v2/Meadow.Cli/DFU/DfuSharp.cs @@ -199,7 +199,6 @@ struct DeviceDescriptor struct ConfigDescriptor { -#pragma warning disable CS0649 public byte bLength; public byte bDescriptorType; public ushort wTotalLength; @@ -211,15 +210,12 @@ struct ConfigDescriptor public IntPtr interfaces; public IntPtr extra; public int extra_length; -#pragma warning restore CS0649 } struct @Interface { -#pragma warning disable CS0649 public IntPtr altsetting; public int num_altsetting; -#pragma warning restore CS0649 public InterfaceDescriptor[] Altsetting { @@ -293,7 +289,7 @@ public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFun throw new Exception("Error opening device"); } - public event UploadingEventHandler? Uploading; + public event UploadingEventHandler Uploading; protected virtual void OnUploading(UploadingEventArgs e) { diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs index 05e37243..ad231b39 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI; +namespace Meadow.CLI; public interface IPackageManager { @@ -10,14 +8,12 @@ bool BuildApplication( string projectFilePath, string configuration = "Release", bool clean = true, - ILogger? logger = null, CancellationToken? cancellationToken = null); Task TrimApplication( FileInfo applicationFilePath, bool includePdbs = false, IList? noLink = null, - ILogger? logger = null, CancellationToken? cancellationToken = null); Task AssemblePackage( @@ -26,14 +22,5 @@ Task AssemblePackage( string osVersion, string filter = "*", bool overwrite = false, - ILogger? logger = null, CancellationToken? cancellationToken = null); - - List? AssemblyDependencies { get; set; } - - IEnumerable? TrimmedDependencies { get; set; } - bool Trimmed { get; set; } - - string? RuntimeVersion { get; set; } - string? MeadowAssembliesPath { get; } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 04e6e8fe..2a8493d2 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,13 +1,10 @@ using Meadow.CLI; using Meadow.Hcom; -using Meadow.LibUsb; using System.Diagnostics; using System.IO.Ports; -using System.Linq; using System.Management; using System.Net; using System.Runtime.InteropServices; -using System.Runtime.Versioning; namespace Meadow.CLI.Commands.DeviceManagement; @@ -37,56 +34,49 @@ public MeadowConnectionManager(ISettingsManager settingsManager) if (_currentConnection != null) return _currentConnection; // try to determine what the route is - if (route == "local") + string? uri = null; + if (route.StartsWith("http")) { - _currentConnection = new LocalConnection(); + uri = route; + } + else if (IPAddress.TryParse(route, out var ipAddress)) + { + uri = $"http://{route}:5000"; + } + else if (IPEndPoint.TryParse(route, out var endpoint)) + { + uri = $"http://{route}"; + } + + if (uri != null) + { + _currentConnection = new TcpConnection(uri); } else { - string? uri = null; - if (route.StartsWith("http")) - { - uri = route; - } - else if (IPAddress.TryParse(route, out var ipAddress)) - { - uri = $"http://{route}:5000"; - } - else if (IPEndPoint.TryParse(route, out var endpoint)) - { - uri = $"http://{route}"; - } + var retryCount = 0; - if (uri != null) + get_serial_connection: + try { - _currentConnection = new TcpConnection(uri); + _currentConnection = new SerialConnection(route); } - else + catch { - var retryCount = 0; - - get_serial_connection: - try - { - _currentConnection = new SerialConnection(route); - } - catch + retryCount++; + if (retryCount > 10) { - retryCount++; - if (retryCount > 10) - { - throw new Exception($"Cannot find port {route}"); - } - Thread.Sleep(500); - goto get_serial_connection; + throw new Exception($"Cannot find port {route}"); } + Thread.Sleep(500); + goto get_serial_connection; } } return _currentConnection; } - public static async Task> GetSerialPorts() + public static async Task> GetSerialPorts() { try { @@ -117,14 +107,14 @@ public static async Task> GetSerialPorts() } } - public static async Task> GetMeadowSerialPortsForOsx() + public static async Task> GetMeadowSerialPortsForOsx() { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) == false) throw new PlatformNotSupportedException("This method is only supported on macOS"); return await Task.Run(() => { - var ports = new List(); + var ports = new List(); var psi = new ProcessStartInfo { @@ -166,10 +156,8 @@ public static async Task> GetMeadowSerialPortsForOsx() int startIndex = line.IndexOf("/"); int endIndex = line.IndexOf("\"", startIndex + 1); var port = line.Substring(startIndex, endIndex - startIndex); - int serialNumberIndex = line.IndexOf("tty.usbmodem") + 12; - var serialNumber = line.Substring(serialNumberIndex, endIndex - serialNumberIndex); - ports.Add(new MeadowSerialPort { Name = port, SerialNumber = serialNumber }); + ports.Add(port); foundMeadow = false; } } @@ -178,7 +166,7 @@ public static async Task> GetMeadowSerialPortsForOsx() }); } - public static async Task> GetMeadowSerialPortsForLinux() + public static async Task> GetMeadowSerialPortsForLinux() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) throw new PlatformNotSupportedException("This method is only supported on Linux"); @@ -196,37 +184,22 @@ public static async Task> GetMeadowSerialPortsForLinux() using var proc = Process.Start(psi); _ = proc?.WaitForExit(1000); - var output = proc?.StandardOutput; - - if (output != null) - { - var outputText = output.ReadToEnd(); - if (!string.IsNullOrEmpty(outputText)) - { - - return outputText - .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(x => x.Contains("Wilderness_Labs")) - .Select( - line => - { - var parts = line.Split(new[] { "-> " }, StringSplitOptions.RemoveEmptyEntries); - var target = parts[1]; - var port = Path.GetFullPath(Path.Combine(devicePath, target)); - int serialNumberIndex = line.IndexOf("ttyACM") + 6; - var serialNumber = line.Substring(serialNumberIndex); - - return new MeadowSerialPort { Name = port, SerialNumber = serialNumber }; - }).ToArray(); - } - } - - return Array.Empty(); + var output = proc?.StandardOutput.ReadToEnd(); + + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Where(x => x.Contains("Wilderness_Labs")) + .Select( + line => + { + var parts = line.Split(new[] { "-> " }, StringSplitOptions.RemoveEmptyEntries); + var target = parts[1]; + var port = Path.GetFullPath(Path.Combine(devicePath, target)); + return port; + }).ToArray(); }); } - [SupportedOSPlatform("windows")] - public static IList GetMeadowSerialPortsForWindows() + public static IList GetMeadowSerialPortsForWindows() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) throw new PlatformNotSupportedException("This method is only supported on Windows"); @@ -244,7 +217,7 @@ public static IList GetMeadowSerialPortsForWindows() // our query for all ports that have a PnP device id starting with Wilderness Labs' USB VID. string query = @$"SELECT Name, Caption, PNPDeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND PNPDeviceID like '{escapedPrefix}%'"; - List results = new(); + List results = new(); // build the searcher for the query using ManagementObjectSearcher searcher = new(wmiScope, query); @@ -252,42 +225,29 @@ public static IList GetMeadowSerialPortsForWindows() // get the query results foreach (ManagementObject moResult in searcher.Get()) { - // Try Caption 1st, then Name, they both seem to contain the COM port - var captionObject = moResult["Caption"]; - - var portLongName = captionObject?.ToString(); + // Try Caption and if not Name, they both seems to contain the COM port + string portLongName = moResult["Caption"].ToString(); if (string.IsNullOrEmpty(portLongName)) - { - var nameObject = moResult["Name"]; - portLongName = nameObject?.ToString(); - } + portLongName = moResult["Name"].ToString(); + string pnpDeviceId = moResult["PNPDeviceID"].ToString(); // we could collect and return a fair bit of other info from the query: + //string description = moResult["Description"].ToString(); //string service = moResult["Service"].ToString(); //string manufacturer = moResult["Manufacturer"].ToString(); - if (!string.IsNullOrEmpty(portLongName)) - { - var comIndex = portLongName.IndexOf("(COM") + 1; - var copyLength = portLongName.IndexOf(")") - comIndex; - var port = portLongName.Substring(comIndex, copyLength); - - var pnpDeviceObject = moResult["PNPDeviceID"]; - var pnpDeviceId = pnpDeviceObject?.ToString(); - - // the meadow serial is in the device id, after - // the characters: USB\VID_XXXX&PID_XXXX\ - // so we'll just split is on \ and grab the 3rd element as the format is standard, but the length may vary. - string? serialNumber = string.Empty; - if (!string.IsNullOrEmpty(pnpDeviceId)) - { - var splits = pnpDeviceId.Split('\\'); - serialNumber = splits[2]; - } - - results.Add(new MeadowSerialPort { Name = port, SerialNumber = serialNumber }); - } + var comIndex = portLongName.IndexOf("(COM") + 1; + var copyLength = portLongName.IndexOf(")") - comIndex; + var port = portLongName.Substring(comIndex, copyLength); + + // the meadow serial is in the device id, after + // the characters: USB\VID_XXXX&PID_XXXX\ + // so we'll just split is on \ and grab the 3rd element as the format is standard, but the length may vary. + var splits = pnpDeviceId.Split('\\'); + var serialNumber = splits[2]; + + results.Add($"{port}"); // removed serial number for consistency and will break fallback ({serialNumber})"); } return results.ToArray(); @@ -300,53 +260,7 @@ public static IList GetMeadowSerialPortsForWindows() //hack to skip COM1 ports = ports.Where((source, index) => source != "COM1").Distinct().ToArray(); - List results = new(); - foreach (var port in ports) - { - results.Add(new MeadowSerialPort { Name = port, SerialNumber = string.Empty }); - } - - return results; - } - } - - public static async Task GetPortFromSerialNumber(string serialNumber) - { - var retryCount = 0; - - string? newPort = null; - - // now wait for the serial port with the passed in serialNumber to appear - while (newPort == null) - { - var ports = await GetSerialPorts(); - - if (ports != null) - { - foreach (var meadowSerialPort in ports) - { - if (meadowSerialPort.SerialNumber != null && meadowSerialPort.SerialNumber.Contains(serialNumber)) - { - newPort = meadowSerialPort.Name; - break; - } - } - - if (retryCount++ > 12) - { - throw new Exception("Meadow device not found"); - } - } - - await Task.Delay(500); + return ports; } - - return newPort; } } - -public class MeadowSerialPort -{ - public string? Name { get; set; } - public string? SerialNumber { get; set; } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs index f5f7c8e8..2d0c4610 100644 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -1,292 +1,36 @@ -using Microsoft.Extensions.Logging; -using Mono.Cecil; -using Mono.Collections.Generic; -using System.Diagnostics; -using System.Reflection; - -namespace Meadow.CLI; +namespace Meadow.CLI; public partial class PackageManager { - private const string IL_LINKER_DIR = "lib"; + private const string PreLinkDirectoryName = "prelink_bin"; public const string PostLinkDirectoryName = "postlink_bin"; - public const string PreLinkDirectoryName = "prelink_bin"; public const string PackageOutputDirectoryName = "mpak"; - - private readonly List dependencyMap = new(); - private string? _meadowAssembliesPath; - public string? MeadowAssembliesPath + private string? MeadowAssembliesPath { get { if (_meadowAssembliesPath == null) - { - // for now we only support F7 + { // for now we only support F7 // TODO: add switch and support for other platforms var store = _fileManager.Firmware["Meadow F7"]; if (store != null) { store.Refresh(); - - if (RuntimeVersion == null) - { - if (store.DefaultPackage != null) - { - var defaultPackage = store.DefaultPackage; - - if (defaultPackage.BclFolder != null) - { - _meadowAssembliesPath = defaultPackage.GetFullyQualifiedPath(defaultPackage.BclFolder); - } - } - } - else + if (store.DefaultPackage != null && store.DefaultPackage.BclFolder != null) { - var existing = store.FirstOrDefault(p => p.Version == RuntimeVersion); - - if (existing == null || existing.BclFolder == null) return null; - - _meadowAssembliesPath = existing.GetFullyQualifiedPath(existing.BclFolder); + _meadowAssembliesPath = store.DefaultPackage.GetFullyQualifiedPath(store.DefaultPackage.BclFolder); } } } - return _meadowAssembliesPath; } } - public List? AssemblyDependencies { get; set; } - - public IEnumerable? TrimmedDependencies { get; set; } - public bool Trimmed { get; set; } = false; - - public string? RuntimeVersion { get; set; } - - public async Task?> TrimDependencies(FileInfo file, List dependencies, IList? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) - { - var directoryName = file.DirectoryName; - if (!string.IsNullOrEmpty(directoryName)) - { - var fileName = file.Name; - var prelink_dir = Path.Combine(directoryName, PreLinkDirectoryName); - var prelink_app = Path.Combine(prelink_dir, fileName); - var prelink_os = Path.Combine(prelink_dir, "Meadow.dll"); - - if (Directory.Exists(prelink_dir)) - { - Directory.Delete(prelink_dir, recursive: true); - } - - Directory.CreateDirectory(prelink_dir); - File.Copy(file.FullName, prelink_app, overwrite: true); - - foreach (var dependency in dependencies) - { - File.Copy(dependency, - Path.Combine(prelink_dir, Path.GetFileName(Path.GetFileName(dependency))), - overwrite: true); - - if (includePdbs) - { - var pdbFile = Path.ChangeExtension(dependency, "pdb"); - if (File.Exists(pdbFile)) - { - File.Copy(pdbFile, - Path.Combine(prelink_dir, Path.GetFileName(pdbFile)), - overwrite: true); - } - } - } - - var postlink_dir = Path.Combine(directoryName, PostLinkDirectoryName); - if (Directory.Exists(postlink_dir)) - { - Directory.Delete(postlink_dir, recursive: true); - } - Directory.CreateDirectory(postlink_dir); - - var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (!string.IsNullOrEmpty(base_path)) - { - var illinker_path = Path.Combine(base_path, IL_LINKER_DIR, "illink.dll"); - var descriptor_path = Path.Combine(base_path, IL_LINKER_DIR, "meadow_link.xml"); - - if (!File.Exists(illinker_path)) - { - throw new FileNotFoundException("Cannot run trimming operation. illink.dll not found."); - } - - if (linkerOptions != null) - { - var fi = new FileInfo(linkerOptions); - - if (fi.Exists) - { - logger?.LogInformation($"Using linker options from '{linkerOptions}'"); - } - else - { - logger?.LogWarning($"Linker options file '{linkerOptions}' not found"); - } - } - - - // add in any run-time no-link arguments - var no_link_args = string.Empty; - if (noLink != null) - { - // no-link options want just the assembly name (i.e. no ".dll" extension) - no_link_args = string.Join(" ", noLink.Select(o => $"-p copy \"{o.Replace(".dll", string.Empty)}\"")); - } - - var monolinker_args = $"\"{illinker_path}\" -x \"{descriptor_path}\" {no_link_args} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlink_dir}\" -r \"{prelink_app}\" -a \"{prelink_os}\" -d \"{prelink_dir}\""; - - logger?.LogInformation($"Trimming assemblies associated with {fileName} to reduce upload size (this may take a few seconds)..."); - if (!string.IsNullOrWhiteSpace(no_link_args)) - { - logger?.LogInformation($"no-link args:'{no_link_args}'"); - } - - using (var process = new Process()) - { - process.StartInfo.FileName = "dotnet"; - process.StartInfo.Arguments = monolinker_args; - process.StartInfo.UseShellExecute = false; - process.StartInfo.CreateNoWindow = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.RedirectStandardOutput = true; - process.Start(); - - // To avoid deadlocks, read the output stream first and then wait - string stdOutReaderResult; - using (StreamReader stdOutReader = process.StandardOutput) - { - stdOutReaderResult = await stdOutReader.ReadToEndAsync(); - if (verbose) - { - logger?.LogInformation("StandardOutput Contains: " + stdOutReaderResult); - } - - } - - string stdErrorReaderResult; - using (StreamReader stdErrorReader = process.StandardError) - { - stdErrorReaderResult = await stdErrorReader.ReadToEndAsync(); - if (!string.IsNullOrEmpty(stdErrorReaderResult)) - { - logger?.LogInformation("StandardError Contains: " + stdErrorReaderResult); - } - } - - process.WaitForExit(60000); - if (process.ExitCode != 0) - { - logger?.LogDebug($"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); - throw new Exception("Trimming failed"); - } - } - - return Directory.EnumerateFiles(postlink_dir); - } - else - { - throw new DirectoryNotFoundException("Trimming failed: base_path is invalid"); - } - } - else - { - throw new ArgumentException("Trimming failed: file.DirectoryName is invalid"); - } - } - public List GetDependencies(FileInfo file) { - dependencyMap.Clear(); - - var directoryName = file.DirectoryName; - if (!string.IsNullOrEmpty(directoryName)) - { - var refs = GetAssemblyReferences(file.Name, directoryName); - - var dependencies = GetDependencies(refs, dependencyMap, directoryName); - - return dependencies; - } - else - { - return new(); - } - } - - private (Collection? References, string? ResolvedPath) GetAssemblyReferences(string fileName, string path) - { - static string? ResolvePath(string fileName, string path) - { - string attemptedPath = Path.Combine(path, fileName); - if (Path.GetExtension(fileName) != ".exe" - && Path.GetExtension(fileName) != ".dll") - { - attemptedPath += ".dll"; - } - return File.Exists(attemptedPath) ? attemptedPath : null; - } - - if (!string.IsNullOrEmpty(MeadowAssembliesPath)) - { - string? resolvedPath = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); - - if (resolvedPath is null) - { - return (null, null); - } - - Collection references; - - try - { - using (var definition = AssemblyDefinition.ReadAssembly(resolvedPath)) - { - references = definition.MainModule.AssemblyReferences; - } - return (references, resolvedPath); - } - catch (Exception ex) - { - // Handle or log the exception appropriately - Console.WriteLine($"Error reading assembly: {ex.Message}"); - return (null, null); - } - } - else - { - return (null, null); - } - } - - private List GetDependencies((Collection? References, string? ResolvedPath) references, List dependencyMap, string folderPath) - { - if (references.ResolvedPath == null || dependencyMap.Contains(references.ResolvedPath)) - return dependencyMap; - - dependencyMap.Add(references.ResolvedPath); - - if (references.References != null) - { - foreach (var ar in references.References) - { - var namedRefs = GetAssemblyReferences(ar.Name, folderPath); - - if (namedRefs.References == null) - continue; - - GetDependencies(namedRefs, dependencyMap, folderPath); - } - } - - return dependencyMap; + return _meadowLinker.MapDependencies(file); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs index 5bec4adc..190f44c8 100644 --- a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs +++ b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs @@ -4,11 +4,11 @@ public partial class PackageManager { private record BuildOptions { - public DeployOptions? Deploy { get; set; } + public DeployOptions Deploy { get; set; } public record DeployOptions { - public List? NoLink { get; set; } + public List NoLink { get; set; } public bool? IncludePDBs { get; set; } } } diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 5423871f..c4881a52 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -1,7 +1,7 @@ using GlobExpressions; +using LinkerTest; using Meadow.Cloud; using Meadow.Software; -using Microsoft.Extensions.Logging; using System.Diagnostics; using System.IO.Compression; using System.Runtime.InteropServices; @@ -15,10 +15,13 @@ public partial class PackageManager : IPackageManager public const string BuildOptionsFileName = "app.build.yaml"; private FileManager _fileManager; + private MeadowLinker _meadowLinker; public PackageManager(FileManager fileManager) { _fileManager = fileManager; + + _meadowLinker = new MeadowLinker(MeadowAssembliesPath, null); } private bool CleanApplication(string projectFilePath, string configuration = "Release", CancellationToken? cancellationToken = null) @@ -67,7 +70,7 @@ private bool CleanApplication(string projectFilePath, string configuration = "Re return success; } - public bool BuildApplication(string projectFilePath, string configuration = "Release", bool clean = true, ILogger? logger = null, CancellationToken? cancellationToken = null) + public bool BuildApplication(string projectFilePath, string configuration = "Release", bool clean = true, CancellationToken? cancellationToken = null) { if (clean && !CleanApplication(projectFilePath, configuration, cancellationToken)) { @@ -99,7 +102,6 @@ public bool BuildApplication(string projectFilePath, string configuration = "Rel Debug.WriteLine(dataLine.Data); if (dataLine.Data.Contains("Build FAILED", StringComparison.InvariantCultureIgnoreCase)) { - logger?.LogError(dataLine.Data); Debug.WriteLine("Build failed"); success = false; } @@ -119,11 +121,10 @@ public bool BuildApplication(string projectFilePath, string configuration = "Rel return success; } - public async Task TrimApplication( + public Task TrimApplication( FileInfo applicationFilePath, bool includePdbs = false, IList? noLink = null, - ILogger? logger = null, CancellationToken? cancellationToken = null) { if (!applicationFilePath.Exists) @@ -131,52 +132,32 @@ public async Task TrimApplication( throw new FileNotFoundException($"{applicationFilePath} not found"); } - Trimmed = false; - - // does an app.build.yaml file exist? + // does a meadow.build.yml file exist? var buildOptionsFile = Path.Combine( applicationFilePath.DirectoryName ?? string.Empty, BuildOptionsFileName); if (File.Exists(buildOptionsFile)) { - logger?.LogInformation($"'{BuildOptionsFileName}' is present"); var yaml = File.ReadAllText(buildOptionsFile); var deserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() .Build(); var opts = deserializer.Deserialize(yaml); - if (opts.Deploy?.NoLink != null && opts.Deploy?.NoLink.Count > 0) + if (opts.Deploy.NoLink != null && opts.Deploy.NoLink.Count > 0) { noLink = opts.Deploy.NoLink; } - if (opts.Deploy?.IncludePDBs != null) + if (opts.Deploy.IncludePDBs != null) { includePdbs = opts.Deploy.IncludePDBs.Value; } } - AssemblyDependencies = GetDependencies(applicationFilePath) - .ToList(); - - try - { - TrimmedDependencies = await TrimDependencies( - applicationFilePath, - AssemblyDependencies, - noLink, - logger, - includePdbs, - verbose: false); - } - catch (Exception) - { - logger?.LogError($"Trimming FAILED. Falling back to untrimmed dependencies"); - Trimmed = false; - } + var linker = new MeadowLinker(MeadowAssembliesPath); - Trimmed = true; + return linker.Trim(applicationFilePath, includePdbs, noLink); } public const string PackageMetadataFileName = "info.json"; @@ -187,7 +168,6 @@ public Task AssemblePackage( string osVersion, string filter = "*", bool overwrite = false, - ILogger? logger = null, CancellationToken? cancellationToken = null) { var di = new DirectoryInfo(outputFolder); @@ -273,10 +253,9 @@ void FindApp(string directory, List fileList) } FindApp(dir, fileList); - } } return files.ToArray(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index 609418a5..bc41606a 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -15,9 +15,14 @@ public class Program public static async Task Main(string[] args) { var logLevel = LogEventLevel.Information; + var logModifier = args.FirstOrDefault(a => a.Contains("-m")) + ?.Count(x => x == 'm') ?? 0; - if (args.Contains("--verbose")) - logLevel = LogEventLevel.Verbose; + logLevel -= logModifier; + if (logLevel < 0) + { + logLevel = 0; + } var outputTemplate = logLevel == LogEventLevel.Verbose ? "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}" @@ -63,8 +68,7 @@ public static async Task Main(string[] args) } else { - // Have an empty Configuration instead. - services.AddScoped(_ => new ConfigurationBuilder().Build()); + services.AddScoped(_ => null); } AddCommandsAsServices(services); @@ -75,7 +79,7 @@ public static async Task Main(string[] args) { await new CliApplicationBuilder() .AddCommandsFromThisAssembly() - .UseTypeActivator(serviceProvider.GetService!) + .UseTypeActivator(serviceProvider.GetService) .SetExecutableName("meadow") .Build() .RunAsync(); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index e0ad7a7b..8e3cb20e 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -173,7 +173,7 @@ }, "App Trim": { "commandName": "Project", - "commandLineArgs": "app trim F:\\temp\\MeadowApplication1" + "commandLineArgs": "app trim H:\\WL\\Meadow.ProjectLab\\Source\\ProjectLab_Demo\\" }, "Dfu Install": { "commandName": "Project", @@ -185,7 +185,7 @@ }, "App Deploy (project folder)": { "commandName": "Project", - "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1" + "commandLineArgs": "app deploy H:\\WL\\Meadow.ProjectLab\\Source\\ProjectLab_Demo\\bin\\Debug\\netstandard2.1\\postlink_bin" }, "App Deploy (untrimmed output)": { "commandName": "Project", diff --git a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs index f942dcef..7b6f40d0 100644 --- a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs +++ b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs @@ -7,11 +7,9 @@ public class LibSecret : IDisposable internal struct GError { -#pragma warning disable CS0649 public uint Domain; public int Code; public string Message; -#pragma warning restore CS0649 } public enum AttributeType @@ -53,7 +51,7 @@ public void SetSecret(String password) HandleError(errorPtr, "An error was encountered while writing secret to keyring"); } - public string? GetSecret() + public string GetSecret() { IntPtr passwordPtr = secret_password_lookup_sync(intPt, IntPtr.Zero, out IntPtr errorPtr, serviceLabel, Service, accountLabel, Account, IntPtr.Zero); HandleError(errorPtr, "An error was encountered while reading secret from keyring"); diff --git a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj index 412313a2..14afa078 100644 --- a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj +++ b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj @@ -7,10 +7,6 @@ True - - 4 - true - diff --git a/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs b/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs index 53e889ed..cee688ad 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs @@ -5,7 +5,7 @@ namespace Meadow.Cloud; public record PackageInfo { [JsonPropertyName("v")] - public string? Version { get; set; } + public string Version { get; set; } [JsonPropertyName("osVersion")] - public string? OsVersion { get; set; } + public string OsVersion { get; set; } } diff --git a/Source/v2/Meadow.Cloud.Client/Messages/User.cs b/Source/v2/Meadow.Cloud.Client/Messages/User.cs index 7cd4b6c2..1951d6e6 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/User.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/User.cs @@ -2,9 +2,9 @@ public record User { - public string? Id { get; set; } - public string? Email { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } - public string? FullName { get; set; } + public string Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string FullName { get; set; } } diff --git a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs index 55710994..14c9691c 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs @@ -5,9 +5,9 @@ namespace Meadow.Cloud; public class UserOrg { [JsonPropertyName("id")] - public string? Id { get; set; } + public string Id { get; set; } [JsonPropertyName("name")] - public string? Name { get; set; } + public string Name { get; set; } [JsonPropertyName("defaultCollectionId")] - public string? DefaultCollectionId { get; set; } + public string DefaultCollectionId { get; set; } } diff --git a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs index 4b619888..02f3f260 100644 --- a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs +++ b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs @@ -86,14 +86,11 @@ private string GetPackageOsVersion(string packagePath) { var content = File.ReadAllText(tempInfoJson); var packageInfo = JsonSerializer.Deserialize(content); - if (packageInfo != null) - { - result = packageInfo.OsVersion; - } + result = packageInfo.OsVersion; File.Delete(tempInfoJson); } - return result!; + return result; } public async Task PublishPackage( diff --git a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj index 1391a119..50cc994e 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj +++ b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj @@ -8,10 +8,6 @@ false - - 4 - true - diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs index c0330d65..92467a24 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -15,11 +15,12 @@ public void TestInvalidPortName() }); } - /* TODO [Fact] + [Fact] public async void TestListen() { using (var connection = new SerialConnection(ValidPortName)) { + /* Assert.Equal(ConnectionState.Disconnected, connection.State); var listener = new TestListener(); @@ -41,8 +42,9 @@ public async void TestListen() } Assert.True(listener.Messages.Count > 0); + */ } - }*/ + } [Fact] public async void TestAttachPositive() diff --git a/Source/v2/Meadow.HCom/Meadow.HCom.csproj b/Source/v2/Meadow.HCom/Meadow.HCom.csproj index 162d093a..a4d58b2c 100644 --- a/Source/v2/Meadow.HCom/Meadow.HCom.csproj +++ b/Source/v2/Meadow.HCom/Meadow.HCom.csproj @@ -7,9 +7,6 @@ 10 - - true - @@ -17,17 +14,9 @@ - - - - - - - - - + diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 0b67a43d..5055f0d3 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -2,33 +2,18 @@ namespace Meadow.Hcom; -public delegate void ConnectionStateChangedHandler(ConnectionBase connection, ConnectionState oldState, ConnectionState newState); - public abstract class ConnectionBase : IMeadowConnection, IDisposable { private bool _isDisposed; - private ConnectionState _state; - public ConnectionState State - { - get => _state; - protected set - { - if (value == State) return; - - var old = _state; - _state = value; - ConnectionStateChanged?.Invoke(this, old, State); - } - } + public ConnectionState State { get; protected set; } public IMeadowDevice? Device { get; protected set; } public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; public event EventHandler ConnectionError = default!; public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress = default!; public event EventHandler ConnectionMessage = default!; - public event EventHandler? FileWriteFailed; - public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + public event EventHandler FileWriteFailed; public abstract string Name { get; } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 44e50c1d..69e6af6e 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -30,7 +30,17 @@ public override async Task WaitForMeadowAttach(CancellationToken? cancellationTo await Task.Delay(500); - Open(); + if (!_port.IsOpen) + { + try + { + Open(); + } + catch (Exception ex) + { + Debug.WriteLine($"Unable to open port: {ex.Message}"); + } + } } throw new TimeoutException(); @@ -55,7 +65,7 @@ private async Task ListenerProc() read: try { - receivedLength = await _port.BaseStream.ReadAsync(readBuffer, 0, readBuffer.Length); + receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); } catch (OperationCanceledException) { @@ -124,7 +134,7 @@ private async Task ListenerProc() var response = SerialResponse.Parse(decodedBuffer, decodedSize); Debug.WriteLine($"{response.RequestType}"); - State = ConnectionState.MeadowAttached; + _state = ConnectionState.MeadowAttached; if (response != null) { @@ -175,7 +185,7 @@ private async Task ListenerProc() if (_reconnectInProgress) { - State = ConnectionState.MeadowAttached; + _state = ConnectionState.MeadowAttached; _reconnectInProgress = false; } else if (_textListComplete != null) @@ -194,8 +204,8 @@ private async Task ListenerProc() } else if (response is ReconnectRequiredResponse rrr) { - // the device is going to restart - we need to wait for a HCOM_HOST_REQUEST_TEXT_CONCLUDED/TextConcludedResponse to know it's back - State = ConnectionState.Disconnected; + // the device is going to restart - we need to wait for a HCOM_HOST_REQUEST_TEXT_CONCLUDED to know it's back + _state = ConnectionState.Disconnected; _reconnectInProgress = true; } else if (response is FileReadInitOkResponse fri) @@ -219,7 +229,7 @@ private async Task ListenerProc() _readFileInfo.FileStream = File.Create(_readFileInfo.LocalFileName); var uploadRequest = RequestBuilder.Build(); - await EncodeAndSendPacket(uploadRequest.Serialize()); + EncodeAndSendPacket(uploadRequest.Serialize()); } else if (response is UploadDataPacketResponse udp) { @@ -243,10 +253,7 @@ private async Task ListenerProc() _readFileInfo.FileStream.Dispose(); _readFileInfo = null; - if (!string.IsNullOrEmpty(fn)) - { - FileReadCompleted?.Invoke(this, fn); - } + FileReadCompleted?.Invoke(this, fn); } else if (response is FileReadInitFailedResponse frf) { @@ -305,20 +312,6 @@ private async Task ListenerProc() //this blocks the thread abort exception when the console app closes Debug.WriteLine($"listen abort"); } - catch (ObjectDisposedException) - { - // On Mac the port gets disposed when the Meadow is reset - await Task.Delay(1000); - CreatePort(); - - // make sure it's been re-opened - while(!_port.IsOpen) - { - _port.Open(); - await Task.Delay(250); - // TODO: add a timeout here - } - } catch (InvalidOperationException) { // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index f40a6317..53b7fe73 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1,1275 +1,1231 @@ -using Meadow.Hardware; -using Microsoft.Extensions.Logging; -using System.Buffers; -using System.Diagnostics; -using System.IO.Ports; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace Meadow.Hcom; - -public partial class SerialConnection : ConnectionBase, IDisposable -{ - public const int DefaultBaudRate = 115200; - public const int ReadBufferSizeBytes = 0x2000; - private const int DefaultTimeout = 5000; - - private event EventHandler? FileReadCompleted = delegate { }; - private event EventHandler? FileWriteAccepted; - private event EventHandler? FileDataReceived; - - private SerialPort _port = default!; - private ILogger? _logger; - private bool _isDisposed; - private List _listeners = new List(); - private Queue _pendingCommands = new Queue(); - private bool _maintainConnection; - private Thread? _connectionManager = null; - private List _textList = new List(); - private int _messageCount = 0; - private ReadFileInfo? _readFileInfo = null; - private string? _lastError = null; - - public override string Name { get; } - - public SerialConnection(string port, ILogger? logger = default) - { - if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) - { - throw new ArgumentException($"Serial Port '{port}' not found."); - } - - Name = port; - State = ConnectionState.Disconnected; - _logger = logger; - - CreatePort(); - - new Task( - () => _ = ListenerProc(), - TaskCreationOptions.LongRunning) - .Start(); - - new Thread(CommandManager) - { - IsBackground = true, - Name = "HCOM Sender" - } - .Start(); - } - - private void CreatePort() - { - _port = new SerialPort(Name); - _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; - _port.Open(); - } - - private bool MaintainConnection - { - get => _maintainConnection; - set - { - if (value == MaintainConnection) return; - - _maintainConnection = value; - - if (value) - { - if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) - { - _connectionManager = new Thread(ConnectionManagerProc) - { - IsBackground = true, - Name = "HCOM Connection Manager" - }; - _connectionManager.Start(); - - } - } - } - } - - private void ConnectionManagerProc() - { - while (_maintainConnection) - { - Open(true); - } - } - - public void AddListener(IConnectionListener listener) - { - lock (_listeners) - { - _listeners.Add(listener); - } - - Open(); - - MaintainConnection = true; - } - - public void RemoveListener(IConnectionListener listener) - { - lock (_listeners) - { - _listeners.Remove(listener); - } - - // TODO: stop maintaining connection? - } - - private void Open(bool inLoop = false) - { - if (!_port.IsOpen) - { - try - { - Debug.WriteLine("Opening COM port..."); - _port.Open(); - } - catch (UnauthorizedAccessException ex) - { - // Handle unauthorized access (e.g., port in use by another application) - throw new Exception($"Serial port '{_port.PortName}' is in use by another application.", ex.InnerException); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - catch (TimeoutException ex) - { - // Handle timeout - throw new Exception($"Timeout occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - } - else if (inLoop) - { - Thread.Sleep(1000); - } - - State = ConnectionState.Connected; - - Debug.WriteLine("Opened COM port"); - } - - private void Close() - { - if (_port.IsOpen) - { - try - { - _port.Close(); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when attempting to close the serial port '{_port.PortName}'.", ex.InnerException); - } - } - - State = ConnectionState.Disconnected; - } - - public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) - { - try - { - // ensure the port is open - Open(); - - // search for the device via HCOM - we'll use a simple command since we don't have a "ping" - var command = RequestBuilder.Build(); - - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang - - _port.DiscardInBuffer(); - - // wait for a response - var timeout = timeoutSeconds * 2; - var dataReceived = false; - - // local function so we can unsubscribe - var count = _messageCount; - - _pendingCommands.Enqueue(command); - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); - - if (count != _messageCount) - { - dataReceived = true; - break; - } - - await Task.Delay(500); - } - - // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no - - // create the device instance - if (dataReceived) - { - Device = new MeadowDevice(this); - } - - return Device; - } - catch (Exception e) - { - _logger?.LogError(e, "Failed to connect"); - throw; - } - } - - private async void CommandManager() - { - await Task.Run(async () => - { - while (!_isDisposed) - { - while (_pendingCommands.Count > 0) - { - Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); - - var command = _pendingCommands.Dequeue() as Request; - - // if this is a file write, we need to packetize for progress - - if (command != null) - { - var payload = command.Serialize(); - await EncodeAndSendPacket(payload); - } - - // TODO: re-queue on fail? - } - - Thread.Sleep(1000); - } - }); - } - - private class ReadFileInfo - { - private string? _localFileName; - - public string MeadowFileName { get; set; } = default!; - public string? LocalFileName - { - get - { - if (_localFileName != null) return _localFileName; - - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); - } - set => _localFileName = value; - } - public FileStream FileStream { get; set; } = default!; - } - - public void EnqueueRequest(IRequest command) - { - // TODO: verify we're connected - - if (command is InitFileReadRequest sfr) - { - _readFileInfo = new ReadFileInfo - { - MeadowFileName = sfr.MeadowFileName, - LocalFileName = sfr.LocalFileName, - }; - } - - _pendingCommands.Enqueue(command); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = default) - { - await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = default) - { - if (messageBytes != null) - { - Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - - while (!_port.IsOpen) - { - State = ConnectionState.Disconnected; - Thread.Sleep(100); - // wait for the port to open - } - - State = ConnectionState.Connected; - - try - { - int encodedToSend; - byte[] encodedBytes; - - // For file download this is a LOT of messages - // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); - - // For testing calculate the crc including the sequence number - //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); - try - { - // The encoded size using COBS is just a bit more than the original size adding 1 byte - // every 254 bytes plus 1 and need room for beginning and ending delimiters. - var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; - encodedBytes = new byte[l + 2]; - - // Skip over first byte so it can be a start delimiter - encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); - - // DEBUG TESTING - if (encodedToSend == -1) - { - _logger?.LogError($"Error - encodedToSend == -1"); - return; - } - - if (_port == null) - { - _logger?.LogError($"Error - SerialPort == null"); - throw new Exception("Port is null"); - } - } - catch (Exception except) - { - string msg = string.Format("Send setup Exception: {0}", except); - _logger?.LogError(msg); - throw; - } - - // Add delimiters to packet boundaries - try - { - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; - } - catch (Exception encodedBytesEx) - { - // This should drop the connection and retry - Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); - Thread.Sleep(500); // Place for break point - throw; - } - - try - { - // Send the data to Meadow - await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken.HasValue ? cancellationToken.Value : default); - } - catch (InvalidOperationException ioe) // Port not opened - { - string msg = string.Format("Write but port not opened. Exception: {0}", ioe); - _logger?.LogError(msg); - throw; - } - catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer - { - string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); - _logger?.LogError(msg); - throw; - } - catch (ArgumentException ae) // offset plus count > buffer length - { - string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); - _logger?.LogError(msg); - throw; - } - catch (TimeoutException te) // Took too long to send - { - string msg = string.Format("Write took too long to send. Exception: {0}", te); - _logger?.LogError(msg); - throw; - } - } - catch (Exception except) - { - // DID YOU RESTART MEADOW? - // This should drop the connection and retry - _logger?.LogError($"EncodeAndSendPacket threw: {except}"); - throw; - } - } - } - - - private class SerialMessage - { - private readonly IList> _segments; - - public SerialMessage(Memory segment) - { - _segments = new List>(); - _segments.Add(segment); - } - - public void AddSegment(Memory segment) - { - _segments.Add(segment); - } - - public byte[] ToArray() - { - using var ms = new MemoryStream(); - foreach (var segment in _segments) - { - // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. - var tmp = ArrayPool.Shared.Rent(segment.Length); - segment.CopyTo(tmp); - ms.Write(tmp, 0, segment.Length); - ArrayPool.Shared.Return(tmp); - } - return ms.ToArray(); - } - } - - private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) - { - var decodedBuffer = ArrayPool.Shared.Rent(8192); - var packetLength = packetBuffer.Length; - // It's possible that we may find a series of 0x00 values in the buffer. - // This is because when the sender is blocked (because this code isn't - // running) it will attempt to send a single 0x00 before the full message. - // This allows it to test for a connection. When the connection is - // unblocked this 0x00 is sent and gets put into the buffer along with - // any others that were queued along the usb serial pipe line. - if (packetLength == 1) - { - //_logger?.LogTrace("Throwing out 0x00 from buffer"); - return false; - } - - var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); - - /* - // If a message is too short it is ignored - if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) - { - return false; - } - - Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); - - // Process the received packet - ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), - cancellationToken); - - */ - ArrayPool.Shared.Return(decodedBuffer); - return true; - } - - protected override void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - Close(); - _port.Dispose(); - } - - _isDisposed = true; - } - } - - // ---------------------------------------------- - // ---------------------------------------------- - // ---------------------------------------------- - - private Exception? _lastException; - private bool? _textListComplete; - private DeviceInfo? _deviceInfo; - private RequestType? _lastRequestConcluded = null; - private List StdOut { get; } = new List(); - private List StdErr { get; } = new List(); - private List InfoMessages { get; } = new List(); - - private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; - private const string RuntimeStateToken = "Mono is"; - private const string RuntimeIsEnabledToken = "Mono is enabled"; - private const string RuntimeIsDisabledToken = "Mono is disabled"; - private const string RuntimeHasBeenToken = "Mono has been"; - private const string RuntimeHasBeenEnabledToken = "Mono has been enabled"; - private const string RuntimeHasBeenDisabledToken = "Mono has been disabled"; - private const string RtcRetrievalToken = "UTC time:"; - - public int CommandTimeoutSeconds { get; set; } = 30; - - private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) - { - var timeout = CommandTimeoutSeconds * 2; - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return false; - if (_lastException != null) return false; - - if (timeout <= 0) throw new TimeoutException(); - - if (checkAction()) - { - break; - } - - await Task.Delay(500); - } - - return true; - } - - private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - if (requestType == null || requestType == _lastRequestConcluded) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.Time = dateTime; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - var success = await WaitForResult(() => - { - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) - { - return true; - } - - return false; - }, cancellationToken); - } - - public override async Task GetRtcTime(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - DateTimeOffset? now = null; - - var success = await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); - if (m != null) - { - var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); - now = DateTimeOffset.Parse(timeString); - return true; - } - } - - return false; - }, cancellationToken); - - return now; - } - - public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - return await WaitForInformationResponse(RuntimeStateToken, RuntimeIsEnabledToken, cancellationToken); - } - - private async Task WaitForInformationResponse(string[] textToWaitOn, CancellationToken? cancellationToken) - { - // wait for an information response - var timeout = CommandTimeoutSeconds * 2; - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return false; - if (timeout <= 0) - throw new TimeoutException(); - - foreach (var t in textToWaitOn) - { - if (InfoMessages.Any(m => m.Contains(t))) return true; - } - - await Task.Delay(500); - } - return false; - } - - private async Task WaitForInformationResponse(string textToContain, string textToVerify, CancellationToken? cancellationToken) - { - // wait for an information response - var timeout = CommandTimeoutSeconds * 2; - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return false; - if (timeout <= 0) - throw new TimeoutException(); - - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(textToContain)); - if (m != null) - { - return m == textToVerify; - } - } - - await Task.Delay(500); - } - return false; - } - - public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeHasBeenEnabledToken }, cancellationToken); - } - - public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeIsDisabledToken }, cancellationToken); - } - - public override async Task TraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task TraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.UserData = (uint)level; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.ExtraData = parameter; - command.UserData = value; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task ResetDevice(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - await WaitForMeadowAttach(cancellationToken); - } - - public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _deviceInfo = null; - - _lastException = null; - EnqueueRequest(command); - - if (!await WaitForResult( - () => _deviceInfo != null, - cancellationToken)) - { - return null; - } - - return _deviceInfo; - } - - public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.IncludeCrcs = includeCrcs; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => _textListComplete ?? false, - cancellationToken)) - { - _textListComplete = null; - return null; - } - - var list = new List(); - - foreach (var candidate in _textList) - { - var fi = MeadowFileInfo.Parse(candidate); - if (fi != null) - { - list.Add(fi); - } - } - - _textListComplete = null; - return list.ToArray(); - } - - public override async Task WriteFile( - string localFileName, - string? meadowFileName = null, - CancellationToken? cancellationToken = null) - { - return await WriteFile(localFileName, meadowFileName, - RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, - 0, - cancellationToken); - } - - public override async Task WriteRuntime( - string localFileName, - CancellationToken? cancellationToken = null) - { - var commandTimeout = CommandTimeoutSeconds; - - - CommandTimeoutSeconds = 120; - _lastRequestConcluded = null; - - try - { - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", - RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, - RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, - 0, - cancellationToken); - - - /* - RaiseConnectionMessage("\nErasing runtime flash blocks..."); - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Mono memory erase success"); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Moving runtime to flash..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Verifying runtime flash operation."); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Verifying..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - return true; - } - - return false; - }, - cancellationToken); - */ - - if (status) - { - await WaitForConcluded(null, cancellationToken); - } - - return status; - } - finally - { - CommandTimeoutSeconds = commandTimeout; - } - } - - public override async Task WriteCoprocessorFile( - string localFileName, - int destinationAddress, - CancellationToken? cancellationToken = null) - { - // make the timeouts much bigger, as the ESP flash takes a lot of time - var readTimeout = _port.ReadTimeout; - var commandTimeout = CommandTimeoutSeconds; - _lastRequestConcluded = null; - - _port.ReadTimeout = 60000; - CommandTimeoutSeconds = 180; - InfoMessages.Clear(); - - try - { - RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); - - // push the file to the device - if (!await WriteFile(localFileName, null, - RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, - destinationAddress, - cancellationToken)) - { - return false; - } - - - _lastRequestConcluded = null; - - // now wait for the STM32 to finish writing to the ESP32 - await WaitForConcluded(null, cancellationToken); - return true; - } - finally - { - _port.ReadTimeout = readTimeout; - CommandTimeoutSeconds = commandTimeout; - } - } - - private async Task WriteFile( - string localFileName, - string? meadowFileName, - RequestType initialRequestType, - RequestType endRequestType, - int writeAddress = 0, - CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - var fileBytes = File.ReadAllBytes(localFileName); - - var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes - if (writeAddress != 0) - { - // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. - using var md5 = MD5.Create(); - var hashBytes = md5.ComputeHash(fileBytes); - var hashString = BitConverter.ToString(hashBytes) - .Replace("-", "") - .ToLowerInvariant(); - fileHash = Encoding.UTF8.GetBytes(hashString); - } - var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); - - command.SetParameters( - localFileName, - meadowFileName ?? Path.GetFileName(localFileName), - fileCrc, - writeAddress, - fileHash, - initialRequestType); - - var accepted = false; - Exception? ex = null; - var needsRetry = false; - - void OnFileWriteAccepted(object? sender, EventArgs a) - { - accepted = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - void OnFileRetry(object? sender, EventArgs e) - { - needsRetry = true; - } - - FileWriteAccepted += OnFileWriteAccepted; - FileException += OnFileError; - FileWriteFailed += OnFileRetry; - - Debug.WriteLine($"Sending '{localFileName}'"); - - EnqueueRequest(command); - - // this will wait for a "file write accepted" from the target - if (!await WaitForResult( - () => - { - if (ex != null) - throw ex; - return accepted; - }, - cancellationToken)) - { - return false; - } - - // now send the file data - // The maximum data bytes is max packet size - 2 bytes for the sequence number - byte[] packet = new byte[Protocol.HCOM_PROTOCOL_PACKET_MAX_SIZE - 2]; - ushort sequenceNumber = 0; - - var progress = 0; - var expected = fileBytes.Length; - - var fileName = Path.GetFileName(localFileName); - var directoryName = Path.GetDirectoryName(localFileName).Split(Path.DirectorySeparatorChar); - var displayedFileName = Path.Combine(directoryName[directoryName.Length - 1], fileName); - - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - - var oldTimeout = _port.ReadTimeout; - _port.ReadTimeout = 60000; - - while (true && !needsRetry) - { - if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) - { - return false; - } - - sequenceNumber++; - - Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); - - var toRead = fileBytes.Length - progress; - if (toRead > packet.Length - 2) - { - toRead = packet.Length - 2; - } - Array.Copy(fileBytes, progress, packet, 2, toRead); - try - { - await EncodeAndSendPacket(packet, toRead + 2, cancellationToken); - } - catch (Exception) - { - break; - } - - progress += toRead; - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - if (progress >= fileBytes.Length) break; - } - - if (!needsRetry) - { - _port.ReadTimeout = oldTimeout; - - base.RaiseFileWriteProgress(displayedFileName, expected, expected); - - // finish with an "end" message - not enqued because this is all a serial operation - var request = RequestBuilder.Build(); - request.SetRequestType(endRequestType); - var p = request.Serialize(); - await EncodeAndSendPacket(p, cancellationToken); - } - - FileWriteAccepted -= OnFileWriteAccepted; - FileException -= OnFileError; - FileWriteFailed -= OnFileRetry; - - return !needsRetry; - } - - public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - command.LocalFileName = localFileName; - - var completed = false; - Exception? ex = null; - - void OnFileReadCompleted(object? sender, string filename) - { - completed = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - - try - { - FileReadCompleted += OnFileReadCompleted; - FileException += OnFileError; - ConnectionError += OnFileError; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => - { - return completed | ex != null; - }, - cancellationToken)) - { - return false; - } - - return ex == null; - } - finally - { - FileReadCompleted -= OnFileReadCompleted; - FileException -= OnFileError; - } - } - - public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = fileName; - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - return contents; - } - - public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task EraseFlash(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - } - - public override async Task GetPublicKey(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - - return contents!; - } - - public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) - { - if (Device != null) - { - logger?.LogDebug($"Start Debugging on port: {port}"); - await Device.StartDebugging(port, logger, cancellationToken); - - /* TODO logger?.LogDebug("Reinitialize the device"); - await ReInitializeMeadow(cancellationToken); */ - - var endpoint = new IPEndPoint(IPAddress.Loopback, port); - var debuggingServer = new DebuggingServer(Device, endpoint, logger!); - - logger?.LogDebug("Tell the Debugging Server to Start Listening"); - await debuggingServer.StartListening(cancellationToken); - return debuggingServer; - } - else - { - throw new DeviceNotFoundException(); - } - } - - public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) - { - var command = RequestBuilder.Build(); - - if (command != null) - { - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForMeadowAttach(cancellationToken); - } - else - { - new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); - } - } +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Diagnostics; +using System.IO.Ports; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Meadow.Hcom; + +public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); + +public partial class SerialConnection : ConnectionBase, IDisposable +{ + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + private const int DefaultTimeout = 5000; + + private event EventHandler FileReadCompleted = delegate { }; + private event EventHandler FileWriteAccepted; + private event EventHandler FileDataReceived; + public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + + private readonly SerialPort _port; + private readonly ILogger? _logger; + private bool _isDisposed; + private ConnectionState _state; + private readonly List _listeners = new List(); + private readonly Queue _pendingCommands = new Queue(); + private bool _maintainConnection; + private Thread? _connectionManager = null; + private readonly List _textList = new List(); + private int _messageCount = 0; + private ReadFileInfo? _readFileInfo = null; + private string? _lastError = null; + + public override string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) + { + if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) + { + throw new ArgumentException($"Serial Port '{port}' not found."); + } + + Name = port; + State = ConnectionState.Disconnected; + _logger = logger; + _port = new SerialPort(port); + _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; + + new Task( + () => _ = ListenerProc(), + TaskCreationOptions.LongRunning) + .Start(); + + new Thread(CommandManager) + { + IsBackground = true, + Name = "HCOM Sender" + } + .Start(); + } + + private bool MaintainConnection + { + get => _maintainConnection; + set + { + if (value == MaintainConnection) return; + + _maintainConnection = value; + + if (value) + { + if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) + { + _connectionManager = new Thread(ConnectionManagerProc) + { + IsBackground = true, + Name = "HCOM Connection Manager" + }; + _connectionManager.Start(); + + } + } + } + } + + private void ConnectionManagerProc() + { + while (_maintainConnection) + { + if (!_port.IsOpen) + { + try + { + Debug.WriteLine("Opening COM port..."); + _port.Open(); + Debug.WriteLine("Opened COM port"); + } + catch (Exception ex) + { + Debug.WriteLine($"{ex.Message}"); + Thread.Sleep(1000); + } + } + else + { + Thread.Sleep(1000); + } + } + } + + public void AddListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Add(listener); + } + + Open(); + + MaintainConnection = true; + } + + public void RemoveListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Remove(listener); + } + + // TODO: stop maintaining connection? + } + + public ConnectionState State + { + get => _state; + private set + { + if (value == State) return; + + var old = _state; + _state = value; + ConnectionStateChanged?.Invoke(this, old, State); + } + } + + private void Open() + { + if (!_port.IsOpen) + { + try + { + _port.Open(); + } + catch (FileNotFoundException) + { + throw new Exception($"Serial port '{_port.PortName}' not found"); + } + } + State = ConnectionState.Connected; + } + + private void Close() + { + if (_port.IsOpen) + { + _port.Close(); + } + + State = ConnectionState.Disconnected; + } + + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + try + { + // ensure the port is open + Open(); + + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" + var command = RequestBuilder.Build(); + + // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + + _port.DiscardInBuffer(); + + // wait for a response + var timeout = timeoutSeconds * 2; + var dataReceived = false; + + // local function so we can unsubscribe + var count = _messageCount; + + _pendingCommands.Enqueue(command); + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); + + if (count != _messageCount) + { + dataReceived = true; + break; + } + + await Task.Delay(500); + } + + // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no + + // create the device instance + if (dataReceived) + { + Device = new MeadowDevice(this); + } + + return Device; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to connect"); + throw; + } + } + + private async void CommandManager() + { + while (!_isDisposed) + { + while (_pendingCommands.Count > 0) + { + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + + var command = _pendingCommands.Dequeue() as Request; + + // if this is a file write, we need to packetize for progress + + var payload = command.Serialize(); + EncodeAndSendPacket(payload); + + // TODO: re-queue on fail? + } + + Thread.Sleep(1000); + } + } + + private class ReadFileInfo + { + private string? _localFileName; + + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get + { + if (_localFileName != null) return _localFileName; + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); + } + set => _localFileName = value; + } + public FileStream FileStream { get; set; } = default!; + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private void EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) + { + EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); + } + + private void EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = null) + { + //Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); + + while (!_port.IsOpen) + { + _state = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } + + _state = ConnectionState.Connected; + + try + { + int encodedToSend; + byte[] encodedBytes; + + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + + // For testing calculate the crc including the sequence number + //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + try + { + // The encoded size using COBS is just a bit more than the original size adding 1 byte + // every 254 bytes plus 1 and need room for beginning and ending delimiters. + var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; + encodedBytes = new byte[l + 2]; + + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + + // DEBUG TESTING + if (encodedToSend == -1) + { + _logger?.LogError($"Error - encodedToSend == -1"); + return; + } + + if (_port == null) + { + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); + } + } + catch (Exception except) + { + string msg = string.Format("Send setup Exception: {0}", except); + _logger?.LogError(msg); + throw; + } + + // Add delimiters to packet boundaries + try + { + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + } + catch (Exception encodedBytesEx) + { + // This should drop the connection and retry + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; + } + + try + { + // Send the data to Meadow + // Debug.Write($"Sending {encodedToSend} bytes..."); + //await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken ?? CancellationToken.None); + _port.Write(encodedBytes, 0, encodedToSend); + // Debug.WriteLine($"sent"); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); + throw; + } + } + catch (Exception except) + { + // DID YOU RESTART MEADOW? + // This should drop the connection and retry + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + throw; + } + } + + + private class SerialMessage + { + private readonly IList> _segments; + + public SerialMessage(Memory segment) + { + _segments = new List>(); + _segments.Add(segment); + } + + public void AddSegment(Memory segment) + { + _segments.Add(segment); + } + + public byte[] ToArray() + { + using var ms = new MemoryStream(); + foreach (var segment in _segments) + { + // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. + var tmp = ArrayPool.Shared.Rent(segment.Length); + segment.CopyTo(tmp); + ms.Write(tmp, 0, segment.Length); + ArrayPool.Shared.Return(tmp); + } + return ms.ToArray(); + } + } + + private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) + { + var decodedBuffer = ArrayPool.Shared.Rent(8192); + var packetLength = packetBuffer.Length; + // It's possible that we may find a series of 0x00 values in the buffer. + // This is because when the sender is blocked (because this code isn't + // running) it will attempt to send a single 0x00 before the full message. + // This allows it to test for a connection. When the connection is + // unblocked this 0x00 is sent and gets put into the buffer along with + // any others that were queued along the usb serial pipe line. + if (packetLength == 1) + { + //_logger.LogTrace("Throwing out 0x00 from buffer"); + return false; + } + + var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); + + /* + // If a message is too short it is ignored + if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + { + return false; + } + + Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + + // Process the received packet + ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), + cancellationToken); + + */ + ArrayPool.Shared.Return(decodedBuffer); + return true; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Close(); + _port.Dispose(); + } + + _isDisposed = true; + } + } + + // ---------------------------------------------- + // ---------------------------------------------- + // ---------------------------------------------- + + private Exception? _lastException; + private bool? _textListComplete; + private DeviceInfo? _deviceInfo; + private RequestType? _lastRequestConcluded = null; + private List StdOut { get; } = new List(); + private List StdErr { get; } = new List(); + private List InfoMessages { get; } = new List(); + + private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; + private const string RuntimeSucessfullyDisabledToken = "Mono is disabled"; + private const string RuntimeStateToken = "Mono is"; + private const string RuntimeIsEnabledToken = "Mono is enabled"; + private const string RtcRetrievalToken = "UTC time:"; + + public int CommandTimeoutSeconds { get; set; } = 30; + + private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) + { + var timeout = CommandTimeoutSeconds * 2; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (_lastException != null) return false; + + if (timeout <= 0) throw new TimeoutException(); + + if (checkAction()) + { + break; + } + + await Task.Delay(500); + } + + return true; + } + + private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + if (requestType == null || requestType == _lastRequestConcluded) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.Time = dateTime; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } + + public override async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + DateTimeOffset? now = null; + + var success = await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); + if (m != null) + { + var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); + now = DateTimeOffset.Parse(timeString); + return true; + } + } + + return false; + }, cancellationToken); + + return now; + } + + public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + // wait for an information response + var timeout = CommandTimeoutSeconds * 2; + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (timeout <= 0) throw new TimeoutException(); + + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeStateToken)); + if (m != null) + { + return m == RuntimeIsEnabledToken; + } + } + + await Task.Delay(500); + } + return false; + } + + public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.UserData = (uint)level; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.ExtraData = parameter; + command.UserData = value; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task ResetDevice(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + await WaitForMeadowAttach(cancellationToken); + } + + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _deviceInfo = null; + + _lastException = null; + EnqueueRequest(command); + + if (!await WaitForResult( + () => _deviceInfo != null, + cancellationToken)) + { + return null; + } + + return _deviceInfo; + } + + public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.IncludeCrcs = includeCrcs; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => _textListComplete ?? false, + cancellationToken)) + { + _textListComplete = null; + return null; + } + + var list = new List(); + + foreach (var candidate in _textList) + { + var fi = MeadowFileInfo.Parse(candidate); + if (fi != null) + { + list.Add(fi); + } + } + + _textListComplete = null; + return list.ToArray(); + } + + public override async Task WriteFile( + string localFileName, + string? meadowFileName = null, + CancellationToken? cancellationToken = null) + { + return await WriteFile(localFileName, meadowFileName, + RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, + 0, + cancellationToken); + } + + public override async Task WriteRuntime( + string localFileName, + CancellationToken? cancellationToken = null) + { + var commandTimeout = CommandTimeoutSeconds; + + + CommandTimeoutSeconds = 120; + _lastRequestConcluded = null; + + try + { + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, + 0, + cancellationToken); + + + /* + RaiseConnectionMessage("\nErasing runtime flash blocks..."); + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Moving runtime to flash..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Verifying..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + return true; + } + + return false; + }, + cancellationToken); + */ + + if (status) + { + await WaitForConcluded(null, cancellationToken); + } + + return status; + } + finally + { + CommandTimeoutSeconds = commandTimeout; + } + } + + public override async Task WriteCoprocessorFile( + string localFileName, + int destinationAddress, + CancellationToken? cancellationToken = null) + { + // make the timeouts much bigger, as the ESP flash takes a lot of time + var readTimeout = _port.ReadTimeout; + var commandTimeout = CommandTimeoutSeconds; + _lastRequestConcluded = null; + + _port.ReadTimeout = 60000; + CommandTimeoutSeconds = 180; + InfoMessages.Clear(); + + try + { + RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); + + // push the file to the device + if (!await WriteFile(localFileName, null, + RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, + destinationAddress, + cancellationToken)) + { + return false; + } + + + _lastRequestConcluded = null; + + // now wait for the STM32 to finish writing to the ESP32 + await WaitForConcluded(null, cancellationToken); + return true; + } + finally + { + _port.ReadTimeout = readTimeout; + CommandTimeoutSeconds = commandTimeout; + } + } + + private async Task WriteFile( + string localFileName, + string? meadowFileName, + RequestType initialRequestType, + RequestType endRequestType, + int writeAddress = 0, + CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + var fileBytes = File.ReadAllBytes(localFileName); + + var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes + if (writeAddress != 0) + { + // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. + using var md5 = MD5.Create(); + var hashBytes = md5.ComputeHash(fileBytes); + var hashString = BitConverter.ToString(hashBytes) + .Replace("-", "") + .ToLowerInvariant(); + fileHash = Encoding.UTF8.GetBytes(hashString); + } + var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); + + command.SetParameters( + localFileName, + meadowFileName ?? Path.GetFileName(localFileName), + fileCrc, + writeAddress, + fileHash, + initialRequestType); + + var accepted = false; + Exception? ex = null; + var needsRetry = false; + + void OnFileWriteAccepted(object? sender, EventArgs a) + { + accepted = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + void OnFileRetry(object? sender, EventArgs e) + { + needsRetry = true; + } + + FileWriteAccepted += OnFileWriteAccepted; + FileException += OnFileError; + FileWriteFailed += OnFileRetry; + + Debug.WriteLine($"Sending '{localFileName}'"); + + EnqueueRequest(command); + + // this will wait for a "file write accepted" from the target + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return accepted; + }, + cancellationToken)) + { + return false; + } + + // now send the file data + // The maximum data bytes is max packet size - 2 bytes for the sequence number + byte[] packet = new byte[Protocol.HCOM_PROTOCOL_PACKET_MAX_SIZE - 2]; + ushort sequenceNumber = 0; + + var progress = 0; + var expected = fileBytes.Length; + + var fileName = Path.GetFileName(localFileName); + + base.RaiseFileWriteProgress(fileName, progress, expected); + + var oldTimeout = _port.ReadTimeout; + _port.ReadTimeout = 60000; + + while (true && !needsRetry) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) + { + return false; + } + + sequenceNumber++; + + Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); + + var toRead = fileBytes.Length - progress; + if (toRead > packet.Length - 2) + { + toRead = packet.Length - 2; + } + Array.Copy(fileBytes, progress, packet, 2, toRead); + try + { + EncodeAndSendPacket(packet, toRead + 2, cancellationToken); + } + catch (Exception) + { + break; + } + + progress += toRead; + base.RaiseFileWriteProgress(fileName, progress, expected); + if (progress >= fileBytes.Length) break; + } + + if (!needsRetry) + { + _port.ReadTimeout = oldTimeout; + + base.RaiseFileWriteProgress(fileName, expected, expected); + + // finish with an "end" message - not enqued because this is all a serial operation + var request = RequestBuilder.Build(); + request.SetRequestType(endRequestType); + var p = request.Serialize(); + EncodeAndSendPacket(p, cancellationToken); + } + + FileWriteAccepted -= OnFileWriteAccepted; + FileException -= OnFileError; + FileWriteFailed -= OnFileRetry; + + return !needsRetry; + } + + public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + command.LocalFileName = localFileName; + + var completed = false; + Exception? ex = null; + + void OnFileReadCompleted(object? sender, string filename) + { + completed = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + + try + { + FileReadCompleted += OnFileReadCompleted; + FileException += OnFileError; + ConnectionError += OnFileError; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + return completed | ex != null; + }, + cancellationToken)) + { + return false; + } + + return ex == null; + } + finally + { + FileReadCompleted -= OnFileReadCompleted; + FileException -= OnFileError; + } + } + + public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = fileName; + + string? contents = null; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + return contents; + } + + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task EraseFlash(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + CommandTimeoutSeconds = lastTimeout; + } + + public override async Task GetPublicKey(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + string? contents = null; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + CommandTimeoutSeconds = lastTimeout; + + return contents; + } + + public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) + { + if (Device != null) + { + logger?.LogDebug($"Start Debugging on port: {port}"); + await Device.StartDebugging(port, logger, cancellationToken); + + /* TODO logger?.LogDebug("Reinitialize the device"); + await ReInitializeMeadow(cancellationToken); */ + + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + var debuggingServer = new DebuggingServer(Device, endpoint, logger); + + logger?.LogDebug("Tell the Debugging Server to Start Listening"); + await debuggingServer.StartListening(cancellationToken); + return debuggingServer; + } + else + { + throw new DeviceNotFoundException(); + } + } + + public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) + { + var command = RequestBuilder.Build(); + + if (command != null) + { + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForMeadowAttach(cancellationToken); + } + else + { + new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); + } + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 34391689..36b76549 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -18,37 +18,34 @@ public TcpConnection(string uri) public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { - return await Task.Run(() => - { - /* - var request = RequestBuilder.Build(); - - base.EnqueueRequest(request); + /* + var request = RequestBuilder.Build(); - // get the info and "attach" - var timeout = timeoutSeconds * 2; + base.EnqueueRequest(request); - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); + // get the info and "attach" + var timeout = timeoutSeconds * 2; - // do we have a device info? + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); - if (State == ConnectionState.MeadowAttached) - { - break; - } + // do we have a device info? - await Task.Delay(500); + if (State == ConnectionState.MeadowAttached) + { + break; } - */ - // TODO: is there a way to "attach"? ping result? device info? - return Device = new MeadowDevice(this); + await Task.Delay(500); + } + */ - // TODO: web socket for listen? - }); + // TODO: is there a way to "attach"? ping result? device info? + return Device = new MeadowDevice(this); + + // TODO: web socket for listen? } public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) @@ -58,14 +55,7 @@ public TcpConnection(string uri) if (response.IsSuccessStatusCode) { var r = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); - if (r != null) - { - return new DeviceInfo(r.ToDictionary()); - } - else - { - return null; - } + return new DeviceInfo(r.ToDictionary()); } else { diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index 4dce0ef6..22e20cad 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -92,7 +92,7 @@ private async Task StartListener() { _listener.Start(); LocalEndpoint = (IPEndPoint)_listener.LocalEndpoint; - _logger?.LogInformation($"Listening for Visual Studio to connect on {LocalEndpoint.Address}:{LocalEndpoint.Port}" + Environment.NewLine); + _logger.LogInformation($"Listening for Visual Studio to connect on {LocalEndpoint.Address}:{LocalEndpoint.Port}" + Environment.NewLine); _isReady = true; // This call will wait for the client to connect, before continuing. We shouldn't need a loop. @@ -101,13 +101,13 @@ private async Task StartListener() } catch (SocketException soex) { - _logger?.LogError("A Socket error occurred. The port may already be in use. Try rebooting to free up the port."); - _logger?.LogError($"Error:\n{soex.Message} \nStack Trace:\n{soex.StackTrace}"); + _logger.LogError("A Socket error occurred. The port may already be in use. Try rebooting to free up the port."); + _logger.LogError($"Error:\n{soex.Message} \nStack Trace:\n{soex.StackTrace}"); } catch (Exception ex) { - _logger?.LogError("An unhandled exception occurred while listening for debugging connections."); - _logger?.LogError($"Error:\n{ex.Message} \nStack Trace:\n{ex.StackTrace}"); + _logger.LogError("An unhandled exception occurred while listening for debugging connections."); + _logger.LogError($"Error:\n{ex.Message} \nStack Trace:\n{ex.StackTrace}"); } } @@ -117,26 +117,22 @@ private void OnConnect(TcpClient tcpClient) { lock (_lck) { - _logger?.LogInformation("Visual Studio has Connected" + Environment.NewLine); + _logger.LogInformation("Visual Studio has Connected" + Environment.NewLine); if (_activeClientCount > 0 && _activeClient?.Disposed == false) { - _logger?.LogDebug("Closing active client"); + _logger.LogDebug("Closing active client"); Debug.Assert(_activeClientCount == 1); Debug.Assert(_activeClient != null); CloseActiveClient(); } - if (_cancellationTokenSource != null - && _logger != null) - { - _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource.Token); - _activeClientCount++; - } + _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource.Token); + _activeClientCount++; } } catch (Exception ex) { - _logger?.LogError(ex, "An error occurred while connecting to Visual Studio"); + _logger.LogError(ex, "An error occurred while connecting to Visual Studio"); } } @@ -170,7 +166,7 @@ private class ActiveClient : IDisposable private readonly CancellationTokenSource _cts; private readonly Task _receiveVsDebugDataTask; private readonly Task _receiveMeadowDebugDataTask; - private readonly ILogger? _logger; + private readonly ILogger _logger; public bool Disposed = false; // Constructor @@ -181,7 +177,7 @@ internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger logger, _meadow = meadow; _tcpClient = tcpClient; _networkStream = tcpClient.GetStream(); - _logger?.LogDebug("Starting receive task"); + _logger.LogDebug("Starting receive task"); _receiveVsDebugDataTask = Task.Factory.StartNew(SendToMeadowAsync, TaskCreationOptions.LongRunning); _receiveMeadowDebugDataTask = Task.Factory.StartNew(SendToVisualStudio, TaskCreationOptions.LongRunning); } @@ -212,12 +208,12 @@ private async Task SendToMeadowAsync() Array.Copy(receiveBuffer, 0, meadowBuffer, destIndex, bytesRead); // Forward the RECIEVE_BUFFER_SIZE chunk to Meadow immediately - _logger?.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", + _logger.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", meadowBuffer.Length, BitConverter.ToString(md5.ComputeHash(meadowBuffer)) .Replace("-", string.Empty) .ToLowerInvariant()); - await _meadow.ForwardVisualStudioDataToMono(meadowBuffer, 0); + // TODO await _meadow.ForwardVisualStudioDataToMono(meadowBuffer, 0); meadowBuffer = Array.Empty(); // Ensure we read all the data in this message before passing it along @@ -227,26 +223,26 @@ private async Task SendToMeadowAsync() else { // User probably hit stop - _logger?.LogInformation("Unable to Read Data from Visual Studio"); - _logger?.LogTrace("Unable to Read Data from Visual Studio"); + _logger.LogInformation("Unable to Read Data from Visual Studio"); + _logger.LogTrace("Unable to Read Data from Visual Studio"); } } } catch (IOException ioe) { // VS client probably died - _logger?.LogInformation("Visual Studio has Disconnected" + Environment.NewLine); - _logger?.LogTrace(ioe, "Visual Studio has Disconnected"); + _logger.LogInformation("Visual Studio has Disconnected" + Environment.NewLine); + _logger.LogTrace(ioe, "Visual Studio has Disconnected"); } catch (ObjectDisposedException ode) { // User probably hit stop - _logger?.LogInformation("Visual Studio has stopped debugging" + Environment.NewLine); - _logger?.LogTrace(ode, "Visual Studio has stopped debugging"); + _logger.LogInformation("Visual Studio has stopped debugging" + Environment.NewLine); + _logger.LogTrace(ode, "Visual Studio has stopped debugging"); } catch (Exception ex) { - _logger?.LogError($"Error receiving data from Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); + _logger.LogError($"Error receiving data from Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); throw; } } @@ -259,38 +255,38 @@ private async Task SendToVisualStudio() { if (_networkStream != null && _networkStream.CanWrite) { - while (_meadow.DataProcessor.DebuggerMessages.Count > 0) + /* TODO while (_meadow.DataProcessor.DebuggerMessages.Count > 0) { var byteData = _meadow.DataProcessor.DebuggerMessages.Take(_cts.Token); - _logger?.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); + _logger.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); if (!_tcpClient.Connected) { - _logger?.LogDebug("Cannot forward data, Visual Studio is not connected"); + _logger.LogDebug("Cannot forward data, Visual Studio is not connected"); return; } await _networkStream.WriteAsync(byteData, 0, byteData.Length, _cts.Token); - _logger?.LogTrace("Forwarded {count} bytes to VS", byteData.Length); - } + _logger.LogTrace("Forwarded {count} bytes to VS", byteData.Length); + }*/ } else { // User probably hit stop - _logger?.LogInformation("Unable to Write Data from Visual Studio"); - _logger?.LogTrace("Unable to Write Data from Visual Studio"); + _logger.LogInformation("Unable to Write Data from Visual Studio"); + _logger.LogTrace("Unable to Write Data from Visual Studio"); } } } - catch (OperationCanceledException) + catch (OperationCanceledException oce) { // User probably hit stop; Removed logging as User doesn't need to see this // Keeping it as a TODO in case we find a side effect that needs logging. - // TODO _logger?.LogInformation("Operation Cancelled"); - // TODO _logger?.LogTrace(oce, "Operation Cancelled"); + // TODO _logger.LogInformation("Operation Cancelled"); + // TODO _logger.LogTrace(oce, "Operation Cancelled"); } catch (Exception ex) { - _logger?.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); + _logger.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); if (_cts.IsCancellationRequested) throw; @@ -304,7 +300,7 @@ public void Dispose() if (Disposed) return; - _logger?.LogTrace("Disposing ActiveClient"); + _logger.LogTrace("Disposing ActiveClient"); _cts.Cancel(false); _receiveVsDebugDataTask.Wait(TimeSpan.FromSeconds(10)); _receiveMeadowDebugDataTask.Wait(TimeSpan.FromSeconds(10)); diff --git a/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs b/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs index 055ccc3b..3a822bc0 100644 --- a/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs +++ b/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs @@ -2,10 +2,6 @@ { public class DeviceNotFoundException : Exception { - public DeviceNotFoundException(string? message = null, Exception? innerException = null) - : base(message ?? "No device found on this connection.", innerException) - { - - } + internal DeviceNotFoundException() : base() { } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 0cb2ffa1..0ce95cd6 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -68,12 +68,12 @@ public DownloadManager(ILogger logger) string versionCheckUrl; if (version is null || string.IsNullOrWhiteSpace(version)) { - _logger?.LogInformation("Downloading latest version file" + Environment.NewLine); + _logger.LogInformation("Downloading latest version file" + Environment.NewLine); versionCheckUrl = VersionCheckUrlRoot + "latest.json"; } else { - _logger?.LogInformation("Downloading version file for Meadow OS " + version + Environment.NewLine); + _logger.LogInformation("Downloading version file for Meadow OS " + version + Environment.NewLine); versionCheckUrl = VersionCheckUrlRoot + version + ".json"; } @@ -98,7 +98,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) if (versionCheckFilePath == null) { - _logger?.LogError($"Meadow OS {version} cannot be downloaded or is not available"); + _logger.LogError($"Meadow OS {version} cannot be downloaded or is not available"); return; } @@ -107,7 +107,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) if (release == null) { - _logger?.LogError($"Unable to read release details for Meadow OS {version}. Payload: {payload}"); + _logger.LogError($"Unable to read release details for Meadow OS {version}. Payload: {payload}"); return; } @@ -122,9 +122,9 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); } - if (release.Version != null && release.Version.ToVersion() < "0.6.0.0".ToVersion()) + if (release.Version.ToVersion() < "0.6.0.0".ToVersion()) { - _logger?.LogInformation( + _logger.LogInformation( $"Downloading OS version {release.Version} is no longer supported. The minimum OS version is 0.6.0.0." + Environment.NewLine); return; } @@ -139,7 +139,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) } else { - _logger?.LogInformation($"Meadow OS version {release.Version} is already downloaded." + Environment.NewLine); + _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded." + Environment.NewLine); return; } } @@ -148,27 +148,27 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) try { - _logger?.LogInformation($"Downloading Meadow OS" + Environment.NewLine); + _logger.LogInformation($"Downloading Meadow OS" + Environment.NewLine); await DownloadAndExtractFile(new Uri(release.DownloadURL), local_path); } catch { - _logger?.LogError($"Unable to download Meadow OS {version}"); + _logger.LogError($"Unable to download Meadow OS {version}"); return; } try { - _logger?.LogInformation("Downloading coprocessor firmware" + Environment.NewLine); + _logger.LogInformation("Downloading coprocessor firmware" + Environment.NewLine); await DownloadAndExtractFile(new Uri(release.NetworkDownloadURL), local_path); } catch { - _logger?.LogError($"Unable to download coprocessor firmware {version}"); + _logger.LogError($"Unable to download coprocessor firmware {version}"); return; } - _logger?.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); + _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); } public async Task InstallDfuUtil(bool is64Bit = true, @@ -176,7 +176,7 @@ public async Task InstallDfuUtil(bool is64Bit = true, { try { - _logger?.LogInformation("Installing dfu-util..."); + _logger.LogInformation("Installing dfu-util..."); if (Directory.Exists(WildernessLabsTemp)) { @@ -236,11 +236,11 @@ public async Task InstallDfuUtil(bool is64Bit = true, File.Delete(libUsbPath); } - _logger?.LogInformation("dfu-util 0.10 installed"); + _logger.LogInformation("dfu-util 0.10 installed"); } catch (Exception ex) { - _logger?.LogError( + _logger.LogError( ex, ex.Message.Contains("Access to the path") ? $"Run terminal as administrator and try again." @@ -277,7 +277,7 @@ public async Task InstallDfuUtil(bool is64Bit = true, } catch (Exception ex) { - _logger?.LogDebug(ex, "Error checking for updates to Meadow.CLI"); + _logger.LogDebug(ex, "Error checking for updates to Meadow.CLI"); } return (false, string.Empty, string.Empty); @@ -291,7 +291,7 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT response.EnsureSuccessStatusCode(); var downloadFileName = Path.GetTempFileName(); - _logger?.LogDebug("Copying downloaded file to temp file {filename}", downloadFileName); + _logger.LogDebug("Copying downloaded file to temp file {filename}", downloadFileName); using (var stream = await response.Content.ReadAsStreamAsync()) using (var downloadFileStream = new DownloadFileStream(stream, _logger)) using (var firmwareFile = File.OpenWrite(downloadFileName)) @@ -305,7 +305,7 @@ private async Task DownloadAndExtractFile(Uri uri, string target_path, Cancellat { var downloadFileName = await DownloadFile(uri, cancellationToken); - _logger?.LogDebug("Extracting firmware to {path}", target_path); + _logger.LogDebug("Extracting firmware to {path}", target_path); ZipFile.ExtractToDirectory( downloadFileName, target_path); @@ -315,8 +315,8 @@ private async Task DownloadAndExtractFile(Uri uri, string target_path, Cancellat } catch (Exception ex) { - _logger?.LogWarning("Unable to delete temporary file"); - _logger?.LogDebug(ex, "Unable to delete temporary file"); + _logger.LogWarning("Unable to delete temporary file"); + _logger.LogDebug(ex, "Unable to delete temporary file"); } } @@ -331,8 +331,8 @@ private void CleanPath(string path) } catch (Exception ex) { - _logger?.LogWarning("Failed to delete file {file} in firmware path", file.FullName); - _logger?.LogDebug(ex, "Failed to delete file"); + _logger.LogWarning("Failed to delete file {file} in firmware path", file.FullName); + _logger.LogDebug(ex, "Failed to delete file"); } } foreach (DirectoryInfo dir in di.GetDirectories()) @@ -343,8 +343,8 @@ private void CleanPath(string path) } catch (Exception ex) { - _logger?.LogWarning("Failed to delete directory {directory} in firmware path", dir.FullName); - _logger?.LogDebug(ex, "Failed to delete directory"); + _logger.LogWarning("Failed to delete directory {directory} in firmware path", dir.FullName); + _logger.LogDebug(ex, "Failed to delete directory"); } } } diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs index 9b83525a..dae02052 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs @@ -40,7 +40,7 @@ public static async Task GetCloudLatestFirmwareVersion() if (json == null) return string.Empty; - return JsonSerializerExtensions.DeserializeAnonymousType(json, new { version = string.Empty })!.version; + return JsonSerializerExtensions.DeserializeAnonymousType(json, new { version = string.Empty }).version; } } @@ -116,9 +116,9 @@ public static FirmwareInfo[] GetAllLocalFirmwareBuilds() return list.ToArray(); } - public static FirmwareUpdater GetFirmwareUpdater(IMeadowConnection connection, ILogger? logger = null) + public static FirmwareUpdater GetFirmwareUpdater(IMeadowConnection connection) { - return new FirmwareUpdater(connection, logger); + return new FirmwareUpdater(connection); } public static async Task PushApplicationToDevice(IMeadowConnection connection, DirectoryInfo appFolder, ILogger? logger = null) diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index 0aa4c3de..03c70cd9 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -6,11 +6,11 @@ namespace Meadow.Hcom; public class FirmwareUpdater { private ILogger? _logger; - // TODO private Task? _updateTask; + private Task? _updateTask; private IMeadowConnection _connection; private UpdateState _state; - private string? RequestedVersion { get; set; } + private string RequestedVersion { get; set; } public enum UpdateState { @@ -31,10 +31,10 @@ public enum UpdateState public UpdateState PreviousState { get; private set; } - internal FirmwareUpdater(IMeadowConnection connection, ILogger? logger = null) + internal FirmwareUpdater(IMeadowConnection connection) { _connection = connection; - _logger = logger; + // _logger = connection.Logger; } public UpdateState CurrentState @@ -45,7 +45,7 @@ private set if (value == _state) return; PreviousState = CurrentState; _state = value; - _logger?.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); + _logger.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); } } @@ -62,22 +62,19 @@ private async void StateMachine() case UpdateState.NotStarted: try { - if (_connection.Device != null) - { - // make sure we have a current device info - info = await _connection.Device.GetDeviceInfo(); - - if (info?.OsVersion == RequestedVersion) - { - // no need to update, it's already there - CurrentState = UpdateState.DFUCompleted; - break; - } + // make sure we have a current device info + info = await _connection.Device.GetDeviceInfo(); - // enter DFU mode - // await _connection.Device.EnterDfuMode(); - CurrentState = UpdateState.EnteringDFUMode; + if (info.OsVersion == RequestedVersion) + { + // no need to update, it's already there + CurrentState = UpdateState.DFUCompleted; + break; } + + // enter DFU mode + // await _connection.Device.EnterDfuMode(); + CurrentState = UpdateState.EnteringDFUMode; } catch (Exception ex) { @@ -99,7 +96,7 @@ private async void StateMachine() ++tries; if (tries > 5) { - _logger?.LogError($"Failed to enter DFU mode: {ex.Message}"); + _logger.LogError($"Failed to enter DFU mode: {ex.Message}"); CurrentState = UpdateState.Error; // exit state machine @@ -140,7 +137,7 @@ private async void StateMachine() await _connection.WaitForMeadowAttach(); await Task.Delay(2000); // wait 2 seconds to allow full boot - if (info == null && _connection.Device != null) + if (info == null) { info = await _connection.Device.GetDeviceInfo(); } @@ -157,10 +154,7 @@ private async void StateMachine() case UpdateState.DisablingMonoForRuntime: try { - if (_connection.Device != null) - { - await _connection.Device.RuntimeDisable(); - } + await _connection.Device.RuntimeDisable(); } catch (Exception ex) { @@ -171,7 +165,7 @@ private async void StateMachine() CurrentState = UpdateState.UpdatingRuntime; break; case UpdateState.UpdatingRuntime: - if (info?.RuntimeVersion == RequestedVersion) + if (info.RuntimeVersion == RequestedVersion) { // no need to update, it's already there } @@ -182,12 +176,12 @@ private async void StateMachine() await _connection.WaitForMeadowAttach(); await Task.Delay(2000); // wait 2 seconds to allow full boot - if (info == null && _connection.Device != null) + if (info == null) { info = await _connection.Device.GetDeviceInfo(); } - // await _connection.Device.FlashRuntime(RequestedVersion); + // await _connection.Device.FlashRuntime(RequestedVersion); } catch (Exception ex) { @@ -201,10 +195,7 @@ private async void StateMachine() case UpdateState.DisablingMonoForCoprocessor: try { - if (_connection.Device != null) - { - await _connection.Device.RuntimeDisable(); - } + await _connection.Device.RuntimeDisable(); CurrentState = UpdateState.UpdatingCoprocessor; } @@ -217,7 +208,7 @@ private async void StateMachine() CurrentState = UpdateState.UpdatingCoprocessor; break; case UpdateState.UpdatingCoprocessor: - if (info?.CoprocessorOsVersion == RequestedVersion) + if (info.CoprocessorOsVersion == RequestedVersion) { // no need to update, it's already there } @@ -230,7 +221,7 @@ private async void StateMachine() Debug.WriteLine(">> delay"); await Task.Delay(3000); // wait to allow full boot - no idea why this takes longer - if (info == null && _connection.Device != null) + if (info == null) { Debug.WriteLine(">> query device info"); info = await _connection.Device.GetDeviceInfo(); @@ -252,10 +243,7 @@ private async void StateMachine() case UpdateState.AllWritesComplete: try { - if (_connection.Device != null) - { - await _connection.Device.Reset(); - } + await _connection.Device.Reset(); } catch (Exception ex) { @@ -263,34 +251,28 @@ private async void StateMachine() CurrentState = UpdateState.Error; return; } - CurrentState = UpdateState.VerifySuccess; break; + CurrentState = UpdateState.VerifySuccess; case UpdateState.VerifySuccess: try { await _connection.WaitForMeadowAttach(); await Task.Delay(2000); // wait 2 seconds to allow full boot - if (_connection.Device != null) + info = await _connection.Device.GetDeviceInfo(); + if (info.OsVersion != RequestedVersion) { - info = await _connection.Device.GetDeviceInfo(); - if (info != null) - { - if (info.OsVersion != RequestedVersion) - { - // this is a failure - _logger?.LogWarning($"OS version {info.OsVersion} does not match requested version {RequestedVersion}"); - } - if (info.RuntimeVersion != RequestedVersion) - { - // this is a failure - _logger?.LogWarning($"Runtime version {info.RuntimeVersion} does not match requested version {RequestedVersion}"); - } - if (info.CoprocessorOsVersion != RequestedVersion) - { - // not necessarily an error - _logger?.LogWarning($"Coprocessor version {info.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); - } - } + // this is a failure + _logger?.LogWarning($"OS version {info.OsVersion} does not match requested version {RequestedVersion}"); + } + if (info.RuntimeVersion != RequestedVersion) + { + // this is a failure + _logger?.LogWarning($"Runtime version {info.RuntimeVersion} does not match requested version {RequestedVersion}"); + } + if (info.CoprocessorOsVersion != RequestedVersion) + { + // not necessarily an error + _logger?.LogWarning($"Coprocessor version {info.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); } } catch (Exception ex) diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs index 1f3ea6d4..e5c460a9 100644 --- a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs +++ b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs @@ -5,5 +5,5 @@ namespace Meadow.Hcom; public class PackageVersions { [JsonPropertyName("versions")] - public string[]? Versions { get; set; } + public string[] Versions { get; set; } } diff --git a/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs b/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs index 477c8eea..823f1e01 100644 --- a/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs +++ b/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs @@ -5,12 +5,12 @@ namespace Meadow.Hcom; public class ReleaseMetadata { [JsonPropertyName("version")] - public string? Version { get; set; } + public string Version { get; set; } [JsonPropertyName("minCLIVersion")] - public string? MinCLIVersion { get; set; } + public string MinCLIVersion { get; set; } [JsonPropertyName("downloadUrl")] - public string? DownloadURL { get; set; } + public string DownloadURL { get; set; } [JsonPropertyName("networkDownloadUrl")] - public string? NetworkDownloadURL { get; set; } + public string NetworkDownloadURL { get; set; } } diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index acb4a5f1..b599cb0a 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -27,8 +27,5 @@ public interface IMeadowDevice Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); Task GetPublicKey(CancellationToken? cancellationToken = null); Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); - Task ForwardVisualStudioDataToMono(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = default); - - MeadowDataProcessor DataProcessor { get; } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln index 06e0064a..93b78ab2 100644 --- a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln +++ b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Hcom", "Meadow.Hcom. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.HCom.Integration.Tests", "..\Meadow.HCom.Integration.Tests\Meadow.HCom.Integration.Tests.csproj", "{F8830C1D-8343-4700-A849-B22537411E98}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.CLI", "..\Meadow.CLI\Meadow.CLI.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cli", "..\Meadow.Cli\Meadow.Cli.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 1e2320cc..f9b55b41 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -6,8 +6,6 @@ public partial class MeadowDevice : IMeadowDevice { private IMeadowConnection _connection; - public MeadowDataProcessor DataProcessor => throw new NotImplementedException(); - internal MeadowDevice(IMeadowConnection connection) { _connection = connection; @@ -158,10 +156,5 @@ public async Task StartDebugging(int port, ILogger? logger, CancellationToken? c { await _connection.StartDebugging(port, logger, cancellationToken); } - - public Task ForwardVisualStudioDataToMono(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs index 71d05ef3..a284dcef 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs @@ -78,7 +78,7 @@ internal class InitFileWriteRequest : Request public byte[] Esp32MD5 { get; set; } = new byte[32]; public string LocalFileName { get; private set; } = default!; - public string? MeadowFileName { get; private set; } + public string MeadowFileName { get; private set; } public void SetParameters( string localFile, diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs index 52311a1f..0fac22e8 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs @@ -2,7 +2,7 @@ { public static class RequestBuilder { - // TODO private static uint _sequenceNumber; + private static uint _sequenceNumber; public static T Build(uint userData = 0, ushort extraData = 0, ushort protocol = Protocol.HCOM_PROTOCOL_HCOM_VERSION_NUMBER) where T : Request, new() diff --git a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs index e7b716cc..96b14b50 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs @@ -10,17 +10,13 @@ public DateTimeOffset? Time { get { - if (Payload?.Length == 0) - return null; + if (Payload.Length == 0) { return null; } return DateTimeOffset.Parse(Encoding.ASCII.GetString(Payload)); } set { - if (value.HasValue) - { - base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); - } + base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); } } diff --git a/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj index cad3692c..8fdf6f7b 100644 --- a/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj +++ b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj @@ -8,10 +8,6 @@ false - - 4 - true - diff --git a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs index e78e56c4..da0f1e66 100644 --- a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs +++ b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs @@ -6,7 +6,7 @@ namespace Meadow.Software; internal class DownloadFileStream : Stream, IDisposable { - public event EventHandler? DownloadProgress; + public event EventHandler DownloadProgress; private readonly Stream _stream; diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 9b7ac9c8..8a14443a 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -11,7 +11,7 @@ namespace Meadow.Software; public class F7FirmwarePackageCollection : IFirmwarePackageCollection { /// - public event EventHandler? DownloadProgress; + public event EventHandler DownloadProgress; public string PackageFileRoot { get; } @@ -70,18 +70,13 @@ public Task DeletePackage(string version) // if we're deleting the default, we need to det another default var i = _f7Packages.Count - 1; - if (DefaultPackage != null) + while (DefaultPackage.Version == _f7Packages[i].Version) { - while (DefaultPackage.Version == _f7Packages[i].Version) - { - i--; - } - var newDefault = _f7Packages[i].Version; - _f7Packages.Remove(DefaultPackage); - - if (!string.IsNullOrEmpty(newDefault)) - SetDefaultPackage(newDefault); + i--; } + var newDefault = _f7Packages[i].Version; + _f7Packages.Remove(DefaultPackage); + SetDefaultPackage(newDefault); var path = Path.Combine(PackageFileRoot, version); @@ -92,9 +87,6 @@ public Task DeletePackage(string version) public Task SetDefaultPackage(string version) { - // Refresh the list, in case we've just downloaded it. - Refresh(); - var existing = _f7Packages.FirstOrDefault(p => p.Version == version); if (existing == null) diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs index 68f154d2..ae2c489c 100644 --- a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -16,8 +16,8 @@ public string GetFullyQualifiedPath(string file) return Path.Combine(_collection.PackageFileRoot, Version, file); } - public string? Version { get; set; } - public string? Targets { get; set; } + public string Version { get; set; } + public string Targets { get; set; } public string? CoprocBootloader { get; set; } public string? CoprocPartitionTable { get; set; } public string? CoprocApplication { get; set; } diff --git a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj index 8fcd9f02..44f93135 100644 --- a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj +++ b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj @@ -6,9 +6,6 @@ 10 - - true - diff --git a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj index ca09c412..ae241512 100644 --- a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj +++ b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj @@ -6,10 +6,6 @@ enable - - 4 - true - diff --git a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj index fc4bddd1..cd055e35 100644 --- a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj +++ b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj @@ -6,10 +6,6 @@ enable - - 4 - true - From a565dca579d3ee3c5c6d507e67bc2e0aff3f3d02 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 21 Dec 2023 12:26:21 +0000 Subject: [PATCH 063/141] Remove Meadow.CLI.Core.Logging reference --- Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj index f796bb81..10fed43f 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj @@ -63,8 +63,6 @@ - - From cbc3ab7e32af89b9ce06a6fd5144817ca148ba8e Mon Sep 17 00:00:00 2001 From: CartBlanche Date: Thu, 21 Dec 2023 12:45:24 +0000 Subject: [PATCH 064/141] Remove VS2019 if def --- Meadow.CLI.Core/Globals.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Meadow.CLI.Core/Globals.cs b/Meadow.CLI.Core/Globals.cs index 766d71a2..26f1b897 100644 --- a/Meadow.CLI.Core/Globals.cs +++ b/Meadow.CLI.Core/Globals.cs @@ -1,6 +1,2 @@ -#if VS2019 -global using Meadow.CLI.Core.Logging; -#else -global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Logging.Abstractions; -#endif \ No newline at end of file +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Abstractions; \ No newline at end of file From fc6d013b85fd4252fedf05841fd93206d04d38cd Mon Sep 17 00:00:00 2001 From: Eduardo Menezes Date: Thu, 28 Dec 2023 15:36:33 -0300 Subject: [PATCH 065/141] Fix Debug.WriteLine exception issue --- Meadow.CLI.Core/lib/meadow_link.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/Meadow.CLI.Core/lib/meadow_link.xml b/Meadow.CLI.Core/lib/meadow_link.xml index 0fe29752..bd47ba33 100644 --- a/Meadow.CLI.Core/lib/meadow_link.xml +++ b/Meadow.CLI.Core/lib/meadow_link.xml @@ -8,4 +8,5 @@ + From aae340d7bddede526d27cb30abfff6b14d6791cc Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 8 Jan 2024 09:17:30 -0800 Subject: [PATCH 066/141] Remove primary ctor --- Source/v2/Meadow.CLI/Linker/MeadowLinker.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs b/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs index 68e64d0c..89ff3540 100644 --- a/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs +++ b/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs @@ -5,7 +5,7 @@ namespace LinkerTest; -public class MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) +public class MeadowLinker { private const string IL_LINKER_DIR = "lib"; private const string IL_LINKER_DLL = "illink.dll"; @@ -14,11 +14,18 @@ public class MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) private const string PostLinkDirectoryName = "postlink_bin"; private const string PreLinkDirectoryName = "prelink_bin"; - readonly ILLinker _linker = new ILLinker(logger); - readonly ILogger? _logger = logger; + readonly ILLinker _linker; + readonly ILogger? _logger; //ToDo ... might need to make this a property or pass it in when used - private readonly string _meadowAssembliesPath = meadowAssembliesPath; + private readonly string _meadowAssembliesPath; + + public MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) + { + _meadowAssembliesPath = meadowAssembliesPath; + _logger = logger; + _linker = new ILLinker(logger); + } public async Task Trim( FileInfo meadowAppFile, From dd4c1f5f7800d0341c4c2781c8091695f4d1ea9e Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Mon, 8 Jan 2024 16:59:02 -0600 Subject: [PATCH 067/141] v2 subdir work --- Source/v2/Meadow.Cli/AppManager.cs | 2 +- .../Current/File/FileDeleteCommand.cs | 2 +- .../Commands/Current/File/FileListCommand.cs | 5 +++- .../Meadow.Cli/Properties/launchSettings.json | 2 +- .../ConnectionManagerTests.cs | 4 +-- .../SerialCommandTests.cs | 4 +-- .../Connections/SimulatorConnection.cs | 2 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- .../Connections/LocalConnection.cs | 4 +-- .../Connections/SerialConnection.cs | 9 +++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowDevice.cs | 2 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 4 +-- Source/v2/Meadow.Hcom/MeadowFileInfo.cs | 29 +++++++++++++++++-- .../SerialRequests/GetFileListRequest.cs | 27 +++++++++++++++-- .../Meadow.Hcom/SerialRequests/RequestType.cs | 2 ++ 17 files changed, 81 insertions(+), 23 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index e587e6bf..41e135fa 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -69,7 +69,7 @@ public static async Task DeployApplication( } // get a list of files on-device, with CRCs - var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); + var deviceFiles = await connection.GetFileList("/meadow0/", true, cancellationToken) ?? Array.Empty(); // get a list of files of the device files that are not in the list we intend to deploy var removeFiles = deviceFiles diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 221db0af..ce6e6001 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -25,7 +25,7 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var fileList = await connection.GetFileList(false); + var fileList = await connection.GetFileList("/meadow0/", false); if (MeadowFile == "all") { diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index ce4c9e69..bfa86eaf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -11,6 +11,9 @@ public class FileListCommand : BaseDeviceCommand [CommandOption("verbose", 'v', IsRequired = false)] public bool Verbose { get; set; } + [CommandParameter(0, Name = "Folder", IsRequired = false)] + public string? Folder { get; set; } = default!; + public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { @@ -28,7 +31,7 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var files = await connection.Device.GetFileList(Verbose, CancellationToken); + var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); if (files == null || files.Length == 0) { diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 8e3cb20e..ad9092c6 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -45,7 +45,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM7" + "commandLineArgs": "config route COM10" }, "Config: Set Route TCP": { "commandName": "Project", diff --git a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs index f5f025d4..d016e798 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs @@ -124,7 +124,7 @@ public async Task TestGetFileListWithoutCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(false); + var files = await device.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); @@ -143,7 +143,7 @@ public async Task TestGetFileListWithCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(true); + var files = await device.GetFileList("/meadow0/", true); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs index d60dc865..74aed9b7 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -39,7 +39,7 @@ public async void TestGetFileListNoCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(false); + var files = await connection.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Length > 0); @@ -53,7 +53,7 @@ public async void TestGetFileListWithCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(true); + var files = await connection.GetFileList("/meadow0/", true); Assert.NotNull(files); Assert.True(files.Length > 0); diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index 90e2d140..c1e04328 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -42,7 +42,7 @@ public override Task EraseFlash(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 5055f0d3..dac98459 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -20,7 +20,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); public abstract Task GetDeviceInfo(CancellationToken? cancellationToken = null); - public abstract Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + public abstract Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index f42afbb7..ed767e34 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -58,7 +58,7 @@ public LocalConnection() _deviceInfo = new DeviceInfo(info); } - return Task.FromResult< DeviceInfo?>(_deviceInfo); + return Task.FromResult(_deviceInfo); } private string ExecuteBashCommandLine(string command) @@ -143,7 +143,7 @@ public override Task EraseFlash(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 53b7fe73..98186f68 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -173,6 +173,11 @@ private void Close() State = ConnectionState.Disconnected; } + public void Detach() + { + Close(); + } + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { try @@ -759,11 +764,13 @@ public override async Task ResetDevice(CancellationToken? cancellationToken = nu return _deviceInfo; } - public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.IncludeCrcs = includeCrcs; + command.Path = folder; + EnqueueRequest(command); if (!await WaitForResult( diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 36b76549..7346c761 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -68,7 +68,7 @@ public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 75dbce9c..aa8b8f80 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -21,7 +21,7 @@ public interface IMeadowConnection Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task RuntimeDisable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index b599cb0a..3d20142b 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -9,7 +9,7 @@ public interface IMeadowDevice Task RuntimeEnable(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index f9b55b41..9b9b3b7c 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -36,9 +36,9 @@ public async Task RuntimeEnable(CancellationToken? cancellationToken = null) return await _connection.GetDeviceInfo(cancellationToken); } - public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { - return await _connection.GetFileList(includeCrcs, cancellationToken); + return await _connection.GetFileList(folder, includeCrcs, cancellationToken); } public async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) diff --git a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs index 46590ad9..a4a06981 100644 --- a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs +++ b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs @@ -3,6 +3,12 @@ public string Name { get; private set; } = default!; public long? Size { get; private set; } public string? Crc { get; private set; } + public bool IsDirectory { get; private set; } + + public override string ToString() + { + return $"{(IsDirectory ? "/" : "")}{Name}"; + } public static MeadowFileInfo? Parse(string info) { @@ -13,11 +19,29 @@ { mfi = new MeadowFileInfo(); - // "/meadow0/App.deps.json [0xa0f6d6a2] 28 KB (26575 bytes)" + mfi.Name = info.Substring(1); + mfi.IsDirectory = true; + } + else + { + // v2 file lists have changed + + if (info.StartsWith("Directory:")) + { + // this is the first line and contains the directory name being parsed + return mfi; + } + else if (info.StartsWith("A total of")) + { + return mfi; + } + + mfi = new MeadowFileInfo(); + var indexOfSquareBracket = info.IndexOf('['); if (indexOfSquareBracket <= 0) { - mfi.Name = info; + mfi.Name = info.Trim(); } else { @@ -28,6 +52,7 @@ mfi.Size = int.Parse(info.Substring(indexOfParen + 1, end - indexOfParen)); } } + return mfi; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs index 117f0468..7d8b4ee9 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs @@ -1,13 +1,34 @@ -namespace Meadow.Hcom +using System.Text; + +namespace Meadow.Hcom { internal class GetFileListRequest : Request { public override RequestType RequestType => IncludeCrcs - ? RequestType.HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC - : RequestType.HCOM_MDOW_REQUEST_LIST_PARTITION_FILES; + ? RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC + : RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR; public bool IncludeCrcs { get; set; } + public string? Path + { + get + { + if (Payload == null) return null; + + if (Payload.Length == 0) { return null; } + + return Encoding.ASCII.GetString(Payload).Trim(); + } + set + { + if (value != null) + { + base.Payload = Encoding.ASCII.GetBytes(value); + } + } + } + public GetFileListRequest() { } diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs index eacee4ca..b00a012d 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs @@ -64,6 +64,8 @@ public enum RequestType : ushort // ToDo HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD doesn't send text, it's a header only message type HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD = 0x04 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, HCOM_MDOW_REQUEST_RTC_WAKEUP_TIME_CMD = 0x05 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR = 0x06 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC = 0x07 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, // This is a simple type with binary data From 15d4821101e264ad30a889626ffa6148f7993051 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 10:25:27 -0600 Subject: [PATCH 068/141] updated Delete api to return a value --- .../v2/Meadow.HCom/Connections/SimulatorConnection.cs | 2 +- Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- Source/v2/Meadow.Hcom/Connections/LocalConnection.cs | 2 +- .../Connections/SerialConnection.ListenerProc.cs | 1 + Source/v2/Meadow.Hcom/Connections/SerialConnection.cs | 10 ++++++++-- Source/v2/Meadow.Hcom/Connections/TcpConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 +- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index c1e04328..296f422d 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -32,7 +32,7 @@ public override Task RuntimeEnable(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index dac98459..650510eb 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -24,7 +24,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); - public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); public abstract Task ResetDevice(CancellationToken? cancellationToken = null); public abstract Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index ed767e34..0861889e 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -133,7 +133,7 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 69e6af6e..6bce3d63 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -262,6 +262,7 @@ private async Task ListenerProc() } else if (response is RequestErrorTextResponse ret) { + Debug.WriteLine(ret.Text); RaiseDeviceMessageReceived(ret.Text, "hcom"); _lastError = ret.Text; } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 98186f68..cf236bbf 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -175,6 +175,11 @@ private void Close() public void Detach() { + if (MaintainConnection) + { + // TODO: close this up + } + Close(); } @@ -1137,7 +1142,7 @@ void OnFileDataReceived(object? sender, string data) return contents; } - public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.MeadowFileName = meadowFileName; @@ -1146,7 +1151,8 @@ public override async Task DeleteFile(string meadowFileName, CancellationToken? EnqueueRequest(command); - await WaitForConcluded(null, cancellationToken); + var result = await WaitForConcluded(null, cancellationToken); + return result; } public override async Task EraseFlash(CancellationToken? cancellationToken = null) diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 7346c761..8e3dd4b6 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -113,7 +113,7 @@ public override Task ReadFile(string meadowFileName, string? localFileName throw new NotImplementedException(); } - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index aa8b8f80..d7479502 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -19,7 +19,7 @@ public interface IMeadowConnection Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); - Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); From d94f37df493e0c8150904f378c72fbd8e62f62f7 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 10:32:32 -0600 Subject: [PATCH 069/141] Updated file list CLI command output --- .../Commands/Current/File/FileListCommand.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index bfa86eaf..77b5ddc2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -17,7 +17,19 @@ public class FileListCommand : BaseDeviceCommand public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogInformation($"Getting file list..."); + if (Folder != null) + { + if (!Folder.EndsWith('/')) + { + Folder += "/"; + } + + Logger?.LogInformation($"Getting file list from '{Folder}'..."); + } + else + { + Logger?.LogInformation($"Getting file list..."); + } } protected override async ValueTask ExecuteCommand() From 8e9a558c63454ec0649e2b685bc375136beba6ff Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 11:16:27 -0600 Subject: [PATCH 070/141] file delete CLI check updates --- .../Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index ce6e6001..30b60f09 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -25,7 +25,9 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var fileList = await connection.GetFileList("/meadow0/", false); + // get a list of files in the target folder + var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); + var fileList = await connection.GetFileList($"{folder}/", false); if (MeadowFile == "all") { @@ -38,7 +40,9 @@ protected override async ValueTask ExecuteCommand() } else { - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; + var requested = Path.GetFileName(MeadowFile); + + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; if (!exists) { From 984ebb4ff66593e81e397f5bf504bb402b156628 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 11:51:34 -0600 Subject: [PATCH 071/141] added a null check --- .../v2/Meadow.Hcom/Connections/SerialConnection.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index cf236bbf..2f4e5f81 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -246,13 +246,15 @@ private async void CommandManager() Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); var command = _pendingCommands.Dequeue() as Request; + if (command != null) + { + // if this is a file write, we need to packetize for progress - // if this is a file write, we need to packetize for progress - - var payload = command.Serialize(); - EncodeAndSendPacket(payload); + var payload = command.Serialize(); + EncodeAndSendPacket(payload); - // TODO: re-queue on fail? + // TODO: re-queue on fail? + } } Thread.Sleep(1000); From 6ad0c2aac17b98d9b195b15002d900756463bf6e Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 11 Jan 2024 15:55:32 -0800 Subject: [PATCH 072/141] Move DFU logic into Meadow.CLI.Core --- Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuContext.cs | 0 Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuSharp.cs | 0 Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuUtils.cs | 0 Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj | 4 ++++ Source/v2/Meadow.Cli/Properties/launchSettings.json | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuContext.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuSharp.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/DFU/DfuUtils.cs (100%) diff --git a/Source/v2/Meadow.Cli/DFU/DfuContext.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs similarity index 100% rename from Source/v2/Meadow.Cli/DFU/DfuContext.cs rename to Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs diff --git a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs similarity index 100% rename from Source/v2/Meadow.Cli/DFU/DfuSharp.cs rename to Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs diff --git a/Source/v2/Meadow.Cli/DFU/DfuUtils.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs similarity index 100% rename from Source/v2/Meadow.Cli/DFU/DfuUtils.cs rename to Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs diff --git a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj index b5cbc219..0f48aa72 100644 --- a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 8e3cb20e..18310fff 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -125,7 +125,7 @@ }, "Firmware Write version": { "commandName": "Project", - "commandLineArgs": "firmware write -v 1.2.0.1" + "commandLineArgs": "firmware write -v 1.7.1.3" }, "Firmware Write runtime": { "commandName": "Project", From 92dbe546c1a0ba38b627def3b21d0f22651612ea Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 11 Jan 2024 16:08:15 -0800 Subject: [PATCH 073/141] Move PackageManager and linker logic into Meadow.CLI.Core --- .../Linker/ILLinker.cs | 0 .../Linker/MeadowLinker.cs | 0 .../v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj | 20 ++++++++++++++++++ .../PackageManager}/IPackageManager.cs | 0 .../PackageManager.AssemblyManager.cs | 0 .../PackageManager.BuildOptions.cs | 0 .../PackageManager}/PackageManager.cs | 0 .../lib/Mono.Cecil.Pdb.dll | Bin .../lib/Mono.Cecil.dll | Bin .../lib/illink.deps.json | 0 .../lib/illink.dll | Bin .../lib/illink.runtimeconfig.json | 0 .../lib/meadow_link.xml | 0 Source/v2/Meadow.CLI/Meadow.CLI.csproj | 17 --------------- 14 files changed, 20 insertions(+), 17 deletions(-) rename Source/v2/{Meadow.CLI => Meadow.CLI.Core}/Linker/ILLinker.cs (100%) rename Source/v2/{Meadow.CLI => Meadow.CLI.Core}/Linker/MeadowLinker.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core/PackageManager}/IPackageManager.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core/PackageManager}/PackageManager.AssemblyManager.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core/PackageManager}/PackageManager.BuildOptions.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core/PackageManager}/PackageManager.cs (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/Mono.Cecil.Pdb.dll (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/Mono.Cecil.dll (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/illink.deps.json (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/illink.dll (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/illink.runtimeconfig.json (100%) rename Source/v2/{Meadow.Cli => Meadow.CLI.Core}/lib/meadow_link.xml (100%) diff --git a/Source/v2/Meadow.CLI/Linker/ILLinker.cs b/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs similarity index 100% rename from Source/v2/Meadow.CLI/Linker/ILLinker.cs rename to Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs diff --git a/Source/v2/Meadow.CLI/Linker/MeadowLinker.cs b/Source/v2/Meadow.CLI.Core/Linker/MeadowLinker.cs similarity index 100% rename from Source/v2/Meadow.CLI/Linker/MeadowLinker.cs rename to Source/v2/Meadow.CLI.Core/Linker/MeadowLinker.cs diff --git a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj index 0f48aa72..e1a160a8 100644 --- a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -7,11 +7,31 @@ + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs similarity index 100% rename from Source/v2/Meadow.Cli/IPackageManager.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs similarity index 100% rename from Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs diff --git a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs similarity index 100% rename from Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs similarity index 100% rename from Source/v2/Meadow.Cli/PackageManager.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.Pdb.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll rename to Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.Pdb.dll diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/Mono.Cecil.dll rename to Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.dll diff --git a/Source/v2/Meadow.Cli/lib/illink.deps.json b/Source/v2/Meadow.CLI.Core/lib/illink.deps.json similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.deps.json rename to Source/v2/Meadow.CLI.Core/lib/illink.deps.json diff --git a/Source/v2/Meadow.Cli/lib/illink.dll b/Source/v2/Meadow.CLI.Core/lib/illink.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.dll rename to Source/v2/Meadow.CLI.Core/lib/illink.dll diff --git a/Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json b/Source/v2/Meadow.CLI.Core/lib/illink.runtimeconfig.json similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json rename to Source/v2/Meadow.CLI.Core/lib/illink.runtimeconfig.json diff --git a/Source/v2/Meadow.Cli/lib/meadow_link.xml b/Source/v2/Meadow.CLI.Core/lib/meadow_link.xml similarity index 100% rename from Source/v2/Meadow.Cli/lib/meadow_link.xml rename to Source/v2/Meadow.CLI.Core/lib/meadow_link.xml diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index c6fc59f0..5b2153d5 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -33,22 +33,17 @@ - - - - - @@ -62,18 +57,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - From 77079ef910525662d1745061c1a05e613b94e2ba Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 11 Jan 2024 16:10:29 -0800 Subject: [PATCH 074/141] Ref cleanup --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 5b2153d5..a8383b9a 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -36,7 +36,6 @@ - From d1f3772e37eace2c51eeefec9c52586c5c91eba6 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 11 Jan 2024 16:19:46 -0800 Subject: [PATCH 075/141] Meadow.UsbLib refactoring --- Source/v2/Meadow.CLI.v2.sln | 9 +++++++-- .../ILibUsbDevice.cs | 0 .../v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj | 9 +++++++++ Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj | 10 +++++----- .../Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj | 11 +++++------ 5 files changed, 26 insertions(+), 13 deletions(-) rename Source/v2/{Meadow.Cli.Core => Meadow.UsbLib.Core}/ILibUsbDevice.cs (100%) create mode 100644 Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln index 4f733098..81933c1e 100644 --- a/Source/v2/Meadow.CLI.v2.sln +++ b/Source/v2/Meadow.CLI.v2.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 @@ -27,7 +26,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.UsbLibClassic", "Mea EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.CLI.Core", "Meadow.CLI.Core\Meadow.CLI.Core.csproj", "{677B1C15-8936-4807-8A4F-4F5219BBDB7C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cloud.Client", "Meadow.Cloud.Client\Meadow.Cloud.Client.csproj", "{A71A3C98-2B11-46FE-AB7A-EAD9271862AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Cloud.Client", "Meadow.Cloud.Client\Meadow.Cloud.Client.csproj", "{A71A3C98-2B11-46FE-AB7A-EAD9271862AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.UsbLib.Core", "Meadow.UsbLib.Core\Meadow.UsbLib.Core.csproj", "{F02ADBEF-4D52-4A71-9D95-74F45D68B43B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +88,10 @@ Global {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Release|Any CPU.Build.0 = Release|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/v2/Meadow.Cli.Core/ILibUsbDevice.cs b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs similarity index 100% rename from Source/v2/Meadow.Cli.Core/ILibUsbDevice.cs rename to Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs diff --git a/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj new file mode 100644 index 00000000..d1692ecb --- /dev/null +++ b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj index ae241512..7229855d 100644 --- a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj +++ b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,12 +6,12 @@ enable - - - - + + + + diff --git a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj index cd055e35..3a4f13c1 100644 --- a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj +++ b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,13 +6,12 @@ enable - - - - - + + + + From 91afa576023a6557db8d054633244045315fab8b Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Mon, 8 Jan 2024 16:59:02 -0600 Subject: [PATCH 076/141] v2 subdir work --- Source/v2/Meadow.Cli/AppManager.cs | 2 +- .../Current/File/FileDeleteCommand.cs | 2 +- .../Commands/Current/File/FileListCommand.cs | 5 +++- .../Meadow.Cli/Properties/launchSettings.json | 2 +- .../ConnectionManagerTests.cs | 4 +-- .../SerialCommandTests.cs | 4 +-- .../Connections/SimulatorConnection.cs | 2 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- .../Connections/LocalConnection.cs | 4 +-- .../Connections/SerialConnection.cs | 9 +++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowDevice.cs | 2 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 4 +-- Source/v2/Meadow.Hcom/MeadowFileInfo.cs | 29 +++++++++++++++++-- .../SerialRequests/GetFileListRequest.cs | 27 +++++++++++++++-- .../Meadow.Hcom/SerialRequests/RequestType.cs | 2 ++ 17 files changed, 81 insertions(+), 23 deletions(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index e587e6bf..41e135fa 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -69,7 +69,7 @@ public static async Task DeployApplication( } // get a list of files on-device, with CRCs - var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); + var deviceFiles = await connection.GetFileList("/meadow0/", true, cancellationToken) ?? Array.Empty(); // get a list of files of the device files that are not in the list we intend to deploy var removeFiles = deviceFiles diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 221db0af..ce6e6001 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -25,7 +25,7 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var fileList = await connection.GetFileList(false); + var fileList = await connection.GetFileList("/meadow0/", false); if (MeadowFile == "all") { diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index ce4c9e69..bfa86eaf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -11,6 +11,9 @@ public class FileListCommand : BaseDeviceCommand [CommandOption("verbose", 'v', IsRequired = false)] public bool Verbose { get; set; } + [CommandParameter(0, Name = "Folder", IsRequired = false)] + public string? Folder { get; set; } = default!; + public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { @@ -28,7 +31,7 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var files = await connection.Device.GetFileList(Verbose, CancellationToken); + var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); if (files == null || files.Length == 0) { diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 18310fff..7dc2f1f4 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -45,7 +45,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM7" + "commandLineArgs": "config route COM10" }, "Config: Set Route TCP": { "commandName": "Project", diff --git a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs index f5f025d4..d016e798 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs @@ -124,7 +124,7 @@ public async Task TestGetFileListWithoutCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(false); + var files = await device.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); @@ -143,7 +143,7 @@ public async Task TestGetFileListWithCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(true); + var files = await device.GetFileList("/meadow0/", true); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs index d60dc865..74aed9b7 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -39,7 +39,7 @@ public async void TestGetFileListNoCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(false); + var files = await connection.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Length > 0); @@ -53,7 +53,7 @@ public async void TestGetFileListWithCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(true); + var files = await connection.GetFileList("/meadow0/", true); Assert.NotNull(files); Assert.True(files.Length > 0); diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index 90e2d140..c1e04328 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -42,7 +42,7 @@ public override Task EraseFlash(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 5055f0d3..dac98459 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -20,7 +20,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); public abstract Task GetDeviceInfo(CancellationToken? cancellationToken = null); - public abstract Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + public abstract Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index f42afbb7..ed767e34 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -58,7 +58,7 @@ public LocalConnection() _deviceInfo = new DeviceInfo(info); } - return Task.FromResult< DeviceInfo?>(_deviceInfo); + return Task.FromResult(_deviceInfo); } private string ExecuteBashCommandLine(string command) @@ -143,7 +143,7 @@ public override Task EraseFlash(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 53b7fe73..98186f68 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -173,6 +173,11 @@ private void Close() State = ConnectionState.Disconnected; } + public void Detach() + { + Close(); + } + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { try @@ -759,11 +764,13 @@ public override async Task ResetDevice(CancellationToken? cancellationToken = nu return _deviceInfo; } - public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.IncludeCrcs = includeCrcs; + command.Path = folder; + EnqueueRequest(command); if (!await WaitForResult( diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 36b76549..7346c761 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -68,7 +68,7 @@ public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 75dbce9c..aa8b8f80 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -21,7 +21,7 @@ public interface IMeadowConnection Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task RuntimeDisable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index b599cb0a..3d20142b 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -9,7 +9,7 @@ public interface IMeadowDevice Task RuntimeEnable(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index f9b55b41..9b9b3b7c 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -36,9 +36,9 @@ public async Task RuntimeEnable(CancellationToken? cancellationToken = null) return await _connection.GetDeviceInfo(cancellationToken); } - public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { - return await _connection.GetFileList(includeCrcs, cancellationToken); + return await _connection.GetFileList(folder, includeCrcs, cancellationToken); } public async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) diff --git a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs index 46590ad9..a4a06981 100644 --- a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs +++ b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs @@ -3,6 +3,12 @@ public string Name { get; private set; } = default!; public long? Size { get; private set; } public string? Crc { get; private set; } + public bool IsDirectory { get; private set; } + + public override string ToString() + { + return $"{(IsDirectory ? "/" : "")}{Name}"; + } public static MeadowFileInfo? Parse(string info) { @@ -13,11 +19,29 @@ { mfi = new MeadowFileInfo(); - // "/meadow0/App.deps.json [0xa0f6d6a2] 28 KB (26575 bytes)" + mfi.Name = info.Substring(1); + mfi.IsDirectory = true; + } + else + { + // v2 file lists have changed + + if (info.StartsWith("Directory:")) + { + // this is the first line and contains the directory name being parsed + return mfi; + } + else if (info.StartsWith("A total of")) + { + return mfi; + } + + mfi = new MeadowFileInfo(); + var indexOfSquareBracket = info.IndexOf('['); if (indexOfSquareBracket <= 0) { - mfi.Name = info; + mfi.Name = info.Trim(); } else { @@ -28,6 +52,7 @@ mfi.Size = int.Parse(info.Substring(indexOfParen + 1, end - indexOfParen)); } } + return mfi; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs index 117f0468..7d8b4ee9 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs @@ -1,13 +1,34 @@ -namespace Meadow.Hcom +using System.Text; + +namespace Meadow.Hcom { internal class GetFileListRequest : Request { public override RequestType RequestType => IncludeCrcs - ? RequestType.HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC - : RequestType.HCOM_MDOW_REQUEST_LIST_PARTITION_FILES; + ? RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC + : RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR; public bool IncludeCrcs { get; set; } + public string? Path + { + get + { + if (Payload == null) return null; + + if (Payload.Length == 0) { return null; } + + return Encoding.ASCII.GetString(Payload).Trim(); + } + set + { + if (value != null) + { + base.Payload = Encoding.ASCII.GetBytes(value); + } + } + } + public GetFileListRequest() { } diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs index eacee4ca..b00a012d 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs @@ -64,6 +64,8 @@ public enum RequestType : ushort // ToDo HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD doesn't send text, it's a header only message type HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD = 0x04 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, HCOM_MDOW_REQUEST_RTC_WAKEUP_TIME_CMD = 0x05 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR = 0x06 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC = 0x07 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, // This is a simple type with binary data From 5e784efa713b566970d7b44666b9891e6a5c5cc3 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 10:25:27 -0600 Subject: [PATCH 077/141] updated Delete api to return a value --- .../v2/Meadow.HCom/Connections/SimulatorConnection.cs | 2 +- Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- Source/v2/Meadow.Hcom/Connections/LocalConnection.cs | 2 +- .../Connections/SerialConnection.ListenerProc.cs | 1 + Source/v2/Meadow.Hcom/Connections/SerialConnection.cs | 10 ++++++++-- Source/v2/Meadow.Hcom/Connections/TcpConnection.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 +- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index c1e04328..296f422d 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -32,7 +32,7 @@ public override Task RuntimeEnable(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index dac98459..650510eb 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -24,7 +24,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); public abstract Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); - public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); public abstract Task ResetDevice(CancellationToken? cancellationToken = null); public abstract Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index ed767e34..0861889e 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -133,7 +133,7 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 69e6af6e..6bce3d63 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -262,6 +262,7 @@ private async Task ListenerProc() } else if (response is RequestErrorTextResponse ret) { + Debug.WriteLine(ret.Text); RaiseDeviceMessageReceived(ret.Text, "hcom"); _lastError = ret.Text; } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 98186f68..cf236bbf 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -175,6 +175,11 @@ private void Close() public void Detach() { + if (MaintainConnection) + { + // TODO: close this up + } + Close(); } @@ -1137,7 +1142,7 @@ void OnFileDataReceived(object? sender, string data) return contents; } - public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.MeadowFileName = meadowFileName; @@ -1146,7 +1151,8 @@ public override async Task DeleteFile(string meadowFileName, CancellationToken? EnqueueRequest(command); - await WaitForConcluded(null, cancellationToken); + var result = await WaitForConcluded(null, cancellationToken); + return result; } public override async Task EraseFlash(CancellationToken? cancellationToken = null) diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 7346c761..8e3dd4b6 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -113,7 +113,7 @@ public override Task ReadFile(string meadowFileName, string? localFileName throw new NotImplementedException(); } - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index aa8b8f80..d7479502 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -19,7 +19,7 @@ public interface IMeadowConnection Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); - Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); From ebe1998038a645ad93826e5602f9a29c1a7cc4f2 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 10:32:32 -0600 Subject: [PATCH 078/141] Updated file list CLI command output --- .../Commands/Current/File/FileListCommand.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index bfa86eaf..77b5ddc2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -17,7 +17,19 @@ public class FileListCommand : BaseDeviceCommand public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogInformation($"Getting file list..."); + if (Folder != null) + { + if (!Folder.EndsWith('/')) + { + Folder += "/"; + } + + Logger?.LogInformation($"Getting file list from '{Folder}'..."); + } + else + { + Logger?.LogInformation($"Getting file list..."); + } } protected override async ValueTask ExecuteCommand() From 4ed92bdbe44d92ae719fe27e73fd468678954fcb Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 11:16:27 -0600 Subject: [PATCH 079/141] file delete CLI check updates --- .../Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index ce6e6001..30b60f09 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -25,7 +25,9 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - var fileList = await connection.GetFileList("/meadow0/", false); + // get a list of files in the target folder + var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); + var fileList = await connection.GetFileList($"{folder}/", false); if (MeadowFile == "all") { @@ -38,7 +40,9 @@ protected override async ValueTask ExecuteCommand() } else { - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; + var requested = Path.GetFileName(MeadowFile); + + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; if (!exists) { From cbc801d6da99574419c680ae73d6ea79ef743f09 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 11 Jan 2024 11:51:34 -0600 Subject: [PATCH 080/141] added a null check --- .../v2/Meadow.Hcom/Connections/SerialConnection.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index cf236bbf..2f4e5f81 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -246,13 +246,15 @@ private async void CommandManager() Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); var command = _pendingCommands.Dequeue() as Request; + if (command != null) + { + // if this is a file write, we need to packetize for progress - // if this is a file write, we need to packetize for progress - - var payload = command.Serialize(); - EncodeAndSendPacket(payload); + var payload = command.Serialize(); + EncodeAndSendPacket(payload); - // TODO: re-queue on fail? + // TODO: re-queue on fail? + } } Thread.Sleep(1000); From 09d8ded35f094e667fd7e34aef3c24e4a3dddf6c Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:17 -0800 Subject: [PATCH 081/141] Code cleanup --- .../Commands/Current/Firmware/FirmwareDownloadCommand.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index 4c931efb..66ae615e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDownloadCommand : BaseFileCommand { public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandOption("force", 'f', IsRequired = false)] public bool Force { get; set; } @@ -59,7 +57,6 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Downloading firmware package '{Version}'..."); - try { collection.DownloadProgress += OnDownloadProgress; @@ -74,7 +71,7 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); - if (explicitVersion) + if (explicitVersion == false) { await collection.SetDefaultPackage(Version); } @@ -86,8 +83,6 @@ protected override async ValueTask ExecuteCommand() } } - private long _lastProgress = 0; - private void OnDownloadProgress(object? sender, long e) { // use Console so we can Write instead of Logger which only supports WriteLine From 1f03cbe52d0bec6402b0954bcadf23fad4439d96 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:44 -0800 Subject: [PATCH 082/141] Code cleanup and better user feedback --- .../Commands/Current/File/FileListCommand.cs | 104 +++++++++--------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 77b5ddc2..bd00759d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -19,7 +19,7 @@ public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory { if (Folder != null) { - if (!Folder.EndsWith('/')) + if (Folder.EndsWith('/') == false) { Folder += "/"; } @@ -36,73 +36,71 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"File list failed - device or connection not found"); return; } - if (connection != null) - { - var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); + var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); - if (files == null || files.Length == 0) - { - Logger?.LogInformation($"No files found"); - } - else + if (files == null || files.Length == 0) + { + Logger?.LogInformation($"No files found"); + } + else + { + if (Verbose) { - if (Verbose) + var longestFileName = files.Select(x => x.Name.Length) + .OrderByDescending(x => x) + .FirstOrDefault(); + + var totalBytesUsed = 0L; + var totalBlocksUsed = 0L; + + foreach (var file in files) { - var longestFileName = files.Select(x => x.Name.Length) - .OrderByDescending(x => x) - .FirstOrDefault(); + totalBytesUsed += file.Size ?? 0; + totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; - var totalBytesUsed = 0L; - var totalBlocksUsed = 0L; + var line = $"{file.Name.PadRight(longestFileName)}"; + line = $"{line}\t{file.Crc:x8}"; - foreach (var file in files) + if (file.Size > 1000000) { - totalBytesUsed += file.Size ?? 0; - totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; - - var line = $"{file.Name.PadRight(longestFileName)}"; - line = $"{line}\t{file.Crc:x8}"; - - if (file.Size > 1000000) - { - line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; - } - else if (file.Size > 1000) - { - line = $"{line}\t{file.Size / 1000,7:0} kB "; - } - else - { - line = $"{line}\t{file.Size,7} bytes"; - } - - Logger?.LogInformation(line); + line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; } - - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files\n" + - $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + - $"\tSpanning {totalBlocksUsed} blocks\n" + - $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); - } - else - { - foreach (var file in files) + else if (file.Size > 1000) + { + line = $"{line}\t{file.Size / 1000,7:0} kB "; + } + else { - Logger?.LogInformation(file.Name); + line = $"{line}\t{file.Size,7} bytes"; } - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files"); + Logger?.LogInformation(line); } + + Logger?.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files\n" + + $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + + $"\tSpanning {totalBlocksUsed} blocks\n" + + $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); + } + else + { + foreach (var file in files) + { + Logger?.LogInformation(file.Name); + } + + Logger?.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files"); } } } -} +} \ No newline at end of file From 6597600193022a36f719d99a8ad4dd16870f00eb Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:54 -0800 Subject: [PATCH 083/141] Validate folder before delete --- .../Current/File/FileDeleteCommand.cs | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 30b60f09..a072cf05 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -20,47 +20,56 @@ protected override async ValueTask ExecuteCommand() if (connection == null) { + Logger?.LogInformation($"File delete failed, no connection to a device"); return; } - if (connection != null) + // get a list of files in the target folder + var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); + if (string.IsNullOrWhiteSpace(folder)) { - // get a list of files in the target folder - var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); - var fileList = await connection.GetFileList($"{folder}/", false); + folder = "/meadow0"; + } + + var fileList = await connection.GetFileList($"{folder}/", false); + + if (fileList == null || fileList.Length == 0) + { + Logger?.LogInformation($"File delete failed, no files found"); + return; + } - if (MeadowFile == "all") + if (MeadowFile == "all") + { + foreach (var f in fileList) { - foreach (var f in fileList) - { - var p = Path.GetFileName(f.Name); - Logger?.LogInformation($"Deleting file '{p}' from device..."); - await connection.Device.DeleteFile(p, CancellationToken); - } + var p = Path.GetFileName(f.Name); + Logger?.LogInformation($"Deleting file '{p}' from device..."); + await connection.Device.DeleteFile(p, CancellationToken); + } + } + else + { + var requested = Path.GetFileName(MeadowFile); + + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; + + if (!exists) + { + Logger?.LogError($"File '{MeadowFile}' not found on device."); } else { - var requested = Path.GetFileName(MeadowFile); + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; - - if (!exists) + if (wasRuntimeEnabled) { - Logger?.LogError($"File '{MeadowFile}' not found on device."); + Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); + return; } - else - { - var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - if (wasRuntimeEnabled) - { - Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); - return; - } - - Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); - await connection.Device.DeleteFile(MeadowFile, CancellationToken); - } + Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await connection.Device.DeleteFile(MeadowFile, CancellationToken); } } } From 7561b04194a02854a2f6145d3b6a33688cca93bf Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:05:09 -0800 Subject: [PATCH 084/141] Minor cleanup --- .../v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs | 7 ++----- Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs | 4 ++-- .../Commands/Current/Cloud/CloudLogoutCommand.cs | 2 +- .../Commands/Current/Cloud/Collection/QualityOfService.cs | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 711f07ab..a29a90ce 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -1,7 +1,4 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; -using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app build", Description = "Compiles a Meadow application")] public class AppBuildCommand : BaseCommand { - private IPackageManager _packageManager; + private readonly IPackageManager _packageManager; [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] public string? Configuration { get; set; } @@ -53,7 +50,7 @@ protected override async ValueTask ExecuteCommand() } else { - Logger?.LogError($"Build success."); + Logger?.LogError($"Build successful"); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index be02b31a..be3063e7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -36,11 +36,11 @@ public async ValueTask ExecuteAsync(IConsole console) if (CancellationToken.IsCancellationRequested) { - Logger?.LogInformation($"Cancelled."); + Logger?.LogInformation($"Cancelled"); } else { - Logger?.LogInformation($"Done."); + Logger?.LogInformation($"Done"); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index 733a4f10..334cf5d6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -24,4 +24,4 @@ protected override async ValueTask ExecuteCommand() IdentityManager.Logout(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs index 8aa8e094..7e824fd5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs @@ -5,4 +5,4 @@ public enum QualityOfService AtLeastOnce = 0, AtMostOnce = 1, ExactlyOnce = 2 -} +} \ No newline at end of file From 2377a5997836ca3d5f8d5bfa21cc05bc3e3135f1 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:20:42 -0800 Subject: [PATCH 085/141] Improved null checking --- .../Commands/Current/File/FileDeleteCommand.cs | 6 +++--- .../Commands/Current/File/FileInitialCommand.cs | 16 +++++++++++----- .../Commands/Current/File/FileListCommand.cs | 8 +++----- .../Commands/Current/File/FileReadCommand.cs | 8 ++++---- .../Commands/Current/File/FileWriteCommand.cs | 6 +++--- .../Commands/Current/Flash/FlashEraseCommand.cs | 7 ++++--- 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index a072cf05..18b81387 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -18,9 +18,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File delete failed, no connection to a device"); + Logger?.LogInformation($"File delete failed - device or connection not found"); return; } @@ -35,7 +35,7 @@ protected override async ValueTask ExecuteCommand() if (fileList == null || fileList.Length == 0) { - Logger?.LogInformation($"File delete failed, no files found"); + Logger?.LogError($"File delete failed, no files found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index 813a0e94..c97568f8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -11,15 +11,15 @@ public class FileInitialCommand : BaseDeviceCommand public FileInitialCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File initial failed - device or connection not found"); return; } @@ -27,6 +27,12 @@ protected override async ValueTask ExecuteCommand() var data = await connection.Device.ReadFileString(MeadowFile, CancellationToken); - Logger.LogInformation(data); + if (data == null) + { + Logger?.LogError($"Failed to retrieve file"); + return; + } + + Logger?.LogInformation(data); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index bd00759d..5a792e14 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -38,7 +38,7 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File list failed - device or connection not found"); + Logger?.LogError($"File list failed - device or connection not found"); return; } @@ -85,7 +85,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation( $"\nSummary:\n" + - $"\t{files.Length} files\n" + + $"\t{files.Length} file(s)\n" + $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + $"\tSpanning {totalBlocksUsed} blocks\n" + $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); @@ -97,9 +97,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation(file.Name); } - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files"); + Logger?.LogInformation($"\t{files.Length} file(s)"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index f17195be..4e2259c8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -14,15 +14,15 @@ public class FileReadCommand : BaseDeviceCommand public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File read failed - device or connection not found"); return; } @@ -39,4 +39,4 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Failed to retrieve file"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index ad74832d..ff7200c8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -21,15 +21,15 @@ public class FileWriteCommand : BaseDeviceCommand public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File write failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index 3f3bbc7e..8dcdd610 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("flash erase", Description = "Erases the device's flash storage")] +[Command("flash erase", Description = "Erase the contents of the device flash storage")] public class FlashEraseCommand : BaseDeviceCommand { public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) @@ -15,8 +15,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Flash erase failed - device or connection not found"); return; } @@ -29,4 +30,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.EraseFlash(CancellationToken); } -} +} \ No newline at end of file From 4dd98de03ba7fff94b3d37d1f1c3d168d2af795a Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:22:13 -0800 Subject: [PATCH 086/141] Move logging and folder validation out of ctor --- .../Commands/Current/File/FileListCommand.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 5a792e14..e0dc3de3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -16,7 +16,18 @@ public class FileListCommand : BaseDeviceCommand public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) + { } + + protected override async ValueTask ExecuteCommand() { + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + Logger?.LogError($"File list failed - device or connection not found"); + return; + } + if (Folder != null) { if (Folder.EndsWith('/') == false) @@ -30,17 +41,6 @@ public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory { Logger?.LogInformation($"Getting file list..."); } - } - - protected override async ValueTask ExecuteCommand() - { - var connection = await GetCurrentConnection(); - - if (connection == null || connection.Device == null) - { - Logger?.LogError($"File list failed - device or connection not found"); - return; - } var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); From c4ee4d76d9134851bb9342c3548384f7d446b776 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:37:26 -0800 Subject: [PATCH 087/141] Formatting and warnings --- .../PackageManager/PackageManager.cs | 2 +- .../Commands/Current/App/AppBuildCommand.cs | 10 +++++-- .../Commands/Current/App/AppRunCommand.cs | 3 +- .../Commands/Current/BaseCloudCommand.cs | 2 +- .../Commands/Current/BaseCommand.cs | 1 - .../Commands/Current/BaseFileCommand.cs | 5 ++-- .../Commands/Current/DeveloperCommand.cs | 30 ++++++++----------- .../Current/Device/DeviceProvisionCommand.cs | 6 ++-- .../Current/Firmware/FirmwareWriteCommand.cs | 7 ++--- .../Commands/Current/ListenCommand.cs | 6 ++-- .../Commands/Legacy/DownloadOsCommand.cs | 3 +- .../Commands/Legacy/FlashOsCommand.cs | 16 +++++----- .../Commands/Legacy/InstallDfuCommand.cs | 6 ++-- .../Commands/Legacy/ListPortsCommand.cs | 6 ++-- .../Commands/Legacy/MonoDisableCommand.cs | 4 +-- .../Commands/Legacy/MonoEnableCommand.cs | 2 +- .../Commands/Legacy/MonoStateCommand.cs | 4 +-- .../Commands/Legacy/UsePortCommand.cs | 8 ++--- .../Connections/SerialConnection.cs | 2 +- 19 files changed, 57 insertions(+), 66 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs index c4881a52..4ff24eee 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs @@ -230,7 +230,7 @@ public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, stri // look for a 'bin' folder var path = Path.Combine(rootFolder, "bin"); - if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); + if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); var files = new List(); FindApp(path, files); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index a29a90ce..9831daf0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -20,7 +20,7 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact _packageManager = packageManager; } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { string path = Path == null ? AppDomain.CurrentDomain.BaseDirectory @@ -33,11 +33,14 @@ protected override async ValueTask ExecuteCommand() if (!Directory.Exists(path)) { Logger?.LogError($"Invalid application path '{path}'"); - return; + return ValueTask.CompletedTask; } } - if (Configuration == null) Configuration = "Release"; + if (Configuration == null) + { + Configuration = "Release"; + } Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); @@ -52,5 +55,6 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Build successful"); } + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index d5a3ba85..f11cc484 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Hcom; using Microsoft.Extensions.Logging; @@ -69,7 +68,7 @@ protected override async ValueTask ExecuteCommand() return; } - // illink returns before all files are actually written. That's not fun, but we must just wait a little while. + // illink returns before all files are written - attempt a delay of 1s await Task.Delay(1000); if (!await DeployApplication(connection, path, CancellationToken)) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index daccc131..618585b0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -65,4 +65,4 @@ public BaseCloudCommand( return org; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index be3063e7..aa083008 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -43,5 +43,4 @@ public async ValueTask ExecuteAsync(IConsole console) Logger?.LogInformation($"Done"); } } - } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs index 09e0d728..6edc65ef 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -1,5 +1,4 @@ -using Meadow.CLI; -using Meadow.Software; +using Meadow.Software; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -13,4 +12,4 @@ public BaseFileCommand(FileManager fileManager, ISettingsManager settingsManager { FileManager = fileManager; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index af1263f8..5fcd5185 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -14,33 +14,29 @@ public class DeveloperCommand : BaseDeviceCommand public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Developer parameter set failed - device or connection not found"); return; } Logger?.LogInformation($"Setting developer parameter {Parameter} to {Value}"); - if (connection != null) + connection.DeviceMessageReceived += (s, e) => { - connection.DeviceMessageReceived += (s, e) => - { - Logger?.LogInformation(e.message); - }; - connection.ConnectionError += (s, e) => - { - Logger?.LogError(e.Message); - }; - - await connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); - } - } -} + Logger?.LogInformation(e.message); + }; + connection.ConnectionError += (s, e) => + { + Logger?.LogError(e.Message); + }; + await connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 23d16c6f..85d51793 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("device provision", Description = "Registers and prepares connected device for use with Meadow Cloud")] public class DeviceProvisionCommand : BaseDeviceCommand { - private DeviceService _deviceService; + private readonly DeviceService _deviceService; public const string DefaultHost = "https://www.meadowcloud.co"; @@ -75,9 +75,9 @@ protected override async ValueTask ExecuteCommand() var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - Logger?.LogError($"No connection path is defined"); + Logger?.LogError($"Device provision failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 942f1577..ae9b0fc1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; @@ -58,7 +57,7 @@ protected override async ValueTask ExecuteCommand() if (!Files.Contains(FirmwareType.OS) && UseDfu) { - Logger?.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); return; } @@ -199,7 +198,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() _libUsbDevice = devices[0]; break; default: - throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } } @@ -298,7 +297,7 @@ private async ValueTask WriteFiles(IMeadowConnection connection) write_runtime: if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) { - Logger?.LogInformation($"Error writing runtime. Retrying."); + Logger?.LogInformation($"Error writing runtime - retrying"); goto write_runtime; } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index 5834937e..e2f703c6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -11,11 +11,11 @@ public class ListenCommand : BaseDeviceCommand public ListenCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } private void Connection_ConnectionMessage(object? sender, string e) { + //ToDo } private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) @@ -49,4 +49,4 @@ protected override async ValueTask ExecuteCommand() await Task.Delay(1000); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs index dd27dbc4..a3ea05cd 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -11,6 +10,6 @@ public class DownloadOsCommand : FirmwareDownloadCommand public DownloadOsCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware download` instead"); + Logger?.LogWarning($"Deprecated command - use `firmware download` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index b81f43aa..3fd95004 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.LibUsb; using Meadow.Software; @@ -11,7 +10,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FlashOsCommand : BaseDeviceCommand { [CommandOption("osFile", 'o', Description = "Path to the Meadow OS binary")] - public string OSFile { get; init; } + public string OSFile { get; init; } = default!; [CommandOption("runtimeFile", 'r', Description = "Path to the Meadow Runtime binary")] public string RuntimeFile { get; init; } @@ -39,10 +38,13 @@ public class FlashOsCommand : BaseDeviceCommand private ILibUsbDevice? _libUsbDevice; - public FlashOsCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public FlashOsCommand(ISettingsManager settingsManager, + FileManager fileManager, + MeadowConnectionManager connectionManager, + ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); + Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); FileManager = fileManager; Settings = settingsManager; @@ -72,7 +74,7 @@ protected override async ValueTask ExecuteCommand() if (!Files.Contains(FirmwareType.OS) && UseDfu) { - Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + Logger.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); return; } @@ -196,7 +198,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() case 1: return devices[0]; default: - throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } } @@ -328,4 +330,4 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs index 97613771..a28bb0c3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,7 +9,6 @@ public class InstallDfuCommand : DfuInstallCommand public InstallDfuCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory, "0.11") { - Logger?.LogWarning($"Deprecated command. Use `dfu install` instead"); + Logger?.LogWarning($"Deprecated command - use `dfu install` instead"); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs index 696c0b44..d99402d2 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,12 +9,11 @@ public class ListPortsCommand : PortListCommand public ListPortsCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `port list` instead"); + Logger?.LogWarning($"Deprecated command - use `port list` instead"); } protected override ValueTask ExecuteCommand() { return base.ExecuteCommand(); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs index 0a0807fe..8d9e63bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs @@ -9,6 +9,6 @@ public class MonoDisableCommand : RuntimeDisableCommand public MonoDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime disable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime disable` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs index f5bdb9f0..d19bafe9 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -9,6 +9,6 @@ public class MonoEnableCommand : RuntimeEnableCommand public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime enable` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs index 02b08da3..4fd2a7d3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs @@ -9,6 +9,6 @@ public class MonoStateCommand : RuntimeStateCommand public MonoStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger.LogWarning($"Deprecated command. Use `runtime state` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime state` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs index 87e4d06e..9413c036 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -7,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("use port", Description = "** Deprecated ** Use `config route` instead")] public class UsePortCommand : BaseCommand { - private ISettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; [CommandParameter(0, Name = "Port", IsRequired = true)] public string Port { get; set; } = default!; @@ -15,7 +14,7 @@ public class UsePortCommand : BaseCommand public UsePortCommand(ILoggerFactory loggerFactory, ISettingsManager settingsManager) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `config route` instead"); + Logger?.LogWarning($"Deprecated command -use `config route` instead"); _settingsManager = settingsManager; } @@ -26,5 +25,4 @@ protected override ValueTask ExecuteCommand() return ValueTask.CompletedTask; } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 2f4e5f81..26663cbb 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -193,7 +193,7 @@ public void Detach() // search for the device via HCOM - we'll use a simple command since we don't have a "ping" var command = RequestBuilder.Build(); - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + // sequence numbers are only for file retrieval - Setting it to non-zero will cause it to hang _port.DiscardInBuffer(); From 51c98c24251a580a333b95fe93f696249008cf50 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 22:07:54 -0800 Subject: [PATCH 088/141] Improved validation and user feedback --- .../Commands/Current/App/AppBuildCommand.cs | 13 +-- .../Commands/Current/App/AppDebugCommand.cs | 24 ++--- .../Commands/Current/App/AppDeployCommand.cs | 3 +- .../Commands/Current/App/AppRunCommand.cs | 93 +++++++++---------- .../Commands/Current/App/AppTrimCommand.cs | 6 +- .../Current/Cloud/CloudLoginCommand.cs | 7 +- .../Current/Cloud/CloudLogoutCommand.cs | 7 +- .../Collection/CloudCollectionListCommand.cs | 7 +- .../JsonDocumentBindingConverter.cs | 6 +- .../Command/CloudCommandPublishCommand.cs | 4 +- .../Package/CloudPackageCreateCommand.cs | 8 +- .../Cloud/Package/CloudPackageListCommand.cs | 10 +- .../Package/CloudPackagePublishCommand.cs | 8 +- .../Package/CloudPackageUploadCommand.cs | 9 +- .../Commands/Current/Config/ConfigCommand.cs | 13 +-- .../Current/Device/DeviceClockCommand.cs | 10 +- .../Current/Device/DeviceInfoCommand.cs | 5 +- .../Current/Device/DeviceProvisionCommand.cs | 2 +- .../Current/Device/DeviceResetCommand.cs | 5 +- .../Commands/Current/Dfu/DfuInstallCommand.cs | 16 +--- .../Current/File/FileDeleteCommand.cs | 4 +- .../Firmware/FirmwareDefaultCommand.cs | 4 +- .../Current/Firmware/FirmwareDeleteCommand.cs | 4 +- .../Current/Firmware/FirmwareListCommand.cs | 7 +- .../Current/Firmware/FirmwareWriteCommand.cs | 3 +- .../Current/Flash/FlashEraseCommand.cs | 3 +- .../Commands/Current/Port/PortListCommand.cs | 4 +- .../Current/Port/PortSelectCommand.cs | 43 ++++----- .../Current/Runtime/RuntimeDisableCommand.cs | 11 ++- .../Current/Runtime/RuntimeEnableCommand.cs | 11 ++- .../Current/Runtime/RuntimeStateCommand.cs | 11 ++- .../Current/Trace/TraceDisableCommand.cs | 9 +- .../Current/Trace/TraceEnableCommand.cs | 9 +- .../Current/Trace/TraceLevelCommand.cs | 8 +- .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 6 +- 35 files changed, 174 insertions(+), 219 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 9831daf0..04c21517 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app build", Description = "Compiles a Meadow application")] +[Command("app build", Description = "Compile a Meadow application")] public class AppBuildCommand : BaseCommand { private readonly IPackageManager _packageManager; @@ -22,9 +22,7 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact protected override ValueTask ExecuteCommand() { - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; // is the path a file? if (!File.Exists(path)) @@ -37,10 +35,7 @@ protected override ValueTask ExecuteCommand() } } - if (Configuration == null) - { - Configuration = "Release"; - } + Configuration ??= "Release"; Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); @@ -53,7 +48,7 @@ protected override ValueTask ExecuteCommand() } else { - Logger?.LogError($"Build successful"); + Logger?.LogInformation($"Build successful"); } return ValueTask.CompletedTask; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index a20fcb83..4655a1c6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app debug", Description = "Debugs a running application")] +[Command("app debug", Description = "Debug a running application")] public class AppDebugCommand : BaseDeviceCommand { // VS 2019 - 4024 @@ -14,8 +14,7 @@ public class AppDebugCommand : BaseDeviceCommand public AppDebugCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { @@ -27,20 +26,17 @@ protected override async ValueTask ExecuteCommand() return; } - if (connection != null) + connection.DeviceMessageReceived += (s, e) => { - connection.DeviceMessageReceived += (s, e) => - { - Logger?.LogInformation(e.message); - }; + Logger?.LogInformation(e.message); + }; - using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) + using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) + { + if (Console != null) { - if (Console != null) - { - Logger?.LogInformation("Debugging server started. Press Enter to exit"); - await Console.Input.ReadLineAsync(); - } + Logger?.LogInformation("Debugging server started - press Enter to exit"); + await Console.Input.ReadLineAsync(); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index a8125b30..8ff2b81a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,11 +1,10 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app deploy", Description = "Deploys a built Meadow application to a target device")] +[Command("app deploy", Description = "Deploy a built Meadow application to a target device")] public class AppDeployCommand : BaseDeviceCommand { private readonly IPackageManager _packageManager; diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index f11cc484..04f997ca 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -35,60 +35,57 @@ protected override async ValueTask ExecuteCommand() return; } - if (connection != null) + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + if (!Directory.Exists(path)) { - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; - - if (!Directory.Exists(path)) - { - Logger?.LogError($"Target directory '{path}' not found."); - return; - } - - var lastFile = string.Empty; - - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); - - await connection.RuntimeDisable(CancellationToken); - } - - if (!await BuildApplication(path, CancellationToken)) - { - return; - } - - if (!await TrimApplication(path, CancellationToken)) - { - return; - } - - // illink returns before all files are written - attempt a delay of 1s - await Task.Delay(1000); + Logger?.LogError($"Target directory '{path}' not found."); + return; + } - if (!await DeployApplication(connection, path, CancellationToken)) - { - return; - } + var lastFile = string.Empty; - Logger?.LogInformation("Enabling the runtime..."); - await connection.RuntimeEnable(CancellationToken); + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling runtime..."); - Logger?.LogInformation("Listening for messages from Meadow...\n"); - connection.DeviceMessageReceived += OnDeviceMessageReceived; + await connection.RuntimeDisable(CancellationToken); + } - while (!CancellationToken.IsCancellationRequested) - { - await Task.Delay(1000); - } + if (!await BuildApplication(path, CancellationToken)) + { + return; + } - Logger?.LogInformation("Listen cancelled..."); + if (!await TrimApplication(path, CancellationToken)) + { + return; } + + // illink returns before all files are written - attempt a delay of 1s + await Task.Delay(1000); + + if (!await DeployApplication(connection, path, CancellationToken)) + { + return; + } + + Logger?.LogInformation("Enabling the runtime..."); + await connection.RuntimeEnable(CancellationToken); + + Logger?.LogInformation("Listening for messages from Meadow...\n"); + connection.DeviceMessageReceived += OnDeviceMessageReceived; + + while (!CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger?.LogInformation("Listen cancelled..."); } private Task BuildApplication(string path, CancellationToken cancellationToken) @@ -170,4 +167,4 @@ private void OnDeviceMessageReceived(object? sender, (string message, string? so Logger?.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 5d35d922..6fb547bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,6 +1,4 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -8,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] public class AppTrimCommand : BaseCommand { - private IPackageManager _packageManager; + private readonly IPackageManager _packageManager; [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] public string? Configuration { get; set; } @@ -61,4 +59,4 @@ protected override async ValueTask ExecuteCommand() await _packageManager.TrimApplication(file, false, null, CancellationToken); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs index 0d617e8d..f7238df3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs @@ -18,12 +18,11 @@ public CloudLoginCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; Logger?.LogInformation($"Logging into {Host}..."); @@ -37,4 +36,4 @@ protected override async ValueTask ExecuteCommand() : "There was a problem retrieving your account information."); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index 334cf5d6..85d99b48 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -15,13 +15,14 @@ public CloudLogoutCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { Logger?.LogInformation($"Logging out of Meadow.Cloud..."); IdentityManager.Logout(); + + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs index 25127cdd..90867fdb 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs @@ -20,12 +20,11 @@ public CloudCollectionListCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); if (org == null) return; @@ -45,4 +44,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs index aa8f4bf2..c9120958 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs @@ -6,15 +6,15 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class JsonDocumentBindingConverter : BindingConverter { - public override JsonDocument Convert(string rawValue) + public override JsonDocument Convert(string? rawValue) { try { - return JsonDocument.Parse(rawValue); + return JsonDocument.Parse(rawValue!); } catch (JsonException ex) { throw new CommandException($"Provided argument is not valid JSON: {ex.Message}", showHelp: false, innerException: ex); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index ba8c9694..9ca34e17 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -11,7 +11,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudCommandPublishCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string CommandName { get; set; } + public string CommandName { get; set; } = default!; [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] public string? CollectionId { get; set; } @@ -54,7 +54,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed.", showHelp: true); } - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var token = await IdentityManager.GetAccessToken(CancellationToken); if (string.IsNullOrWhiteSpace(token)) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index f9a01c73..718d8fa3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -41,10 +41,7 @@ public CloudPackageCreateCommand( protected override async ValueTask ExecuteCommand() { - if (ProjectPath == null) - { - ProjectPath = AppDomain.CurrentDomain.BaseDirectory; - } + ProjectPath ??= AppDomain.CurrentDomain.BaseDirectory; // build Logger?.LogInformation($"Building {Configuration} version of application..."); @@ -85,6 +82,5 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Package assembly failed."); } - } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 2a4d531d..29a0584b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package list", Description = "Lists all Meadow Packages (MPAK)")] public class CloudPackageListCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandOption("orgId", 'o', Description = "Optional organization ID", IsRequired = false)] public string? OrgId { get; set; } @@ -30,16 +30,16 @@ public CloudPackageListCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) return; + if (org == null) { return; } var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); if (packages == null || packages.Count == 0) { - Logger?.LogInformation("No packages found."); + Logger?.LogInformation("No packages found"); } else { @@ -50,4 +50,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index 9f0e440e..7aa75daf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package publish", Description = "Publishes a Meadow Package (MPAK)")] public class CloudPackagePublishCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandParameter(0, Name = "PackageID", Description = "ID of the package to publish", IsRequired = true)] public string PackageId { get; init; } @@ -36,18 +36,18 @@ public CloudPackagePublishCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; try { Logger?.LogInformation($"Publishing package {PackageId} to collection {CollectionId}..."); await _packageService.PublishPackage(PackageId, CollectionId, Metadata, Host, CancellationToken); - Logger?.LogInformation("Publish successful."); + Logger?.LogInformation("Publish successful"); } catch (MeadowCloudException mex) { Logger?.LogError($"Publish failed: {mex.Message}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 6d9db340..d91df4a6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -20,7 +20,7 @@ public class CloudPackageUploadCommand : BaseCloudCommand public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) : base(settingsManager, loggerFactory) - { - - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { if (List) { @@ -64,5 +59,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException($"Too many parameters provided"); } } + + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index dbf5fc88..9b217762 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -11,16 +11,15 @@ public class DeviceClockCommand : BaseDeviceCommand public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - + Logger?.LogInformation($"Device clock failed - device or connection not found"); return; } @@ -47,6 +46,5 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); } } - } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs index 2c4c6265..5b92bc89 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -16,8 +16,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Device info failed - device or connection not found"); return; } @@ -27,4 +28,4 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation(deviceInfo.ToString()); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 85d51793..9f155d95 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -107,4 +107,4 @@ protected override async ValueTask ExecuteCommand() return; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs index 833809a2..ed3c83db 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -16,11 +16,12 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Device reset failed - device or connection not found"); return; } await connection.Device.Reset(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs index 5c1b173e..f9708726 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -1,8 +1,5 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; -using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; @@ -26,15 +23,11 @@ protected DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory log public DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Version == null) - { - Version = DefaultVersion; - } + Version ??= DefaultVersion; switch (Version) { @@ -43,7 +36,7 @@ protected override async ValueTask ExecuteCommand() // valid break; default: - Logger?.LogError("Only versions 0.10 and 0.11 are supported."); + Logger?.LogError("Only DFU versions 0.10 and 0.11 are supported"); return; } @@ -64,8 +57,7 @@ protected override async ValueTask ExecuteCommand() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Logger?.LogWarning( - "To install DFU on Linux, use the package manager to install the dfu-util package"); + Logger?.LogWarning("To install DFU on Linux, use the package manager to install the dfu-util package"); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 18b81387..010038ef 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -20,7 +20,7 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File delete failed - device or connection not found"); + Logger?.LogError($"File delete failed - device or connection not found"); return; } @@ -73,4 +73,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 7fbfeac1..978ff994 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDefaultCommand : BaseFileCommand { public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to use as default", IsRequired = false)] public string? Version { get; set; } = null; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 30dbc2f1..f6625ab9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDeleteCommand : BaseFileCommand { public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] public string Version { get; set; } = default!; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index ce9d6c6b..ffb763c7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,7 +1,4 @@ -using CliFx; -using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; +using CliFx.Attributes; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -107,4 +104,4 @@ private async Task DisplayTerseResults(FileManager manager) } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index ae9b0fc1..96f5c52d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -344,5 +344,4 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index 8dcdd610..8a458a9c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -8,8 +8,7 @@ public class FlashEraseCommand : BaseDeviceCommand { public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs index 4b959b55..995d480a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs @@ -10,12 +10,12 @@ public class PortListCommand : BaseCommand public PortListCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { Portlist = await MeadowConnectionManager.GetSerialPorts(); + if (Portlist.Count > 0) { var plural = Portlist.Count > 1 ? "s" : string.Empty; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index a5b43034..6e2e26af 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,40 +8,36 @@ public class PortSelectCommand : BaseCommand { public PortSelectCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (LoggerFactory != null) + if (LoggerFactory != null && Console != null) { - if (Console != null) - { - var portListCommand = new PortListCommand(LoggerFactory); + var portListCommand = new PortListCommand(LoggerFactory); - await portListCommand.ExecuteAsync(Console); + await portListCommand.ExecuteAsync(Console); - if (portListCommand.Portlist?.Count > 0) + if (portListCommand.Portlist?.Count > 0) + { + if (portListCommand.Portlist?.Count > 1) { - if (portListCommand.Portlist?.Count > 1) - { - Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); + Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); - byte deviceSelected; - if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + byte deviceSelected; + if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + { + if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) { - if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) - { - await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); - } + await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); } } - else - { - // Only 1 device attached, let's auto select it - if (portListCommand.Portlist != null) - await CallConfigCommand(portListCommand.Portlist[0]); - } + } + else + { + // Only 1 device attached, let's auto select it + if (portListCommand.Portlist != null) + await CallConfigCommand(portListCommand.Portlist[0]); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs index e1fc867e..908c2d01 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -8,23 +8,24 @@ public class RuntimeDisableCommand : BaseDeviceCommand { public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Disabling runtime..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } + Logger?.LogInformation($"Disabling runtime..."); + await connection.Device.RuntimeDisable(CancellationToken); var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs index 8061a64b..066dba13 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -8,23 +8,24 @@ public class RuntimeEnableCommand : BaseDeviceCommand { public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Enabling runtime..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } + Logger?.LogInformation($"Enabling runtime..."); + await connection.Device.RuntimeEnable(CancellationToken); var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs index 19569c1c..0c772ee4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -8,21 +8,22 @@ public class RuntimeStateCommand : BaseDeviceCommand { public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Querying runtime state..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime state failed - device or connection not found"); return; } + Logger?.LogInformation($"Querying runtime state..."); + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs index c3aa27f5..94b7a602 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -8,15 +8,15 @@ public class TraceDisableCommand : BaseDeviceCommand { public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace disable failed - device or connection not found"); return; } @@ -29,5 +29,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceDisable(CancellationToken); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs index faafb992..31e393be 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -11,15 +11,15 @@ public class TraceEnableCommand : BaseDeviceCommand public TraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace enable failed - device or connection not found"); return; } @@ -38,5 +38,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceEnable(CancellationToken); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index 73309878..1b66ff11 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -11,15 +11,15 @@ public class TraceLevelCommand : BaseDeviceCommand public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace level failed - device or connection not found"); return; } @@ -43,4 +43,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceEnable(CancellationToken); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index 03c70cd9..5bf5aea1 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -5,8 +5,8 @@ namespace Meadow.Hcom; public class FirmwareUpdater { - private ILogger? _logger; - private Task? _updateTask; + private readonly ILogger? _logger; + private readonly Task? _updateTask; private IMeadowConnection _connection; private UpdateState _state; @@ -130,7 +130,7 @@ private async void StateMachine() } break; case UpdateState.DFUCompleted: - // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after + // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after try { // wait for device to reconnect From 27651cc367ede72d55331a19dc82fe3642a46803 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:17 -0800 Subject: [PATCH 089/141] Code cleanup --- .../Commands/Current/Firmware/FirmwareDownloadCommand.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index 4c931efb..66ae615e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDownloadCommand : BaseFileCommand { public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandOption("force", 'f', IsRequired = false)] public bool Force { get; set; } @@ -59,7 +57,6 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Downloading firmware package '{Version}'..."); - try { collection.DownloadProgress += OnDownloadProgress; @@ -74,7 +71,7 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); - if (explicitVersion) + if (explicitVersion == false) { await collection.SetDefaultPackage(Version); } @@ -86,8 +83,6 @@ protected override async ValueTask ExecuteCommand() } } - private long _lastProgress = 0; - private void OnDownloadProgress(object? sender, long e) { // use Console so we can Write instead of Logger which only supports WriteLine From 5a9e07400c55e135b74d96e5589b188dfe1a6b54 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:44 -0800 Subject: [PATCH 090/141] Code cleanup and better user feedback --- .../Commands/Current/File/FileListCommand.cs | 104 +++++++++--------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 77b5ddc2..bd00759d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -19,7 +19,7 @@ public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory { if (Folder != null) { - if (!Folder.EndsWith('/')) + if (Folder.EndsWith('/') == false) { Folder += "/"; } @@ -36,73 +36,71 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"File list failed - device or connection not found"); return; } - if (connection != null) - { - var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); + var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); - if (files == null || files.Length == 0) - { - Logger?.LogInformation($"No files found"); - } - else + if (files == null || files.Length == 0) + { + Logger?.LogInformation($"No files found"); + } + else + { + if (Verbose) { - if (Verbose) + var longestFileName = files.Select(x => x.Name.Length) + .OrderByDescending(x => x) + .FirstOrDefault(); + + var totalBytesUsed = 0L; + var totalBlocksUsed = 0L; + + foreach (var file in files) { - var longestFileName = files.Select(x => x.Name.Length) - .OrderByDescending(x => x) - .FirstOrDefault(); + totalBytesUsed += file.Size ?? 0; + totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; - var totalBytesUsed = 0L; - var totalBlocksUsed = 0L; + var line = $"{file.Name.PadRight(longestFileName)}"; + line = $"{line}\t{file.Crc:x8}"; - foreach (var file in files) + if (file.Size > 1000000) { - totalBytesUsed += file.Size ?? 0; - totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; - - var line = $"{file.Name.PadRight(longestFileName)}"; - line = $"{line}\t{file.Crc:x8}"; - - if (file.Size > 1000000) - { - line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; - } - else if (file.Size > 1000) - { - line = $"{line}\t{file.Size / 1000,7:0} kB "; - } - else - { - line = $"{line}\t{file.Size,7} bytes"; - } - - Logger?.LogInformation(line); + line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; } - - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files\n" + - $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + - $"\tSpanning {totalBlocksUsed} blocks\n" + - $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); - } - else - { - foreach (var file in files) + else if (file.Size > 1000) + { + line = $"{line}\t{file.Size / 1000,7:0} kB "; + } + else { - Logger?.LogInformation(file.Name); + line = $"{line}\t{file.Size,7} bytes"; } - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files"); + Logger?.LogInformation(line); } + + Logger?.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files\n" + + $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + + $"\tSpanning {totalBlocksUsed} blocks\n" + + $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); + } + else + { + foreach (var file in files) + { + Logger?.LogInformation(file.Name); + } + + Logger?.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files"); } } } -} +} \ No newline at end of file From 2216dfc55d9f9d0b5b969377173d6c7225111b49 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:04:54 -0800 Subject: [PATCH 091/141] Validate folder before delete --- .../Current/File/FileDeleteCommand.cs | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 30b60f09..a072cf05 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -20,47 +20,56 @@ protected override async ValueTask ExecuteCommand() if (connection == null) { + Logger?.LogInformation($"File delete failed, no connection to a device"); return; } - if (connection != null) + // get a list of files in the target folder + var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); + if (string.IsNullOrWhiteSpace(folder)) { - // get a list of files in the target folder - var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); - var fileList = await connection.GetFileList($"{folder}/", false); + folder = "/meadow0"; + } + + var fileList = await connection.GetFileList($"{folder}/", false); + + if (fileList == null || fileList.Length == 0) + { + Logger?.LogInformation($"File delete failed, no files found"); + return; + } - if (MeadowFile == "all") + if (MeadowFile == "all") + { + foreach (var f in fileList) { - foreach (var f in fileList) - { - var p = Path.GetFileName(f.Name); - Logger?.LogInformation($"Deleting file '{p}' from device..."); - await connection.Device.DeleteFile(p, CancellationToken); - } + var p = Path.GetFileName(f.Name); + Logger?.LogInformation($"Deleting file '{p}' from device..."); + await connection.Device.DeleteFile(p, CancellationToken); + } + } + else + { + var requested = Path.GetFileName(MeadowFile); + + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; + + if (!exists) + { + Logger?.LogError($"File '{MeadowFile}' not found on device."); } else { - var requested = Path.GetFileName(MeadowFile); + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; - - if (!exists) + if (wasRuntimeEnabled) { - Logger?.LogError($"File '{MeadowFile}' not found on device."); + Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); + return; } - else - { - var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); - if (wasRuntimeEnabled) - { - Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); - return; - } - - Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); - await connection.Device.DeleteFile(MeadowFile, CancellationToken); - } + Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await connection.Device.DeleteFile(MeadowFile, CancellationToken); } } } From e989acb05e31ce947c0269f904a829f48e1d1c16 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:05:09 -0800 Subject: [PATCH 092/141] Minor cleanup --- .../v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs | 7 ++----- Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs | 4 ++-- .../Commands/Current/Cloud/CloudLogoutCommand.cs | 2 +- .../Commands/Current/Cloud/Collection/QualityOfService.cs | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 711f07ab..a29a90ce 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -1,7 +1,4 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; -using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app build", Description = "Compiles a Meadow application")] public class AppBuildCommand : BaseCommand { - private IPackageManager _packageManager; + private readonly IPackageManager _packageManager; [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] public string? Configuration { get; set; } @@ -53,7 +50,7 @@ protected override async ValueTask ExecuteCommand() } else { - Logger?.LogError($"Build success."); + Logger?.LogError($"Build successful"); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index be02b31a..be3063e7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -36,11 +36,11 @@ public async ValueTask ExecuteAsync(IConsole console) if (CancellationToken.IsCancellationRequested) { - Logger?.LogInformation($"Cancelled."); + Logger?.LogInformation($"Cancelled"); } else { - Logger?.LogInformation($"Done."); + Logger?.LogInformation($"Done"); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index 733a4f10..334cf5d6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -24,4 +24,4 @@ protected override async ValueTask ExecuteCommand() IdentityManager.Logout(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs index 8aa8e094..7e824fd5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs @@ -5,4 +5,4 @@ public enum QualityOfService AtLeastOnce = 0, AtMostOnce = 1, ExactlyOnce = 2 -} +} \ No newline at end of file From 35164cea5668bda96363c09cd9e52cca65c0e87b Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:20:42 -0800 Subject: [PATCH 093/141] Improved null checking --- .../Commands/Current/File/FileDeleteCommand.cs | 6 +++--- .../Commands/Current/File/FileInitialCommand.cs | 16 +++++++++++----- .../Commands/Current/File/FileListCommand.cs | 8 +++----- .../Commands/Current/File/FileReadCommand.cs | 8 ++++---- .../Commands/Current/File/FileWriteCommand.cs | 6 +++--- .../Commands/Current/Flash/FlashEraseCommand.cs | 7 ++++--- 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index a072cf05..18b81387 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -18,9 +18,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File delete failed, no connection to a device"); + Logger?.LogInformation($"File delete failed - device or connection not found"); return; } @@ -35,7 +35,7 @@ protected override async ValueTask ExecuteCommand() if (fileList == null || fileList.Length == 0) { - Logger?.LogInformation($"File delete failed, no files found"); + Logger?.LogError($"File delete failed, no files found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index 813a0e94..c97568f8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -11,15 +11,15 @@ public class FileInitialCommand : BaseDeviceCommand public FileInitialCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File initial failed - device or connection not found"); return; } @@ -27,6 +27,12 @@ protected override async ValueTask ExecuteCommand() var data = await connection.Device.ReadFileString(MeadowFile, CancellationToken); - Logger.LogInformation(data); + if (data == null) + { + Logger?.LogError($"Failed to retrieve file"); + return; + } + + Logger?.LogInformation(data); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index bd00759d..5a792e14 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -38,7 +38,7 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File list failed - device or connection not found"); + Logger?.LogError($"File list failed - device or connection not found"); return; } @@ -85,7 +85,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation( $"\nSummary:\n" + - $"\t{files.Length} files\n" + + $"\t{files.Length} file(s)\n" + $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + $"\tSpanning {totalBlocksUsed} blocks\n" + $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); @@ -97,9 +97,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation(file.Name); } - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files"); + Logger?.LogInformation($"\t{files.Length} file(s)"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index f17195be..4e2259c8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -14,15 +14,15 @@ public class FileReadCommand : BaseDeviceCommand public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File read failed - device or connection not found"); return; } @@ -39,4 +39,4 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Failed to retrieve file"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index ad74832d..ff7200c8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -21,15 +21,15 @@ public class FileWriteCommand : BaseDeviceCommand public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"File write failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index 3f3bbc7e..8dcdd610 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("flash erase", Description = "Erases the device's flash storage")] +[Command("flash erase", Description = "Erase the contents of the device flash storage")] public class FlashEraseCommand : BaseDeviceCommand { public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) @@ -15,8 +15,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Flash erase failed - device or connection not found"); return; } @@ -29,4 +30,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.EraseFlash(CancellationToken); } -} +} \ No newline at end of file From 7e9c79e20cba56bc783da8ead7531138f2cefb81 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:22:13 -0800 Subject: [PATCH 094/141] Move logging and folder validation out of ctor --- .../Commands/Current/File/FileListCommand.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 5a792e14..e0dc3de3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -16,7 +16,18 @@ public class FileListCommand : BaseDeviceCommand public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) + { } + + protected override async ValueTask ExecuteCommand() { + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + Logger?.LogError($"File list failed - device or connection not found"); + return; + } + if (Folder != null) { if (Folder.EndsWith('/') == false) @@ -30,17 +41,6 @@ public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory { Logger?.LogInformation($"Getting file list..."); } - } - - protected override async ValueTask ExecuteCommand() - { - var connection = await GetCurrentConnection(); - - if (connection == null || connection.Device == null) - { - Logger?.LogError($"File list failed - device or connection not found"); - return; - } var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); From 5356dbf83a1ae3e47173a3e37f818f2520d70e82 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 21:37:26 -0800 Subject: [PATCH 095/141] Formatting and warnings --- .../PackageManager/PackageManager.cs | 2 +- .../Commands/Current/App/AppBuildCommand.cs | 10 +++++-- .../Commands/Current/App/AppRunCommand.cs | 3 +- .../Commands/Current/BaseCloudCommand.cs | 2 +- .../Commands/Current/BaseCommand.cs | 1 - .../Commands/Current/BaseFileCommand.cs | 5 ++-- .../Commands/Current/DeveloperCommand.cs | 30 ++++++++----------- .../Current/Device/DeviceProvisionCommand.cs | 6 ++-- .../Current/Firmware/FirmwareWriteCommand.cs | 7 ++--- .../Commands/Current/ListenCommand.cs | 6 ++-- .../Commands/Legacy/DownloadOsCommand.cs | 3 +- .../Commands/Legacy/FlashOsCommand.cs | 16 +++++----- .../Commands/Legacy/InstallDfuCommand.cs | 6 ++-- .../Commands/Legacy/ListPortsCommand.cs | 6 ++-- .../Commands/Legacy/MonoDisableCommand.cs | 4 +-- .../Commands/Legacy/MonoEnableCommand.cs | 2 +- .../Commands/Legacy/MonoStateCommand.cs | 4 +-- .../Commands/Legacy/UsePortCommand.cs | 8 ++--- .../Connections/SerialConnection.cs | 2 +- 19 files changed, 57 insertions(+), 66 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs index c4881a52..4ff24eee 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs @@ -230,7 +230,7 @@ public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, stri // look for a 'bin' folder var path = Path.Combine(rootFolder, "bin"); - if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); + if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); var files = new List(); FindApp(path, files); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index a29a90ce..9831daf0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -20,7 +20,7 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact _packageManager = packageManager; } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { string path = Path == null ? AppDomain.CurrentDomain.BaseDirectory @@ -33,11 +33,14 @@ protected override async ValueTask ExecuteCommand() if (!Directory.Exists(path)) { Logger?.LogError($"Invalid application path '{path}'"); - return; + return ValueTask.CompletedTask; } } - if (Configuration == null) Configuration = "Release"; + if (Configuration == null) + { + Configuration = "Release"; + } Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); @@ -52,5 +55,6 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Build successful"); } + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index d5a3ba85..f11cc484 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Hcom; using Microsoft.Extensions.Logging; @@ -69,7 +68,7 @@ protected override async ValueTask ExecuteCommand() return; } - // illink returns before all files are actually written. That's not fun, but we must just wait a little while. + // illink returns before all files are written - attempt a delay of 1s await Task.Delay(1000); if (!await DeployApplication(connection, path, CancellationToken)) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index daccc131..618585b0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -65,4 +65,4 @@ public BaseCloudCommand( return org; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index be3063e7..aa083008 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -43,5 +43,4 @@ public async ValueTask ExecuteAsync(IConsole console) Logger?.LogInformation($"Done"); } } - } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs index 09e0d728..6edc65ef 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -1,5 +1,4 @@ -using Meadow.CLI; -using Meadow.Software; +using Meadow.Software; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -13,4 +12,4 @@ public BaseFileCommand(FileManager fileManager, ISettingsManager settingsManager { FileManager = fileManager; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index af1263f8..5fcd5185 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -14,33 +14,29 @@ public class DeveloperCommand : BaseDeviceCommand public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Developer parameter set failed - device or connection not found"); return; } Logger?.LogInformation($"Setting developer parameter {Parameter} to {Value}"); - if (connection != null) + connection.DeviceMessageReceived += (s, e) => { - connection.DeviceMessageReceived += (s, e) => - { - Logger?.LogInformation(e.message); - }; - connection.ConnectionError += (s, e) => - { - Logger?.LogError(e.Message); - }; - - await connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); - } - } -} + Logger?.LogInformation(e.message); + }; + connection.ConnectionError += (s, e) => + { + Logger?.LogError(e.Message); + }; + await connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 23d16c6f..85d51793 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("device provision", Description = "Registers and prepares connected device for use with Meadow Cloud")] public class DeviceProvisionCommand : BaseDeviceCommand { - private DeviceService _deviceService; + private readonly DeviceService _deviceService; public const string DefaultHost = "https://www.meadowcloud.co"; @@ -75,9 +75,9 @@ protected override async ValueTask ExecuteCommand() var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - Logger?.LogError($"No connection path is defined"); + Logger?.LogError($"Device provision failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 942f1577..ae9b0fc1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; @@ -58,7 +57,7 @@ protected override async ValueTask ExecuteCommand() if (!Files.Contains(FirmwareType.OS) && UseDfu) { - Logger?.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); return; } @@ -199,7 +198,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() _libUsbDevice = devices[0]; break; default: - throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } } @@ -298,7 +297,7 @@ private async ValueTask WriteFiles(IMeadowConnection connection) write_runtime: if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) { - Logger?.LogInformation($"Error writing runtime. Retrying."); + Logger?.LogInformation($"Error writing runtime - retrying"); goto write_runtime; } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index 5834937e..e2f703c6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -11,11 +11,11 @@ public class ListenCommand : BaseDeviceCommand public ListenCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } private void Connection_ConnectionMessage(object? sender, string e) { + //ToDo } private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) @@ -49,4 +49,4 @@ protected override async ValueTask ExecuteCommand() await Task.Delay(1000); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs index dd27dbc4..a3ea05cd 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -11,6 +10,6 @@ public class DownloadOsCommand : FirmwareDownloadCommand public DownloadOsCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware download` instead"); + Logger?.LogWarning($"Deprecated command - use `firmware download` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index b81f43aa..3fd95004 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.LibUsb; using Meadow.Software; @@ -11,7 +10,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FlashOsCommand : BaseDeviceCommand { [CommandOption("osFile", 'o', Description = "Path to the Meadow OS binary")] - public string OSFile { get; init; } + public string OSFile { get; init; } = default!; [CommandOption("runtimeFile", 'r', Description = "Path to the Meadow Runtime binary")] public string RuntimeFile { get; init; } @@ -39,10 +38,13 @@ public class FlashOsCommand : BaseDeviceCommand private ILibUsbDevice? _libUsbDevice; - public FlashOsCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public FlashOsCommand(ISettingsManager settingsManager, + FileManager fileManager, + MeadowConnectionManager connectionManager, + ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); + Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); FileManager = fileManager; Settings = settingsManager; @@ -72,7 +74,7 @@ protected override async ValueTask ExecuteCommand() if (!Files.Contains(FirmwareType.OS) && UseDfu) { - Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + Logger.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); return; } @@ -196,7 +198,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() case 1: return devices[0]; default: - throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } } @@ -328,4 +330,4 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs index 97613771..a28bb0c3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,7 +9,6 @@ public class InstallDfuCommand : DfuInstallCommand public InstallDfuCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory, "0.11") { - Logger?.LogWarning($"Deprecated command. Use `dfu install` instead"); + Logger?.LogWarning($"Deprecated command - use `dfu install` instead"); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs index 696c0b44..d99402d2 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,12 +9,11 @@ public class ListPortsCommand : PortListCommand public ListPortsCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `port list` instead"); + Logger?.LogWarning($"Deprecated command - use `port list` instead"); } protected override ValueTask ExecuteCommand() { return base.ExecuteCommand(); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs index 0a0807fe..8d9e63bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs @@ -9,6 +9,6 @@ public class MonoDisableCommand : RuntimeDisableCommand public MonoDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime disable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime disable` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs index f5bdb9f0..d19bafe9 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -9,6 +9,6 @@ public class MonoEnableCommand : RuntimeEnableCommand public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime enable` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs index 02b08da3..4fd2a7d3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs @@ -9,6 +9,6 @@ public class MonoStateCommand : RuntimeStateCommand public MonoStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger.LogWarning($"Deprecated command. Use `runtime state` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime state` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs index 87e4d06e..9413c036 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -7,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("use port", Description = "** Deprecated ** Use `config route` instead")] public class UsePortCommand : BaseCommand { - private ISettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; [CommandParameter(0, Name = "Port", IsRequired = true)] public string Port { get; set; } = default!; @@ -15,7 +14,7 @@ public class UsePortCommand : BaseCommand public UsePortCommand(ILoggerFactory loggerFactory, ISettingsManager settingsManager) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `config route` instead"); + Logger?.LogWarning($"Deprecated command -use `config route` instead"); _settingsManager = settingsManager; } @@ -26,5 +25,4 @@ protected override ValueTask ExecuteCommand() return ValueTask.CompletedTask; } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 2f4e5f81..26663cbb 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -193,7 +193,7 @@ public void Detach() // search for the device via HCOM - we'll use a simple command since we don't have a "ping" var command = RequestBuilder.Build(); - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + // sequence numbers are only for file retrieval - Setting it to non-zero will cause it to hang _port.DiscardInBuffer(); From b3204d3ac29d9e7db32da7bb6451675b46a6f237 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 14 Jan 2024 22:07:54 -0800 Subject: [PATCH 096/141] Improved validation and user feedback --- .../Commands/Current/App/AppBuildCommand.cs | 13 +-- .../Commands/Current/App/AppDebugCommand.cs | 24 ++--- .../Commands/Current/App/AppDeployCommand.cs | 3 +- .../Commands/Current/App/AppRunCommand.cs | 93 +++++++++---------- .../Commands/Current/App/AppTrimCommand.cs | 6 +- .../Current/Cloud/CloudLoginCommand.cs | 7 +- .../Current/Cloud/CloudLogoutCommand.cs | 7 +- .../Collection/CloudCollectionListCommand.cs | 7 +- .../JsonDocumentBindingConverter.cs | 6 +- .../Command/CloudCommandPublishCommand.cs | 4 +- .../Package/CloudPackageCreateCommand.cs | 8 +- .../Cloud/Package/CloudPackageListCommand.cs | 10 +- .../Package/CloudPackagePublishCommand.cs | 8 +- .../Package/CloudPackageUploadCommand.cs | 9 +- .../Commands/Current/Config/ConfigCommand.cs | 13 +-- .../Current/Device/DeviceClockCommand.cs | 10 +- .../Current/Device/DeviceInfoCommand.cs | 5 +- .../Current/Device/DeviceProvisionCommand.cs | 2 +- .../Current/Device/DeviceResetCommand.cs | 5 +- .../Commands/Current/Dfu/DfuInstallCommand.cs | 16 +--- .../Current/File/FileDeleteCommand.cs | 4 +- .../Firmware/FirmwareDefaultCommand.cs | 4 +- .../Current/Firmware/FirmwareDeleteCommand.cs | 4 +- .../Current/Firmware/FirmwareListCommand.cs | 7 +- .../Current/Firmware/FirmwareWriteCommand.cs | 3 +- .../Current/Flash/FlashEraseCommand.cs | 3 +- .../Commands/Current/Port/PortListCommand.cs | 4 +- .../Current/Port/PortSelectCommand.cs | 43 ++++----- .../Current/Runtime/RuntimeDisableCommand.cs | 11 ++- .../Current/Runtime/RuntimeEnableCommand.cs | 11 ++- .../Current/Runtime/RuntimeStateCommand.cs | 11 ++- .../Current/Trace/TraceDisableCommand.cs | 9 +- .../Current/Trace/TraceEnableCommand.cs | 9 +- .../Current/Trace/TraceLevelCommand.cs | 8 +- .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 6 +- 35 files changed, 174 insertions(+), 219 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 9831daf0..04c21517 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app build", Description = "Compiles a Meadow application")] +[Command("app build", Description = "Compile a Meadow application")] public class AppBuildCommand : BaseCommand { private readonly IPackageManager _packageManager; @@ -22,9 +22,7 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact protected override ValueTask ExecuteCommand() { - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; // is the path a file? if (!File.Exists(path)) @@ -37,10 +35,7 @@ protected override ValueTask ExecuteCommand() } } - if (Configuration == null) - { - Configuration = "Release"; - } + Configuration ??= "Release"; Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); @@ -53,7 +48,7 @@ protected override ValueTask ExecuteCommand() } else { - Logger?.LogError($"Build successful"); + Logger?.LogInformation($"Build successful"); } return ValueTask.CompletedTask; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index a20fcb83..4655a1c6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -3,7 +3,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app debug", Description = "Debugs a running application")] +[Command("app debug", Description = "Debug a running application")] public class AppDebugCommand : BaseDeviceCommand { // VS 2019 - 4024 @@ -14,8 +14,7 @@ public class AppDebugCommand : BaseDeviceCommand public AppDebugCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { @@ -27,20 +26,17 @@ protected override async ValueTask ExecuteCommand() return; } - if (connection != null) + connection.DeviceMessageReceived += (s, e) => { - connection.DeviceMessageReceived += (s, e) => - { - Logger?.LogInformation(e.message); - }; + Logger?.LogInformation(e.message); + }; - using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) + using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) + { + if (Console != null) { - if (Console != null) - { - Logger?.LogInformation("Debugging server started. Press Enter to exit"); - await Console.Input.ReadLineAsync(); - } + Logger?.LogInformation("Debugging server started - press Enter to exit"); + await Console.Input.ReadLineAsync(); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index a8125b30..8ff2b81a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,11 +1,10 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app deploy", Description = "Deploys a built Meadow application to a target device")] +[Command("app deploy", Description = "Deploy a built Meadow application to a target device")] public class AppDeployCommand : BaseDeviceCommand { private readonly IPackageManager _packageManager; diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index f11cc484..04f997ca 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -35,60 +35,57 @@ protected override async ValueTask ExecuteCommand() return; } - if (connection != null) + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + if (!Directory.Exists(path)) { - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; - - if (!Directory.Exists(path)) - { - Logger?.LogError($"Target directory '{path}' not found."); - return; - } - - var lastFile = string.Empty; - - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); - - await connection.RuntimeDisable(CancellationToken); - } - - if (!await BuildApplication(path, CancellationToken)) - { - return; - } - - if (!await TrimApplication(path, CancellationToken)) - { - return; - } - - // illink returns before all files are written - attempt a delay of 1s - await Task.Delay(1000); + Logger?.LogError($"Target directory '{path}' not found."); + return; + } - if (!await DeployApplication(connection, path, CancellationToken)) - { - return; - } + var lastFile = string.Empty; - Logger?.LogInformation("Enabling the runtime..."); - await connection.RuntimeEnable(CancellationToken); + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling runtime..."); - Logger?.LogInformation("Listening for messages from Meadow...\n"); - connection.DeviceMessageReceived += OnDeviceMessageReceived; + await connection.RuntimeDisable(CancellationToken); + } - while (!CancellationToken.IsCancellationRequested) - { - await Task.Delay(1000); - } + if (!await BuildApplication(path, CancellationToken)) + { + return; + } - Logger?.LogInformation("Listen cancelled..."); + if (!await TrimApplication(path, CancellationToken)) + { + return; } + + // illink returns before all files are written - attempt a delay of 1s + await Task.Delay(1000); + + if (!await DeployApplication(connection, path, CancellationToken)) + { + return; + } + + Logger?.LogInformation("Enabling the runtime..."); + await connection.RuntimeEnable(CancellationToken); + + Logger?.LogInformation("Listening for messages from Meadow...\n"); + connection.DeviceMessageReceived += OnDeviceMessageReceived; + + while (!CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger?.LogInformation("Listen cancelled..."); } private Task BuildApplication(string path, CancellationToken cancellationToken) @@ -170,4 +167,4 @@ private void OnDeviceMessageReceived(object? sender, (string message, string? so Logger?.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 5d35d922..6fb547bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,6 +1,4 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -8,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] public class AppTrimCommand : BaseCommand { - private IPackageManager _packageManager; + private readonly IPackageManager _packageManager; [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] public string? Configuration { get; set; } @@ -61,4 +59,4 @@ protected override async ValueTask ExecuteCommand() await _packageManager.TrimApplication(file, false, null, CancellationToken); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs index 0d617e8d..f7238df3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs @@ -18,12 +18,11 @@ public CloudLoginCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; Logger?.LogInformation($"Logging into {Host}..."); @@ -37,4 +36,4 @@ protected override async ValueTask ExecuteCommand() : "There was a problem retrieving your account information."); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index 334cf5d6..85d99b48 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -15,13 +15,14 @@ public CloudLogoutCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { Logger?.LogInformation($"Logging out of Meadow.Cloud..."); IdentityManager.Logout(); + + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs index 25127cdd..90867fdb 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs @@ -20,12 +20,11 @@ public CloudCollectionListCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); if (org == null) return; @@ -45,4 +44,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs index aa8f4bf2..c9120958 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/JsonDocumentBindingConverter.cs @@ -6,15 +6,15 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class JsonDocumentBindingConverter : BindingConverter { - public override JsonDocument Convert(string rawValue) + public override JsonDocument Convert(string? rawValue) { try { - return JsonDocument.Parse(rawValue); + return JsonDocument.Parse(rawValue!); } catch (JsonException ex) { throw new CommandException($"Provided argument is not valid JSON: {ex.Message}", showHelp: false, innerException: ex); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index ba8c9694..9ca34e17 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -11,7 +11,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudCommandPublishCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string CommandName { get; set; } + public string CommandName { get; set; } = default!; [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] public string? CollectionId { get; set; } @@ -54,7 +54,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed.", showHelp: true); } - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var token = await IdentityManager.GetAccessToken(CancellationToken); if (string.IsNullOrWhiteSpace(token)) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index f9a01c73..718d8fa3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -41,10 +41,7 @@ public CloudPackageCreateCommand( protected override async ValueTask ExecuteCommand() { - if (ProjectPath == null) - { - ProjectPath = AppDomain.CurrentDomain.BaseDirectory; - } + ProjectPath ??= AppDomain.CurrentDomain.BaseDirectory; // build Logger?.LogInformation($"Building {Configuration} version of application..."); @@ -85,6 +82,5 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Package assembly failed."); } - } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 2a4d531d..29a0584b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package list", Description = "Lists all Meadow Packages (MPAK)")] public class CloudPackageListCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandOption("orgId", 'o', Description = "Optional organization ID", IsRequired = false)] public string? OrgId { get; set; } @@ -30,16 +30,16 @@ public CloudPackageListCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) return; + if (org == null) { return; } var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); if (packages == null || packages.Count == 0) { - Logger?.LogInformation("No packages found."); + Logger?.LogInformation("No packages found"); } else { @@ -50,4 +50,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index 9f0e440e..7aa75daf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package publish", Description = "Publishes a Meadow Package (MPAK)")] public class CloudPackagePublishCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandParameter(0, Name = "PackageID", Description = "ID of the package to publish", IsRequired = true)] public string PackageId { get; init; } @@ -36,18 +36,18 @@ public CloudPackagePublishCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; try { Logger?.LogInformation($"Publishing package {PackageId} to collection {CollectionId}..."); await _packageService.PublishPackage(PackageId, CollectionId, Metadata, Host, CancellationToken); - Logger?.LogInformation("Publish successful."); + Logger?.LogInformation("Publish successful"); } catch (MeadowCloudException mex) { Logger?.LogError($"Publish failed: {mex.Message}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 6d9db340..d91df4a6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -20,7 +20,7 @@ public class CloudPackageUploadCommand : BaseCloudCommand public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) : base(settingsManager, loggerFactory) - { - - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { if (List) { @@ -64,5 +59,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException($"Too many parameters provided"); } } + + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index dbf5fc88..9b217762 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -11,16 +11,15 @@ public class DeviceClockCommand : BaseDeviceCommand public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - + Logger?.LogInformation($"Device clock failed - device or connection not found"); return; } @@ -47,6 +46,5 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); } } - } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs index 2c4c6265..5b92bc89 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -16,8 +16,9 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Device info failed - device or connection not found"); return; } @@ -27,4 +28,4 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation(deviceInfo.ToString()); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 85d51793..9f155d95 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -107,4 +107,4 @@ protected override async ValueTask ExecuteCommand() return; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs index 833809a2..ed3c83db 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -16,11 +16,12 @@ protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogInformation($"Device reset failed - device or connection not found"); return; } await connection.Device.Reset(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs index 5c1b173e..f9708726 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -1,8 +1,5 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; -using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; @@ -26,15 +23,11 @@ protected DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory log public DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Version == null) - { - Version = DefaultVersion; - } + Version ??= DefaultVersion; switch (Version) { @@ -43,7 +36,7 @@ protected override async ValueTask ExecuteCommand() // valid break; default: - Logger?.LogError("Only versions 0.10 and 0.11 are supported."); + Logger?.LogError("Only DFU versions 0.10 and 0.11 are supported"); return; } @@ -64,8 +57,7 @@ protected override async ValueTask ExecuteCommand() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Logger?.LogWarning( - "To install DFU on Linux, use the package manager to install the dfu-util package"); + Logger?.LogWarning("To install DFU on Linux, use the package manager to install the dfu-util package"); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 18b81387..010038ef 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -20,7 +20,7 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"File delete failed - device or connection not found"); + Logger?.LogError($"File delete failed - device or connection not found"); return; } @@ -73,4 +73,4 @@ protected override async ValueTask ExecuteCommand() } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 7fbfeac1..978ff994 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDefaultCommand : BaseFileCommand { public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to use as default", IsRequired = false)] public string? Version { get; set; } = null; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 30dbc2f1..f6625ab9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,8 +9,7 @@ public class FirmwareDeleteCommand : BaseFileCommand { public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] public string Version { get; set; } = default!; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index ce9d6c6b..ffb763c7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,7 +1,4 @@ -using CliFx; -using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; +using CliFx.Attributes; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -107,4 +104,4 @@ private async Task DisplayTerseResults(FileManager manager) } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index ae9b0fc1..96f5c52d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -344,5 +344,4 @@ await DfuUtils.FlashFile( logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index 8dcdd610..8a458a9c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -8,8 +8,7 @@ public class FlashEraseCommand : BaseDeviceCommand { public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs index 4b959b55..995d480a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs @@ -10,12 +10,12 @@ public class PortListCommand : BaseCommand public PortListCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { Portlist = await MeadowConnectionManager.GetSerialPorts(); + if (Portlist.Count > 0) { var plural = Portlist.Count > 1 ? "s" : string.Empty; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index a5b43034..6e2e26af 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,40 +8,36 @@ public class PortSelectCommand : BaseCommand { public PortSelectCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (LoggerFactory != null) + if (LoggerFactory != null && Console != null) { - if (Console != null) - { - var portListCommand = new PortListCommand(LoggerFactory); + var portListCommand = new PortListCommand(LoggerFactory); - await portListCommand.ExecuteAsync(Console); + await portListCommand.ExecuteAsync(Console); - if (portListCommand.Portlist?.Count > 0) + if (portListCommand.Portlist?.Count > 0) + { + if (portListCommand.Portlist?.Count > 1) { - if (portListCommand.Portlist?.Count > 1) - { - Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); + Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); - byte deviceSelected; - if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + byte deviceSelected; + if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + { + if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) { - if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) - { - await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); - } + await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); } } - else - { - // Only 1 device attached, let's auto select it - if (portListCommand.Portlist != null) - await CallConfigCommand(portListCommand.Portlist[0]); - } + } + else + { + // Only 1 device attached, let's auto select it + if (portListCommand.Portlist != null) + await CallConfigCommand(portListCommand.Portlist[0]); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs index e1fc867e..908c2d01 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -8,23 +8,24 @@ public class RuntimeDisableCommand : BaseDeviceCommand { public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Disabling runtime..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } + Logger?.LogInformation($"Disabling runtime..."); + await connection.Device.RuntimeDisable(CancellationToken); var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs index 8061a64b..066dba13 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -8,23 +8,24 @@ public class RuntimeEnableCommand : BaseDeviceCommand { public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Enabling runtime..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } + Logger?.LogInformation($"Enabling runtime..."); + await connection.Device.RuntimeEnable(CancellationToken); var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs index 19569c1c..0c772ee4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -8,21 +8,22 @@ public class RuntimeStateCommand : BaseDeviceCommand { public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - Logger?.LogInformation($"Querying runtime state..."); - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Runtime state failed - device or connection not found"); return; } + Logger?.LogInformation($"Querying runtime state..."); + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs index c3aa27f5..94b7a602 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -8,15 +8,15 @@ public class TraceDisableCommand : BaseDeviceCommand { public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace disable failed - device or connection not found"); return; } @@ -29,5 +29,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceDisable(CancellationToken); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs index faafb992..31e393be 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -11,15 +11,15 @@ public class TraceEnableCommand : BaseDeviceCommand public TraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace enable failed - device or connection not found"); return; } @@ -38,5 +38,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceEnable(CancellationToken); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index 73309878..1b66ff11 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -11,15 +11,15 @@ public class TraceLevelCommand : BaseDeviceCommand public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Trace level failed - device or connection not found"); return; } @@ -43,4 +43,4 @@ protected override async ValueTask ExecuteCommand() await connection.Device.TraceEnable(CancellationToken); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index 03c70cd9..5bf5aea1 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -5,8 +5,8 @@ namespace Meadow.Hcom; public class FirmwareUpdater { - private ILogger? _logger; - private Task? _updateTask; + private readonly ILogger? _logger; + private readonly Task? _updateTask; private IMeadowConnection _connection; private UpdateState _state; @@ -130,7 +130,7 @@ private async void StateMachine() } break; case UpdateState.DFUCompleted: - // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after + // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after try { // wait for device to reconnect From 9f24f3e8d3045d685ff47a9ee709ef879eb730d6 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Mon, 15 Jan 2024 09:58:47 -0600 Subject: [PATCH 097/141] added indexers to FirmwarePackageCollection --- .../F7FirmwarePackageCollection.cs | 10 +++++++--- .../IFirmwarePackageCollection.cs | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 8a14443a..5e5fdb0e 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -29,6 +29,10 @@ internal F7FirmwarePackageCollection() { } + public FirmwarePackage? this[string version] => _f7Packages.FirstOrDefault(p => p.Version == version); + + public FirmwarePackage this[int index] => _f7Packages[index]; + internal F7FirmwarePackageCollection(string rootPath) { if (!Directory.Exists(rootPath)) @@ -85,8 +89,10 @@ public Task DeletePackage(string version) return Task.CompletedTask; } - public Task SetDefaultPackage(string version) + public async Task SetDefaultPackage(string version) { + await Refresh(); + var existing = _f7Packages.FirstOrDefault(p => p.Version == version); if (existing == null) @@ -96,8 +102,6 @@ public Task SetDefaultPackage(string version) var downloadManager = new F7FirmwareDownloadManager(); downloadManager.SetDefaultVersion(PackageFileRoot, version); - - return Task.CompletedTask; } public async Task IsVersionAvailableForDownload(string version) diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index 5d0cf341..e33cb196 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -23,6 +23,7 @@ public interface IFirmwarePackageCollection : IEnumerable Task UpdateAvailable(); Task IsVersionAvailableForDownload(string version); Task RetrievePackage(string version, bool overwrite = false); - + FirmwarePackage this[int index] { get; } + FirmwarePackage? this[string version] { get; } string PackageFileRoot { get; } } From cb850c23cdc8c3c27847b5ad0c8078bb26f6a0e1 Mon Sep 17 00:00:00 2001 From: Steven Kuhn Date: Tue, 19 Dec 2023 10:57:13 -0600 Subject: [PATCH 098/141] Add ability to manage Meadow.Cloud api keys --- .../Cloud/ApiKey/CloudApiKeyCreateCommand.cs | 73 +++++++++++ .../Cloud/ApiKey/CloudApiKeyDeleteCommand.cs | 65 ++++++++++ .../Cloud/ApiKey/CloudApiKeyListCommand.cs | 71 +++++++++++ .../Cloud/ApiKey/CloudApiKeyUpdateCommand.cs | 75 +++++++++++ .../Commands/Current/Cloud/ConsoleTable.cs | 107 ++++++++++++++++ .../Current/Cloud/CloudLoginCommand.cs | 2 +- .../Current/Cloud/CloudLogoutCommand.cs | 2 +- .../Command/CloudCommandPublishCommand.cs | 7 +- .../Cloud/Package/CloudPackageListCommand.cs | 2 +- .../Package/CloudPackagePublishCommand.cs | 12 +- .../Package/CloudPackageUploadCommand.cs | 2 +- Source/v2/Meadow.Cli/Program.cs | 1 + .../Services/ApiTokenService.cs | 117 ++++++++++++++++++ .../Services/CommandService.cs | 3 - 14 files changed, 522 insertions(+), 17 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs create mode 100644 Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs new file mode 100644 index 00000000..73fccc32 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs @@ -0,0 +1,73 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey create", Description = "Create a Meadow.Cloud API key")] +public class CloudApiKeyCreateCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name of the API key", IsRequired = true, Name = "NAME")] + public string? Name { get; set; } + + [CommandOption("duration", 'd', Description = "The duration of the API key, in days", IsRequired = true)] + public int Duration { get; set; } + + [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key", IsRequired = true)] + public string[]? Scopes { get; set; } + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})")] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyCreateCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + if (Duration < 1 || Duration > 90) + { + throw new CommandException("Duration (-d|--duration) must be between 1 and 90 days.", showHelp: true); + } + + Host ??= DefaultHost; + + Logger?.LogInformation($"Creating an API key on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var request = new CreateApiTokenRequest(Name!, Duration, Scopes!); + var response = await ApiTokenService.CreateApiToken(request, Host, CancellationToken); + + Logger?.LogInformation($"Your API key '{response.Name}' (expiring {response.ExpiresAt:G} UTC) is:"); + Logger?.LogInformation($"\n{response.Token}\n"); + Logger?.LogInformation("Make sure to copy this key now as you will not be able to see this again."); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs new file mode 100644 index 00000000..9a6044d2 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs @@ -0,0 +1,65 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey delete", Description = "Delete a Meadow.Cloud API key")] +public class CloudApiKeyDeleteCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] + public string? NameOrId { get; set; } + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyDeleteCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Deleting API key `{NameOrId}` on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var getRequest = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiKey = getRequest.FirstOrDefault(x => x.Id == NameOrId || string.Equals(x.Name, NameOrId, StringComparison.OrdinalIgnoreCase)); + + if (apiKey == null) + { + throw new CommandException($"API key `{NameOrId}` not found."); + } + + await ApiTokenService.DeleteApiToken(apiKey.Id, Host, CancellationToken); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs new file mode 100644 index 00000000..692c40c5 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs @@ -0,0 +1,71 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey list", Description = "List your Meadow.Cloud API keys")] +public class CloudApiKeyListCommand : BaseCloudCommand +{ + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyListCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected override async ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Retrieving your API keys from Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var response = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiTokens = response.OrderBy(a => a.Name); + + if (!apiTokens.Any()) + { + Logger?.LogInformation("You have no API keys."); + return; + } + + var table = new ConsoleTable("Id", "Name", $"Expires (UTC)", "Scopes"); + foreach (var apiToken in apiTokens) + { + table.AddRow(apiToken.Id, apiToken.Name, $"{apiToken.ExpiresAt:G}", string.Join(", ", apiToken.Scopes.OrderBy(t => t))); + } + + Logger?.LogInformation(table); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Get API keys command failed: {ex.Message}", innerException: ex); + } + } +} + + diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs new file mode 100644 index 00000000..944a6cf5 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs @@ -0,0 +1,75 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey update", Description = "Update a Meadow.Cloud API key")] +public class CloudApiKeyUpdateCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] + public string? NameOrId { get; set; } + + [CommandOption("name", 'n', Description = "The new name to use for the API key")] + public string? NewName { get; set; } + + [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key")] + public string[]? Scopes { get; set; } + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyUpdateCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Updating API key `{NameOrId}` on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var getRequest = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiKey = getRequest.FirstOrDefault(x => x.Id == NameOrId || string.Equals(x.Name, NameOrId, StringComparison.OrdinalIgnoreCase)); + + if (apiKey == null) + { + throw new CommandException($"API key `{NameOrId}` not found."); + } + + NewName ??= apiKey.Name; + Scopes ??= apiKey.Scopes; + + var updateRequest = new UpdateApiTokenRequest(NewName!, Scopes!); + await ApiTokenService.UpdateApiToken(apiKey.Id, updateRequest, Host, CancellationToken); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs new file mode 100644 index 00000000..40be5333 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs @@ -0,0 +1,107 @@ +using System.Text; + +namespace Meadow.CLI; + +public class ConsoleTable +{ + private readonly string[] _columns; + private IList _rows; + + public ConsoleTable(params string[] columns) + { + _columns = columns; + _rows = new List(); + } + + public void AddRow(params object[] values) + { + if (values.Length != _columns.Length) + { + throw new InvalidOperationException("The number of values for the given row does not match the number of columns."); + } + + _rows.Add(values.Select(v => Convert.ToString(v) ?? string.Empty).ToArray()); + } + + public static implicit operator string(ConsoleTable table) => table.Render(); + + public string Render() + { + var maxWidths = new int[_columns.Length]; + for (var i = 0; i < _columns.Length; i++) + { + maxWidths[i] = _columns[i].Length; + } + + for (var i = 0; i < _rows.Count; i++) + { + for (var j = 0; j < _rows[i].Length; j++) + { + maxWidths[j] = Math.Max(maxWidths[j], _rows[i][j].Length); + } + } + + var sb = new StringBuilder(); + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-+-"); + } + } + + // Header + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(_columns[i].PadRight(maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append(" | "); + } + } + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-|-"); + } + } + + // Rows + for (var i = 0; i < _rows.Count; i++) + { + sb.AppendLine(); + for (var j = 0; j < _rows[i].Length; j++) + { + sb.Append(_rows[i][j].PadRight(maxWidths[j])); + if (j < _rows[i].Length - 1) + { + sb.Append(" | "); + } + } + } + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-+-"); + } + } + + sb.AppendLine(); + return sb.ToString(); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs index f7238df3..fa4ca8fd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs @@ -5,7 +5,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud login", Description = "Log into the Meadow Service")] +[Command("cloud login", Description = "Log in to Meadow.Cloud")] public class CloudLoginCommand : BaseCloudCommand { [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index 85d99b48..23161fd6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -5,7 +5,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud logout", Description = "Log out of the Meadow Service")] +[Command("cloud logout", Description = "Log out of Meadow.Cloud")] public class CloudLogoutCommand : BaseCloudCommand { public CloudLogoutCommand( diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index 9ca34e17..2671b317 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -7,11 +7,11 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud command publish", Description = "Publish a command to Meadow devices via the Meadow Service")] +[Command("cloud command publish", Description = "Publish a command to Meadow devices via Meadow.Cloud")] public class CloudCommandPublishCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string CommandName { get; set; } = default!; + public string CommandName { get; set; } = string.Empty; [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] public string? CollectionId { get; set; } @@ -64,12 +64,11 @@ protected override async ValueTask ExecuteCommand() try { - Logger?.LogInformation($"Publishing '{CommandName}' command to Meadow.Cloud. Please wait..."); if (!string.IsNullOrWhiteSpace(CollectionId)) { await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } - else if (DeviceIds.Any()) + else if (DeviceIds?.Length > 0) { await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 29a0584b..9794c1e9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -14,7 +14,7 @@ public class CloudPackageListCommand : BaseCloudCommand public string? OrgId { get; set; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] - public string Host { get; set; } + public string? Host { get; set; } public CloudPackageListCommand( IdentityManager identityManager, diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index 7aa75daf..b1f516f9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -11,16 +11,16 @@ public class CloudPackagePublishCommand : BaseCloudCommand { [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] - public string MpakPath { get; init; } + public string MpakPath { get; init; } = string.Empty; [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] public string? OrgId { get; set; } diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index bc41606a..8cbb90ad 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -55,6 +55,7 @@ public static async Task Main(string[] args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); if (File.Exists("appsettings.json")) diff --git a/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs b/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs new file mode 100644 index 00000000..e35ba82e --- /dev/null +++ b/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs @@ -0,0 +1,117 @@ +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; + +namespace Meadow.Cloud; + +public class ApiTokenService : CloudServiceBase +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web); + + public ApiTokenService(IdentityManager identityManager) : base(identityManager) + { + } + + public async Task> GetApiTokens(string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var response = await httpClient.GetAsync($"{host}/api/auth/tokens", cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + return await response.Content.ReadFromJsonAsync>(JsonSerializerOptions, cancellationToken ?? CancellationToken.None) + ?? Enumerable.Empty(); + } + + public async Task CreateApiToken(CreateApiTokenRequest request, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var content = new StringContent(JsonSerializer.Serialize(request, JsonSerializerOptions), Encoding.UTF8, "application/json"); + var response = await httpClient.PostAsync($"{host}/api/auth/tokens", content, cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + var result = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken ?? CancellationToken.None); + return result!; + } + + public async Task UpdateApiToken(string id, UpdateApiTokenRequest request, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var content = new StringContent(JsonSerializer.Serialize(request, JsonSerializerOptions), Encoding.UTF8, "application/json"); + var response = await httpClient.PutAsync($"{host}/api/auth/tokens/{id}", content, cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + var result = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken ?? CancellationToken.None); + return result!; + } + + public async Task DeleteApiToken(string id, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var response = await httpClient.DeleteAsync($"{host}/api/auth/tokens/{id}", cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + } +} + +public record GetApiTokenResponse( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes +) +{ } + +public record CreateApiTokenRequest +( + string Name, + int Duration, + string[] Scopes +) +{ } + +public record CreateApiTokenResponse +( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes, + string Token +) +{ } + +public record UpdateApiTokenRequest +( + string Name, + string[] Scopes +) +{ } + +public record UpdateApiTokenResponse +( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes +) +{ } diff --git a/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs b/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs index 8554f092..b6dba3a7 100644 --- a/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs +++ b/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs @@ -6,11 +6,8 @@ namespace Meadow.Cloud; public class CommandService : CloudServiceBase { - private readonly IdentityManager _identityManager; - public CommandService(IdentityManager identityManager) : base(identityManager) { - _identityManager = identityManager; } public async Task PublishCommandForCollection( From e39b159d8e922b2e18bf3d368a3de2c85bcf997b Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 15 Jan 2024 23:27:25 -0800 Subject: [PATCH 099/141] Fixes and cleanup --- Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs | 47 ++-- Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs | 20 +- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- Source/v2/Meadow.Cli/AppManager.cs | 2 +- .../Current/File/FileDeleteCommand.cs | 3 +- .../Current/Firmware/FirmwareWriteCommand.cs | 233 +++++++++--------- .../Current/Uart/UartTraceDisableCommand.cs | 8 +- .../Current/Uart/UartTraceEnableCommand.cs | 6 +- .../Commands/Legacy/FlashOsCommand.cs | 41 +-- .../v2/Meadow.Cli/MeadowConnectionManager.cs | 11 +- .../v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- .../Meadow.Cli/Properties/launchSettings.json | 6 +- .../Meadow.Hcom/Firmware/DownloadManager.cs | 86 +------ .../Meadow.SoftwareManager/FirmwarePackage.cs | 2 +- 14 files changed, 199 insertions(+), 270 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs index 04888f92..e88d813a 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs @@ -1,9 +1,5 @@ -using System; -using System.IO; -using System.Threading; -using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.InteropServices; -using System.Diagnostics; namespace DfuSharp { @@ -23,7 +19,6 @@ public enum LogLevel public delegate void HotplugCallback(IntPtr ctx, IntPtr device, HotplugEventType eventType, IntPtr userData); - class NativeMethods { @@ -266,7 +261,7 @@ public class UploadingEventArgs : EventArgs public UploadingEventArgs(int bytesUploaded) { - this.BytesUploaded = bytesUploaded; + BytesUploaded = bytesUploaded; } } @@ -277,16 +272,19 @@ public class DfuDevice : IDisposable const int transfer_size = 0x800; const int address = 0x08000000; - IntPtr handle; + readonly IntPtr handle; InterfaceDescriptor interface_descriptor; DfuFunctionDescriptor dfu_descriptor; public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFunctionDescriptor dfu_descriptor) { - this.interface_descriptor = interface_descriptor; - this.dfu_descriptor = dfu_descriptor; + interface_descriptor = interface_descriptor; + dfu_descriptor = dfu_descriptor; + if (NativeMethods.libusb_open(device, ref handle) < 0) + { throw new Exception("Error opening device"); + } } public event UploadingEventHandler Uploading; @@ -411,6 +409,7 @@ public void Download(FileStream file) { int count = 0; ushort transaction = 2; + using (var writer = new BinaryWriter(file)) { while (count < flash_size) @@ -659,7 +658,7 @@ public class Context : IDisposable IntPtr _callbackHandle = IntPtr.Zero; - IntPtr handle; + readonly IntPtr handle; public Context(LogLevel debug_level = LogLevel.None) { var ret = NativeMethods.libusb_init(ref handle); @@ -669,12 +668,12 @@ public Context(LogLevel debug_level = LogLevel.None) throw new Exception(string.Format("Error: {0} while trying to initialize libusb", ret)); // instantiate our callback handler - this._hotplugCallbackHandler = new HotplugCallback(HandleHotplugCallback); + _hotplugCallbackHandler = new HotplugCallback(HandleHotplugCallback); } public void Dispose() { - //this.StopListeningForHotplugEvents(); // not needed, they're automatically deregistered in libusb_exit. + //StopListeningForHotplugEvents(); // not needed, they're automatically deregistered in libusb_exit. NativeMethods.libusb_exit(handle); } @@ -707,8 +706,10 @@ public List GetDfuDevices(List idVendors) var ret2 = NativeMethods.libusb_get_config_descriptor(devices[i], (ushort)j, out ptr); if (ret2 < 0) + { continue; - //throw new Exception(string.Format("Error: {0} while trying to get the config descriptor", ret2)); + } + //throw new Exception(string.Format("Error: {0} while trying to get the config descriptor", ret2)); var config_descriptor = Marshal.PtrToStructure(ptr); @@ -717,7 +718,9 @@ public List GetDfuDevices(List idVendors) var p = config_descriptor.interfaces + j * Marshal.SizeOf<@Interface>(); if (p == IntPtr.Zero) + { continue; + } var @interface = Marshal.PtrToStructure<@Interface>(p); for (int l = 0; l < @interface.num_altsetting; l++) @@ -726,11 +729,16 @@ public List GetDfuDevices(List idVendors) // Ensure this is a DFU descriptor if (interface_descriptor.bInterfaceClass != 0xfe || interface_descriptor.bInterfaceSubClass != 0x1) + { continue; + } var dfu_descriptor = FindDescriptor(interface_descriptor.extra, interface_descriptor.extra_length, (byte)Consts.USB_DT_DFU); + if (dfu_descriptor != null) + { dfu_devices.Add(new DfuDevice(devices[i], interface_descriptor, dfu_descriptor.Value)); + } } } } @@ -763,7 +771,7 @@ public List GetDfuDevices(List idVendors) public bool HasCapability(Capabilities caps) { - return NativeMethods.libusb_has_capability(caps) == 0 ? false : true; + return NativeMethods.libusb_has_capability(caps) != 0; } public void BeginListeningForHotplugEvents() @@ -791,8 +799,8 @@ public void BeginListeningForHotplugEvents() int deviceClass = -1; IntPtr userData = IntPtr.Zero; - ErrorCodes success = NativeMethods.libusb_hotplug_register_callback(this.handle, HotplugEventType.DeviceArrived | HotplugEventType.DeviceLeft, HotplugFlags.DefaultNoFlags, - vendorID, productID, deviceClass, this._hotplugCallbackHandler, userData, out _callbackHandle); + ErrorCodes success = NativeMethods.libusb_hotplug_register_callback(handle, HotplugEventType.DeviceArrived | HotplugEventType.DeviceLeft, HotplugFlags.DefaultNoFlags, + vendorID, productID, deviceClass, _hotplugCallbackHandler, userData, out _callbackHandle); if (success == ErrorCodes.Success) { @@ -813,15 +821,14 @@ public void StopListeningForHotplugEvents() return; } - NativeMethods.libusb_hotplug_deregister_callback(this.handle, this._callbackHandle); - + NativeMethods.libusb_hotplug_deregister_callback(handle, _callbackHandle); } public void HandleHotplugCallback(IntPtr ctx, IntPtr device, HotplugEventType eventType, IntPtr userData) { Debug.WriteLine("Hotplug Callback called, event type: " + eventType.ToString()); // raise the event - this.DeviceConnected(this, new EventArgs()); + DeviceConnected(this, new EventArgs()); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs index 3a6e2079..6dd7d738 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs @@ -11,9 +11,7 @@ namespace Meadow.CLI.Core.Internals.Dfu; public static class DfuUtils { - private static int _osAddress = 0x08000000; - - // public static string LastSerialNumber { get; private set; } = ""; + private static readonly int _osAddress = 0x08000000; public enum DfuFlashFormat { @@ -98,15 +96,15 @@ public static async Task FlashFile(string fileName, string dfuSerialNumber { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - logger.LogError("dfu-util update required. To update, run in administrator mode: `meadow install dfu-util`"); + logger.LogError("dfu-util update required - to update, run in administrator mode: `meadow install dfu-util`"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - logger.LogError("dfu-util update required. To update, run: `brew upgrade dfu-util`"); + logger.LogError("dfu-util update required - to update, run: `brew upgrade dfu-util`"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - logger.LogError("dfu-util update required. To update , run: `apt upgrade dfu-util` or the equivalent for your Linux distribution"); + logger.LogError("dfu-util update required - to update , run: `apt upgrade dfu-util` or the equivalent for your Linux distribution"); } else { @@ -282,8 +280,11 @@ public static async Task InstallDfuUtil( File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); // clean up from previous version - var dfuPath = Path.Combine(@"C:\Windows\System", dfuUtilExe.Name); - var libUsbPath = Path.Combine(@"C:\Windows\System", libUsbDll.Name); + var systemDirectory = Environment.SystemDirectory; + + var dfuPath = Path.Combine(systemDirectory, dfuUtilExe.Name); + var libUsbPath = Path.Combine(systemDirectory, libUsbDll.Name); + if (File.Exists(dfuPath)) { File.Delete(dfuPath); @@ -302,5 +303,4 @@ public static async Task InstallDfuUtil( } } } - -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index a8383b9a..766ae353 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0.0 + 2.0.0.5 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 41e135fa..c52d63d1 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -113,4 +113,4 @@ public static async Task DeployApplication( } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 010038ef..850f9951 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -11,8 +11,7 @@ public class FileDeleteCommand : BaseDeviceCommand public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 96f5c52d..c26db8f8 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -24,7 +24,7 @@ public class FirmwareWriteCommand : BaseDeviceCommand public bool UseDfu { get; set; } [CommandParameter(0, Name = "Files to write", IsRequired = false)] - public FirmwareType[]? Files { get; set; } = default!; + public FirmwareType[]? FirmwareFileTypes { get; set; } = default!; private FileManager FileManager { get; } private ISettingsManager Settings { get; } @@ -43,22 +43,22 @@ protected override async ValueTask ExecuteCommand() { var package = await GetSelectedPackage(); - if (Files == null) + if (package == null) + { + Logger?.LogError($"Firware write failed - No package selected"); + return; + } + + if (FirmwareFileTypes == null) { Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); - Files = new FirmwareType[] - { + FirmwareFileTypes = + [ FirmwareType.OS, FirmwareType.Runtime, FirmwareType.ESP - }; - } - - if (!Files.Contains(FirmwareType.OS) && UseDfu) - { - Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); - return; + ]; } bool deviceSupportsOta = false; // TODO: get this based on device OS version @@ -70,96 +70,47 @@ protected override async ValueTask ExecuteCommand() UseDfu = true; } - IMeadowConnection connection; + if (!FirmwareFileTypes.Contains(FirmwareType.OS) && UseDfu) + { + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); + return; + } - if (UseDfu && Files.Contains(FirmwareType.OS)) + if (UseDfu && FirmwareFileTypes.Contains(FirmwareType.OS)) { - // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + var osFile = package.GetFullyQualifiedPath(package.OSWithBootloader); - // get the device's serial number via DFU - we'll need it to find the device after it resets - ILibUsbDevice libUsbDevice; - try + if (osFile == null) { - libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); - } - catch (Exception ex) - { - Logger?.LogError(ex.Message); + Logger?.LogError($"OS file not found for version '{package.Version}'"); return; } - - var serial = libUsbDevice.GetDeviceSerialNumber(); - - // no connection is required here - in fact one won't exist - // unless maybe we add a "DFUConnection"? - - try + if (await WriteOsWithDfu(osFile) == false) { - await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); - } - catch (Exception ex) - { - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - - Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); return; } + //remove from collection to enable writing of other files - ToDo rework this logic + FirmwareFileTypes = FirmwareFileTypes.Where(t => t != FirmwareType.OS).ToArray(); + } - // now wait for a new serial port to appear - var ports = await MeadowConnectionManager.GetSerialPorts(); - var retryCount = 0; - - var newPort = ports.Except(initialPorts).FirstOrDefault(); - while (newPort == null) - { - if (retryCount++ > 10) - { - throw new Exception("New meadow device not found"); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); - newPort = ports.Except(initialPorts).FirstOrDefault(); - } - - Logger?.LogInformation($"Meadow found at {newPort}"); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - + IMeadowConnection? connection = null; + try + { connection = await GetCurrentConnection(); - - if (connection == null) - { - return; - } - - if (Files.Any(f => f != FirmwareType.OS)) - { - await connection.WaitForMeadowAttach(); - - if (CancellationToken.IsCancellationRequested) - { - return; - } - - await WriteFiles(connection); - } } - else + catch (Exception ex) { - connection = await GetCurrentConnection(); - if (connection == null) - { - return; - } - await WriteFiles(connection); + Logger?.LogError(ex.Message); + return; + } + + if (connection == null || connection.Device == null) + { + return; } + await WriteFiles(connection, FirmwareFileTypes); + await connection.ResetDevice(CancellationToken); await connection.WaitForMeadowAttach(); @@ -190,16 +141,12 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() var devices = provider.GetDevicesInBootloaderMode(); - switch (devices.Count) + _libUsbDevice = devices.Count switch { - case 0: - throw new Exception("No device found in bootloader mode"); - case 1: - _libUsbDevice = devices[0]; - break; - default: - throw new Exception("Multiple devices found in bootloader mode - only connect one device"); - } + 0 => throw new Exception("No device found in bootloader mode"), + 1 => devices[0], + _ => throw new Exception("Multiple devices found in bootloader mode - only connect one device"), + }; } return _libUsbDevice; @@ -219,7 +166,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() if (existing == null) { - Logger?.LogError($"Requested version '{Version}' not found."); + Logger?.LogError($"Requested version '{Version}' not found"); return null; } package = existing; @@ -235,14 +182,13 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() return package; } - private async ValueTask WriteFiles(IMeadowConnection connection) + private async ValueTask WriteFiles(IMeadowConnection connection, FirmwareType[] firmwareFileTypes) { // the connection passes messages back to us (info about actions happening on-device connection.DeviceMessageReceived += (s, e) => { if (e.message.Contains("% downloaded")) - { - // don't echo this, as we're already reporting % written + { // don't echo this, as we're already reporting % written } else { @@ -260,7 +206,7 @@ private async ValueTask WriteFiles(IMeadowConnection connection) var package = await GetSelectedPackage(); - var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + var wasRuntimeEnabled = await connection!.Device!.IsRuntimeEnabled(CancellationToken); if (wasRuntimeEnabled) { @@ -274,20 +220,13 @@ private async ValueTask WriteFiles(IMeadowConnection connection) Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); }; - if (Files.Contains(FirmwareType.OS)) + if (firmwareFileTypes.Contains(FirmwareType.OS)) { - if (UseDfu) - { - // this would have already happened before now (in ExecuteAsync) so ignore - } - else - { - Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - throw new NotSupportedException("OtA writes for the OS are not yet supported"); - } + throw new NotSupportedException("OtA writes for the OS are not yet supported"); } - if (Files.Contains(FirmwareType.Runtime)) + if (firmwareFileTypes.Contains(FirmwareType.Runtime)) { Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); @@ -307,7 +246,7 @@ private async ValueTask WriteFiles(IMeadowConnection connection) return; } - if (Files.Contains(FirmwareType.ESP)) + if (FirmwareFileTypes.Contains(FirmwareType.ESP)) { Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); @@ -336,12 +275,78 @@ private async ValueTask WriteFiles(IMeadowConnection connection) // TODO: if we're an F7 device, we need to reset } - private async Task WriteOsWithDfu(string osFile, string serialNumber) + private async Task WriteOsWithDfu(string osFile) { - await DfuUtils.FlashFile( + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + // get the device's serial number via DFU - we'll need it to find the device after it resets + ILibUsbDevice libUsbDevice; + try + { + libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); + } + catch (Exception ex) + { + Logger?.LogError(ex.Message); + return false; + } + + string serialNumber; + + try + { + serialNumber = libUsbDevice.GetDeviceSerialNumber(); + } + catch + { + Logger?.LogError("Firmware write failed - unable to read device serial number (make sure device is connected)"); + return false; + } + + try + { + await DfuUtils.FlashFile( osFile, serialNumber, logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); + + } + catch (Exception ex) + { + Logger?.LogError($"Exception type: {ex.GetType().Name}"); + + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); + + Logger?.LogWarning("This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."); + return false; + } + + // now wait for a new serial port to appear + var ports = await MeadowConnectionManager.GetSerialPorts(); + var retryCount = 0; + + var newPort = ports.Except(initialPorts).FirstOrDefault(); + + while (newPort == null) + { + if (retryCount++ > 10) + { + throw new Exception("New meadow device not found"); + } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + newPort = ports.Except(initialPorts).FirstOrDefault(); + } + + Logger?.LogInformation($"Meadow found at {newPort}"); + + // configure the route to that port for the user + Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + return true; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs index 646492d4..be478492 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs @@ -8,15 +8,15 @@ public class UartTraceDisableCommand : BaseDeviceCommand { public UartTraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { + Logger?.LogError($"Uart trace enable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index 3fd95004..7acc1849 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -13,7 +13,7 @@ public class FlashOsCommand : BaseDeviceCommand public string OSFile { get; init; } = default!; [CommandOption("runtimeFile", 'r', Description = "Path to the Meadow Runtime binary")] - public string RuntimeFile { get; init; } + public string RuntimeFile { get; init; } = default!; [CommandOption("skipDfu", 'd', Description = "Skip DFU flash")] public bool SkipOS { get; init; } @@ -22,13 +22,13 @@ public class FlashOsCommand : BaseDeviceCommand public bool SkipEsp { get; init; } [CommandOption("skipRuntime", 'k', Description = "Skip updating the runtime")] - public bool SkipRuntime { get; init; } + public bool SkipRuntime { get; init; } = default!; [CommandOption("dontPrompt", 'p', Description = "Don't show bulk erase prompt")] public bool DontPrompt { get; init; } [CommandOption("osVersion", 'v', Description = "Flash a specific downloaded OS version - x.x.x.x")] - public string Version { get; private set; } + public string Version { get; private set; } = default!; private FirmwareType[]? Files { get; set; } = default!; private bool UseDfu = true; @@ -74,13 +74,13 @@ protected override async ValueTask ExecuteCommand() if (!Files.Contains(FirmwareType.OS) && UseDfu) { - Logger.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); return; } bool deviceSupportsOta = false; // TODO: get this based on device OS version - if (package.OsWithoutBootloader == null + if (package?.OsWithoutBootloader == null || !deviceSupportsOta || UseDfu) { @@ -100,7 +100,7 @@ protected override async ValueTask ExecuteCommand() } catch (Exception ex) { - Logger.LogError(ex.Message); + Logger?.LogError(ex.Message); return; } @@ -115,13 +115,13 @@ protected override async ValueTask ExecuteCommand() } catch (Exception ex) { - Logger.LogError($"Exception type: {ex.GetType().Name}"); + Logger?.LogError($"Exception type: {ex.GetType().Name}"); // TODO: scope this to the right exception type for Win 10 access violation thing // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - Logger.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); + Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); return; } @@ -145,13 +145,14 @@ protected override async ValueTask ExecuteCommand() Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); var connection = ConnectionManager.GetCurrentConnection(); - if (connection == null) + + if (connection == null || connection.Device == null) { - Logger.LogError($"No connection path is defined"); + Logger?.LogError($"No connection path is defined"); return; } - var cancellationToken = Console.RegisterCancellationHandler(); + var cancellationToken = Console?.RegisterCancellationHandler(); if (Files.Any(f => f != FirmwareType.OS)) { @@ -164,8 +165,8 @@ protected override async ValueTask ExecuteCommand() if (deviceInfo != null) { - Logger.LogInformation($"Done."); - Logger.LogInformation(deviceInfo.ToString()); + Logger?.LogInformation($"Done."); + Logger?.LogInformation(deviceInfo.ToString()); } } else @@ -216,7 +217,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() if (existing == null) { - Logger.LogError($"Requested version '{Version}' not found."); + Logger?.LogError($"Requested version '{Version}' not found."); return null; } package = existing; @@ -236,7 +237,7 @@ private async ValueTask WriteFiles() { var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { return; } @@ -255,7 +256,7 @@ private async ValueTask WriteFiles() }; connection.ConnectionMessage += (s, message) => { - Logger.LogInformation(message); + Logger?.LogInformation(message); }; var package = await GetSelectedPackage(); @@ -282,14 +283,14 @@ private async ValueTask WriteFiles() } else { - Logger.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); throw new NotSupportedException("OtA writes for the OS are not yet supported"); } } if (Files.Contains(FirmwareType.Runtime)) { - Logger.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); // get the path to the runtime file var rtpath = package.GetFullyQualifiedPath(package.Runtime); @@ -300,7 +301,7 @@ private async ValueTask WriteFiles() } if (Files.Contains(FirmwareType.ESP)) { - Logger.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); var fileList = new string[] { @@ -312,7 +313,7 @@ private async ValueTask WriteFiles() await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); } - Logger.LogInformation($"{Environment.NewLine}"); + Logger?.LogInformation($"{Environment.NewLine}"); if (wasRuntimeEnabled) { diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 2a8493d2..f00084ce 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,5 +1,4 @@ -using Meadow.CLI; -using Meadow.Hcom; +using Meadow.Hcom; using System.Diagnostics; using System.IO.Ports; using System.Management; @@ -11,9 +10,9 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class MeadowConnectionManager { public const string WILDERNESS_LABS_USB_VID = "2E6A"; - private static object _lockObject = new(); + private static readonly object _lockObject = new(); - private ISettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; private IMeadowConnection? _currentConnection; public MeadowConnectionManager(ISettingsManager settingsManager) @@ -169,7 +168,9 @@ public static async Task> GetMeadowSerialPortsForOsx() public static async Task> GetMeadowSerialPortsForLinux() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) + { throw new PlatformNotSupportedException("This method is only supported on Linux"); + } return await Task.Run(() => { @@ -263,4 +264,4 @@ public static IList GetMeadowSerialPortsForWindows() return ports; } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index 0e39f0aa..4600f0e0 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.0"; + public const string CLI_VERSION = "2.0.0.5"; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 7dc2f1f4..11356d1a 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -81,7 +81,7 @@ }, "File Delete": { "commandName": "Project", - "commandLineArgs": "file delete meadow.log" + "commandLineArgs": "file delete Juego.pdb" }, "File Read": { "commandName": "Project", @@ -105,7 +105,7 @@ }, "Firmware Download version": { "commandName": "Project", - "commandLineArgs": "firmware download -v 1.0.2.0 --force" + "commandLineArgs": "firmware download -v 1.7.1.4 --force" }, "Firmware Default get": { "commandName": "Project", @@ -125,7 +125,7 @@ }, "Firmware Write version": { "commandName": "Project", - "commandLineArgs": "firmware write -v 1.7.1.3" + "commandLineArgs": "firmware write -v 1.8.0.0" }, "Firmware Write runtime": { "commandName": "Project", diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 0ce95cd6..74f7e0a2 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -171,90 +171,6 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); } - public async Task InstallDfuUtil(bool is64Bit = true, - CancellationToken cancellationToken = default) - { - try - { - _logger.LogInformation("Installing dfu-util..."); - - if (Directory.Exists(WildernessLabsTemp)) - { - Directory.Delete(WildernessLabsTemp, true); - } - - Directory.CreateDirectory(WildernessLabsTemp); - - const string downloadUrl = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-0.10-binaries.zip"; - - var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); - var response = await Client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - if (response.IsSuccessStatusCode == false) - { - throw new Exception("Failed to download dfu-util"); - } - - using (var stream = await response.Content.ReadAsStreamAsync()) - using (var downloadFileStream = new DownloadFileStream(stream, _logger)) - using (var fs = File.OpenWrite(Path.Combine(WildernessLabsTemp, downloadFileName))) - { - await downloadFileStream.CopyToAsync(fs); - } - - ZipFile.ExtractToDirectory( - Path.Combine(WildernessLabsTemp, downloadFileName), - WildernessLabsTemp); - - var dfuUtilExe = new FileInfo( - Path.Combine(WildernessLabsTemp, is64Bit ? "win64" : "win32", "dfu-util.exe")); - - var libUsbDll = new FileInfo( - Path.Combine( - WildernessLabsTemp, - is64Bit ? "win64" : "win32", - "libusb-1.0.dll")); - - var targetDir = is64Bit - ? Environment.GetFolderPath(Environment.SpecialFolder.System) - : Environment.GetFolderPath( - Environment.SpecialFolder.SystemX86); - - File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); - File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); - - // clean up from previous version - var dfuPath = Path.Combine(@"C:\Windows\System", dfuUtilExe.Name); - var libUsbPath = Path.Combine(@"C:\Windows\System", libUsbDll.Name); - if (File.Exists(dfuPath)) - { - File.Delete(dfuPath); - } - - if (File.Exists(libUsbPath)) - { - File.Delete(libUsbPath); - } - - _logger.LogInformation("dfu-util 0.10 installed"); - } - catch (Exception ex) - { - _logger.LogError( - ex, - ex.Message.Contains("Access to the path") - ? $"Run terminal as administrator and try again." - : "Unexpected error"); - } - finally - { - if (Directory.Exists(WildernessLabsTemp)) - { - Directory.Delete(WildernessLabsTemp, true); - } - } - } - public async Task<(bool updateExists, string latestVersion, string currentVersion)> CheckForUpdates() { try @@ -348,4 +264,4 @@ private void CleanPath(string path) } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs index ae2c489c..ac963c90 100644 --- a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -25,4 +25,4 @@ public string GetFullyQualifiedPath(string file) public string? OsWithoutBootloader { get; set; } public string? Runtime { get; set; } public string? BclFolder { get; set; } -} +} \ No newline at end of file From 3cd3d704740aa0e09d297a2fca2e5070b2bf43c3 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 15 Jan 2024 23:55:21 -0800 Subject: [PATCH 100/141] Remove legacy cleanup code --- Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs | 19 +------------------ .../Current/Firmware/FirmwareWriteCommand.cs | 1 - 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs index 6dd7d738..dde5938b 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs @@ -273,27 +273,10 @@ public static async Task InstallDfuUtil( var targetDir = is64Bit ? Environment.GetFolderPath(Environment.SpecialFolder.System) - : Environment.GetFolderPath( - Environment.SpecialFolder.SystemX86); + : Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); - - // clean up from previous version - var systemDirectory = Environment.SystemDirectory; - - var dfuPath = Path.Combine(systemDirectory, dfuUtilExe.Name); - var libUsbPath = Path.Combine(systemDirectory, libUsbDll.Name); - - if (File.Exists(dfuPath)) - { - File.Delete(dfuPath); - } - - if (File.Exists(libUsbPath)) - { - File.Delete(libUsbPath); - } } finally { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index c26db8f8..fd38bf15 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -293,7 +293,6 @@ private async Task WriteOsWithDfu(string osFile) } string serialNumber; - try { serialNumber = libUsbDevice.GetDeviceSerialNumber(); From 989b1c27bc40d3a28d97a5880500cbbc82cee291 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 16 Jan 2024 00:06:52 -0800 Subject: [PATCH 101/141] Reformat collection literal --- .../Current/Firmware/FirmwareWriteCommand.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index cbc78c61..fc220b81 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -53,12 +53,12 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); - FirmwareFileTypes = - [ - FirmwareType.OS, - FirmwareType.Runtime, - FirmwareType.ESP - ]; + FirmwareFileTypes = new FirmwareType[] + { + FirmwareType.OS, + FirmwareType.Runtime, + FirmwareType.ESP + }; } bool deviceSupportsOta = false; // TODO: get this based on device OS version From 4b9557811b293d8894b3942e5ccf01fa0c214923 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 16 Jan 2024 17:39:50 -0800 Subject: [PATCH 102/141] Cleanup and remove unused methods --- .../Meadow.Hcom/Firmware/DownloadManager.cs | 137 ++++++------------ 1 file changed, 44 insertions(+), 93 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 74f7e0a2..4378c6bd 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -1,40 +1,14 @@ using Microsoft.Extensions.Logging; using System.IO.Compression; -using System.Reflection; using System.Text.Json; namespace Meadow.Hcom; public class DownloadManager { - public static readonly string FirmwareDownloadsFilePathRoot = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "WildernessLabs", - "Firmware"); - - public static string FirmwareLatestVersion - { - get - { - string latest_txt = Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"); - if (File.Exists(latest_txt)) - return File.ReadAllText(latest_txt); - else - throw new FileNotFoundException("OS download was not found."); - } - } - - public static string FirmwareDownloadsFilePath => FirmwarePathForVersion(FirmwareLatestVersion); - - public static string FirmwarePathForVersion(string firmwareVersion) - { - return Path.Combine(FirmwareDownloadsFilePathRoot, firmwareVersion); - } - - public static readonly string WildernessLabsTemp = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "WildernessLabs", - "temp"); + static readonly string RootFolder = "WildernessLabs"; + static readonly string FirmwareFolder = "Firmware"; + static readonly string LatestFilename = "latest.txt"; public static readonly string OsFilename = "Meadow.OS.bin"; public static readonly string RuntimeFilename = "Meadow.OS.Runtime.bin"; @@ -46,18 +20,27 @@ public static string FirmwarePathForVersion(string firmwareVersion) public static readonly string UpdateCommand = "dotnet tool update WildernessLabs.Meadow.CLI --global"; - private static readonly HttpClient Client = new() - { - Timeout = TimeSpan.FromMinutes(5) - }; - - private readonly ILogger _logger; + public static readonly string FirmwareDownloadsFilePathRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + RootFolder, FirmwareFolder); - public DownloadManager(ILoggerFactory loggerFactory) + public static string FirmwareLatestVersion { - _logger = loggerFactory.CreateLogger(); + get + { + string latestPath = Path.Combine(FirmwareDownloadsFilePathRoot, LatestFilename); + if (File.Exists(latestPath)) + { + return File.ReadAllText(latestPath); + } + throw new FileNotFoundException("Latest firmware not found"); + } } + private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromMinutes(1) }; + + private readonly ILogger _logger; + public DownloadManager(ILogger logger) { _logger = logger; @@ -91,7 +74,6 @@ public DownloadManager(ILogger logger) return versionCheckFile; } - //ToDo rename this method - DownloadOSAsync? public async Task DownloadOsBinaries(string? version = null, bool force = false) { var versionCheckFilePath = await DownloadMeadowOSVersionFile(version); @@ -135,7 +117,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) { if (force) { - CleanPath(local_path); + DeleteDirectoryContents(local_path); } else { @@ -171,34 +153,6 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); } - public async Task<(bool updateExists, string latestVersion, string currentVersion)> CheckForUpdates() - { - try - { - var packageId = "WildernessLabs.Meadow.CLI"; - var appVersion = Assembly.GetEntryAssembly()! - .GetCustomAttribute() - .Version; - - var json = await Client.GetStringAsync( - $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLower()}/index.json"); - - var result = JsonSerializer.Deserialize(json); - - if (!string.IsNullOrEmpty(result?.Versions.LastOrDefault())) - { - var latest = result!.Versions!.Last(); - return (latest.ToVersion() > appVersion.ToVersion(), latest, appVersion); - } - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error checking for updates to Meadow.CLI"); - } - - return (false, string.Empty, string.Empty); - } - private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, uri); @@ -208,23 +162,24 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT var downloadFileName = Path.GetTempFileName(); _logger.LogDebug("Copying downloaded file to temp file {filename}", downloadFileName); - using (var stream = await response.Content.ReadAsStreamAsync()) - using (var downloadFileStream = new DownloadFileStream(stream, _logger)) - using (var firmwareFile = File.OpenWrite(downloadFileName)) - { - await downloadFileStream.CopyToAsync(firmwareFile); - } + + using var stream = await response.Content.ReadAsStreamAsync(); + using var downloadFileStream = new DownloadFileStream(stream, _logger); + using var firmwareFile = File.OpenWrite(downloadFileName); + + await downloadFileStream.CopyToAsync(firmwareFile); + return downloadFileName; } - private async Task DownloadAndExtractFile(Uri uri, string target_path, CancellationToken cancellationToken = default) + private async Task DownloadAndExtractFile(Uri uri, string targetPath, CancellationToken cancellationToken = default) { var downloadFileName = await DownloadFile(uri, cancellationToken); - _logger.LogDebug("Extracting firmware to {path}", target_path); + _logger.LogDebug($"Extracting firmware to {targetPath}"); ZipFile.ExtractToDirectory( downloadFileName, - target_path); + targetPath); try { File.Delete(downloadFileName); @@ -236,31 +191,27 @@ private async Task DownloadAndExtractFile(Uri uri, string target_path, Cancellat } } - private void CleanPath(string path) + private void DeleteDirectoryContents(string path) { var di = new DirectoryInfo(path); - foreach (FileInfo file in di.GetFiles()) - { - try - { - file.Delete(); - } - catch (Exception ex) - { - _logger.LogWarning("Failed to delete file {file} in firmware path", file.FullName); - _logger.LogDebug(ex, "Failed to delete file"); - } - } - foreach (DirectoryInfo dir in di.GetDirectories()) + foreach (var fileSystemInfo in di.GetFileSystemInfos()) { try { - dir.Delete(true); + if (fileSystemInfo is FileInfo file) + { + file.Delete(); + } + else if (fileSystemInfo is DirectoryInfo dir) + { + dir.Delete(true); + } } catch (Exception ex) { - _logger.LogWarning("Failed to delete directory {directory} in firmware path", dir.FullName); - _logger.LogDebug(ex, "Failed to delete directory"); + var type = fileSystemInfo is FileInfo ? "file" : "directory"; + _logger.LogWarning("Failed to delete {type} {path} in firmware path", type, fileSystemInfo.FullName); + _logger.LogDebug(ex, "Failed to delete {type}", type); } } } From b89bfca58aaba3451c912f14681927aa60aa3439 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 16 Jan 2024 17:54:08 -0800 Subject: [PATCH 103/141] Cleanup --- .../Meadow.Hcom/Firmware/DownloadManager.cs | 85 ++++++++----------- .../Meadow.Hcom/Firmware/FirmwareManager.cs | 4 +- .../v2/Meadow.Hcom/Firmware/PackageManager.cs | 2 +- 3 files changed, 37 insertions(+), 54 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 4378c6bd..90c6c5da 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -15,12 +15,11 @@ public class DownloadManager public static readonly string NetworkBootloaderFilename = "bootloader.bin"; public static readonly string NetworkMeadowCommsFilename = "MeadowComms.bin"; public static readonly string NetworkPartitionTableFilename = "partition-table.bin"; - internal static readonly string VersionCheckUrlRoot = - "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; + internal static readonly string VersionCheckUrlRoot = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; public static readonly string UpdateCommand = "dotnet tool update WildernessLabs.Meadow.CLI --global"; - public static readonly string FirmwareDownloadsFilePathRoot = Path.Combine( + public static readonly string FirmwareDownloadsFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), RootFolder, FirmwareFolder); @@ -28,7 +27,7 @@ public static string FirmwareLatestVersion { get { - string latestPath = Path.Combine(FirmwareDownloadsFilePathRoot, LatestFilename); + string latestPath = Path.Combine(FirmwareDownloadsFolder, LatestFilename); if (File.Exists(latestPath)) { return File.ReadAllText(latestPath); @@ -93,35 +92,28 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) return; } - if (!Directory.Exists(FirmwareDownloadsFilePathRoot)) + if (Directory.Exists(FirmwareDownloadsFolder) == false) { - Directory.CreateDirectory(FirmwareDownloadsFilePathRoot); + Directory.CreateDirectory(FirmwareDownloadsFolder); //we'll write latest.txt regardless of version if it doesn't exist - File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, "latest.txt"), release.Version); } else if (version == null) { //otherwise only update if we're pulling the latest release OS - File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, "latest.txt"), release.Version); } - if (release.Version.ToVersion() < "0.6.0.0".ToVersion()) - { - _logger.LogInformation( - $"Downloading OS version {release.Version} is no longer supported. The minimum OS version is 0.6.0.0." + Environment.NewLine); - return; - } - - var local_path = Path.Combine(FirmwareDownloadsFilePathRoot, release.Version); + var local_path = Path.Combine(FirmwareDownloadsFolder, release.Version); if (Directory.Exists(local_path)) { if (force) { - DeleteDirectoryContents(local_path); + DeleteDirectory(local_path); } else { - _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded." + Environment.NewLine); + _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded" + Environment.NewLine); return; } } @@ -131,7 +123,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) try { _logger.LogInformation($"Downloading Meadow OS" + Environment.NewLine); - await DownloadAndExtractFile(new Uri(release.DownloadURL), local_path); + await DownloadAndUnpack(new Uri(release.DownloadURL), local_path); } catch { @@ -141,8 +133,8 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) try { - _logger.LogInformation("Downloading coprocessor firmware" + Environment.NewLine); - await DownloadAndExtractFile(new Uri(release.NetworkDownloadURL), local_path); + _logger.LogInformation("Downloading coprocessor firmware"); + await DownloadAndUnpack(new Uri(release.NetworkDownloadURL), local_path); } catch { @@ -150,7 +142,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) return; } - _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); + _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}"); } private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) @@ -161,7 +153,7 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT response.EnsureSuccessStatusCode(); var downloadFileName = Path.GetTempFileName(); - _logger.LogDebug("Copying downloaded file to temp file {filename}", downloadFileName); + _logger.LogDebug($"Copying downloaded file to temp file {downloadFileName}"); using var stream = await response.Content.ReadAsStreamAsync(); using var downloadFileStream = new DownloadFileStream(stream, _logger); @@ -172,47 +164,38 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT return downloadFileName; } - private async Task DownloadAndExtractFile(Uri uri, string targetPath, CancellationToken cancellationToken = default) + private async Task DownloadAndUnpack(Uri uri, string targetPath, CancellationToken cancellationToken = default) { - var downloadFileName = await DownloadFile(uri, cancellationToken); + var file = await DownloadFile(uri, cancellationToken); + + _logger.LogDebug($"Extracting {file} to {targetPath}"); + + ZipFile.ExtractToDirectory(file, targetPath); - _logger.LogDebug($"Extracting firmware to {targetPath}"); - ZipFile.ExtractToDirectory( - downloadFileName, - targetPath); try { - File.Delete(downloadFileName); + File.Delete(file); } catch (Exception ex) { - _logger.LogWarning("Unable to delete temporary file"); _logger.LogDebug(ex, "Unable to delete temporary file"); } } - private void DeleteDirectoryContents(string path) + /// + /// Delete all files and sub directorines in a directory + /// + /// The directory path + /// Optional ILogger for exception reporting + public static void DeleteDirectory(string path, ILogger? logger = null) { - var di = new DirectoryInfo(path); - foreach (var fileSystemInfo in di.GetFileSystemInfos()) + try { - try - { - if (fileSystemInfo is FileInfo file) - { - file.Delete(); - } - else if (fileSystemInfo is DirectoryInfo dir) - { - dir.Delete(true); - } - } - catch (Exception ex) - { - var type = fileSystemInfo is FileInfo ? "file" : "directory"; - _logger.LogWarning("Failed to delete {type} {path} in firmware path", type, fileSystemInfo.FullName); - _logger.LogDebug(ex, "Failed to delete {type}", type); - } + Directory.Delete(path, true); + } + catch (IOException e) + { + logger?.LogWarning($"Failed to delete {path} - {e.Message}"); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs index dae02052..c0d6aaa0 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs @@ -46,7 +46,7 @@ public static async Task GetCloudLatestFirmwareVersion() public static string GetLocalLatestFirmwareVersion() { - var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFilePathRoot); + var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFolder); var latest = string.Empty; var latestFile = di.GetFiles("latest.txt").FirstOrDefault(); if (latestFile != null) @@ -60,7 +60,7 @@ public static FirmwareInfo[] GetAllLocalFirmwareBuilds() { var list = new List(); - var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFilePathRoot); + var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFolder); var latest = GetLocalLatestFirmwareVersion(); diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs b/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs index ec08c6f1..a314d527 100644 --- a/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs @@ -21,7 +21,7 @@ public string CreatePackage(string applicationPath, string osVersion) throw new ArgumentException($"Invalid applicationPath: {applicationPath}"); } - var osFilePath = Path.Combine(DownloadManager.FirmwareDownloadsFilePathRoot, osVersion); + var osFilePath = Path.Combine(DownloadManager.FirmwareDownloadsFolder, osVersion); if (!Directory.Exists(osFilePath)) { throw new ArgumentException($"osVersion {osVersion} not found. Please download."); From 0b0383ee06982c2bbe7f0f11d3d1f436bd3d658f Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 16 Jan 2024 18:10:40 -0800 Subject: [PATCH 104/141] Cleanup --- .../Meadow.Hcom/Firmware/DownloadManager.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 90c6c5da..0be1cb43 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -47,8 +47,9 @@ public DownloadManager(ILogger logger) internal async Task DownloadMeadowOSVersionFile(string? version) { - string versionCheckUrl; - if (version is null || string.IsNullOrWhiteSpace(version)) + string versionCheckUrl, versionCheckFile; + + if (string.IsNullOrWhiteSpace(version)) { _logger.LogInformation("Downloading latest version file" + Environment.NewLine); versionCheckUrl = VersionCheckUrlRoot + "latest.json"; @@ -59,8 +60,6 @@ public DownloadManager(ILogger logger) versionCheckUrl = VersionCheckUrlRoot + version + ".json"; } - string versionCheckFile; - try { versionCheckFile = await DownloadFile(new Uri(versionCheckUrl)); @@ -75,20 +74,20 @@ public DownloadManager(ILogger logger) public async Task DownloadOsBinaries(string? version = null, bool force = false) { - var versionCheckFilePath = await DownloadMeadowOSVersionFile(version); + var versionFilePath = await DownloadMeadowOSVersionFile(version); - if (versionCheckFilePath == null) + if (versionFilePath == null) { _logger.LogError($"Meadow OS {version} cannot be downloaded or is not available"); return; } - var payload = File.ReadAllText(versionCheckFilePath); + var payload = File.ReadAllText(versionFilePath); var release = JsonSerializer.Deserialize(payload); if (release == null) { - _logger.LogError($"Unable to read release details for Meadow OS {version}. Payload: {payload}"); + _logger.LogError($"Unable to read release details for Meadow OS"); return; } @@ -96,34 +95,34 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) { Directory.CreateDirectory(FirmwareDownloadsFolder); //we'll write latest.txt regardless of version if it doesn't exist - File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, "latest.txt"), release.Version); + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, LatestFilename), release.Version); } else if (version == null) - { //otherwise only update if we're pulling the latest release OS - File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, "latest.txt"), release.Version); + { //otherwise update if we're pulling the latest release OS + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, LatestFilename), release.Version); } - var local_path = Path.Combine(FirmwareDownloadsFolder, release.Version); + var firmwareVersionPath = Path.Combine(FirmwareDownloadsFolder, release.Version); - if (Directory.Exists(local_path)) + if (Directory.Exists(firmwareVersionPath)) { if (force) { - DeleteDirectory(local_path); + DeleteDirectory(firmwareVersionPath); } else { - _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded" + Environment.NewLine); + _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded"); return; } } - Directory.CreateDirectory(local_path); + Directory.CreateDirectory(firmwareVersionPath); try { - _logger.LogInformation($"Downloading Meadow OS" + Environment.NewLine); - await DownloadAndUnpack(new Uri(release.DownloadURL), local_path); + _logger.LogInformation($"Downloading Meadow OS"); + await DownloadAndUnpack(new Uri(release.DownloadURL), firmwareVersionPath); } catch { @@ -134,7 +133,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) try { _logger.LogInformation("Downloading coprocessor firmware"); - await DownloadAndUnpack(new Uri(release.NetworkDownloadURL), local_path); + await DownloadAndUnpack(new Uri(release.NetworkDownloadURL), firmwareVersionPath); } catch { @@ -142,7 +141,7 @@ public async Task DownloadOsBinaries(string? version = null, bool force = false) return; } - _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}"); + _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {firmwareVersionPath}"); } private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) From 7c6172fe30ccd2835a3af0d3cd56d3a46481ea2e Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 16 Jan 2024 18:14:53 -0800 Subject: [PATCH 105/141] Logger output cleanup --- .../Commands/Current/Firmware/FirmwareDownloadCommand.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index 66ae615e..e11e4660 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -34,7 +34,7 @@ protected override async ValueTask ExecuteCommand() if (latest == null) { - Logger?.LogError($"Unable to get latest version information."); + Logger?.LogError($"Unable to get latest version information"); return; } @@ -65,11 +65,11 @@ protected override async ValueTask ExecuteCommand() if (!result) { - Logger?.LogError($"Unable to download package '{Version}'."); + Logger?.LogError($"Unable to download package '{Version}'"); } else { - Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + Logger?.LogInformation($"Firmware package '{Version}' downloaded"); if (explicitVersion == false) { From 15c6d1583ec11a752c3ffe43fd21843eea86312e Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 17 Jan 2024 12:06:51 -0600 Subject: [PATCH 106/141] more housekeeping/hardening/etc --- .../Connections/SimulatorConnection.cs | 5 +++++ .../TextRequestRejectedResponse.cs | 15 +++++++++++++++ .../v2/Meadow.Hcom/Connections/ConnectionBase.cs | 1 + .../v2/Meadow.Hcom/Connections/LocalConnection.cs | 5 +++++ .../Meadow.Hcom/Connections/SerialConnection.cs | 2 +- .../v2/Meadow.Hcom/Connections/TcpConnection.cs | 5 +++++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 1 + .../FileWriteInitFailedSerialResponse.cs | 8 +++++++- .../Meadow.Hcom/SerialResponses/SerialResponse.cs | 2 ++ .../F7FirmwarePackageCollection.cs | 15 +++++++++++++-- .../IFirmwarePackageCollection.cs | 1 + 11 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index 296f422d..78c1b92c 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -141,4 +141,9 @@ public override Task WriteRuntime(string localFileName, CancellationToken? { throw new NotImplementedException(); } + + public override void Detach() + { + throw new NotImplementedException(); + } } diff --git a/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs b/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs new file mode 100644 index 00000000..2f6e0023 --- /dev/null +++ b/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Text; + +namespace Meadow.Hcom; + +internal class TextRequestRejectedResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextRequestRejectedResponse(byte[] data, int length) + : base(data, length) + { + Debug.WriteLine(Text); + } +} diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 650510eb..f1119097 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -19,6 +19,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + public abstract void Detach(); public abstract Task GetDeviceInfo(CancellationToken? cancellationToken = null); public abstract Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index 0861889e..f0cdeae5 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -247,4 +247,9 @@ public override Task WriteRuntime(string localFileName, CancellationToken? { throw new NotImplementedException(); } + + public override void Detach() + { + throw new NotImplementedException(); + } } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 26663cbb..698af035 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -173,7 +173,7 @@ private void Close() State = ConnectionState.Disconnected; } - public void Detach() + public override void Detach() { if (MaintainConnection) { diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 8e3dd4b6..a7b3157f 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -181,4 +181,9 @@ public override Task StartDebugging(int port, ILogger? logger, CancellationToken { throw new NotImplementedException(); } + + public override void Detach() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index d7479502..16e3dc67 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -13,6 +13,7 @@ public interface IMeadowConnection string Name { get; } IMeadowDevice? Device { get; } Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + void Detach(); Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); ConnectionState State { get; } diff --git a/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs b/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs index 653d532b..2135f2ea 100644 --- a/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs +++ b/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs @@ -1,9 +1,15 @@ -namespace Meadow.Hcom; +using System.Diagnostics; +using System.Text; + +namespace Meadow.Hcom; internal class FileWriteInitFailedSerialResponse : SerialResponse { + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + internal FileWriteInitFailedSerialResponse(byte[] data, int length) : base(data, length) { + Debug.Write(Text); } } diff --git a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs index 22836753..bdb66bab 100644 --- a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs +++ b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs @@ -32,6 +32,8 @@ public static SerialResponse Parse(byte[] data, int length) return new TextInformationResponse(data, length); case ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: return new TextRequestResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_REJECTED: + return new TextRequestRejectedResponse(data, length); case ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: return new DeviceInfoSerialResponse(data, length); case ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 5e5fdb0e..9d13bed8 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -13,16 +13,17 @@ public class F7FirmwarePackageCollection : IFirmwarePackageCollection /// public event EventHandler DownloadProgress; + public event EventHandler DefaultVersionChanged; + public string PackageFileRoot { get; } private List _f7Packages = new(); - public FirmwarePackage? DefaultPackage { get; private set; } - public static string DefaultF7FirmwareStoreRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "Firmware"); + private FirmwarePackage? _defaultPackage; internal F7FirmwarePackageCollection() : this(DefaultF7FirmwareStoreRoot) @@ -43,6 +44,16 @@ internal F7FirmwarePackageCollection(string rootPath) PackageFileRoot = rootPath; } + public FirmwarePackage? DefaultPackage + { + get => _defaultPackage; + private set + { + _defaultPackage = value; + DefaultVersionChanged?.Invoke(this, value); + } + } + /// /// Checks the remote (i.e. cloud) store to see if a new firmware package is available. /// diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index e33cb196..822cea0c 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -14,6 +14,7 @@ public interface IFirmwarePackageCollection : IEnumerable /// EventArgs are the total number of bytes retrieved /// public event EventHandler DownloadProgress; + public event EventHandler DefaultVersionChanged; FirmwarePackage? DefaultPackage { get; } Task SetDefaultPackage(string version); From 7afc560634ba406b8fd78a756bda26e1a976a5e3 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 17 Jan 2024 16:50:41 -0800 Subject: [PATCH 107/141] Warnings and defensive coding --- Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs | 9 +++---- .../PackageManager.AssemblyManager.cs | 6 ++--- .../PackageManager.BuildOptions.cs | 2 +- Source/v2/Meadow.Cli/AppManager.cs | 12 ++++----- .../Commands/Current/App/AppDeployCommand.cs | 2 +- .../Commands/Current/App/AppRunCommand.cs | 2 +- .../Command/CloudCommandPublishCommand.cs | 2 +- .../Cloud/Package/CloudPackageListCommand.cs | 2 +- .../Current/Device/DeviceProvisionCommand.cs | 2 +- .../Firmware/FirmwareDefaultCommand.cs | 2 +- .../Current/Firmware/FirmwareWriteCommand.cs | 11 +++++--- .../Current/Port/PortSelectCommand.cs | 5 +++- .../Commands/Legacy/FlashOsCommand.cs | 27 +++++++++++++------ .../v2/Meadow.Cloud.Client/Messages/User.cs | 2 +- .../SerialConnectionTests.cs | 2 +- .../Connections/SerialConnection.cs | 10 +++---- .../Meadow.Hcom/Connections/TcpConnection.cs | 4 +-- .../SerialRequests/RequestBuilder.cs | 2 +- .../DownloadFileStream.cs | 2 +- .../F7FirmwarePackageCollection.cs | 6 ++--- .../Meadow.SoftwareManager/FirmwarePackage.cs | 2 +- 21 files changed, 66 insertions(+), 48 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs index e88d813a..13faa55f 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs @@ -278,8 +278,8 @@ public class DfuDevice : IDisposable public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFunctionDescriptor dfu_descriptor) { - interface_descriptor = interface_descriptor; - dfu_descriptor = dfu_descriptor; + this.interface_descriptor = interface_descriptor; + this.dfu_descriptor = dfu_descriptor; if (NativeMethods.libusb_open(device, ref handle) < 0) { @@ -287,12 +287,11 @@ public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFun } } - public event UploadingEventHandler Uploading; + public event UploadingEventHandler Uploading = default!; protected virtual void OnUploading(UploadingEventArgs e) { - if (Uploading != null) - Uploading(this, e); + Uploading?.Invoke(this, e); } public void ClaimInterface() { diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs index 2d0c4610..2df69f04 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs @@ -6,13 +6,13 @@ public partial class PackageManager public const string PostLinkDirectoryName = "postlink_bin"; public const string PackageOutputDirectoryName = "mpak"; - private string? _meadowAssembliesPath; + private string _meadowAssembliesPath = string.Empty; - private string? MeadowAssembliesPath + private string MeadowAssembliesPath { get { - if (_meadowAssembliesPath == null) + if (string.IsNullOrWhiteSpace(_meadowAssembliesPath)) { // for now we only support F7 // TODO: add switch and support for other platforms var store = _fileManager.Firmware["Meadow F7"]; diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs index 190f44c8..412c1c5a 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs @@ -12,4 +12,4 @@ public record DeployOptions public bool? IncludePDBs { get; set; } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index c52d63d1..edebb7d5 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -32,7 +32,7 @@ public static async Task DeployApplication( string localBinaryDirectory, bool includePdbs, bool includeXmlDocs, - ILogger logger, + ILogger? logger, CancellationToken cancellationToken) { // TODO: add sub-folder support when HCOM supports it @@ -43,7 +43,7 @@ public static async Task DeployApplication( var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); dependencies.Add(Path.Combine(localBinaryDirectory, "App.dll")); - logger.LogInformation("Generating the list of files to deploy..."); + logger?.LogInformation("Generating the list of files to deploy..."); foreach (var file in dependencies) { // TODO: add any other filtering capability here @@ -65,7 +65,7 @@ public static async Task DeployApplication( if (localFiles.Count() == 0) { - logger.LogInformation($"No new files to deploy"); + logger?.LogInformation($"No new files to deploy"); } // get a list of files on-device, with CRCs @@ -85,7 +85,7 @@ public static async Task DeployApplication( // delete those files foreach (var file in removeFiles) { - logger.LogInformation($"Deleting file '{file}'..."); + logger?.LogInformation($"Deleting file '{file}'..."); await connection.DeleteFile(file, cancellationToken); } @@ -94,7 +94,7 @@ public static async Task DeployApplication( { var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == Path.GetFileName(localFile.Key)); - if (existing != null) + if (existing != null && existing.Crc != null) { if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) { @@ -107,7 +107,7 @@ public static async Task DeployApplication( if (!await connection?.WriteFile(localFile.Key, null, cancellationToken)) { - logger.LogWarning($"Error sending'{Path.GetFileName(localFile.Key)}'. Retrying."); + logger?.LogWarning($"Error sending'{Path.GetFileName(localFile.Key)}'. Retrying."); await Task.Delay(100); goto send_file; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 8ff2b81a..7eee58ae 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -92,7 +92,7 @@ protected override async ValueTask ExecuteCommand() file = new FileInfo(path); } - var targetDirectory = file.DirectoryName; + var targetDirectory = file.DirectoryName!; await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, CancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 04f997ca..a5542d8d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class AppRunCommand : BaseDeviceCommand { private readonly IPackageManager _packageManager; - private string _lastFile; + private string? _lastFile; [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] public bool NoPrefix { get; set; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index 9ca34e17..54a6e394 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -69,7 +69,7 @@ protected override async ValueTask ExecuteCommand() { await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } - else if (DeviceIds.Any()) + else if (DeviceIds != null && DeviceIds.Any()) { await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 29a0584b..9794c1e9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -14,7 +14,7 @@ public class CloudPackageListCommand : BaseCloudCommand public string? OrgId { get; set; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] - public string Host { get; set; } + public string? Host { get; set; } public CloudPackageListCommand( IdentityManager identityManager, diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 9f155d95..792d9284 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -44,7 +44,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Retrieving your user and organization information..."); var userOrgs = await _userService.GetUserOrgs(Host, CancellationToken).ConfigureAwait(false); - if (!userOrgs.Any()) + if (userOrgs == null || !userOrgs.Any()) { Logger?.LogInformation($"Please visit {Host} to register your account."); return; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 978ff994..a0b33ef2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -24,7 +24,7 @@ protected override async ValueTask ExecuteCommand() if (Version == null) { - Logger?.LogInformation($"Default firmware is '{collection.DefaultPackage.Version}'."); + Logger?.LogInformation($"Default firmware is '{collection?.DefaultPackage?.Version}'."); } else { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index fc220b81..c5b54558 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -30,7 +30,6 @@ public class FirmwareWriteCommand : BaseDeviceCommand private ISettingsManager Settings { get; } private ILibUsbDevice? _libUsbDevice; - private bool _fileWriteError = false; public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -201,11 +200,17 @@ private async ValueTask WriteFiles(IMeadowConnection connection, FirmwareType[] }; connection.FileWriteFailed += (s, e) => { - _fileWriteError = true; + Logger?.LogError("Error writing file"); }; var package = await GetSelectedPackage(); + if (package == null) + { + Logger?.LogError($"Firware write failed - unable to find selected package"); + return; + } + var wasRuntimeEnabled = await connection!.Device!.IsRuntimeEnabled(CancellationToken); if (wasRuntimeEnabled) @@ -246,7 +251,7 @@ private async ValueTask WriteFiles(IMeadowConnection connection, FirmwareType[] return; } - if (FirmwareFileTypes.Contains(FirmwareType.ESP)) + if (FirmwareFileTypes != null && FirmwareFileTypes.Contains(FirmwareType.ESP)) { Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index 6e2e26af..bf740db6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -50,6 +50,9 @@ private async Task CallConfigCommand(string selectedPort) Settings = new string[] { "route", selectedPort } }; - await setCommand.ExecuteAsync(Console); + if (Console != null) + { + await setCommand.ExecuteAsync(Console); + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index 7acc1849..57a9578c 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -54,6 +54,12 @@ protected override async ValueTask ExecuteCommand() { var package = await GetSelectedPackage(); + if (package == null) + { + Logger?.LogError($"Unable to get selected OS package"); + return; + } + var files = new List(); if (!SkipOS) files.Add(FirmwareType.OS); if (!SkipEsp) files.Add(FirmwareType.ESP); @@ -62,7 +68,7 @@ protected override async ValueTask ExecuteCommand() if (Files == null) { - Logger.LogInformation($"Writing all firmware for version '{package.Version}'..."); + Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); Files = new FirmwareType[] { @@ -80,7 +86,7 @@ protected override async ValueTask ExecuteCommand() bool deviceSupportsOta = false; // TODO: get this based on device OS version - if (package?.OsWithoutBootloader == null + if (package.OsWithoutBootloader == null || !deviceSupportsOta || UseDfu) { @@ -259,7 +265,14 @@ private async ValueTask WriteFiles() Logger?.LogInformation(message); }; - var package = await GetSelectedPackage(); + + var pack = await GetSelectedPackage(); + + if (pack == null) + { + Logger?.LogError($"Unable to get selected OS package"); + } + FirmwarePackage package = pack!; var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); @@ -275,7 +288,7 @@ private async ValueTask WriteFiles() Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); }; - if (Files.Contains(FirmwareType.OS)) + if (Files!.Contains(FirmwareType.OS)) { if (UseDfu) { @@ -288,7 +301,7 @@ private async ValueTask WriteFiles() throw new NotSupportedException("OtA writes for the OS are not yet supported"); } } - if (Files.Contains(FirmwareType.Runtime)) + if (Files!.Contains(FirmwareType.Runtime)) { Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); @@ -299,7 +312,7 @@ private async ValueTask WriteFiles() await connection.Device.WriteRuntime(rtpath, CancellationToken); } - if (Files.Contains(FirmwareType.ESP)) + if (Files!.Contains(FirmwareType.ESP)) { Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); @@ -313,8 +326,6 @@ private async ValueTask WriteFiles() await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); } - Logger?.LogInformation($"{Environment.NewLine}"); - if (wasRuntimeEnabled) { await connection.Device.RuntimeEnable(); diff --git a/Source/v2/Meadow.Cloud.Client/Messages/User.cs b/Source/v2/Meadow.Cloud.Client/Messages/User.cs index 1951d6e6..4c6ac125 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/User.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/User.cs @@ -7,4 +7,4 @@ public record User public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get; set; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs index 92467a24..707a94d0 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -16,7 +16,7 @@ public void TestInvalidPortName() } [Fact] - public async void TestListen() + public void TestListen() { using (var connection = new SerialConnection(ValidPortName)) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 698af035..2ca4f621 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -16,10 +16,10 @@ public partial class SerialConnection : ConnectionBase, IDisposable public const int ReadBufferSizeBytes = 0x2000; private const int DefaultTimeout = 5000; - private event EventHandler FileReadCompleted = delegate { }; - private event EventHandler FileWriteAccepted; - private event EventHandler FileDataReceived; - public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + private event EventHandler FileReadCompleted = default!; + private event EventHandler FileWriteAccepted = default!; + private event EventHandler FileDataReceived = default!; + public event ConnectionStateChangedHandler ConnectionStateChanged = default!; private readonly SerialPort _port; private readonly ILogger? _logger; @@ -1178,7 +1178,7 @@ public override async Task GetPublicKey(CancellationToken? cancellationT { var command = RequestBuilder.Build(); - string? contents = null; + string contents = string.Empty; void OnFileDataReceived(object? sender, string data) { diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index a7b3157f..207d6a4f 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -5,8 +5,8 @@ namespace Meadow.Hcom; public class TcpConnection : ConnectionBase { - private HttpClient _client; - private string _baseUri; + private readonly HttpClient _client; + private readonly string _baseUri; public override string Name => _baseUri; diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs index 0fac22e8..c20117a2 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs @@ -2,7 +2,7 @@ { public static class RequestBuilder { - private static uint _sequenceNumber; + //private static uint _sequenceNumber; public static T Build(uint userData = 0, ushort extraData = 0, ushort protocol = Protocol.HCOM_PROTOCOL_HCOM_VERSION_NUMBER) where T : Request, new() diff --git a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs index da0f1e66..adad19f6 100644 --- a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs +++ b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs @@ -6,7 +6,7 @@ namespace Meadow.Software; internal class DownloadFileStream : Stream, IDisposable { - public event EventHandler DownloadProgress; + public event EventHandler DownloadProgress = default!; private readonly Stream _stream; diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 9d13bed8..ffac2127 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -11,13 +11,13 @@ namespace Meadow.Software; public class F7FirmwarePackageCollection : IFirmwarePackageCollection { /// - public event EventHandler DownloadProgress; + public event EventHandler DownloadProgress = default!; public event EventHandler DefaultVersionChanged; public string PackageFileRoot { get; } - private List _f7Packages = new(); + private readonly List _f7Packages = new(); public static string DefaultF7FirmwareStoreRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @@ -85,7 +85,7 @@ public Task DeletePackage(string version) // if we're deleting the default, we need to det another default var i = _f7Packages.Count - 1; - while (DefaultPackage.Version == _f7Packages[i].Version) + while (DefaultPackage?.Version == _f7Packages[i].Version) { i--; } diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs index ac963c90..339953f9 100644 --- a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -11,7 +11,7 @@ internal FirmwarePackage(IFirmwarePackageCollection collection) _collection = collection; } - public string GetFullyQualifiedPath(string file) + public string GetFullyQualifiedPath(string? file) { return Path.Combine(_collection.PackageFileRoot, Version, file); } From b85c0c3854409cd3df4c0f356b96a62ffb0c1ef5 Mon Sep 17 00:00:00 2001 From: Steven Kuhn Date: Wed, 17 Jan 2024 20:01:38 -0600 Subject: [PATCH 108/141] Updated command parameters and options to be more consistent --- .../Cloud/ApiKey/CloudApiKeyCreateCommand.cs | 8 ++++---- .../Cloud/ApiKey/CloudApiKeyDeleteCommand.cs | 2 +- .../Cloud/ApiKey/CloudApiKeyUpdateCommand.cs | 6 +++--- .../Commands/Current/App/AppBuildCommand.cs | 2 +- .../Commands/Current/App/AppDebugCommand.cs | 2 +- .../Commands/Current/App/AppDeployCommand.cs | 2 +- .../Commands/Current/App/AppRunCommand.cs | 6 +++--- .../Commands/Current/App/AppTrimCommand.cs | 4 ++-- .../Collection/CloudCollectionListCommand.cs | 4 +++- .../Command/CloudCommandPublishCommand.cs | 20 +++++++++---------- .../Package/CloudPackageCreateCommand.cs | 6 +++--- .../Cloud/Package/CloudPackageListCommand.cs | 2 +- .../Package/CloudPackagePublishCommand.cs | 6 +++--- .../Package/CloudPackageUploadCommand.cs | 6 +++--- .../Commands/Current/Config/ConfigCommand.cs | 4 ++-- .../Commands/Current/DeveloperCommand.cs | 8 ++++---- .../Current/Device/DeviceClockCommand.cs | 2 +- .../Current/Device/DeviceProvisionCommand.cs | 6 +++--- .../Current/File/FileDeleteCommand.cs | 2 +- .../Current/File/FileInitialCommand.cs | 2 +- .../Commands/Current/File/FileListCommand.cs | 4 ++-- .../Commands/Current/File/FileReadCommand.cs | 4 ++-- .../Commands/Current/File/FileWriteCommand.cs | 3 ++- .../Firmware/FirmwareDefaultCommand.cs | 2 +- .../Current/Firmware/FirmwareDeleteCommand.cs | 2 +- .../Firmware/FirmwareDownloadCommand.cs | 4 ++-- .../Current/Firmware/FirmwareListCommand.cs | 2 +- .../Commands/Current/ListenCommand.cs | 4 ++-- .../Current/Trace/TraceLevelCommand.cs | 2 +- 29 files changed, 65 insertions(+), 62 deletions(-) diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs index 73fccc32..adba5992 100644 --- a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs @@ -10,15 +10,15 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudApiKeyCreateCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the API key", IsRequired = true, Name = "NAME")] - public string? Name { get; set; } + public string Name { get; init; } = default!; [CommandOption("duration", 'd', Description = "The duration of the API key, in days", IsRequired = true)] - public int Duration { get; set; } + public int Duration { get; init; } = default!; [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key", IsRequired = true)] - public string[]? Scopes { get; set; } + public string[] Scopes { get; init; } = default!; - [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})")] + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] public string? Host { get; set; } private ApiTokenService ApiTokenService { get; } diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs index 9a6044d2..206acf01 100644 --- a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs @@ -10,7 +10,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudApiKeyDeleteCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] - public string? NameOrId { get; set; } + public string NameOrId { get; init; } = default!; [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] public string? Host { get; set; } diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs index 944a6cf5..88b00336 100644 --- a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs @@ -10,12 +10,12 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudApiKeyUpdateCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] - public string? NameOrId { get; set; } + public string NameOrId { get; init; } = default!; - [CommandOption("name", 'n', Description = "The new name to use for the API key")] + [CommandOption("name", 'n', Description = "The new name to use for the API key", IsRequired = false)] public string? NewName { get; set; } - [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key")] + [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key", IsRequired = false)] public string[]? Scopes { get; set; } [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index 04c21517..9e169169 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -12,7 +12,7 @@ public class AppBuildCommand : BaseCommand public string? Configuration { get; set; } [CommandParameter(0, Name = "Path to project file", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) : base(loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index 4655a1c6..be9cca93 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -9,7 +9,7 @@ public class AppDebugCommand : BaseDeviceCommand // VS 2019 - 4024 // VS 2017 - 4022 // VS 2015 - 4020 - [CommandOption("Port", 'p', Description = "The port to run the debug server on")] + [CommandOption("Port", 'p', Description = "The port to run the debug server on", IsRequired = false)] public int Port { get; init; } = 4024; public AppDebugCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 7eee58ae..a7601165 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -10,7 +10,7 @@ public class AppDeployCommand : BaseDeviceCommand private readonly IPackageManager _packageManager; [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index a5542d8d..b211797e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -10,14 +10,14 @@ public class AppRunCommand : BaseDeviceCommand private readonly IPackageManager _packageManager; private string? _lastFile; - [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] - public bool NoPrefix { get; set; } + [CommandOption("no-prefix", 'n', Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'", IsRequired = false)] + public bool NoPrefix { get; init; } [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] public string? Configuration { get; set; } [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 6fb547bf..0fc8f67d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -9,10 +9,10 @@ public class AppTrimCommand : BaseCommand private readonly IPackageManager _packageManager; [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] - public string? Configuration { get; set; } + public string? Configuration { get; init; } [CommandParameter(0, Name = "Path to project file", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppTrimCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) : base(loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs index 90867fdb..684e917d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs @@ -10,8 +10,9 @@ public class CloudCollectionListCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string CommandName { get; set; } = string.Empty; + public string CommandName { get; init; } = default!; - [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] - public string? CollectionId { get; set; } + [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command", IsRequired = false)] + public string? CollectionId { get; init; } - [CommandOption("deviceIds", 'd', Description = "The target devices for publishing the command")] - public string[]? DeviceIds { get; set; } + [CommandOption("deviceIds", 'd', Description = "The target devices for publishing the command", IsRequired = false)] + public string[]? DeviceIds { get; init; } - [CommandOption("args", 'a', Description = "The arguments for the command as a JSON string", Converter = typeof(JsonDocumentBindingConverter))] - public JsonDocument? Arguments { get; set; } + [CommandOption("args", 'a', Description = "The arguments for the command as a JSON string", Converter = typeof(JsonDocumentBindingConverter), IsRequired = false)] + public JsonDocument? Arguments { get; init; } - [CommandOption("qos", 'q', Description = "The MQTT-defined quality of service for the command")] - public QualityOfService QualityOfService { get; set; } = QualityOfService.AtLeastOnce; + [CommandOption("qos", 'q', Description = "The MQTT-defined quality of service for the command", IsRequired = false)] + public QualityOfService QualityOfService { get; init; } = QualityOfService.AtLeastOnce; - [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)")] + [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } private CommandService CommandService { get; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index 718d8fa3..34a8601f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -10,13 +10,13 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class CloudPackageCreateCommand : BaseCloudCommand { [CommandParameter(0, Name = "Path to project file", IsRequired = false)] - public string? ProjectPath { get; set; } = default!; + public string? ProjectPath { get; set; } [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] - public string Configuration { get; set; } = "Release"; + public string Configuration { get; init; } = "Release"; [CommandOption("name", 'n', Description = "Name of the mpak file to be created", IsRequired = false)] - public string? MpakName { get; init; } = default!; + public string? MpakName { get; init; } [CommandOption("filter", 'f', Description = "Glob pattern to filter files. ex ('app.dll', 'app*','{app.dll,meadow.dll}')", IsRequired = false)] diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 9794c1e9..2bdbf2be 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -11,7 +11,7 @@ public class CloudPackageListCommand : BaseCloudCommand private readonly PackageService _packageService; [CommandOption("orgId", 'o', Description = "Optional organization ID", IsRequired = false)] - public string? OrgId { get; set; } + public string? OrgId { get; init; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index b1f516f9..d4232729 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -11,13 +11,13 @@ public class CloudPackagePublishCommand : BaseCloudCommand { [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] - public string MpakPath { get; init; } = string.Empty; + public string MpakPath { get; init; } = default!; [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] - public string? OrgId { get; set; } + public string? OrgId { get; init; } [CommandOption("description", 'd', Description = "Description of the package", IsRequired = false)] - public string? Description { get; set; } + public string? Description { get; init; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs index d5bcbad8..b1e47b1f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs @@ -8,10 +8,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class ConfigCommand : BaseSettingsCommand { [CommandOption("list", IsRequired = false)] - public bool List { get; set; } + public bool List { get; init; } [CommandParameter(0, Name = "Settings", IsRequired = false)] - public string[]? Settings { get; set; } + public string[]? Settings { get; init; } public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) : base(settingsManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index 5fcd5185..d09ee1f6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -6,11 +6,11 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("developer", Description = "Sets a specified developer parameter on the Meadow")] public class DeveloperCommand : BaseDeviceCommand { - [CommandOption("param", 'p', Description = "The parameter to set.")] - public ushort Parameter { get; set; } + [CommandOption("param", 'p', Description = "The parameter to set.", IsRequired = false)] + public ushort Parameter { get; init; } - [CommandOption("value", 'v', Description = "The value to apply to the parameter. Valid values are 0 to 4,294,967,295")] - public uint Value { get; set; } + [CommandOption("value", 'v', Description = "The value to apply to the parameter. Valid values are 0 to 4,294,967,295", IsRequired = false)] + public uint Value { get; init; } public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index 9b217762..0d31628c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class DeviceClockCommand : BaseDeviceCommand { [CommandParameter(0, Name = "Time", IsRequired = false)] - public string? Time { get; set; } + public string? Time { get; init; } public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 792d9284..441afceb 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -16,10 +16,10 @@ public class DeviceProvisionCommand : BaseDeviceCommand public string? OrgId { get; set; } [CommandOption("collectionId", 'c', Description = "The target collection for device registration", IsRequired = false)] - public string? CollectionId { get; set; } + public string? CollectionId { get; init; } [CommandOption("name", 'n', Description = "Device friendly name", IsRequired = false)] - public string? Name { get; set; } + public string? Name { get; init; } [CommandOption("host", 'h', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } @@ -36,7 +36,7 @@ protected override async ValueTask ExecuteCommand() try { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var identityManager = new IdentityManager(Logger); var _userService = new UserService(identityManager); diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 850f9951..60b0c73b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FileDeleteCommand : BaseDeviceCommand { [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; + public string MeadowFile { get; init; } = default!; public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index c97568f8..d850566b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FileInitialCommand : BaseDeviceCommand { [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; + public string MeadowFile { get; init; } = default!; public FileInitialCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index e0dc3de3..20be06d3 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -9,10 +9,10 @@ public class FileListCommand : BaseDeviceCommand public const int FileSystemBlockSize = 4096; [CommandOption("verbose", 'v', IsRequired = false)] - public bool Verbose { get; set; } + public bool Verbose { get; init; } [CommandParameter(0, Name = "Folder", IsRequired = false)] - public string? Folder { get; set; } = default!; + public string? Folder { get; set; } public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index 4e2259c8..384276f1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -7,10 +7,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FileReadCommand : BaseDeviceCommand { [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; + public string MeadowFile { get; init; } = default!; [CommandParameter(1, Name = "LocalFile", IsRequired = false)] - public string? LocalFile { get; set; } + public string? LocalFile { get; init; } public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index ff7200c8..b736055f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -16,7 +16,8 @@ public class FileWriteCommand : BaseDeviceCommand [CommandOption( "targetFiles", 't', - Description = "The filename(s) to use on the Meadow File System")] + Description = "The filename(s) to use on the Meadow File System", + IsRequired = false)] public IList TargetFileNames { get; init; } = Array.Empty(); public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index a0b33ef2..8fb3c02e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -12,7 +12,7 @@ public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settings { } [CommandParameter(0, Name = "Version number to use as default", IsRequired = false)] - public string? Version { get; set; } = null; + public string? Version { get; init; } protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index f6625ab9..32188417 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -12,7 +12,7 @@ public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsM { } [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] - public string Version { get; set; } = default!; + public string Version { get; init; } = default!; protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index e11e4660..d12e4928 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -12,10 +12,10 @@ public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager setting { } [CommandOption("force", 'f', IsRequired = false)] - public bool Force { get; set; } + public bool Force { get; init; } [CommandOption("version", 'v', IsRequired = false)] - public string? Version { get; set; } = default!; + public string? Version { get; set; } protected override async ValueTask ExecuteCommand() { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index ffb763c7..7140fb31 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -8,7 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FirmwareListCommand : BaseCommand { [CommandOption("verbose", 'v', IsRequired = false)] - public bool Verbose { get; set; } + public bool Verbose { get; init; } private FileManager FileManager { get; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index e2f703c6..d1d3e559 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -6,8 +6,8 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("listen", Description = "Listen for console output from Meadow")] public class ListenCommand : BaseDeviceCommand { - [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed")] - public bool NoPrefix { get; set; } + [CommandOption("no-prefix", 'n', Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed", IsRequired = false)] + public bool NoPrefix { get; init; } public ListenCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index 1b66ff11..27889c80 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class TraceLevelCommand : BaseDeviceCommand { [CommandParameter(0, Name = "Level", IsRequired = true)] - public int Level { get; set; } + public int Level { get; init; } public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) From 63d94132cd586c2f0d059529794712f98cff55a6 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 17 Jan 2024 21:04:34 -0800 Subject: [PATCH 109/141] Warnings cleanup --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- Source/v2/Meadow.Cli/AppManager.cs | 6 +- .../Commands/Current/App/AppDeployCommand.cs | 2 +- .../Commands/Current/App/AppRunCommand.cs | 2 +- .../Package/CloudPackageUploadCommand.cs | 2 +- .../Current/Device/DeviceProvisionCommand.cs | 6 +- .../v2/Meadow.Cli/MeadowConnectionManager.cs | 8 +- .../v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- .../Meadow.Cloud.Client/Identity/LibSecret.cs | 4 +- .../Services/PackageService.cs | 4 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- .../Connections/SerialConnection.cs | 36 ++++---- .../Meadow.Hcom/Connections/TcpConnection.cs | 6 +- .../Meadow.Hcom/Debugging/DebuggingServer.cs | 92 ++++++++++--------- .../Meadow.Hcom/Firmware/FirmwareManager.cs | 2 +- .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 46 +++++----- .../SerialRequests/SetRtcTimeRequest.cs | 7 +- .../F7FirmwarePackageCollection.cs | 8 +- 18 files changed, 122 insertions(+), 115 deletions(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 766ae353..40124bf4 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0.5 + 2.0.0.6 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index edebb7d5..994a8fac 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -77,9 +77,9 @@ public static async Task DeployApplication( .Except(localFiles.Keys .Select(f => Path.GetFileName(f))); - if (removeFiles.Count() == 0) + if (!removeFiles.Any()) { - logger.LogInformation($"No files to delete"); + logger?.LogInformation($"No files to delete"); } // delete those files @@ -105,7 +105,7 @@ public static async Task DeployApplication( send_file: - if (!await connection?.WriteFile(localFile.Key, null, cancellationToken)) + if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) { logger?.LogWarning($"Error sending'{Path.GetFileName(localFile.Key)}'. Retrying."); await Task.Delay(100); diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 7eee58ae..b8df22da 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -99,7 +99,7 @@ protected override async ValueTask ExecuteCommand() if (wasRuntimeEnabled) { // restore runtime state - Logger.LogInformation("Enabling runtime..."); + Logger?.LogInformation("Enabling runtime..."); await connection.RuntimeEnable(CancellationToken); } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index a5542d8d..7328dafe 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -135,7 +135,7 @@ private async Task DeployApplication(IMeadowConnection connection, string Logger?.LogInformation($"Deploying app from {file.DirectoryName}..."); - await AppManager.DeployApplication(_packageManager, connection, file.DirectoryName, true, false, Logger, CancellationToken); + await AppManager.DeployApplication(_packageManager, connection, file.DirectoryName!, true, false, Logger, CancellationToken); connection.FileWriteProgress -= OnFileWriteProgress; diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index f3614c18..1db57b0f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -51,7 +51,7 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); - var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken); + var package = await _packageService.UploadPackage(MpakPath, org.Id, Description ?? string.Empty, Host, CancellationToken); Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); } catch (MeadowCloudException mex) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 792d9284..f5c9284b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -32,11 +32,11 @@ public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManag protected override async ValueTask ExecuteCommand() { - UserOrg org; + UserOrg? org; try { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; var identityManager = new IdentityManager(Logger); var _userService = new UserService(identityManager); @@ -44,6 +44,7 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Retrieving your user and organization information..."); var userOrgs = await _userService.GetUserOrgs(Host, CancellationToken).ConfigureAwait(false); + if (userOrgs == null || !userOrgs.Any()) { Logger?.LogInformation($"Please visit {Host} to register your account."); @@ -60,6 +61,7 @@ protected override async ValueTask ExecuteCommand() } org = userOrgs.FirstOrDefault(o => o.Id == OrgId || o.Name == OrgId); + if (org == null) { Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{OrgId}'"); diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index f00084ce..0e392ddd 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -227,10 +227,12 @@ public static IList GetMeadowSerialPortsForWindows() foreach (ManagementObject moResult in searcher.Get()) { // Try Caption and if not Name, they both seems to contain the COM port - string portLongName = moResult["Caption"].ToString(); + string portLongName = $"{moResult["Caption"]}"; if (string.IsNullOrEmpty(portLongName)) - portLongName = moResult["Name"].ToString(); - string pnpDeviceId = moResult["PNPDeviceID"].ToString(); + { + portLongName = $"{moResult["Name"]}"; + } + string pnpDeviceId = $"{moResult["PNPDeviceID"]}"; // we could collect and return a fair bit of other info from the query: diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index 4600f0e0..e5fd00c5 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.5"; + public const string CLI_VERSION = "2.0.0.6"; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs index 7b6f40d0..73530971 100644 --- a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs +++ b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs @@ -45,13 +45,13 @@ public LibSecret(String service, String account) (int)AttributeType.STRING, IntPtr.Zero); } - public void SetSecret(String password) + public void SetSecret(string password) { _ = secret_password_store_sync(intPt, COLLECTION_SESSION, $"{Service}/{Account}", password, IntPtr.Zero, out IntPtr errorPtr, serviceLabel, Service, accountLabel, Account, IntPtr.Zero); HandleError(errorPtr, "An error was encountered while writing secret to keyring"); } - public string GetSecret() + public string? GetSecret() { IntPtr passwordPtr = secret_password_lookup_sync(intPt, IntPtr.Zero, out IntPtr errorPtr, serviceLabel, Service, accountLabel, Account, IntPtr.Zero); HandleError(errorPtr, "An error was encountered while reading secret from keyring"); diff --git a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs index 02f3f260..8ee60027 100644 --- a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs +++ b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs @@ -9,7 +9,7 @@ namespace Meadow.Cloud; public class PackageService : CloudServiceBase { - private string _info_json = "info.json"; + private readonly string _info_json = "info.json"; public PackageService(IdentityManager identityManager) : base(identityManager) { @@ -86,7 +86,7 @@ private string GetPackageOsVersion(string packagePath) { var content = File.ReadAllText(tempInfoJson); var packageInfo = JsonSerializer.Deserialize(content); - result = packageInfo.OsVersion; + result = packageInfo?.OsVersion ?? string.Empty; File.Delete(tempInfoJson); } diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index f1119097..85b906cc 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -6,7 +6,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable { private bool _isDisposed; - public ConnectionState State { get; protected set; } + public virtual ConnectionState State { get; protected set; } public IMeadowDevice? Device { get; protected set; } public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 2ca4f621..fea79e9e 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -134,12 +134,12 @@ public void RemoveListener(IConnectionListener listener) // TODO: stop maintaining connection? } - public ConnectionState State + public override ConnectionState State { get => _state; - private set + protected set { - if (value == State) return; + if (value == State) { return; } var old = _state; _state = value; @@ -237,7 +237,7 @@ public override void Detach() } } - private async void CommandManager() + private void CommandManager() { while (!_isDisposed) { @@ -1203,25 +1203,23 @@ void OnFileDataReceived(object? sender, string data) public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) { - if (Device != null) + if (Device == null) { - logger?.LogDebug($"Start Debugging on port: {port}"); - await Device.StartDebugging(port, logger, cancellationToken); + throw new DeviceNotFoundException(); + } - /* TODO logger?.LogDebug("Reinitialize the device"); - await ReInitializeMeadow(cancellationToken); */ + logger?.LogDebug($"Start Debugging on port: {port}"); + await Device.StartDebugging(port, logger, cancellationToken); - var endpoint = new IPEndPoint(IPAddress.Loopback, port); - var debuggingServer = new DebuggingServer(Device, endpoint, logger); + /* TODO logger?.LogDebug("Reinitialize the device"); + await ReInitializeMeadow(cancellationToken); */ - logger?.LogDebug("Tell the Debugging Server to Start Listening"); - await debuggingServer.StartListening(cancellationToken); - return debuggingServer; - } - else - { - throw new DeviceNotFoundException(); - } + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + var debuggingServer = new DebuggingServer(Device, endpoint, logger); + + logger?.LogDebug("Tell the Debugging Server to Start Listening"); + await debuggingServer.StartListening(cancellationToken); + return debuggingServer; } public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 207d6a4f..022011bc 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -16,7 +16,7 @@ public TcpConnection(string uri) _client = new HttpClient(); } - public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + public override Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { /* var request = RequestBuilder.Build(); @@ -43,7 +43,9 @@ public TcpConnection(string uri) */ // TODO: is there a way to "attach"? ping result? device info? - return Device = new MeadowDevice(this); + Device = new MeadowDevice(this); + + return Task.FromResult(Device); // TODO: web socket for listen? } diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index 22e20cad..cddd4fd3 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -1,9 +1,9 @@ -using System.Buffers; +using Microsoft.Extensions.Logging; +using System.Buffers; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; -using Microsoft.Extensions.Logging; namespace Meadow.Hcom; @@ -18,12 +18,12 @@ public class DebuggingServer : IDisposable public IPEndPoint LocalEndpoint { get; private set; } private readonly object _lck = new object(); - private CancellationTokenSource? _cancellationTokenSource; - private readonly ILogger _logger; + private CancellationTokenSource _cancellationTokenSource; + private readonly ILogger? _logger; private readonly IMeadowDevice _meadow; private ActiveClient? _activeClient; private int _activeClientCount = 0; - private TcpListener _listener; + private readonly TcpListener _listener; private Task? _listenerTask; private bool _isReady; public bool Disposed; @@ -35,7 +35,7 @@ public class DebuggingServer : IDisposable /// The to debug /// The to listen for incoming debugger connections /// The to logging state information - public DebuggingServer(IMeadowDevice meadow, IPEndPoint localEndpoint, ILogger logger) + public DebuggingServer(IMeadowDevice meadow, IPEndPoint localEndpoint, ILogger? logger) { LocalEndpoint = localEndpoint; _meadow = meadow; @@ -77,8 +77,7 @@ public async Task StopListening() { _listener?.Stop(); - if (_cancellationTokenSource != null) - _cancellationTokenSource?.Cancel(false); + _cancellationTokenSource?.Cancel(false); if (_listenerTask != null) { @@ -92,7 +91,7 @@ private async Task StartListener() { _listener.Start(); LocalEndpoint = (IPEndPoint)_listener.LocalEndpoint; - _logger.LogInformation($"Listening for Visual Studio to connect on {LocalEndpoint.Address}:{LocalEndpoint.Port}" + Environment.NewLine); + _logger?.LogInformation($"Listening for Visual Studio to connect on {LocalEndpoint.Address}:{LocalEndpoint.Port}" + Environment.NewLine); _isReady = true; // This call will wait for the client to connect, before continuing. We shouldn't need a loop. @@ -101,13 +100,13 @@ private async Task StartListener() } catch (SocketException soex) { - _logger.LogError("A Socket error occurred. The port may already be in use. Try rebooting to free up the port."); - _logger.LogError($"Error:\n{soex.Message} \nStack Trace:\n{soex.StackTrace}"); + _logger?.LogError("A Socket error occurred. The port may already be in use. Try rebooting to free up the port."); + _logger?.LogError($"Error:\n{soex.Message} \nStack Trace:\n{soex.StackTrace}"); } catch (Exception ex) { - _logger.LogError("An unhandled exception occurred while listening for debugging connections."); - _logger.LogError($"Error:\n{ex.Message} \nStack Trace:\n{ex.StackTrace}"); + _logger?.LogError("An unhandled exception occurred while listening for debugging connections."); + _logger?.LogError($"Error:\n{ex.Message} \nStack Trace:\n{ex.StackTrace}"); } } @@ -117,10 +116,10 @@ private void OnConnect(TcpClient tcpClient) { lock (_lck) { - _logger.LogInformation("Visual Studio has Connected" + Environment.NewLine); + _logger?.LogInformation("Visual Studio has Connected" + Environment.NewLine); if (_activeClientCount > 0 && _activeClient?.Disposed == false) { - _logger.LogDebug("Closing active client"); + _logger?.LogDebug("Closing active client"); Debug.Assert(_activeClientCount == 1); Debug.Assert(_activeClient != null); CloseActiveClient(); @@ -132,7 +131,7 @@ private void OnConnect(TcpClient tcpClient) } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while connecting to Visual Studio"); + _logger?.LogError(ex, "An error occurred while connecting to Visual Studio"); } } @@ -147,8 +146,10 @@ public void Dispose() { lock (_lck) { - if (Disposed) - return; + if (Disposed) + { + return; + } _cancellationTokenSource?.Cancel(false); _activeClient?.Dispose(); _listenerTask?.Dispose(); @@ -166,18 +167,18 @@ private class ActiveClient : IDisposable private readonly CancellationTokenSource _cts; private readonly Task _receiveVsDebugDataTask; private readonly Task _receiveMeadowDebugDataTask; - private readonly ILogger _logger; + private readonly ILogger? _logger; public bool Disposed = false; // Constructor - internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger logger, CancellationToken cancellationToken) + internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger? logger, CancellationToken cancellationToken) { _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _logger = logger; _meadow = meadow; _tcpClient = tcpClient; _networkStream = tcpClient.GetStream(); - _logger.LogDebug("Starting receive task"); + _logger?.LogDebug("Starting receive task"); _receiveVsDebugDataTask = Task.Factory.StartNew(SendToMeadowAsync, TaskCreationOptions.LongRunning); _receiveMeadowDebugDataTask = Task.Factory.StartNew(SendToVisualStudio, TaskCreationOptions.LongRunning); } @@ -208,7 +209,7 @@ private async Task SendToMeadowAsync() Array.Copy(receiveBuffer, 0, meadowBuffer, destIndex, bytesRead); // Forward the RECIEVE_BUFFER_SIZE chunk to Meadow immediately - _logger.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", + _logger?.LogTrace("Received {count} bytes from VS, will forward to HCOM/Meadow. {hash}", meadowBuffer.Length, BitConverter.ToString(md5.ComputeHash(meadowBuffer)) .Replace("-", string.Empty) @@ -223,31 +224,31 @@ private async Task SendToMeadowAsync() else { // User probably hit stop - _logger.LogInformation("Unable to Read Data from Visual Studio"); - _logger.LogTrace("Unable to Read Data from Visual Studio"); + _logger?.LogInformation("Unable to Read Data from Visual Studio"); + _logger?.LogTrace("Unable to Read Data from Visual Studio"); } } } catch (IOException ioe) { // VS client probably died - _logger.LogInformation("Visual Studio has Disconnected" + Environment.NewLine); - _logger.LogTrace(ioe, "Visual Studio has Disconnected"); + _logger?.LogInformation("Visual Studio has Disconnected" + Environment.NewLine); + _logger?.LogTrace(ioe, "Visual Studio has Disconnected"); } catch (ObjectDisposedException ode) { // User probably hit stop - _logger.LogInformation("Visual Studio has stopped debugging" + Environment.NewLine); - _logger.LogTrace(ode, "Visual Studio has stopped debugging"); + _logger?.LogInformation("Visual Studio has stopped debugging" + Environment.NewLine); + _logger?.LogTrace(ode, "Visual Studio has stopped debugging"); } catch (Exception ex) { - _logger.LogError($"Error receiving data from Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); + _logger?.LogError($"Error receiving data from Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); throw; } } - private async Task SendToVisualStudio() + private Task SendToVisualStudio() { try { @@ -258,22 +259,22 @@ private async Task SendToVisualStudio() /* TODO while (_meadow.DataProcessor.DebuggerMessages.Count > 0) { var byteData = _meadow.DataProcessor.DebuggerMessages.Take(_cts.Token); - _logger.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); + _logger?.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); if (!_tcpClient.Connected) { - _logger.LogDebug("Cannot forward data, Visual Studio is not connected"); + _logger?.LogDebug("Cannot forward data, Visual Studio is not connected"); return; } await _networkStream.WriteAsync(byteData, 0, byteData.Length, _cts.Token); - _logger.LogTrace("Forwarded {count} bytes to VS", byteData.Length); + _logger?.LogTrace("Forwarded {count} bytes to VS", byteData.Length); }*/ } else { // User probably hit stop - _logger.LogInformation("Unable to Write Data from Visual Studio"); - _logger.LogTrace("Unable to Write Data from Visual Studio"); + _logger?.LogInformation("Unable to Write Data from Visual Studio"); + _logger?.LogTrace("Unable to Write Data from Visual Studio"); } } } @@ -281,26 +282,31 @@ private async Task SendToVisualStudio() { // User probably hit stop; Removed logging as User doesn't need to see this // Keeping it as a TODO in case we find a side effect that needs logging. - // TODO _logger.LogInformation("Operation Cancelled"); - // TODO _logger.LogTrace(oce, "Operation Cancelled"); + // TODO _logger?.LogInformation("Operation Cancelled"); + // TODO _logger?.LogTrace(oce, "Operation Cancelled"); } catch (Exception ex) { - _logger.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); + _logger?.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); - if (_cts.IsCancellationRequested) - throw; + if (_cts.IsCancellationRequested) + { + throw; + } } + return Task.CompletedTask; } public void Dispose() { lock (_tcpClient) { - if (Disposed) - return; + if (Disposed) + { + return; + } - _logger.LogTrace("Disposing ActiveClient"); + _logger?.LogTrace("Disposing ActiveClient"); _cts.Cancel(false); _receiveVsDebugDataTask.Wait(TimeSpan.FromSeconds(10)); _receiveMeadowDebugDataTask.Wait(TimeSpan.FromSeconds(10)); diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs index c0d6aaa0..63483930 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs @@ -38,7 +38,7 @@ public static async Task GetCloudLatestFirmwareVersion() { var json = await reader.ReadToEndAsync(); - if (json == null) return string.Empty; + if (json == null) { return string.Empty; } return JsonSerializerExtensions.DeserializeAnonymousType(json, new { version = string.Empty }).version; } diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index 5bf5aea1..c1aeeaba 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -6,7 +6,6 @@ namespace Meadow.Hcom; public class FirmwareUpdater { private readonly ILogger? _logger; - private readonly Task? _updateTask; private IMeadowConnection _connection; private UpdateState _state; @@ -45,7 +44,7 @@ private set if (value == _state) return; PreviousState = CurrentState; _state = value; - _logger.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); + _logger?.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); } } @@ -63,9 +62,9 @@ private async void StateMachine() try { // make sure we have a current device info - info = await _connection.Device.GetDeviceInfo(); + info = await _connection.Device!.GetDeviceInfo(); - if (info.OsVersion == RequestedVersion) + if (info?.OsVersion == RequestedVersion) { // no need to update, it's already there CurrentState = UpdateState.DFUCompleted; @@ -96,7 +95,7 @@ private async void StateMachine() ++tries; if (tries > 5) { - _logger.LogError($"Failed to enter DFU mode: {ex.Message}"); + _logger?.LogError($"Failed to enter DFU mode: {ex.Message}"); CurrentState = UpdateState.Error; // exit state machine @@ -137,10 +136,7 @@ private async void StateMachine() await _connection.WaitForMeadowAttach(); await Task.Delay(2000); // wait 2 seconds to allow full boot - if (info == null) - { - info = await _connection.Device.GetDeviceInfo(); - } + info ??= await _connection.Device!.GetDeviceInfo(); CurrentState = UpdateState.DisablingMonoForRuntime; } @@ -154,7 +150,7 @@ private async void StateMachine() case UpdateState.DisablingMonoForRuntime: try { - await _connection.Device.RuntimeDisable(); + await _connection.Device!.RuntimeDisable(); } catch (Exception ex) { @@ -165,7 +161,7 @@ private async void StateMachine() CurrentState = UpdateState.UpdatingRuntime; break; case UpdateState.UpdatingRuntime: - if (info.RuntimeVersion == RequestedVersion) + if (info?.RuntimeVersion == RequestedVersion) { // no need to update, it's already there } @@ -178,7 +174,7 @@ private async void StateMachine() if (info == null) { - info = await _connection.Device.GetDeviceInfo(); + info = await _connection.Device!.GetDeviceInfo(); } // await _connection.Device.FlashRuntime(RequestedVersion); @@ -195,7 +191,7 @@ private async void StateMachine() case UpdateState.DisablingMonoForCoprocessor: try { - await _connection.Device.RuntimeDisable(); + await _connection.Device!.RuntimeDisable(); CurrentState = UpdateState.UpdatingCoprocessor; } @@ -208,7 +204,7 @@ private async void StateMachine() CurrentState = UpdateState.UpdatingCoprocessor; break; case UpdateState.UpdatingCoprocessor: - if (info.CoprocessorOsVersion == RequestedVersion) + if (info?.CoprocessorOsVersion == RequestedVersion) { // no need to update, it's already there } @@ -224,7 +220,7 @@ private async void StateMachine() if (info == null) { Debug.WriteLine(">> query device info"); - info = await _connection.Device.GetDeviceInfo(); + info = await _connection.Device!.GetDeviceInfo(); } Debug.WriteLine(">> flashing ESP"); @@ -243,7 +239,7 @@ private async void StateMachine() case UpdateState.AllWritesComplete: try { - await _connection.Device.Reset(); + await _connection.Device!.Reset(); } catch (Exception ex) { @@ -252,27 +248,26 @@ private async void StateMachine() return; } break; - CurrentState = UpdateState.VerifySuccess; case UpdateState.VerifySuccess: try { await _connection.WaitForMeadowAttach(); await Task.Delay(2000); // wait 2 seconds to allow full boot - info = await _connection.Device.GetDeviceInfo(); - if (info.OsVersion != RequestedVersion) + info = await _connection.Device!.GetDeviceInfo(); + if (info?.OsVersion != RequestedVersion) { // this is a failure - _logger?.LogWarning($"OS version {info.OsVersion} does not match requested version {RequestedVersion}"); + _logger?.LogWarning($"OS version {info?.OsVersion} does not match requested version {RequestedVersion}"); } - if (info.RuntimeVersion != RequestedVersion) + if (info?.RuntimeVersion != RequestedVersion) { // this is a failure - _logger?.LogWarning($"Runtime version {info.RuntimeVersion} does not match requested version {RequestedVersion}"); + _logger?.LogWarning($"Runtime version {info?.RuntimeVersion} does not match requested version {RequestedVersion}"); } - if (info.CoprocessorOsVersion != RequestedVersion) + if (info?.CoprocessorOsVersion != RequestedVersion) { // not necessarily an error - _logger?.LogWarning($"Coprocessor version {info.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); + _logger?.LogWarning($"Coprocessor version {info?.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); } } catch (Exception ex) @@ -306,6 +301,7 @@ public Task Update(IMeadowConnection? connection, string? version = null) { // verify the version requested is valid var build = FirmwareManager.GetAllLocalFirmwareBuilds().FirstOrDefault(b => b.Version == version); + if (build == null) { throw new Exception($"Unknown build: '{version}'"); @@ -328,4 +324,4 @@ public Task Update(IMeadowConnection? connection, string? version = null) return Task.Run(StateMachine); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs index 96b14b50..41bb7400 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs @@ -10,13 +10,16 @@ public DateTimeOffset? Time { get { - if (Payload.Length == 0) { return null; } + if (Payload?.Length == 0) { return null; } return DateTimeOffset.Parse(Encoding.ASCII.GetString(Payload)); } set { - base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); + if (value != null) + { + base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); + } } } diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index ffac2127..d20eddbc 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -13,7 +13,7 @@ public class F7FirmwarePackageCollection : IFirmwarePackageCollection /// public event EventHandler DownloadProgress = default!; - public event EventHandler DefaultVersionChanged; + public event EventHandler DefaultVersionChanged = default!; public string PackageFileRoot { get; } @@ -74,7 +74,7 @@ private set return null; } - public Task DeletePackage(string version) + public async Task DeletePackage(string version) { var existing = _f7Packages.FirstOrDefault(p => p.Version == version); @@ -91,13 +91,11 @@ public Task DeletePackage(string version) } var newDefault = _f7Packages[i].Version; _f7Packages.Remove(DefaultPackage); - SetDefaultPackage(newDefault); + await SetDefaultPackage(newDefault); var path = Path.Combine(PackageFileRoot, version); Directory.Delete(path, true); - - return Task.CompletedTask; } public async Task SetDefaultPackage(string version) From 61c0e1157fcd40fac6e03cd22e0c65b16b4d9aea Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 18 Jan 2024 11:14:28 -0600 Subject: [PATCH 110/141] working on improving DFU --- Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs | 99 +++++++++---------- .../Firmware/FirmwareWriter.cs | 56 +++++++++++ .../v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj | 3 + Source/v2/Meadow.UsbLib/LibUsbDevice.cs | 13 +-- 4 files changed, 112 insertions(+), 59 deletions(-) create mode 100644 Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs index c42c2ff7..216fb844 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs @@ -1,57 +1,54 @@ -using System; -using System.Collections.Generic; -using DfuSharp; +using DfuSharp; -namespace MeadowCLI +namespace MeadowCLI; + +public class DfuContext { - public class DfuContext + private List validVendorIDs = new List { - private List validVendorIDs = new List - { - 0x22B1, // secret labs - 0x1B9F, // ghi - 0x05A, // who knows - 0x0483 // bootloader - }; - - // --------------------------- INSTANCE - public static DfuContext? Current; - - public static void Init() - { - Current = new DfuContext(); - Current._context = new Context(); - } - - public static void Dispose() - { - Current?._context?.Dispose(); - } - // --------------------------- INSTANCE - - private Context? _context; - - public List? GetDevices() - { - if (_context != null) - return _context.GetDfuDevices(validVendorIDs); - else - return null; - } - - public bool HasCapability(Capabilities caps) - { - if (_context != null) - return _context.HasCapability(caps); - else - return false; - } - - public void BeginListeningForHotplugEvents() - { - if (_context != null) - _context.BeginListeningForHotplugEvents(); - } + 0x22B1, // secret labs + 0x1B9F, // ghi + 0x05A, // who knows + 0x0483 // bootloader + }; + + // --------------------------- INSTANCE + public static DfuContext? Current; + public static void Init() + { + Current = new DfuContext(); + Current._context = new Context(); } + + public static void Dispose() + { + Current?._context?.Dispose(); + } + // --------------------------- INSTANCE + + private Context? _context; + + public List? GetDevices() + { + if (_context != null) + return _context.GetDfuDevices(validVendorIDs); + else + return null; + } + + public bool HasCapability(Capabilities caps) + { + if (_context != null) + return _context.HasCapability(caps); + else + return false; + } + + public void BeginListeningForHotplugEvents() + { + if (_context != null) + _context.BeginListeningForHotplugEvents(); + } + } \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs new file mode 100644 index 00000000..35dae8f2 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs @@ -0,0 +1,56 @@ +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.LibUsb; +using Microsoft.Extensions.Logging; + +namespace MeadowCLI; + +public class FirmwareWriter +{ + public IEnumerable GetLibUsbDevices(bool useLegacyLibUsb = false) + { + ILibUsbProvider provider; + + if (useLegacyLibUsb) + { + provider = new ClassicLibUsbProvider(); + } + else + { + provider = new LibUsbProvider(); + } + + return provider.GetDevicesInBootloaderMode(); + } + + public bool IsDfuDeviceAvailable(bool useLegacyLibUsb = false) + { + try + { + return GetLibUsbDevices(useLegacyLibUsb).Count() > 0; + } + catch + { + return false; + } + } + + public async Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacyLibUsb = false) + { + var devices = GetLibUsbDevices(useLegacyLibUsb); + + switch (devices.Count()) + { + case 0: throw new Exception("No device found in bootloader mode"); + case 1: break; + default: throw new Exception("Multiple devices found in bootloader mode - only connect one device"); + } + + var serialNumber = devices.First().GetDeviceSerialNumber(); + + await DfuUtils.FlashFile( + osFile, + serialNumber, + logger: logger, + format: DfuUtils.DfuFlashFormat.ConsoleOut); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj index e1a160a8..c2c1bd24 100644 --- a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -17,6 +17,9 @@ + + + diff --git a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs index 7c5e4398..d0a78185 100644 --- a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs @@ -18,14 +18,11 @@ static LibUsbProvider() public List GetDevicesInBootloaderMode() { - if (_devices == null) - { - _devices = _context - .List() - .Where(d => d.Info.VendorId == UsbBootLoaderVendorID) - .Select(d => new LibUsbDevice(d)) - .ToList(); - } + _devices = _context + .List() + .Where(d => d.Info.VendorId == UsbBootLoaderVendorID) + .Select(d => new LibUsbDevice(d)) + .ToList(); return _devices; } From 3514ddffa6a24d530011773ddea7dbc6eae2f5d9 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 18 Jan 2024 17:22:30 -0600 Subject: [PATCH 111/141] add debug writes for viz --- Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs | 1 + Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs index dde5938b..e43b6774 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs @@ -55,6 +55,7 @@ private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFor } else //Console out { + Debug.WriteLine(logLine); Console.Write(logLine); Console.Write(logLine.Contains("%") ? "\r" : "\r\n"); diff --git a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs index 35dae8f2..e8abb06d 100644 --- a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs +++ b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs @@ -1,6 +1,7 @@ using Meadow.CLI.Core.Internals.Dfu; using Meadow.LibUsb; using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace MeadowCLI; @@ -47,6 +48,8 @@ public async Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool use var serialNumber = devices.First().GetDeviceSerialNumber(); + Debug.WriteLine($"DFU Writing file {osFile}"); + await DfuUtils.FlashFile( osFile, serialNumber, From 58c38135991024737e5422b05ba3dd175605b64c Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 18 Jan 2024 18:51:31 -0600 Subject: [PATCH 112/141] more firmware work --- .../v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs index e8abb06d..fb52fdcf 100644 --- a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs +++ b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs @@ -1,4 +1,5 @@ using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; using Meadow.LibUsb; using Microsoft.Extensions.Logging; using System.Diagnostics; @@ -56,4 +57,18 @@ await DfuUtils.FlashFile( logger: logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } + + public async Task WriteRuntimeWithHcom(IMeadowConnection connection, string firmwareFile, ILogger? logger = null) + { + if (connection.Device == null) throw new Exception("No connected device"); + + await connection.Device.WriteRuntime(firmwareFile); + } + + public async Task WriteCoprocessorFilesWithHcom(IMeadowConnection connection, string[] files, ILogger? logger = null) + { + if (connection.Device == null) throw new Exception("No connected device"); + + await connection.Device.WriteCoprocessorFiles(files); + } } \ No newline at end of file From e73f6c46f06d36360594699211afc4ecbbeeab86 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 19 Jan 2024 16:15:49 -0600 Subject: [PATCH 113/141] updated async calls --- Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs index fb52fdcf..29e8c45c 100644 --- a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs +++ b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs @@ -36,7 +36,7 @@ public bool IsDfuDeviceAvailable(bool useLegacyLibUsb = false) } } - public async Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacyLibUsb = false) + public Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacyLibUsb = false) { var devices = GetLibUsbDevices(useLegacyLibUsb); @@ -51,24 +51,24 @@ public async Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool use Debug.WriteLine($"DFU Writing file {osFile}"); - await DfuUtils.FlashFile( + return DfuUtils.FlashFile( osFile, serialNumber, logger: logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); } - public async Task WriteRuntimeWithHcom(IMeadowConnection connection, string firmwareFile, ILogger? logger = null) + public Task WriteRuntimeWithHcom(IMeadowConnection connection, string firmwareFile, ILogger? logger = null) { if (connection.Device == null) throw new Exception("No connected device"); - await connection.Device.WriteRuntime(firmwareFile); + return connection.Device.WriteRuntime(firmwareFile); } - public async Task WriteCoprocessorFilesWithHcom(IMeadowConnection connection, string[] files, ILogger? logger = null) + public Task WriteCoprocessorFilesWithHcom(IMeadowConnection connection, string[] files, ILogger? logger = null) { if (connection.Device == null) throw new Exception("No connected device"); - await connection.Device.WriteCoprocessorFiles(files); + return connection.Device.WriteCoprocessorFiles(files); } } \ No newline at end of file From f6cd9f7110f3232e9bc648c3645ea689e418f2f2 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 19 Jan 2024 16:19:00 -0600 Subject: [PATCH 114/141] Update dotnet.yml --- .github/workflows/dotnet.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8dfb0e61..31b83240 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,4 +1,4 @@ -name: Meadow.CLI +name: Meadow.CLI Packaging env: CLI_RELEASE_VERSION: 1.5.0.0 IDE_TOOLS_RELEASE_VERSION: 1.5.0 @@ -7,13 +7,9 @@ env: VS_MAC_2022_VERSION: 17.6 on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -# Allows you to run this workflow manually from the Actions tab workflow_dispatch: + push: + branches: [ main ] jobs: build-and-optionally-publish-nuget: @@ -592,4 +588,4 @@ jobs: name: Publish VSCode Extension run: | cd vs-code - vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} \ No newline at end of file + vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} From 9887b64002ac36c4c2e6eb4cf80d199b9fffac01 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 19 Jan 2024 16:26:24 -0600 Subject: [PATCH 115/141] Create dotnet-ci.yml --- .github/workflows/dotnet-ci.yml | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/dotnet-ci.yml diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml new file mode 100644 index 00000000..badf55e4 --- /dev/null +++ b/.github/workflows/dotnet-ci.yml @@ -0,0 +1,53 @@ +name: Develop Build + +on: + workflow_dispatch: + pull_request: + branches: [ develop ] + push: + branches: [ develop ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - name: Checkout Meadow.Logging + uses: actions/checkout@v3 + with: + repository: WildernessLabs/Meadow.Logging + path: Meadow.Logging + ref: develop + + - name: Checkout Meadow.Units + uses: actions/checkout@v3 + with: + repository: WildernessLabs/Meadow.Units + path: Meadow.Units + ref: develop + + - name: Checkout Meadow.Contracts + uses: actions/checkout@v3 + with: + path: Meadow.Contracts + ref: develop + + - name: Checkout Meadow.CLI + uses: actions/checkout@v3 + with: + path: Meadow.CLI + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: + 8.0.x + + - name: Build CLI v1 + run: dotnet build -c Release Meadow.CLI/MeadowCLI.sln + + - name: Build CLI v2 + run: dotnet build -c Release Meadow.CLI/Source/v2/Meadow.CLI.v2.sln + + From e31b30f1741a140be4f82aa90c575ff649b7924d Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 19 Jan 2024 16:31:02 -0600 Subject: [PATCH 116/141] Update dotnet-ci.yml --- .github/workflows/dotnet-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index badf55e4..3b5b3975 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -30,6 +30,7 @@ jobs: - name: Checkout Meadow.Contracts uses: actions/checkout@v3 with: + repository: WildernessLabs/Meadow.Contracts path: Meadow.Contracts ref: develop From 28219726450c53504a468b67a349353ff2085a2e Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 21:50:57 -0800 Subject: [PATCH 117/141] Remove linker console output --- Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs b/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs index 825aee14..29c14943 100644 --- a/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs +++ b/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs @@ -68,8 +68,6 @@ public async Task RunILLink( { stdOutReaderResult = await stdOutReader.ReadToEndAsync(); - Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); - _logger?.Log(LogLevel.Debug, "StandardOutput Contains: " + stdOutReaderResult); } From 670020eb88571314f7b9a3d0b34a9011b851813b Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 21:51:28 -0800 Subject: [PATCH 118/141] Code cleanup --- .../PackageManager/IPackageManager.cs | 2 +- .../PackageManager/PackageManager.cs | 16 +++++++++++----- .../Commands/Current/App/AppDeployCommand.cs | 4 +--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs index ad231b39..389e69cf 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs @@ -23,4 +23,4 @@ Task AssemblePackage( string filter = "*", bool overwrite = false, CancellationToken? cancellationToken = null); -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs index 4ff24eee..39fbb307 100644 --- a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs @@ -176,7 +176,7 @@ public Task AssemblePackage( di.Create(); } - var mpakName = Path.Combine(outputFolder, $"{DateTime.UtcNow.ToString("yyyyMMddff")}.mpak"); + var mpakName = Path.Combine(outputFolder, $"{DateTime.UtcNow:yyyyMMddff}.mpak"); if (File.Exists(mpakName)) { @@ -199,7 +199,7 @@ public Task AssemblePackage( // write a metadata file info.json in the mpak // TODO: we need to see what is necessary and meaningful here and pass it in via param (or the entire file via param?) - PackageInfo info = new PackageInfo() + PackageInfo info = new() { Version = "1.0", OsVersion = osVersion @@ -226,7 +226,13 @@ private void CreateEntry(ZipArchive archive, string fromFile, string entryPath) public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, string appName = "App.dll") { - if (!Directory.Exists(rootFolder)) throw new FileNotFoundException(); + if (!Directory.Exists(rootFolder)) { throw new FileNotFoundException(); } + + //see if this is a fully qualified path to the app.dll + if (File.Exists(Path.Combine(rootFolder, appName))) + { + return new FileInfo[] { new(Path.Combine(rootFolder, appName)) }; + } // look for a 'bin' folder var path = Path.Combine(rootFolder, "bin"); @@ -239,9 +245,9 @@ void FindApp(string directory, List fileList) { foreach (var dir in Directory.GetDirectories(directory)) { - var shortname = System.IO.Path.GetFileName(dir); + var shortname = Path.GetFileName(dir); - if (shortname == PackageManager.PostLinkDirectoryName || shortname == PackageManager.PreLinkDirectoryName) + if (shortname == PostLinkDirectoryName || shortname == PreLinkDirectoryName) { continue; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index a5928ba5..249fc1cb 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -29,9 +29,7 @@ protected override async ValueTask ExecuteCommand() if (connection != null) { - string path = Path == null - ? Environment.CurrentDirectory - : Path; + string path = Path ?? Environment.CurrentDirectory; // is the path a file? FileInfo file; From 50d0a7dd29820ba00ec02a78dcbaa49952fa9ed4 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 21:51:37 -0800 Subject: [PATCH 119/141] Add console spinner --- Source/v2/Meadow.CLI/ConsoleSpinner.cs | 37 +++++++++++++++++++ .../Commands/Current/App/AppRunCommand.cs | 21 ++++++----- .../Commands/Current/App/AppTrimCommand.cs | 11 ++++-- 3 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 Source/v2/Meadow.CLI/ConsoleSpinner.cs diff --git a/Source/v2/Meadow.CLI/ConsoleSpinner.cs b/Source/v2/Meadow.CLI/ConsoleSpinner.cs new file mode 100644 index 00000000..40547722 --- /dev/null +++ b/Source/v2/Meadow.CLI/ConsoleSpinner.cs @@ -0,0 +1,37 @@ +using CliFx.Infrastructure; + +namespace Meadow.CLI +{ + public static class ConsoleSpinner + { + private static readonly char[] sequence = { '|', '/', '-', '\\' }; + + private static CancellationToken? token; + + public static void Spin(IConsole? console, int udpateInterval_ms = 100, CancellationToken cancellationToken = default) + { + if (console == null) + { + throw new ArgumentNullException(nameof(console)); + } + + if (token != null) + { + throw new InvalidOperationException("A spinner is already running"); + } + token = cancellationToken; + + _ = Task.Run(async () => + { + int index = 0; + + while (cancellationToken.IsCancellationRequested == false) + { + index++; + console?.Output.WriteAsync($"{sequence[index % 4]} \r"); + await Task.Delay(udpateInterval_ms, CancellationToken.None); + } + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index c70b137a..a62c51ba 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -31,17 +31,15 @@ protected override async ValueTask ExecuteCommand() if (connection == null) { - Logger?.LogError($"No connection path is defined"); + Logger?.LogError("No connection path is defined"); return; } - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; if (!Directory.Exists(path)) { - Logger?.LogError($"Target directory '{path}' not found."); + Logger?.LogError($"Target directory '{path}' not found"); return; } @@ -90,7 +88,7 @@ protected override async ValueTask ExecuteCommand() private Task BuildApplication(string path, CancellationToken cancellationToken) { - if (Configuration == null) Configuration = "Debug"; + if (Configuration == null) { Configuration = "Debug"; } Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); @@ -112,9 +110,14 @@ private async Task TrimApplication(string path, CancellationToken cancella var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); // if no configuration was provided, find the most recently built - Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + Logger?.LogInformation($"Trimming {file.FullName}"); + Logger?.LogInformation("This may take a few seconds..."); - await _packageManager.TrimApplication(file, false, null, cancellationToken); + var cts = new CancellationTokenSource(); + ConsoleSpinner.Spin(Console, cancellationToken: cts.Token); + + await _packageManager.TrimApplication(file, false, null, CancellationToken); + cts.Cancel(); return true; } @@ -144,7 +147,7 @@ private async Task DeployApplication(IMeadowConnection connection, string private void OnFileWriteProgress(object? sender, (string fileName, long completed, long total) e) { - var p = (e.completed / (double)e.total) * 100d; + var p = e.completed / (double)e.total * 100d; if (e.fileName != _lastFile) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 0fc8f67d..56c4cea9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -22,9 +22,7 @@ public AppTrimCommand(IPackageManager packageManager, ILoggerFactory loggerFacto protected override async ValueTask ExecuteCommand() { - string path = Path == null - ? AppDomain.CurrentDomain.BaseDirectory - : Path; + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; // is the path a file? FileInfo file; @@ -55,8 +53,13 @@ protected override async ValueTask ExecuteCommand() } // if no configuration was provided, find the most recently built - Logger?.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + Logger?.LogInformation($"Trimming {file.FullName}"); + Logger?.LogInformation("This may take a few seconds..."); + + var cts = new CancellationTokenSource(); + ConsoleSpinner.Spin(Console, cancellationToken: cts.Token); await _packageManager.TrimApplication(file, false, null, CancellationToken); + cts.Cancel(); } } \ No newline at end of file From b0411a67516f974ebf5100612a31653e07618474 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 21:57:03 -0800 Subject: [PATCH 120/141] bump version to 2.0.0.7 --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 40124bf4..c6885f97 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -11,7 +11,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0.6 + 2.0.0.7 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ icon.png diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index e5fd00c5..33b4818b 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.6"; + public const string CLI_VERSION = "2.0.0.7"; } } \ No newline at end of file From b96e260c7f3600bf49707ffaec2fbf557268dc40 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 22:11:58 -0800 Subject: [PATCH 121/141] warnings --- Source/v2/Meadow.Cli/Program.cs | 2 +- Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index 8cbb90ad..d57c3b7a 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -80,7 +80,7 @@ public static async Task Main(string[] args) { await new CliApplicationBuilder() .AddCommandsFromThisAssembly() - .UseTypeActivator(serviceProvider.GetService) + .UseTypeActivator(serviceProvider.GetService!) .SetExecutableName("meadow") .Build() .RunAsync(); diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 85b906cc..10b763fe 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -13,7 +13,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public event EventHandler ConnectionError = default!; public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress = default!; public event EventHandler ConnectionMessage = default!; - public event EventHandler FileWriteFailed; + public event EventHandler FileWriteFailed = default!; public abstract string Name { get; } From 03d9ac29fd09bd0ffebd76339277969a9dccc00d Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 22:12:09 -0800 Subject: [PATCH 122/141] Remove unused firmware classes --- .../Meadow.Hcom/Firmware/FirmwareManager.cs | 146 -------- .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 327 ------------------ 2 files changed, 473 deletions(-) delete mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs delete mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs deleted file mode 100644 index 63483930..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Net; -using System.Text.Json; - -namespace Meadow.Hcom; - -public static partial class JsonSerializerExtensions -{ - public static T? DeserializeAnonymousType(string json, T anonymousTypeObject, JsonSerializerOptions? options = default) - => JsonSerializer.Deserialize(json, options); - - public static ValueTask DeserializeAnonymousTypeAsync(Stream stream, TValue anonymousTypeObject, JsonSerializerOptions? options = default, CancellationToken cancellationToken = default) - => JsonSerializer.DeserializeAsync(stream, options, cancellationToken); // Method to deserialize from a stream added for completeness -} - -public static class FirmwareManager -{ - public static async Task GetRemoteFirmwareInfo(string versionNumber, ILogger logger) - { - var manager = new DownloadManager(logger); - - return await manager.DownloadMeadowOSVersionFile(versionNumber); - } - - public static async Task GetRemoteFirmware(string versionNumber, ILogger logger) - { - var manager = new DownloadManager(logger); - - await manager.DownloadOsBinaries(versionNumber, true); - } - - public static async Task GetCloudLatestFirmwareVersion() - { - var request = (HttpWebRequest)WebRequest.Create($"{DownloadManager.VersionCheckUrlRoot}latest.json"); - using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync()) - using (Stream stream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) - { - var json = await reader.ReadToEndAsync(); - - if (json == null) { return string.Empty; } - - return JsonSerializerExtensions.DeserializeAnonymousType(json, new { version = string.Empty }).version; - } - } - - public static string GetLocalLatestFirmwareVersion() - { - var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFolder); - var latest = string.Empty; - var latestFile = di.GetFiles("latest.txt").FirstOrDefault(); - if (latestFile != null) - { - latest = File.ReadAllText(latestFile.FullName).Trim(); - } - return latest; - } - - public static FirmwareInfo[] GetAllLocalFirmwareBuilds() - { - var list = new List(); - - var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFolder); - - var latest = GetLocalLatestFirmwareVersion(); - - var options = new JsonSerializerOptions(); - options.Converters.Add(new BuildDateConverter()); - - FirmwareInfo? ParseInfo(string version, string json) - { - var fi = JsonSerializer.Deserialize(json, options); - if (fi == null) return null; - fi.Version = version; - fi.IsLatest = version == latest; - return fi; - } - - foreach (var dir in di.EnumerateDirectories()) - { - var info = dir.GetFiles("build-info.json").FirstOrDefault(); - if (info == null) continue; - var json = File.ReadAllText(info.FullName); - try - { - var fi = ParseInfo(dir.Name, json); - if (fi != null) - { - list.Add(fi); - } - } - catch (JsonException) - { - // work around for Issue #229 (bad json) - var index = json.IndexOf(']'); - if (index != -1 && json[index + 1] == ',') - { - var fix = $"{json.Substring(0, index + 1)}{json.Substring(index + 2)}"; - try - { - var fi = ParseInfo(dir.Name, fix); - if (fi != null) - { - list.Add(fi); - } - } - catch - { - continue; - } - } - - continue; - } - } - return list.ToArray(); - } - - public static FirmwareUpdater GetFirmwareUpdater(IMeadowConnection connection) - { - return new FirmwareUpdater(connection); - } - - public static async Task PushApplicationToDevice(IMeadowConnection connection, DirectoryInfo appFolder, ILogger? logger = null) - { - try - { - if (connection == null) throw new ArgumentNullException("connection"); - if (connection.Device == null) throw new ArgumentNullException("connection.Device"); - - - var info = await connection.Device.GetDeviceInfo(); - - await connection.Device.RuntimeDisable(); - // the device will disconnect and reconnect here - - // await connection.Device.DeployApp(Path.Combine(appFolder.FullName, "App.dll"), osVersion); - - await connection.Device.RuntimeEnable(); - } - catch (Exception ex) - { - logger?.LogError(ex, "Error flashing OS to Meadow"); - } - } -} diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs deleted file mode 100644 index c1aeeaba..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ /dev/null @@ -1,327 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace Meadow.Hcom; - -public class FirmwareUpdater -{ - private readonly ILogger? _logger; - private IMeadowConnection _connection; - private UpdateState _state; - - private string RequestedVersion { get; set; } - - public enum UpdateState - { - NotStarted, - EnteringDFUMode, - InDFUMode, - UpdatingOS, - DFUCompleted, - DisablingMonoForRuntime, - UpdatingRuntime, - DisablingMonoForCoprocessor, - UpdatingCoprocessor, - AllWritesComplete, - VerifySuccess, - UpdateSuccess, - Error - } - - public UpdateState PreviousState { get; private set; } - - internal FirmwareUpdater(IMeadowConnection connection) - { - _connection = connection; - // _logger = connection.Logger; - } - - public UpdateState CurrentState - { - get => _state; - private set - { - if (value == _state) return; - PreviousState = CurrentState; - _state = value; - _logger?.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); - } - } - - private async void StateMachine() - { - var tries = 0; - - DeviceInfo? info = null; - - while (true) - { - switch (CurrentState) - { - case UpdateState.NotStarted: - try - { - // make sure we have a current device info - info = await _connection.Device!.GetDeviceInfo(); - - if (info?.OsVersion == RequestedVersion) - { - // no need to update, it's already there - CurrentState = UpdateState.DFUCompleted; - break; - } - - // enter DFU mode - // await _connection.Device.EnterDfuMode(); - CurrentState = UpdateState.EnteringDFUMode; - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - - break; - case UpdateState.EnteringDFUMode: - // look for DFU device - try - { - //var dfu = DfuUtils.GetDeviceInBootloaderMode(); - CurrentState = UpdateState.InDFUMode; - } - catch (Exception ex) - { - ++tries; - if (tries > 5) - { - _logger?.LogError($"Failed to enter DFU mode: {ex.Message}"); - CurrentState = UpdateState.Error; - - // exit state machine - return; - } - await Task.Delay(1000); - } - break; - case UpdateState.InDFUMode: - try - { - //var success = await DfuUtils.FlashVersion(RequestedVersion, _logger); - var success = false; - if (success) - { - CurrentState = UpdateState.DFUCompleted; - } - else - { - CurrentState = UpdateState.Error; - - // exit state machine - return; - } - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - break; - case UpdateState.DFUCompleted: - // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after - try - { - // wait for device to reconnect - await _connection.WaitForMeadowAttach(); - await Task.Delay(2000); // wait 2 seconds to allow full boot - - info ??= await _connection.Device!.GetDeviceInfo(); - - CurrentState = UpdateState.DisablingMonoForRuntime; - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - break; - case UpdateState.DisablingMonoForRuntime: - try - { - await _connection.Device!.RuntimeDisable(); - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - CurrentState = UpdateState.UpdatingRuntime; - break; - case UpdateState.UpdatingRuntime: - if (info?.RuntimeVersion == RequestedVersion) - { - // no need to update, it's already there - } - else - { - try - { - await _connection.WaitForMeadowAttach(); - await Task.Delay(2000); // wait 2 seconds to allow full boot - - if (info == null) - { - info = await _connection.Device!.GetDeviceInfo(); - } - - // await _connection.Device.FlashRuntime(RequestedVersion); - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - } - CurrentState = UpdateState.DisablingMonoForCoprocessor; - break; - case UpdateState.DisablingMonoForCoprocessor: - try - { - await _connection.Device!.RuntimeDisable(); - - CurrentState = UpdateState.UpdatingCoprocessor; - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - CurrentState = UpdateState.UpdatingCoprocessor; - break; - case UpdateState.UpdatingCoprocessor: - if (info?.CoprocessorOsVersion == RequestedVersion) - { - // no need to update, it's already there - } - else - { - try - { - Debug.WriteLine(">> waiting for connection"); - await _connection.WaitForMeadowAttach(); - Debug.WriteLine(">> delay"); - await Task.Delay(3000); // wait to allow full boot - no idea why this takes longer - - if (info == null) - { - Debug.WriteLine(">> query device info"); - info = await _connection.Device!.GetDeviceInfo(); - } - - Debug.WriteLine(">> flashing ESP"); - //await _connection.Device.FlashCoprocessor(RequestedVersion); - // await _connection.Device.FlashCoprocessor(DownloadManager.FirmwareDownloadsFilePath, RequestedVersion); - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - } - CurrentState = UpdateState.AllWritesComplete; - break; - case UpdateState.AllWritesComplete: - try - { - await _connection.Device!.Reset(); - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - break; - case UpdateState.VerifySuccess: - try - { - await _connection.WaitForMeadowAttach(); - await Task.Delay(2000); // wait 2 seconds to allow full boot - info = await _connection.Device!.GetDeviceInfo(); - if (info?.OsVersion != RequestedVersion) - { - // this is a failure - _logger?.LogWarning($"OS version {info?.OsVersion} does not match requested version {RequestedVersion}"); - } - if (info?.RuntimeVersion != RequestedVersion) - { - // this is a failure - _logger?.LogWarning($"Runtime version {info?.RuntimeVersion} does not match requested version {RequestedVersion}"); - } - if (info?.CoprocessorOsVersion != RequestedVersion) - { - // not necessarily an error - _logger?.LogWarning($"Coprocessor version {info?.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); - } - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - CurrentState = UpdateState.UpdateSuccess; - break; - case UpdateState.UpdateSuccess: - _logger?.LogInformation("Update complete"); - return; - default: - break; - } - - await Task.Delay(1000); - } - } - - public Task Update(IMeadowConnection? connection, string? version = null) - { - string updateVersion; - if (version == null) - { - // use "latest" - updateVersion = FirmwareManager.GetLocalLatestFirmwareVersion(); - } - else - { - // verify the version requested is valid - var build = FirmwareManager.GetAllLocalFirmwareBuilds().FirstOrDefault(b => b.Version == version); - - if (build == null) - { - throw new Exception($"Unknown build: '{version}'"); - } - updateVersion = build.Version; - } - - RequestedVersion = updateVersion; - - if (connection == null) - { - // assume DFU mode startup - CurrentState = UpdateState.EnteringDFUMode; - } - else - { - _connection = connection; - CurrentState = UpdateState.NotStarted; - } - - return Task.Run(StateMachine); - } -} \ No newline at end of file From ba7054657c5efe578ca26bc6620e01e5b38fa814 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 22:18:59 -0800 Subject: [PATCH 123/141] Normalize folder names step 1 of 2 --- {Meadow.CLI => Meadow.CLI_rename}/AssemblyInfo.cs | 0 .../CliFxConsoleLoggerProvider.cs | 0 .../Commands/App/DeployAppCommand.cs | 0 .../Commands/Cloud/CloudCommand.cs | 0 .../Cloud/Collection/ListCollectionCommand.cs | 0 .../Cloud/Command/JsonDocumentBindingConverter.cs | 0 .../Commands/Cloud/Command/PublishCommand.cs | 0 .../Commands/Cloud/LoginCommand.cs | 0 .../Commands/Cloud/LogoutCommand.cs | 0 .../Commands/Cloud/Package/CreatePackageCommand.cs | 0 .../Commands/Cloud/Package/ListPackagesCommand.cs | 0 .../Commands/Cloud/Package/PublishPackageCommand.cs | 0 .../Commands/Cloud/Package/UploadPackageCommand.cs | 0 .../Commands/DeviceManagement/FlashEspCommand.cs | 0 .../Commands/DeviceManagement/FlashOsCommand.cs | 0 .../DeviceManagement/GetDeviceInfoCommand.cs | 0 .../DeviceManagement/GetDeviceMacAddressCommand.cs | 0 .../DeviceManagement/GetDeviceNameCommand.cs | 0 .../DeviceManagement/ProvisionDeviceCommand.cs | 0 .../Commands/DeviceManagement/UsePortCommand.cs | 0 .../Commands/Esp32/RestartEsp32Command.cs | 0 .../Commands/Esp32/WriteEsp32FileCommand.cs | 0 .../Commands/FileSystem/FormatFileSystemCommand.cs | 0 .../Commands/FileSystem/RenewFileSystemCommand.cs | 0 .../Commands/Files/DeleteAllFilesCommand.cs | 0 .../Commands/Files/DeleteFileCommand.cs | 0 .../Commands/Files/InitialFileBytesCommand.cs | 0 .../Commands/Files/ListFilesCommand.cs | 0 .../Commands/Files/WriteFileCommand.cs | 0 .../Commands/MeadowCommand.cs | 0 .../Commands/MeadowSerialCommand.cs | 0 .../Commands/Mono/MonoDisableCommand.cs | 0 .../Commands/Mono/MonoEnableCommand.cs | 0 .../Commands/Mono/MonoFlashCommand.cs | 0 .../Commands/Mono/MonoRunStateCommand.cs | 0 .../Commands/Mono/MonoUpdateRuntimeCommand.cs | 0 .../Commands/Nsh/NshDisableCommand.cs | 0 .../Commands/Nsh/NshEnableCommand.cs | 0 .../Commands/Qspi/QspiInitCommand.cs | 0 .../Commands/Qspi/QspiReadCommand.cs | 0 .../Commands/Qspi/QspiWriteCommand.cs | 0 .../Commands/Storage/EraseFlashCommand.cs | 0 .../Commands/Storage/VerifyFlashCommand.cs | 0 .../Commands/Trace/SetDeveloperValueCommand.cs | 0 .../Commands/Trace/TraceDisableCommand.cs | 0 .../Commands/Trace/TraceEnableCommand.cs | 0 .../Commands/Trace/TraceLevelCommand.cs | 0 .../Commands/Trace/UartTraceCommand.cs | 0 .../Commands/Utility/ConsoleSpinner.cs | 0 .../Commands/Utility/DebugCommand.cs | 0 .../Commands/Utility/DownloadOsCommand.cs | 0 .../Commands/Utility/InstallDfuUtilCommand.cs | 0 .../Commands/Utility/ListPortsCommand.cs | 0 .../Commands/Utility/ListenCommand.cs | 0 .../Meadow.CLI.Classic.csproj | 0 {Meadow.CLI => Meadow.CLI_rename}/Meadow.CLI.csproj | 0 {Meadow.CLI => Meadow.CLI_rename}/Program.cs | 0 .../Properties/launchSettings.json | 0 {Meadow.CLI => Meadow.CLI_rename}/appsettings.json | 0 {Meadow.CLI => Meadow.CLI_rename}/images/icon.png | Bin .../HcomHostRequestType.cs | 0 .../HcomMeadowRequestType.cs | 0 .../HcomProtocolHeaderOffsets.cs | 0 .../HcomProtocolHeaderTypes.cs | 0 .../Meadow.Hcom.6.0.0.csproj | 0 .../Meadow.Hcom.Classic.csproj | 0 .../Meadow.Hcom.csproj | 0 .../MeadowCLIKey.snk | Bin 68 files changed, 0 insertions(+), 0 deletions(-) rename {Meadow.CLI => Meadow.CLI_rename}/AssemblyInfo.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/CliFxConsoleLoggerProvider.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/App/DeployAppCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/CloudCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Collection/ListCollectionCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Command/JsonDocumentBindingConverter.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Command/PublishCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/LoginCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/LogoutCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Package/CreatePackageCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Package/ListPackagesCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Package/PublishPackageCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Cloud/Package/UploadPackageCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/FlashEspCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/FlashOsCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/GetDeviceInfoCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/GetDeviceNameCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/ProvisionDeviceCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/DeviceManagement/UsePortCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Esp32/RestartEsp32Command.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Esp32/WriteEsp32FileCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/FileSystem/FormatFileSystemCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/FileSystem/RenewFileSystemCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Files/DeleteAllFilesCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Files/DeleteFileCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Files/InitialFileBytesCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Files/ListFilesCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Files/WriteFileCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/MeadowCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/MeadowSerialCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Mono/MonoDisableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Mono/MonoEnableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Mono/MonoFlashCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Mono/MonoRunStateCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Mono/MonoUpdateRuntimeCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Nsh/NshDisableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Nsh/NshEnableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Qspi/QspiInitCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Qspi/QspiReadCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Qspi/QspiWriteCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Storage/EraseFlashCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Storage/VerifyFlashCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Trace/SetDeveloperValueCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Trace/TraceDisableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Trace/TraceEnableCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Trace/TraceLevelCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Trace/UartTraceCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/ConsoleSpinner.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/DebugCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/DownloadOsCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/InstallDfuUtilCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/ListPortsCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Commands/Utility/ListenCommand.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Meadow.CLI.Classic.csproj (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Meadow.CLI.csproj (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Program.cs (100%) rename {Meadow.CLI => Meadow.CLI_rename}/Properties/launchSettings.json (100%) rename {Meadow.CLI => Meadow.CLI_rename}/appsettings.json (100%) rename {Meadow.CLI => Meadow.CLI_rename}/images/icon.png (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/HcomHostRequestType.cs (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/HcomMeadowRequestType.cs (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/HcomProtocolHeaderOffsets.cs (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/HcomProtocolHeaderTypes.cs (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/Meadow.Hcom.6.0.0.csproj (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/Meadow.Hcom.Classic.csproj (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/Meadow.Hcom.csproj (100%) rename {Meadow.Hcom => Meadow.Hcom_rename}/MeadowCLIKey.snk (100%) diff --git a/Meadow.CLI/AssemblyInfo.cs b/Meadow.CLI_rename/AssemblyInfo.cs similarity index 100% rename from Meadow.CLI/AssemblyInfo.cs rename to Meadow.CLI_rename/AssemblyInfo.cs diff --git a/Meadow.CLI/CliFxConsoleLoggerProvider.cs b/Meadow.CLI_rename/CliFxConsoleLoggerProvider.cs similarity index 100% rename from Meadow.CLI/CliFxConsoleLoggerProvider.cs rename to Meadow.CLI_rename/CliFxConsoleLoggerProvider.cs diff --git a/Meadow.CLI/Commands/App/DeployAppCommand.cs b/Meadow.CLI_rename/Commands/App/DeployAppCommand.cs similarity index 100% rename from Meadow.CLI/Commands/App/DeployAppCommand.cs rename to Meadow.CLI_rename/Commands/App/DeployAppCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/CloudCommand.cs b/Meadow.CLI_rename/Commands/Cloud/CloudCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/CloudCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/CloudCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Collection/ListCollectionCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Collection/ListCollectionCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Collection/ListCollectionCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Collection/ListCollectionCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Command/JsonDocumentBindingConverter.cs b/Meadow.CLI_rename/Commands/Cloud/Command/JsonDocumentBindingConverter.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Command/JsonDocumentBindingConverter.cs rename to Meadow.CLI_rename/Commands/Cloud/Command/JsonDocumentBindingConverter.cs diff --git a/Meadow.CLI/Commands/Cloud/Command/PublishCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Command/PublishCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Command/PublishCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Command/PublishCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/LoginCommand.cs b/Meadow.CLI_rename/Commands/Cloud/LoginCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/LoginCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/LoginCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/LogoutCommand.cs b/Meadow.CLI_rename/Commands/Cloud/LogoutCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/LogoutCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/LogoutCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Package/CreatePackageCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Package/CreatePackageCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Package/CreatePackageCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Package/CreatePackageCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Package/ListPackagesCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Package/ListPackagesCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Package/ListPackagesCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Package/ListPackagesCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Package/PublishPackageCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Package/PublishPackageCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Package/PublishPackageCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Package/PublishPackageCommand.cs diff --git a/Meadow.CLI/Commands/Cloud/Package/UploadPackageCommand.cs b/Meadow.CLI_rename/Commands/Cloud/Package/UploadPackageCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Cloud/Package/UploadPackageCommand.cs rename to Meadow.CLI_rename/Commands/Cloud/Package/UploadPackageCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/FlashEspCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/FlashEspCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/FlashOsCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/FlashOsCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceInfoCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceInfoCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceNameCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceNameCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/ProvisionDeviceCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/ProvisionDeviceCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/ProvisionDeviceCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/ProvisionDeviceCommand.cs diff --git a/Meadow.CLI/Commands/DeviceManagement/UsePortCommand.cs b/Meadow.CLI_rename/Commands/DeviceManagement/UsePortCommand.cs similarity index 100% rename from Meadow.CLI/Commands/DeviceManagement/UsePortCommand.cs rename to Meadow.CLI_rename/Commands/DeviceManagement/UsePortCommand.cs diff --git a/Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs b/Meadow.CLI_rename/Commands/Esp32/RestartEsp32Command.cs similarity index 100% rename from Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs rename to Meadow.CLI_rename/Commands/Esp32/RestartEsp32Command.cs diff --git a/Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs b/Meadow.CLI_rename/Commands/Esp32/WriteEsp32FileCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs rename to Meadow.CLI_rename/Commands/Esp32/WriteEsp32FileCommand.cs diff --git a/Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs b/Meadow.CLI_rename/Commands/FileSystem/FormatFileSystemCommand.cs similarity index 100% rename from Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs rename to Meadow.CLI_rename/Commands/FileSystem/FormatFileSystemCommand.cs diff --git a/Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs b/Meadow.CLI_rename/Commands/FileSystem/RenewFileSystemCommand.cs similarity index 100% rename from Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs rename to Meadow.CLI_rename/Commands/FileSystem/RenewFileSystemCommand.cs diff --git a/Meadow.CLI/Commands/Files/DeleteAllFilesCommand.cs b/Meadow.CLI_rename/Commands/Files/DeleteAllFilesCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Files/DeleteAllFilesCommand.cs rename to Meadow.CLI_rename/Commands/Files/DeleteAllFilesCommand.cs diff --git a/Meadow.CLI/Commands/Files/DeleteFileCommand.cs b/Meadow.CLI_rename/Commands/Files/DeleteFileCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Files/DeleteFileCommand.cs rename to Meadow.CLI_rename/Commands/Files/DeleteFileCommand.cs diff --git a/Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs b/Meadow.CLI_rename/Commands/Files/InitialFileBytesCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs rename to Meadow.CLI_rename/Commands/Files/InitialFileBytesCommand.cs diff --git a/Meadow.CLI/Commands/Files/ListFilesCommand.cs b/Meadow.CLI_rename/Commands/Files/ListFilesCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Files/ListFilesCommand.cs rename to Meadow.CLI_rename/Commands/Files/ListFilesCommand.cs diff --git a/Meadow.CLI/Commands/Files/WriteFileCommand.cs b/Meadow.CLI_rename/Commands/Files/WriteFileCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Files/WriteFileCommand.cs rename to Meadow.CLI_rename/Commands/Files/WriteFileCommand.cs diff --git a/Meadow.CLI/Commands/MeadowCommand.cs b/Meadow.CLI_rename/Commands/MeadowCommand.cs similarity index 100% rename from Meadow.CLI/Commands/MeadowCommand.cs rename to Meadow.CLI_rename/Commands/MeadowCommand.cs diff --git a/Meadow.CLI/Commands/MeadowSerialCommand.cs b/Meadow.CLI_rename/Commands/MeadowSerialCommand.cs similarity index 100% rename from Meadow.CLI/Commands/MeadowSerialCommand.cs rename to Meadow.CLI_rename/Commands/MeadowSerialCommand.cs diff --git a/Meadow.CLI/Commands/Mono/MonoDisableCommand.cs b/Meadow.CLI_rename/Commands/Mono/MonoDisableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Mono/MonoDisableCommand.cs rename to Meadow.CLI_rename/Commands/Mono/MonoDisableCommand.cs diff --git a/Meadow.CLI/Commands/Mono/MonoEnableCommand.cs b/Meadow.CLI_rename/Commands/Mono/MonoEnableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Mono/MonoEnableCommand.cs rename to Meadow.CLI_rename/Commands/Mono/MonoEnableCommand.cs diff --git a/Meadow.CLI/Commands/Mono/MonoFlashCommand.cs b/Meadow.CLI_rename/Commands/Mono/MonoFlashCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Mono/MonoFlashCommand.cs rename to Meadow.CLI_rename/Commands/Mono/MonoFlashCommand.cs diff --git a/Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs b/Meadow.CLI_rename/Commands/Mono/MonoRunStateCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs rename to Meadow.CLI_rename/Commands/Mono/MonoRunStateCommand.cs diff --git a/Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs b/Meadow.CLI_rename/Commands/Mono/MonoUpdateRuntimeCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs rename to Meadow.CLI_rename/Commands/Mono/MonoUpdateRuntimeCommand.cs diff --git a/Meadow.CLI/Commands/Nsh/NshDisableCommand.cs b/Meadow.CLI_rename/Commands/Nsh/NshDisableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Nsh/NshDisableCommand.cs rename to Meadow.CLI_rename/Commands/Nsh/NshDisableCommand.cs diff --git a/Meadow.CLI/Commands/Nsh/NshEnableCommand.cs b/Meadow.CLI_rename/Commands/Nsh/NshEnableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Nsh/NshEnableCommand.cs rename to Meadow.CLI_rename/Commands/Nsh/NshEnableCommand.cs diff --git a/Meadow.CLI/Commands/Qspi/QspiInitCommand.cs b/Meadow.CLI_rename/Commands/Qspi/QspiInitCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Qspi/QspiInitCommand.cs rename to Meadow.CLI_rename/Commands/Qspi/QspiInitCommand.cs diff --git a/Meadow.CLI/Commands/Qspi/QspiReadCommand.cs b/Meadow.CLI_rename/Commands/Qspi/QspiReadCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Qspi/QspiReadCommand.cs rename to Meadow.CLI_rename/Commands/Qspi/QspiReadCommand.cs diff --git a/Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs b/Meadow.CLI_rename/Commands/Qspi/QspiWriteCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs rename to Meadow.CLI_rename/Commands/Qspi/QspiWriteCommand.cs diff --git a/Meadow.CLI/Commands/Storage/EraseFlashCommand.cs b/Meadow.CLI_rename/Commands/Storage/EraseFlashCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Storage/EraseFlashCommand.cs rename to Meadow.CLI_rename/Commands/Storage/EraseFlashCommand.cs diff --git a/Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs b/Meadow.CLI_rename/Commands/Storage/VerifyFlashCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs rename to Meadow.CLI_rename/Commands/Storage/VerifyFlashCommand.cs diff --git a/Meadow.CLI/Commands/Trace/SetDeveloperValueCommand.cs b/Meadow.CLI_rename/Commands/Trace/SetDeveloperValueCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Trace/SetDeveloperValueCommand.cs rename to Meadow.CLI_rename/Commands/Trace/SetDeveloperValueCommand.cs diff --git a/Meadow.CLI/Commands/Trace/TraceDisableCommand.cs b/Meadow.CLI_rename/Commands/Trace/TraceDisableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Trace/TraceDisableCommand.cs rename to Meadow.CLI_rename/Commands/Trace/TraceDisableCommand.cs diff --git a/Meadow.CLI/Commands/Trace/TraceEnableCommand.cs b/Meadow.CLI_rename/Commands/Trace/TraceEnableCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Trace/TraceEnableCommand.cs rename to Meadow.CLI_rename/Commands/Trace/TraceEnableCommand.cs diff --git a/Meadow.CLI/Commands/Trace/TraceLevelCommand.cs b/Meadow.CLI_rename/Commands/Trace/TraceLevelCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Trace/TraceLevelCommand.cs rename to Meadow.CLI_rename/Commands/Trace/TraceLevelCommand.cs diff --git a/Meadow.CLI/Commands/Trace/UartTraceCommand.cs b/Meadow.CLI_rename/Commands/Trace/UartTraceCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Trace/UartTraceCommand.cs rename to Meadow.CLI_rename/Commands/Trace/UartTraceCommand.cs diff --git a/Meadow.CLI/Commands/Utility/ConsoleSpinner.cs b/Meadow.CLI_rename/Commands/Utility/ConsoleSpinner.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/ConsoleSpinner.cs rename to Meadow.CLI_rename/Commands/Utility/ConsoleSpinner.cs diff --git a/Meadow.CLI/Commands/Utility/DebugCommand.cs b/Meadow.CLI_rename/Commands/Utility/DebugCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/DebugCommand.cs rename to Meadow.CLI_rename/Commands/Utility/DebugCommand.cs diff --git a/Meadow.CLI/Commands/Utility/DownloadOsCommand.cs b/Meadow.CLI_rename/Commands/Utility/DownloadOsCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/DownloadOsCommand.cs rename to Meadow.CLI_rename/Commands/Utility/DownloadOsCommand.cs diff --git a/Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs b/Meadow.CLI_rename/Commands/Utility/InstallDfuUtilCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs rename to Meadow.CLI_rename/Commands/Utility/InstallDfuUtilCommand.cs diff --git a/Meadow.CLI/Commands/Utility/ListPortsCommand.cs b/Meadow.CLI_rename/Commands/Utility/ListPortsCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/ListPortsCommand.cs rename to Meadow.CLI_rename/Commands/Utility/ListPortsCommand.cs diff --git a/Meadow.CLI/Commands/Utility/ListenCommand.cs b/Meadow.CLI_rename/Commands/Utility/ListenCommand.cs similarity index 100% rename from Meadow.CLI/Commands/Utility/ListenCommand.cs rename to Meadow.CLI_rename/Commands/Utility/ListenCommand.cs diff --git a/Meadow.CLI/Meadow.CLI.Classic.csproj b/Meadow.CLI_rename/Meadow.CLI.Classic.csproj similarity index 100% rename from Meadow.CLI/Meadow.CLI.Classic.csproj rename to Meadow.CLI_rename/Meadow.CLI.Classic.csproj diff --git a/Meadow.CLI/Meadow.CLI.csproj b/Meadow.CLI_rename/Meadow.CLI.csproj similarity index 100% rename from Meadow.CLI/Meadow.CLI.csproj rename to Meadow.CLI_rename/Meadow.CLI.csproj diff --git a/Meadow.CLI/Program.cs b/Meadow.CLI_rename/Program.cs similarity index 100% rename from Meadow.CLI/Program.cs rename to Meadow.CLI_rename/Program.cs diff --git a/Meadow.CLI/Properties/launchSettings.json b/Meadow.CLI_rename/Properties/launchSettings.json similarity index 100% rename from Meadow.CLI/Properties/launchSettings.json rename to Meadow.CLI_rename/Properties/launchSettings.json diff --git a/Meadow.CLI/appsettings.json b/Meadow.CLI_rename/appsettings.json similarity index 100% rename from Meadow.CLI/appsettings.json rename to Meadow.CLI_rename/appsettings.json diff --git a/Meadow.CLI/images/icon.png b/Meadow.CLI_rename/images/icon.png similarity index 100% rename from Meadow.CLI/images/icon.png rename to Meadow.CLI_rename/images/icon.png diff --git a/Meadow.Hcom/HcomHostRequestType.cs b/Meadow.Hcom_rename/HcomHostRequestType.cs similarity index 100% rename from Meadow.Hcom/HcomHostRequestType.cs rename to Meadow.Hcom_rename/HcomHostRequestType.cs diff --git a/Meadow.Hcom/HcomMeadowRequestType.cs b/Meadow.Hcom_rename/HcomMeadowRequestType.cs similarity index 100% rename from Meadow.Hcom/HcomMeadowRequestType.cs rename to Meadow.Hcom_rename/HcomMeadowRequestType.cs diff --git a/Meadow.Hcom/HcomProtocolHeaderOffsets.cs b/Meadow.Hcom_rename/HcomProtocolHeaderOffsets.cs similarity index 100% rename from Meadow.Hcom/HcomProtocolHeaderOffsets.cs rename to Meadow.Hcom_rename/HcomProtocolHeaderOffsets.cs diff --git a/Meadow.Hcom/HcomProtocolHeaderTypes.cs b/Meadow.Hcom_rename/HcomProtocolHeaderTypes.cs similarity index 100% rename from Meadow.Hcom/HcomProtocolHeaderTypes.cs rename to Meadow.Hcom_rename/HcomProtocolHeaderTypes.cs diff --git a/Meadow.Hcom/Meadow.Hcom.6.0.0.csproj b/Meadow.Hcom_rename/Meadow.Hcom.6.0.0.csproj similarity index 100% rename from Meadow.Hcom/Meadow.Hcom.6.0.0.csproj rename to Meadow.Hcom_rename/Meadow.Hcom.6.0.0.csproj diff --git a/Meadow.Hcom/Meadow.Hcom.Classic.csproj b/Meadow.Hcom_rename/Meadow.Hcom.Classic.csproj similarity index 100% rename from Meadow.Hcom/Meadow.Hcom.Classic.csproj rename to Meadow.Hcom_rename/Meadow.Hcom.Classic.csproj diff --git a/Meadow.Hcom/Meadow.Hcom.csproj b/Meadow.Hcom_rename/Meadow.Hcom.csproj similarity index 100% rename from Meadow.Hcom/Meadow.Hcom.csproj rename to Meadow.Hcom_rename/Meadow.Hcom.csproj diff --git a/Meadow.Hcom/MeadowCLIKey.snk b/Meadow.Hcom_rename/MeadowCLIKey.snk similarity index 100% rename from Meadow.Hcom/MeadowCLIKey.snk rename to Meadow.Hcom_rename/MeadowCLIKey.snk From 1119dc507b614f37d23b5350f4b027e9a384576a Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Mon, 22 Jan 2024 22:19:19 -0800 Subject: [PATCH 124/141] Normalize folders step 2 of 2 --- {Meadow.CLI_rename => Meadow.CLI}/AssemblyInfo.cs | 0 .../CliFxConsoleLoggerProvider.cs | 0 .../Commands/App/DeployAppCommand.cs | 0 .../Commands/Cloud/CloudCommand.cs | 0 .../Cloud/Collection/ListCollectionCommand.cs | 0 .../Cloud/Command/JsonDocumentBindingConverter.cs | 0 .../Commands/Cloud/Command/PublishCommand.cs | 0 .../Commands/Cloud/LoginCommand.cs | 0 .../Commands/Cloud/LogoutCommand.cs | 0 .../Commands/Cloud/Package/CreatePackageCommand.cs | 0 .../Commands/Cloud/Package/ListPackagesCommand.cs | 0 .../Commands/Cloud/Package/PublishPackageCommand.cs | 0 .../Commands/Cloud/Package/UploadPackageCommand.cs | 0 .../Commands/DeviceManagement/FlashEspCommand.cs | 0 .../Commands/DeviceManagement/FlashOsCommand.cs | 0 .../DeviceManagement/GetDeviceInfoCommand.cs | 0 .../DeviceManagement/GetDeviceMacAddressCommand.cs | 0 .../DeviceManagement/GetDeviceNameCommand.cs | 0 .../DeviceManagement/ProvisionDeviceCommand.cs | 0 .../Commands/DeviceManagement/UsePortCommand.cs | 0 .../Commands/Esp32/RestartEsp32Command.cs | 0 .../Commands/Esp32/WriteEsp32FileCommand.cs | 0 .../Commands/FileSystem/FormatFileSystemCommand.cs | 0 .../Commands/FileSystem/RenewFileSystemCommand.cs | 0 .../Commands/Files/DeleteAllFilesCommand.cs | 0 .../Commands/Files/DeleteFileCommand.cs | 0 .../Commands/Files/InitialFileBytesCommand.cs | 0 .../Commands/Files/ListFilesCommand.cs | 0 .../Commands/Files/WriteFileCommand.cs | 0 .../Commands/MeadowCommand.cs | 0 .../Commands/MeadowSerialCommand.cs | 0 .../Commands/Mono/MonoDisableCommand.cs | 0 .../Commands/Mono/MonoEnableCommand.cs | 0 .../Commands/Mono/MonoFlashCommand.cs | 0 .../Commands/Mono/MonoRunStateCommand.cs | 0 .../Commands/Mono/MonoUpdateRuntimeCommand.cs | 0 .../Commands/Nsh/NshDisableCommand.cs | 0 .../Commands/Nsh/NshEnableCommand.cs | 0 .../Commands/Qspi/QspiInitCommand.cs | 0 .../Commands/Qspi/QspiReadCommand.cs | 0 .../Commands/Qspi/QspiWriteCommand.cs | 0 .../Commands/Storage/EraseFlashCommand.cs | 0 .../Commands/Storage/VerifyFlashCommand.cs | 0 .../Commands/Trace/SetDeveloperValueCommand.cs | 0 .../Commands/Trace/TraceDisableCommand.cs | 0 .../Commands/Trace/TraceEnableCommand.cs | 0 .../Commands/Trace/TraceLevelCommand.cs | 0 .../Commands/Trace/UartTraceCommand.cs | 0 .../Commands/Utility/ConsoleSpinner.cs | 0 .../Commands/Utility/DebugCommand.cs | 0 .../Commands/Utility/DownloadOsCommand.cs | 0 .../Commands/Utility/InstallDfuUtilCommand.cs | 0 .../Commands/Utility/ListPortsCommand.cs | 0 .../Commands/Utility/ListenCommand.cs | 0 .../Meadow.CLI.Classic.csproj | 0 {Meadow.CLI_rename => Meadow.CLI}/Meadow.CLI.csproj | 0 {Meadow.CLI_rename => Meadow.CLI}/Program.cs | 0 .../Properties/launchSettings.json | 0 {Meadow.CLI_rename => Meadow.CLI}/appsettings.json | 0 {Meadow.CLI_rename => Meadow.CLI}/images/icon.png | Bin .../HcomHostRequestType.cs | 0 .../HcomMeadowRequestType.cs | 0 .../HcomProtocolHeaderOffsets.cs | 0 .../HcomProtocolHeaderTypes.cs | 0 .../Meadow.Hcom.6.0.0.csproj | 0 .../Meadow.Hcom.Classic.csproj | 0 .../Meadow.Hcom.csproj | 0 .../MeadowCLIKey.snk | Bin 68 files changed, 0 insertions(+), 0 deletions(-) rename {Meadow.CLI_rename => Meadow.CLI}/AssemblyInfo.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/CliFxConsoleLoggerProvider.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/App/DeployAppCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/CloudCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Collection/ListCollectionCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Command/JsonDocumentBindingConverter.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Command/PublishCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/LoginCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/LogoutCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Package/CreatePackageCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Package/ListPackagesCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Package/PublishPackageCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Cloud/Package/UploadPackageCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/FlashEspCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/FlashOsCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/GetDeviceInfoCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/GetDeviceNameCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/ProvisionDeviceCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/DeviceManagement/UsePortCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Esp32/RestartEsp32Command.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Esp32/WriteEsp32FileCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/FileSystem/FormatFileSystemCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/FileSystem/RenewFileSystemCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Files/DeleteAllFilesCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Files/DeleteFileCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Files/InitialFileBytesCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Files/ListFilesCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Files/WriteFileCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/MeadowCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/MeadowSerialCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Mono/MonoDisableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Mono/MonoEnableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Mono/MonoFlashCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Mono/MonoRunStateCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Mono/MonoUpdateRuntimeCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Nsh/NshDisableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Nsh/NshEnableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Qspi/QspiInitCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Qspi/QspiReadCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Qspi/QspiWriteCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Storage/EraseFlashCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Storage/VerifyFlashCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Trace/SetDeveloperValueCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Trace/TraceDisableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Trace/TraceEnableCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Trace/TraceLevelCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Trace/UartTraceCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/ConsoleSpinner.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/DebugCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/DownloadOsCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/InstallDfuUtilCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/ListPortsCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Commands/Utility/ListenCommand.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Meadow.CLI.Classic.csproj (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Meadow.CLI.csproj (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Program.cs (100%) rename {Meadow.CLI_rename => Meadow.CLI}/Properties/launchSettings.json (100%) rename {Meadow.CLI_rename => Meadow.CLI}/appsettings.json (100%) rename {Meadow.CLI_rename => Meadow.CLI}/images/icon.png (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/HcomHostRequestType.cs (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/HcomMeadowRequestType.cs (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/HcomProtocolHeaderOffsets.cs (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/HcomProtocolHeaderTypes.cs (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/Meadow.Hcom.6.0.0.csproj (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/Meadow.Hcom.Classic.csproj (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/Meadow.Hcom.csproj (100%) rename {Meadow.Hcom_rename => Meadow.Hcom}/MeadowCLIKey.snk (100%) diff --git a/Meadow.CLI_rename/AssemblyInfo.cs b/Meadow.CLI/AssemblyInfo.cs similarity index 100% rename from Meadow.CLI_rename/AssemblyInfo.cs rename to Meadow.CLI/AssemblyInfo.cs diff --git a/Meadow.CLI_rename/CliFxConsoleLoggerProvider.cs b/Meadow.CLI/CliFxConsoleLoggerProvider.cs similarity index 100% rename from Meadow.CLI_rename/CliFxConsoleLoggerProvider.cs rename to Meadow.CLI/CliFxConsoleLoggerProvider.cs diff --git a/Meadow.CLI_rename/Commands/App/DeployAppCommand.cs b/Meadow.CLI/Commands/App/DeployAppCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/App/DeployAppCommand.cs rename to Meadow.CLI/Commands/App/DeployAppCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/CloudCommand.cs b/Meadow.CLI/Commands/Cloud/CloudCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/CloudCommand.cs rename to Meadow.CLI/Commands/Cloud/CloudCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Collection/ListCollectionCommand.cs b/Meadow.CLI/Commands/Cloud/Collection/ListCollectionCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Collection/ListCollectionCommand.cs rename to Meadow.CLI/Commands/Cloud/Collection/ListCollectionCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Command/JsonDocumentBindingConverter.cs b/Meadow.CLI/Commands/Cloud/Command/JsonDocumentBindingConverter.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Command/JsonDocumentBindingConverter.cs rename to Meadow.CLI/Commands/Cloud/Command/JsonDocumentBindingConverter.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Command/PublishCommand.cs b/Meadow.CLI/Commands/Cloud/Command/PublishCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Command/PublishCommand.cs rename to Meadow.CLI/Commands/Cloud/Command/PublishCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/LoginCommand.cs b/Meadow.CLI/Commands/Cloud/LoginCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/LoginCommand.cs rename to Meadow.CLI/Commands/Cloud/LoginCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/LogoutCommand.cs b/Meadow.CLI/Commands/Cloud/LogoutCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/LogoutCommand.cs rename to Meadow.CLI/Commands/Cloud/LogoutCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Package/CreatePackageCommand.cs b/Meadow.CLI/Commands/Cloud/Package/CreatePackageCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Package/CreatePackageCommand.cs rename to Meadow.CLI/Commands/Cloud/Package/CreatePackageCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Package/ListPackagesCommand.cs b/Meadow.CLI/Commands/Cloud/Package/ListPackagesCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Package/ListPackagesCommand.cs rename to Meadow.CLI/Commands/Cloud/Package/ListPackagesCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Package/PublishPackageCommand.cs b/Meadow.CLI/Commands/Cloud/Package/PublishPackageCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Package/PublishPackageCommand.cs rename to Meadow.CLI/Commands/Cloud/Package/PublishPackageCommand.cs diff --git a/Meadow.CLI_rename/Commands/Cloud/Package/UploadPackageCommand.cs b/Meadow.CLI/Commands/Cloud/Package/UploadPackageCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Cloud/Package/UploadPackageCommand.cs rename to Meadow.CLI/Commands/Cloud/Package/UploadPackageCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/FlashEspCommand.cs b/Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/FlashEspCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/FlashOsCommand.cs b/Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/FlashOsCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceInfoCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceInfoCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceNameCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/GetDeviceNameCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/ProvisionDeviceCommand.cs b/Meadow.CLI/Commands/DeviceManagement/ProvisionDeviceCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/ProvisionDeviceCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/ProvisionDeviceCommand.cs diff --git a/Meadow.CLI_rename/Commands/DeviceManagement/UsePortCommand.cs b/Meadow.CLI/Commands/DeviceManagement/UsePortCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/DeviceManagement/UsePortCommand.cs rename to Meadow.CLI/Commands/DeviceManagement/UsePortCommand.cs diff --git a/Meadow.CLI_rename/Commands/Esp32/RestartEsp32Command.cs b/Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Esp32/RestartEsp32Command.cs rename to Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs diff --git a/Meadow.CLI_rename/Commands/Esp32/WriteEsp32FileCommand.cs b/Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Esp32/WriteEsp32FileCommand.cs rename to Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs diff --git a/Meadow.CLI_rename/Commands/FileSystem/FormatFileSystemCommand.cs b/Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/FileSystem/FormatFileSystemCommand.cs rename to Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs diff --git a/Meadow.CLI_rename/Commands/FileSystem/RenewFileSystemCommand.cs b/Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/FileSystem/RenewFileSystemCommand.cs rename to Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs diff --git a/Meadow.CLI_rename/Commands/Files/DeleteAllFilesCommand.cs b/Meadow.CLI/Commands/Files/DeleteAllFilesCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Files/DeleteAllFilesCommand.cs rename to Meadow.CLI/Commands/Files/DeleteAllFilesCommand.cs diff --git a/Meadow.CLI_rename/Commands/Files/DeleteFileCommand.cs b/Meadow.CLI/Commands/Files/DeleteFileCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Files/DeleteFileCommand.cs rename to Meadow.CLI/Commands/Files/DeleteFileCommand.cs diff --git a/Meadow.CLI_rename/Commands/Files/InitialFileBytesCommand.cs b/Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Files/InitialFileBytesCommand.cs rename to Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs diff --git a/Meadow.CLI_rename/Commands/Files/ListFilesCommand.cs b/Meadow.CLI/Commands/Files/ListFilesCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Files/ListFilesCommand.cs rename to Meadow.CLI/Commands/Files/ListFilesCommand.cs diff --git a/Meadow.CLI_rename/Commands/Files/WriteFileCommand.cs b/Meadow.CLI/Commands/Files/WriteFileCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Files/WriteFileCommand.cs rename to Meadow.CLI/Commands/Files/WriteFileCommand.cs diff --git a/Meadow.CLI_rename/Commands/MeadowCommand.cs b/Meadow.CLI/Commands/MeadowCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/MeadowCommand.cs rename to Meadow.CLI/Commands/MeadowCommand.cs diff --git a/Meadow.CLI_rename/Commands/MeadowSerialCommand.cs b/Meadow.CLI/Commands/MeadowSerialCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/MeadowSerialCommand.cs rename to Meadow.CLI/Commands/MeadowSerialCommand.cs diff --git a/Meadow.CLI_rename/Commands/Mono/MonoDisableCommand.cs b/Meadow.CLI/Commands/Mono/MonoDisableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Mono/MonoDisableCommand.cs rename to Meadow.CLI/Commands/Mono/MonoDisableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Mono/MonoEnableCommand.cs b/Meadow.CLI/Commands/Mono/MonoEnableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Mono/MonoEnableCommand.cs rename to Meadow.CLI/Commands/Mono/MonoEnableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Mono/MonoFlashCommand.cs b/Meadow.CLI/Commands/Mono/MonoFlashCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Mono/MonoFlashCommand.cs rename to Meadow.CLI/Commands/Mono/MonoFlashCommand.cs diff --git a/Meadow.CLI_rename/Commands/Mono/MonoRunStateCommand.cs b/Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Mono/MonoRunStateCommand.cs rename to Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs diff --git a/Meadow.CLI_rename/Commands/Mono/MonoUpdateRuntimeCommand.cs b/Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Mono/MonoUpdateRuntimeCommand.cs rename to Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs diff --git a/Meadow.CLI_rename/Commands/Nsh/NshDisableCommand.cs b/Meadow.CLI/Commands/Nsh/NshDisableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Nsh/NshDisableCommand.cs rename to Meadow.CLI/Commands/Nsh/NshDisableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Nsh/NshEnableCommand.cs b/Meadow.CLI/Commands/Nsh/NshEnableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Nsh/NshEnableCommand.cs rename to Meadow.CLI/Commands/Nsh/NshEnableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Qspi/QspiInitCommand.cs b/Meadow.CLI/Commands/Qspi/QspiInitCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Qspi/QspiInitCommand.cs rename to Meadow.CLI/Commands/Qspi/QspiInitCommand.cs diff --git a/Meadow.CLI_rename/Commands/Qspi/QspiReadCommand.cs b/Meadow.CLI/Commands/Qspi/QspiReadCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Qspi/QspiReadCommand.cs rename to Meadow.CLI/Commands/Qspi/QspiReadCommand.cs diff --git a/Meadow.CLI_rename/Commands/Qspi/QspiWriteCommand.cs b/Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Qspi/QspiWriteCommand.cs rename to Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs diff --git a/Meadow.CLI_rename/Commands/Storage/EraseFlashCommand.cs b/Meadow.CLI/Commands/Storage/EraseFlashCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Storage/EraseFlashCommand.cs rename to Meadow.CLI/Commands/Storage/EraseFlashCommand.cs diff --git a/Meadow.CLI_rename/Commands/Storage/VerifyFlashCommand.cs b/Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Storage/VerifyFlashCommand.cs rename to Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs diff --git a/Meadow.CLI_rename/Commands/Trace/SetDeveloperValueCommand.cs b/Meadow.CLI/Commands/Trace/SetDeveloperValueCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Trace/SetDeveloperValueCommand.cs rename to Meadow.CLI/Commands/Trace/SetDeveloperValueCommand.cs diff --git a/Meadow.CLI_rename/Commands/Trace/TraceDisableCommand.cs b/Meadow.CLI/Commands/Trace/TraceDisableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Trace/TraceDisableCommand.cs rename to Meadow.CLI/Commands/Trace/TraceDisableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Trace/TraceEnableCommand.cs b/Meadow.CLI/Commands/Trace/TraceEnableCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Trace/TraceEnableCommand.cs rename to Meadow.CLI/Commands/Trace/TraceEnableCommand.cs diff --git a/Meadow.CLI_rename/Commands/Trace/TraceLevelCommand.cs b/Meadow.CLI/Commands/Trace/TraceLevelCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Trace/TraceLevelCommand.cs rename to Meadow.CLI/Commands/Trace/TraceLevelCommand.cs diff --git a/Meadow.CLI_rename/Commands/Trace/UartTraceCommand.cs b/Meadow.CLI/Commands/Trace/UartTraceCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Trace/UartTraceCommand.cs rename to Meadow.CLI/Commands/Trace/UartTraceCommand.cs diff --git a/Meadow.CLI_rename/Commands/Utility/ConsoleSpinner.cs b/Meadow.CLI/Commands/Utility/ConsoleSpinner.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/ConsoleSpinner.cs rename to Meadow.CLI/Commands/Utility/ConsoleSpinner.cs diff --git a/Meadow.CLI_rename/Commands/Utility/DebugCommand.cs b/Meadow.CLI/Commands/Utility/DebugCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/DebugCommand.cs rename to Meadow.CLI/Commands/Utility/DebugCommand.cs diff --git a/Meadow.CLI_rename/Commands/Utility/DownloadOsCommand.cs b/Meadow.CLI/Commands/Utility/DownloadOsCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/DownloadOsCommand.cs rename to Meadow.CLI/Commands/Utility/DownloadOsCommand.cs diff --git a/Meadow.CLI_rename/Commands/Utility/InstallDfuUtilCommand.cs b/Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/InstallDfuUtilCommand.cs rename to Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs diff --git a/Meadow.CLI_rename/Commands/Utility/ListPortsCommand.cs b/Meadow.CLI/Commands/Utility/ListPortsCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/ListPortsCommand.cs rename to Meadow.CLI/Commands/Utility/ListPortsCommand.cs diff --git a/Meadow.CLI_rename/Commands/Utility/ListenCommand.cs b/Meadow.CLI/Commands/Utility/ListenCommand.cs similarity index 100% rename from Meadow.CLI_rename/Commands/Utility/ListenCommand.cs rename to Meadow.CLI/Commands/Utility/ListenCommand.cs diff --git a/Meadow.CLI_rename/Meadow.CLI.Classic.csproj b/Meadow.CLI/Meadow.CLI.Classic.csproj similarity index 100% rename from Meadow.CLI_rename/Meadow.CLI.Classic.csproj rename to Meadow.CLI/Meadow.CLI.Classic.csproj diff --git a/Meadow.CLI_rename/Meadow.CLI.csproj b/Meadow.CLI/Meadow.CLI.csproj similarity index 100% rename from Meadow.CLI_rename/Meadow.CLI.csproj rename to Meadow.CLI/Meadow.CLI.csproj diff --git a/Meadow.CLI_rename/Program.cs b/Meadow.CLI/Program.cs similarity index 100% rename from Meadow.CLI_rename/Program.cs rename to Meadow.CLI/Program.cs diff --git a/Meadow.CLI_rename/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json similarity index 100% rename from Meadow.CLI_rename/Properties/launchSettings.json rename to Meadow.CLI/Properties/launchSettings.json diff --git a/Meadow.CLI_rename/appsettings.json b/Meadow.CLI/appsettings.json similarity index 100% rename from Meadow.CLI_rename/appsettings.json rename to Meadow.CLI/appsettings.json diff --git a/Meadow.CLI_rename/images/icon.png b/Meadow.CLI/images/icon.png similarity index 100% rename from Meadow.CLI_rename/images/icon.png rename to Meadow.CLI/images/icon.png diff --git a/Meadow.Hcom_rename/HcomHostRequestType.cs b/Meadow.Hcom/HcomHostRequestType.cs similarity index 100% rename from Meadow.Hcom_rename/HcomHostRequestType.cs rename to Meadow.Hcom/HcomHostRequestType.cs diff --git a/Meadow.Hcom_rename/HcomMeadowRequestType.cs b/Meadow.Hcom/HcomMeadowRequestType.cs similarity index 100% rename from Meadow.Hcom_rename/HcomMeadowRequestType.cs rename to Meadow.Hcom/HcomMeadowRequestType.cs diff --git a/Meadow.Hcom_rename/HcomProtocolHeaderOffsets.cs b/Meadow.Hcom/HcomProtocolHeaderOffsets.cs similarity index 100% rename from Meadow.Hcom_rename/HcomProtocolHeaderOffsets.cs rename to Meadow.Hcom/HcomProtocolHeaderOffsets.cs diff --git a/Meadow.Hcom_rename/HcomProtocolHeaderTypes.cs b/Meadow.Hcom/HcomProtocolHeaderTypes.cs similarity index 100% rename from Meadow.Hcom_rename/HcomProtocolHeaderTypes.cs rename to Meadow.Hcom/HcomProtocolHeaderTypes.cs diff --git a/Meadow.Hcom_rename/Meadow.Hcom.6.0.0.csproj b/Meadow.Hcom/Meadow.Hcom.6.0.0.csproj similarity index 100% rename from Meadow.Hcom_rename/Meadow.Hcom.6.0.0.csproj rename to Meadow.Hcom/Meadow.Hcom.6.0.0.csproj diff --git a/Meadow.Hcom_rename/Meadow.Hcom.Classic.csproj b/Meadow.Hcom/Meadow.Hcom.Classic.csproj similarity index 100% rename from Meadow.Hcom_rename/Meadow.Hcom.Classic.csproj rename to Meadow.Hcom/Meadow.Hcom.Classic.csproj diff --git a/Meadow.Hcom_rename/Meadow.Hcom.csproj b/Meadow.Hcom/Meadow.Hcom.csproj similarity index 100% rename from Meadow.Hcom_rename/Meadow.Hcom.csproj rename to Meadow.Hcom/Meadow.Hcom.csproj diff --git a/Meadow.Hcom_rename/MeadowCLIKey.snk b/Meadow.Hcom/MeadowCLIKey.snk similarity index 100% rename from Meadow.Hcom_rename/MeadowCLIKey.snk rename to Meadow.Hcom/MeadowCLIKey.snk From d0e82e82641f6a890c4ad1b7a80a9b1f749ff088 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 11:34:44 -0800 Subject: [PATCH 125/141] Update metadata and readme --- README.md | 193 ++----------------------- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 9 +- 2 files changed, 15 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 739eaa31..ea5719f8 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,6 @@ ## Getting Started -The CLI tool supports DFU flashing for `nuttx.bin` and `nuttx_user.bin`. When the application is run with `-d`, it looks for `nuttx.bin` and `nuttx_user.bin` in the application directory and if not found, it will abort. Optionally, paths for the files can be specific with `--osFile` and `--userFile`. - -The CLI tool also supports device and file management including file transfers, flash partitioning, and MCU reset. - To install the latest Meadow.CLI release, run the .NET tool install command to get the latest package from NuGet. ```console @@ -25,183 +21,25 @@ Once installed, run the Meadow.CLI from a command line with `meadow`. To see the options, run the application with the --help arg. -## Running Commands - -### Specifying the Serial Port -File and device commands require you to specify the serial port (`-s` or `--SerialPort`). You can determine the serial port name in Windows by viewing the Device Manager. The CLI will remember the last Serial Port used, so you only need to specify it if you need to change the value. - -On Mac and Linux, the serial port will show up in the **/dev** folder, generally with the prefix **tty.usb**. You can likely find the serial port name by running the command `ls /dev/tty.usb`. - -### Setting the Log Verbosity -Appending `-v` or `-vv` to any command will increase the logging verbosity to `Debug` and `Trace` respectively. `Trace` should only be necessary when debugging issues with the CLI. +## Useful commands -### Available Commands +### Download Meadow OS -```console -meadow v1.0.0 - -USAGE - meadow [options] - meadow [command] [...] - -OPTIONS - -h|--help Shows help text. - --version Shows version information. - -COMMANDS - app deploy Deploy the specified app to the Meadow - cloud login Log into the Meadow Service - cloud logout Logout of the Meadow Service - debug Debug a Meadow Application - device info Get the device info - device mac Read the ESP32's MAC address - device name Get the name of the Meadow - device provision Registers and prepares connected device for use with Meadow Cloud - download os Downloads the latest Meadow.OS to the host PC - esp32 file write Write files to the ESP File System - esp32 restart Restart the ESP32 - file delete Delete files from the Meadow File System Subcommands: file delete all. - file initial Get the initial bytes from a file - file list List files in the on-board filesystem - file write Write files to the Meadow File System - flash erase Erase the flash on the Meadow Board - flash esp Flash the ESP co-processor - flash os Update the OS on the Meadow Board - flash verify Verify the contents of the flash were deleted - fs renew Create a File System on the Meadow Board - install dfu-util Install the DfuUtil utility - list ports List available COM ports - listen Listen for console output from Meadow - mono disable Sets mono to NOT run on the Meadow board then resets it - mono enable Sets mono to run on the Meadow board and then resets it - mono flash Uploads the mono runtime file to the Meadow device. Does NOT move it into place - mono state Returns whether or not mono is enabled or disabled on the Meadow device - mono update rt Uploads the mono runtime files to the Meadow device and moves it into place - nsh disable Disables NSH on the Meadow device - nsh enable Enables NSH on the Meadow device - package create Create Meadow Package - package list List Meadow Packages - package publish List Meadow Packages - package upload Upload Meadow Package - qspi init Init the QSPI on the Meadow - qspi read Read a QSPI value from the Meadow - qspi write Write a QSPI value to the Meadow - set developer Set developer value - trace disable Disable Trace Logging on the Meadow - trace enable Enable trace logging on the Meadow - trace level Enable trace logging on the Meadow - uart trace Configure trace logs to go to UART - use port Set the preferred serial port ``` - -### Getting Help - -Specifying `--help` with no command will output the list of available commands. Specifying `--help` after a command (e.g., `meadow file delete --help`) will output command specific help. - -```console -meadow v1.0.0 - -USAGE - meadow file delete --files [options] - meadow file delete [command] [...] - -DESCRIPTION - Delete files from the Meadow File System - -OPTIONS -* -f|--files The file(s) to delete from the Meadow Files System - -s|--SerialPort Meadow COM port Default: "COM10". - -g|--LogVerbosity Log verbosity - -h|--help Shows help text. - -COMMANDS - all Delete all files from the Meadow File System - -You can run `meadow file delete [command] --help` to show help on a specific command. -Done! +meadow firmware download ``` -## Useful commands - ### Update the Meadow OS + ``` -meadow flash os +meadow firmware write ``` -#### Meadow.CLI download location - -If you need to find or clear out any of the OS download files retrieved by Meadow.CLI, they are located in a WildernessLabs folder in the user directory. - -macOS: `~/.local/share/WildernessLabs/Firmware/` -Windows: `%LOCALAPPDATA%\WildernessLabs\Firmware` - ### Listen for Meadow Console.WriteLine ``` meadow listen ``` -### Set the trace level - -You can set the debug trace level to values 0, 1, 2, or 3. 2 is the most useful. -``` -meadow trace enable --level 2 -``` - -### File transfers -``` -meadow files write -f [NameOfFile] -``` -You may specify multiple instances of `-f` to send multiple files - -### List files in flash -``` -meadow files list -``` - -### Delete a File - -``` -meadow files delete -f [NameOfFile] -``` -You may specify multiple instances of `-f` to send multiple files - -### Stop/start the installed application from running automatically -``` -meadow mono disable -meadow mono enable -``` -### Useful utilities -``` -meadow device info -meadow device name -``` - -### Debugging -**NOTE THIS IS NOT YET FULLY IMPLEMENTED, IT WILL NOT WORK** -``` -meadow debug --DebugPort XXXX -``` -This starts listening on the specified port for a debugger to attach - -Note: you can use SDB command line debugger from https://github.com/mono/sdb. Just build it according to its readme, run the above command and then: - -``` -sdb "connect 127.0.0.1 XXXX" -``` -Substitute XXXX for the same port number as above - -## Running applications - -You'll typically need at least 5 files installed to the Meadow flash to run a Meadow app: - -1. System.dll -2. System.Core.dll -3. mscorlib.dll -4. Meadow.Core.dll -5. App.exe (your app) - -It's a good idea to disable mono first, copy the files, and then enable mono - ## Uninstall the Meadow.CLI tool If you ever need to remove the Meadow.CLI tool, you can remove it through the .NET command-line tool as you would any other global tool. @@ -243,18 +81,9 @@ dotnet tool uninstall WildernessLabs.Meadow.CLI --global dotnet tool install WildernessLabs.Meadow.CLI --global ``` -# License - -Copyright Wilderness Labs Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +### Meadow.CLI download location + +If you need to find or clear out any of the OS download files retrieved by Meadow.CLI, they are located in a WildernessLabs folder in the user directory. + +macOS: `~/.local/share/WildernessLabs/Firmware/` +Windows: `%LOCALAPPDATA%\WildernessLabs\Firmware` diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index c6885f97..9772f486 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -5,17 +5,16 @@ net6.0 enable true - Wilderness Labs, Inc meadow WildernessLabs.Meadow.CLI Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0.7 + 2.0.0.7-beta AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ - icon.png https://github.com/WildernessLabs/Meadow.CLI + icon.png Meadow, Meadow.Foundation, Meadow.CLI Command-line interface for Meadow false @@ -23,8 +22,8 @@ false false meadow - latest - Copyright 2020-2023 Wilderness Labs + 11.0 + Copyright 2020-2024 Wilderness Labs enable Apache-2.0 README.md From cbec09a80e8fabc89745aacf40a289a5c09fcf9c Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 11:46:42 -0800 Subject: [PATCH 126/141] Update nuget package name --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 9772f486..8712b420 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0.7-beta + 2.0.0-beta.1 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ https://github.com/WildernessLabs/Meadow.CLI From f73d089c813c6ceb18f0782580dbca2067a8a1a4 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 11:48:26 -0800 Subject: [PATCH 127/141] Fix project url --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 8712b420..0b1c230e 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -12,7 +12,7 @@ true 2.0.0-beta.1 AnyCPU - http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ + http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI icon.png Meadow, Meadow.Foundation, Meadow.CLI From c9aebb766aae16f12a039a9eeb1a7b5afdee4205 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 11:50:36 -0800 Subject: [PATCH 128/141] Remove banner from readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ea5719f8..f7b52339 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -Meadow CLI project banner stating Meadow's Command-Line-Interface to interact with the board and perform functions via a terminal/command-line window. - ## Build Status [![Build](https://github.com/WildernessLabs/Meadow.CLI/actions/workflows/dotnet.yml/badge.svg)](https://github.com/WildernessLabs/Meadow.CLI/actions) From 465c8c772cd6d4119fcb2291286698c827aad2a7 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 20:11:43 -0800 Subject: [PATCH 129/141] Debugger data pumbing --- .../v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- .../Connections/SimulatorConnection.cs | 5 ++ .../SerialRequests/DebuggerDataRequest.cs | 20 ++++++ .../Meadow.Hcom/Connections/ConnectionBase.cs | 2 + .../Connections/LocalConnection.cs | 5 ++ .../SerialConnection.ListenerProc.cs | 3 +- .../Connections/SerialConnection.cs | 26 ++++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 5 ++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 1 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 1 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 7 +- .../SerialResponses/SerialResponse.cs | 70 +++++++------------ 12 files changed, 96 insertions(+), 51 deletions(-) create mode 100644 Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index 33b4818b..b08591a3 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.7"; + public const string CLI_VERSION = "2.0.0.8"; } } \ No newline at end of file diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs index 78c1b92c..fb9933a0 100644 --- a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -102,6 +102,11 @@ public override Task StartDebuggingSession(int port, ILogger? l throw new NotImplementedException(); } + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + public override Task TraceDisable(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); diff --git a/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs b/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs new file mode 100644 index 00000000..b41fc660 --- /dev/null +++ b/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs @@ -0,0 +1,20 @@ +namespace Meadow.Hcom +{ + internal class DebuggerDataRequest : Request + { + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA; + + public byte[] DebuggerData + { + get + { + if (Payload == null) return new byte[0]; + return Payload; + } + set + { + Payload = value; + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 10b763fe..cf01b887 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -45,6 +45,8 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken); public abstract Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); + public abstract Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); + public ConnectionBase() { } diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index f0cdeae5..ee188ec8 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -208,6 +208,11 @@ public override Task StartDebuggingSession(int port, ILogger? l throw new NotImplementedException(); } + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + public override Task TraceDisable(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 6bce3d63..7482b4d7 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -196,7 +196,8 @@ private async Task ListenerProc() else if (response is TextRequestResponse trr) { // this is a response to a text request - the exact request is cached - Debug.WriteLine($"RESPONSE> {trr.Text}"); + _lastRequestConcluded = (RequestType)trr.RequestType; + //Debug.WriteLine($"RESPONSE> {trr.Text}"); } else if (response is DeviceInfoSerialResponse dir) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index fea79e9e..1d295601 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -418,8 +418,10 @@ private class SerialMessage public SerialMessage(Memory segment) { - _segments = new List>(); - _segments.Add(segment); + _segments = new List> + { + segment + }; } public void AddSegment(Memory segment) @@ -1241,4 +1243,24 @@ public override async Task StartDebugging(int port, ILogger? logger, Cancellatio new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); } } + + public override async Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) + { + var command = RequestBuilder.Build(userData); + command.DebuggerData = debuggerData; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 022011bc..ae7de6b5 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -184,6 +184,11 @@ public override Task StartDebugging(int port, ILogger? logger, CancellationToken throw new NotImplementedException(); } + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) + { + throw new NotImplementedException(); + } + public override void Detach() { throw new NotImplementedException(); diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 16e3dc67..2990b98a 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -46,5 +46,6 @@ public interface IMeadowConnection Task GetPublicKey(CancellationToken? cancellationToken = null); Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken); Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); + Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index 3d20142b..590caa26 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -27,5 +27,6 @@ public interface IMeadowDevice Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); Task GetPublicKey(CancellationToken? cancellationToken = null); Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); + Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 9b9b3b7c..e270958a 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -4,7 +4,7 @@ namespace Meadow.Hcom { public partial class MeadowDevice : IMeadowDevice { - private IMeadowConnection _connection; + private readonly IMeadowConnection _connection; internal MeadowDevice(IMeadowConnection connection) { @@ -156,5 +156,10 @@ public async Task StartDebugging(int port, ILogger? logger, CancellationToken? c { await _connection.StartDebugging(port, logger, cancellationToken); } + + public async Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) + { + await _connection.SendDebuggerData(debuggerData, userData, cancellationToken); + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs index bdb66bab..759e215c 100644 --- a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs +++ b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs @@ -22,53 +22,31 @@ public static SerialResponse Parse(byte[] data, int length) { var type = (ResponseType)BitConverter.ToUInt16(data, HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET); - switch (type) + return type switch { - case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR: - return new TextStdErrResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT: - return new TextStdOutResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_INFORMATION: - return new TextInformationResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: - return new TextRequestResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_REJECTED: - return new TextRequestRejectedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: - return new DeviceInfoSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: - return new TextConcludedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER: - return new TextListHeaderResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER: - return new TextListMemberResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER: - return new TextCrcMemberResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL: - return new FileReadInitFailedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY: - return new FileReadInitOkResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA: - return new UploadDataPacketResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED: - return new UploadCompletedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_ERROR: - return new RequestErrorTextResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_RECONNECT: - return new ReconnectRequiredResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL: - return new FileWriteInitFailedSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY: - return new FileWriteInitOkSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_SEND_INITIAL_FILE_BYTES: - return new TextPayloadSerialResponse(data, length); - case ResponseType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE: - return new TextPayloadSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_DNLD_FAIL_RESEND: - return new FileDownloadFailedResponse(data, length); - default: - return new SerialResponse(data, length); - } + ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR => new TextStdErrResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT => new TextStdOutResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_INFORMATION => new TextInformationResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED => new TextRequestResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_REJECTED => new TextRequestRejectedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO => new DeviceInfoSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED => new TextConcludedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER => new TextListHeaderResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER => new TextListMemberResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER => new TextCrcMemberResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL => new FileReadInitFailedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY => new FileReadInitOkResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA => new UploadDataPacketResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED => new UploadCompletedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_ERROR => new RequestErrorTextResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_RECONNECT => new ReconnectRequiredResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL => new FileWriteInitFailedSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY => new FileWriteInitOkSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_SEND_INITIAL_FILE_BYTES => new TextPayloadSerialResponse(data, length), + ResponseType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE => new TextPayloadSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_DNLD_FAIL_RESEND => new FileDownloadFailedResponse(data, length), + _ => new SerialResponse(data, length), + }; } protected SerialResponse(byte[] data, int length) From 69a7931ad7abd7fc6a8ae49ac56c2f3c81cc2178 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 21:29:07 -0800 Subject: [PATCH 130/141] Update debugging server to use SendDebuggerData method --- .../Commands/Current/App/AppDebugCommand.cs | 11 +++++------ Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs | 11 ++++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index be9cca93..0d210acd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -31,13 +31,12 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation(e.message); }; - using (var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken)) + using var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken); + + if (Console != null) { - if (Console != null) - { - Logger?.LogInformation("Debugging server started - press Enter to exit"); - await Console.Input.ReadLineAsync(); - } + Logger?.LogInformation("Debugging server started - press Enter to exit"); + await Console.Input.ReadLineAsync(); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index cddd4fd3..7547ad2f 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -17,7 +17,7 @@ public class DebuggingServer : IDisposable // VS 2015 - 4020 public IPEndPoint LocalEndpoint { get; private set; } - private readonly object _lck = new object(); + private readonly object _lck = new(); private CancellationTokenSource _cancellationTokenSource; private readonly ILogger? _logger; private readonly IMeadowDevice _meadow; @@ -193,6 +193,7 @@ private async Task SendToMeadowAsync() // Receive from Visual Studio and send to Meadow var receiveBuffer = ArrayPool.Shared.Rent(RECEIVE_BUFFER_SIZE); var meadowBuffer = Array.Empty(); + while (!_cts.IsCancellationRequested) { if (_networkStream != null && _networkStream.CanRead) @@ -201,8 +202,11 @@ private async Task SendToMeadowAsync() do { bytesRead = await _networkStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, _cts.Token); + if (bytesRead == 0 || _cts.IsCancellationRequested) - continue; + { + continue; + } var destIndex = meadowBuffer.Length; Array.Resize(ref meadowBuffer, destIndex + bytesRead); @@ -214,7 +218,8 @@ private async Task SendToMeadowAsync() BitConverter.ToString(md5.ComputeHash(meadowBuffer)) .Replace("-", string.Empty) .ToLowerInvariant()); - // TODO await _meadow.ForwardVisualStudioDataToMono(meadowBuffer, 0); + + await _meadow.SendDebuggerData(meadowBuffer, 0, _cts.Token); meadowBuffer = Array.Empty(); // Ensure we read all the data in this message before passing it along From 96d9baa31f8c9696944589a62c8e27eef7add515 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 21:40:37 -0800 Subject: [PATCH 131/141] Remove redundant error messages --- .../Commands/Current/App/AppDebugCommand.cs | 1 - .../Meadow.Cli/Commands/Current/App/AppRunCommand.cs | 1 - .../Meadow.Cli/Commands/Current/DeveloperCommand.cs | 1 - .../Commands/Current/Device/DeviceClockCommand.cs | 1 - .../Commands/Current/Device/DeviceInfoCommand.cs | 1 - .../Commands/Current/Device/DeviceProvisionCommand.cs | 1 - .../Commands/Current/Device/DeviceResetCommand.cs | 1 - .../Commands/Current/File/FileDeleteCommand.cs | 1 - .../Commands/Current/File/FileInitialCommand.cs | 1 - .../Commands/Current/File/FileListCommand.cs | 1 - .../Commands/Current/File/FileReadCommand.cs | 1 - .../Commands/Current/File/FileWriteCommand.cs | 1 - .../Commands/Current/Firmware/FirmwareWriteCommand.cs | 11 ++--------- .../Commands/Current/Flash/FlashEraseCommand.cs | 1 - .../Commands/Current/Runtime/RuntimeDisableCommand.cs | 1 - .../Commands/Current/Runtime/RuntimeEnableCommand.cs | 1 - .../Commands/Current/Runtime/RuntimeStateCommand.cs | 1 - .../Commands/Current/Trace/TraceDisableCommand.cs | 1 - .../Commands/Current/Trace/TraceEnableCommand.cs | 1 - .../Commands/Current/Trace/TraceLevelCommand.cs | 1 - .../Commands/Current/Uart/UartTraceDisableCommand.cs | 1 - .../Commands/Current/Uart/UartTraceEnableCommand.cs | 1 - .../v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs | 1 - 23 files changed, 2 insertions(+), 31 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index 0d210acd..50190451 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -22,7 +22,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null) { - Logger?.LogError($"No connection path is defined"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index a62c51ba..c21b1798 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -31,7 +31,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null) { - Logger?.LogError("No connection path is defined"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index d09ee1f6..15fa96a1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -22,7 +22,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Developer parameter set failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index 0d31628c..b0e73ba7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -19,7 +19,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Device clock failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs index 5b92bc89..ec412c4f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -18,7 +18,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Device info failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index 4bab3747..fba0d8c7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -79,7 +79,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Device provision failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs index ed3c83db..6baf6e7c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -18,7 +18,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Device reset failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index 60b0c73b..4b5ba86b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -19,7 +19,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"File delete failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index d850566b..24067f2f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -19,7 +19,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"File initial failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 20be06d3..8358fb2e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -24,7 +24,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"File list failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index 384276f1..c816c473 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -22,7 +22,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"File read failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index b736055f..60eefa5b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -30,7 +30,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"File write failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index c5b54558..296de248 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -93,15 +93,8 @@ protected override async ValueTask ExecuteCommand() } IMeadowConnection? connection = null; - try - { - connection = await GetCurrentConnection(); - } - catch (Exception ex) - { - Logger?.LogError(ex.Message); - return; - } + + connection = await GetCurrentConnection(); if (connection == null || connection.Device == null) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index 8a458a9c..ad32bb10 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Flash erase failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs index 908c2d01..5a098164 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs index 066dba13..bc955c0c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Runtime disable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs index 0c772ee4..b29b9170 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Runtime state failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs index 94b7a602..e013fc59 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Trace disable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs index 31e393be..fb107fe4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -19,7 +19,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Trace enable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index 27889c80..df7e984d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -19,7 +19,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Trace level failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs index be478492..4bc77908 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Uart trace disable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs index 5a6ece03..833bdb48 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs @@ -16,7 +16,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"Uart trace enable failed - device or connection not found"); return; } diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index 57a9578c..bc7981d5 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -154,7 +154,6 @@ protected override async ValueTask ExecuteCommand() if (connection == null || connection.Device == null) { - Logger?.LogError($"No connection path is defined"); return; } From a00ee6881a97fd8f97324a912f760adbac8b2ee3 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 21:41:34 -0800 Subject: [PATCH 132/141] Cleanup --- .../Commands/Current/App/AppDeployCommand.cs | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index 249fc1cb..5a3b70d6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -27,80 +27,78 @@ protected override async ValueTask ExecuteCommand() return; } - if (connection != null) - { - string path = Path ?? Environment.CurrentDirectory; + string path = Path ?? Environment.CurrentDirectory; - // is the path a file? - FileInfo file; + // is the path a file? + FileInfo file; - var lastFile = string.Empty; + var lastFile = string.Empty; - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); + // in order to deploy, the runtime must be disabled + var isRuntimeEnabled = await connection.IsRuntimeEnabled(); - await connection.RuntimeDisable(CancellationToken); - } + if (isRuntimeEnabled) + { + Logger?.LogInformation("Disabling runtime..."); + + await connection.RuntimeDisable(CancellationToken); + } + + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; - connection.FileWriteProgress += (s, e) => + if (e.fileName != lastFile) { - var p = (e.completed / (double)e.total) * 100d; + Console?.Output.WriteAsync("\n"); + lastFile = e.fileName; + } - if (e.fileName != lastFile) - { - Console?.Output.WriteAsync("\n"); - lastFile = e.fileName; - } + // Console instead of Logger due to line breaking for progress bar + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + }; - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) + { + Logger?.LogError($"Invalid application path '{path}'"); + return; + } - if (!File.Exists(path)) + // does the directory have an App.dll in it? + file = new FileInfo(System.IO.Path.Combine(path, "App.dll")); + if (!file.Exists) { - // is it a valid directory? - if (!Directory.Exists(path)) + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) { - Logger?.LogError($"Invalid application path '{path}'"); + Logger?.LogError($"Cannot find a compiled application at '{path}'"); return; } - // does the directory have an App.dll in it? - file = new FileInfo(System.IO.Path.Combine(path, "App.dll")); - if (!file.Exists) - { - // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{path}'"); - return; - } - - file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - } - } - else - { - // TODO: only deploy if it's App.dll - file = new FileInfo(path); + file = candidates.OrderByDescending(c => c.LastWriteTime).First(); } + } + else + { + // TODO: only deploy if it's App.dll + file = new FileInfo(path); + } - var targetDirectory = file.DirectoryName!; + var targetDirectory = file.DirectoryName!; - await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, CancellationToken); + await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, CancellationToken); - if (wasRuntimeEnabled) - { - // restore runtime state - Logger?.LogInformation("Enabling runtime..."); + if (isRuntimeEnabled) + { + // restore runtime state + Logger?.LogInformation("Enabling runtime..."); - await connection.RuntimeEnable(CancellationToken); - } + await connection.RuntimeEnable(CancellationToken); } } } \ No newline at end of file From a7a44a8b3dfec709949df8074968d1f63cf52340 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Wed, 24 Jan 2024 21:58:29 -0800 Subject: [PATCH 133/141] Minor output cleanup --- Source/v2/Meadow.Cli/AppManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index 994a8fac..9f0a3caf 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -43,7 +43,7 @@ public static async Task DeployApplication( var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); dependencies.Add(Path.Combine(localBinaryDirectory, "App.dll")); - logger?.LogInformation("Generating the list of files to deploy..."); + logger?.LogInformation("Generating list of files to deploy..."); foreach (var file in dependencies) { // TODO: add any other filtering capability here From 7cdec7b4d5b4f03942c858676f33250559f5844a Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 28 Jan 2024 15:31:16 -0800 Subject: [PATCH 134/141] Minor cleanup and version bump --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- .../Commands/Current/File/FileWriteCommand.cs | 7 +++---- .../Meadow.Cloud.Client/Messages/UserOrg.cs | 2 +- .../Meadow.Hcom/Debugging/DebuggingServer.cs | 21 ++++++++++++------- .../SerialRequests/InitFileWriteRequest.cs | 2 +- .../F7FirmwarePackageCollection.cs | 6 +++++- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 0b1c230e..1de59e7f 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0-beta.1 + 2.0.0-beta.2 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index 60eefa5b..aff9cc96 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -11,7 +11,7 @@ public class FileWriteCommand : BaseDeviceCommand 'f', Description = "The file(s) to write to the Meadow Files System", IsRequired = true)] - public IList Files { get; init; } + public IList Files { get; init; } = Array.Empty(); [CommandOption( "targetFiles", @@ -35,15 +35,14 @@ protected override async ValueTask ExecuteCommand() if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) { - Logger?.LogError( - $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); + Logger?.LogError($"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); return; } connection.FileWriteProgress += (s, e) => { - var p = (e.completed / (double)e.total) * 100d; + var p = e.completed / (double)e.total * 100d; // Console instead of Logger due to line breaking for progress bar Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); diff --git a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs index 14c9691c..cc4373df 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs @@ -10,4 +10,4 @@ public class UserOrg public string Name { get; set; } [JsonPropertyName("defaultCollectionId")] public string DefaultCollectionId { get; set; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index 7547ad2f..3de0a43c 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -18,7 +18,7 @@ public class DebuggingServer : IDisposable public IPEndPoint LocalEndpoint { get; private set; } private readonly object _lck = new(); - private CancellationTokenSource _cancellationTokenSource; + private CancellationTokenSource? _cancellationTokenSource; private readonly ILogger? _logger; private readonly IMeadowDevice _meadow; private ActiveClient? _activeClient; @@ -125,7 +125,7 @@ private void OnConnect(TcpClient tcpClient) CloseActiveClient(); } - _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource.Token); + _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource?.Token); _activeClientCount++; } } @@ -171,9 +171,17 @@ private class ActiveClient : IDisposable public bool Disposed = false; // Constructor - internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger? logger, CancellationToken cancellationToken) + internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger? logger, CancellationToken? cancellationToken) { - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (cancellationToken != null) + { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value); + } + else + { + _cts = new CancellationTokenSource(); + } + _logger = logger; _meadow = meadow; _tcpClient = tcpClient; @@ -279,7 +287,6 @@ private Task SendToVisualStudio() { // User probably hit stop _logger?.LogInformation("Unable to Write Data from Visual Studio"); - _logger?.LogTrace("Unable to Write Data from Visual Studio"); } } } @@ -287,8 +294,8 @@ private Task SendToVisualStudio() { // User probably hit stop; Removed logging as User doesn't need to see this // Keeping it as a TODO in case we find a side effect that needs logging. - // TODO _logger?.LogInformation("Operation Cancelled"); - // TODO _logger?.LogTrace(oce, "Operation Cancelled"); + _logger?.LogInformation("Operation Cancelled"); + _logger?.LogTrace(oce, "Operation Cancelled"); } catch (Exception ex) { diff --git a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs index a284dcef..ad6cb803 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs @@ -78,7 +78,7 @@ internal class InitFileWriteRequest : Request public byte[] Esp32MD5 { get; set; } = new byte[32]; public string LocalFileName { get; private set; } = default!; - public string MeadowFileName { get; private set; } + public string MeadowFileName { get; private set; } = default!; public void SetParameters( string localFile, diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index d20eddbc..ead22476 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -90,7 +90,11 @@ public async Task DeletePackage(string version) i--; } var newDefault = _f7Packages[i].Version; - _f7Packages.Remove(DefaultPackage); + + if (DefaultPackage != null) + { + _f7Packages.Remove(DefaultPackage); + } await SetDefaultPackage(newDefault); var path = Path.Combine(PackageFileRoot, version); From 5515243554e618b4dfb18c3afbeebc823d975a32 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 28 Jan 2024 15:38:45 -0800 Subject: [PATCH 135/141] Output cleanup --- Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs | 4 ---- Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index aa083008..e0bc422f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -38,9 +38,5 @@ public async ValueTask ExecuteAsync(IConsole console) { Logger?.LogInformation($"Cancelled"); } - else - { - Logger?.LogInformation($"Done"); - } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs index 995d480a..3a89bb2d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs @@ -19,7 +19,9 @@ protected override async ValueTask ExecuteCommand() if (Portlist.Count > 0) { var plural = Portlist.Count > 1 ? "s" : string.Empty; - Logger?.LogInformation($"Found the following device{plural}:"); + + Logger?.LogInformation($"Found device{plural} on port{plural}:"); + for (int i = 0; i < Portlist.Count; i++) { Logger?.LogInformation($" {i + 1}: {Portlist[i]}"); From 13f7c3ac71e2c97710f698c05000e9954c8fb698 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Sun, 28 Jan 2024 22:02:35 -0800 Subject: [PATCH 136/141] Remove legacy unused code --- Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs | 54 ------------------- Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs | 3 +- .../Debugging/MeadowDataProcessor.cs | 10 ---- .../Debugging/MeadowMessageEventArgs.cs | 13 ----- .../Debugging/MeadowMessageType.cs | 23 -------- Source/v2/Meadow.Hcom/Extensions.cs | 16 ------ .../v2/Meadow.Hcom/Firmware/FirmwareInfo.cs | 51 ------------------ .../Meadow.Hcom/Firmware/PackageVersions.cs | 9 ---- 8 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs delete mode 100644 Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs delete mode 100644 Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs delete mode 100644 Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs delete mode 100644 Source/v2/Meadow.Hcom/Extensions.cs delete mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs delete mode 100644 Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs deleted file mode 100644 index 216fb844..00000000 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -using DfuSharp; - -namespace MeadowCLI; - -public class DfuContext -{ - private List validVendorIDs = new List - { - 0x22B1, // secret labs - 0x1B9F, // ghi - 0x05A, // who knows - 0x0483 // bootloader - }; - - // --------------------------- INSTANCE - public static DfuContext? Current; - - public static void Init() - { - Current = new DfuContext(); - Current._context = new Context(); - } - - public static void Dispose() - { - Current?._context?.Dispose(); - } - // --------------------------- INSTANCE - - private Context? _context; - - public List? GetDevices() - { - if (_context != null) - return _context.GetDfuDevices(validVendorIDs); - else - return null; - } - - public bool HasCapability(Capabilities caps) - { - if (_context != null) - return _context.HasCapability(caps); - else - return false; - } - - public void BeginListeningForHotplugEvents() - { - if (_context != null) - _context.BeginListeningForHotplugEvents(); - } - -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs index e43b6774..50d0401c 100644 --- a/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs @@ -39,10 +39,9 @@ private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFor { if (logLine.Contains("%")) { - var operation = logLine.Substring(0, - logLine.IndexOf("\t", StringComparison.Ordinal)).Trim(); var progressBarEnd = logLine.IndexOf("]", StringComparison.Ordinal) + 1; var progress = logLine.Substring(progressBarEnd, logLine.IndexOf("%", StringComparison.Ordinal) - progressBarEnd + 1).TrimStart(); + if (progress != "100%") { logger?.LogInformation(progress); diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs deleted file mode 100644 index daadd4ad..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Concurrent; - -namespace Meadow.Hcom; - -public abstract class MeadowDataProcessor : IDisposable -{ - public EventHandler? OnReceiveData; - public BlockingCollection DebuggerMessages = new BlockingCollection(); - public abstract void Dispose(); -} diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs deleted file mode 100644 index aff5a9e9..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Meadow.Hcom; - -public class MeadowMessageEventArgs : EventArgs -{ - public string Message { get; private set; } - public MeadowMessageType MessageType { get; private set; } - - public MeadowMessageEventArgs(MeadowMessageType messageType, string message = "") - { - Message = message; - MessageType = messageType; - } -} diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs deleted file mode 100644 index 6e8daf25..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Meadow.Hcom; - -// For data received due to a CLI request these provide a secondary -// type of identification. The primary being the protocol request value -public enum MeadowMessageType -{ - AppOutput, - ErrOutput, - DeviceInfo, - FileListTitle, - FileListMember, - FileListCrcMember, - Data, - InitialFileData, - MeadowTrace, - SerialReconnect, - Accepted, - Concluded, - DownloadStartOkay, - DownloadStartFail, - DownloadFailed, - DevicePublicKey -} diff --git a/Source/v2/Meadow.Hcom/Extensions.cs b/Source/v2/Meadow.Hcom/Extensions.cs deleted file mode 100644 index a1c27863..00000000 --- a/Source/v2/Meadow.Hcom/Extensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Meadow.Hcom; - -public static class Extensions -{ - public static Version ToVersion(this string s) - { - if (Version.TryParse(s, out var result)) - { - return result; - } - else - { - return new Version(); - } - } -} diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs deleted file mode 100644 index 0d8f448f..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Meadow.Hcom; - -public class BuildDateConverter : JsonConverter -{ - // build date is in the format "2022-09-01 09:47:26" - private const string FormatString = "yyyy-MM-dd HH:mm:ss"; - - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - Debug.Assert(typeToConvert == typeof(DateTime)); - - if (!reader.TryGetDateTime(out DateTime value)) - { - value = DateTime.ParseExact(reader.GetString(), FormatString, CultureInfo.InvariantCulture); - } - - return value; - } - - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString(FormatString)); - } -} - -public class FirmwareInfo -{ - public string Version { get; set; } = string.Empty; - [JsonPropertyName("build-date")] - public DateTime BuildDate { get; set; } - [JsonPropertyName("build-hash")] - public string BuildHash { get; set; } = string.Empty; - public bool IsLatest { get; set; } - - public override bool Equals(object obj) - { - var other = obj as FirmwareInfo; - if (other == null) return false; - return BuildHash.Equals(other.BuildHash); - } - - public override int GetHashCode() - { - return BuildHash.GetHashCode(); - } -} diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs deleted file mode 100644 index e5c460a9..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Meadow.Hcom; - -public class PackageVersions -{ - [JsonPropertyName("versions")] - public string[] Versions { get; set; } -} From 8721d6736c5d5e07393f5d098aa7ad945207265f Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Tue, 30 Jan 2024 14:47:17 -0800 Subject: [PATCH 137/141] Fix provisioning --- Source/v2/Meadow.Cli/Properties/launchSettings.json | 2 +- .../v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 11356d1a..47efd3dc 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -205,7 +205,7 @@ }, "Device provision": { "commandName": "Project", - "commandLineArgs": "device provision -o christacke6612" + "commandLineArgs": "device provision" }, "Cloud login": { "commandName": "Project", diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 7482b4d7..2d402dfe 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -196,7 +196,7 @@ private async Task ListenerProc() else if (response is TextRequestResponse trr) { // this is a response to a text request - the exact request is cached - _lastRequestConcluded = (RequestType)trr.RequestType; + //_lastRequestConcluded = (RequestType)trr.RequestType; //Debug.WriteLine($"RESPONSE> {trr.Text}"); } else if (response is DeviceInfoSerialResponse dir) From 2c9a5176049c6e4caac806b7c9d6d92916eaff8d Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 31 Jan 2024 17:15:54 -0600 Subject: [PATCH 138/141] fix for provisioning --- .../Current/Device/DeviceProvisionCommand.cs | 41 +++++++++++++++---- .../SerialConnection.ListenerProc.cs | 1 - .../Connections/SerialConnection.cs | 11 ++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index fba0d8c7..11362377 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -1,6 +1,5 @@ using CliFx.Attributes; using Meadow.Cloud; -using Meadow.Cloud.Identity; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,6 +8,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class DeviceProvisionCommand : BaseDeviceCommand { private readonly DeviceService _deviceService; + private readonly UserService _userService; public const string DefaultHost = "https://www.meadowcloud.co"; @@ -24,10 +24,11 @@ public class DeviceProvisionCommand : BaseDeviceCommand [CommandOption("host", 'h', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } - public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public DeviceProvisionCommand(UserService userService, DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { _deviceService = deviceService; + _userService = userService; } protected override async ValueTask ExecuteCommand() @@ -38,9 +39,6 @@ protected override async ValueTask ExecuteCommand() { Host ??= DefaultHost; - var identityManager = new IdentityManager(Logger); - var _userService = new UserService(identityManager); - Logger?.LogInformation("Retrieving your user and organization information..."); var userOrgs = await _userService.GetUserOrgs(Host, CancellationToken).ConfigureAwait(false); @@ -87,9 +85,36 @@ protected override async ValueTask ExecuteCommand() Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); - var delim = "-----END RSA PUBLIC KEY-----\n"; - publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + if (string.IsNullOrWhiteSpace(publicKey)) + { + Logger?.LogError("Could not retrieve device's public key."); + return; + } + + var delimiters = new string[] + { + "-----END PUBLIC KEY-----\n", // F7 delimiter + "-----END RSA PUBLIC KEY-----\n" // linux/mac/windows delimiter + }; + + var valid = false; + + foreach (var delim in delimiters) + { + var index = publicKey.IndexOf(delim); + if (index > 0) + { + valid = true; + publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + break; + } + } + if (!valid) + { + Logger?.LogError("Device returned an invali dpublic key"); + return; + } Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); var provisioningID = !string.IsNullOrWhiteSpace(info?.ProcessorId) ? info.ProcessorId : info?.SerialNumber; @@ -105,7 +130,5 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Failed to provision device: {result.message}"); } - - return; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 2d402dfe..876979b1 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -196,7 +196,6 @@ private async Task ListenerProc() else if (response is TextRequestResponse trr) { // this is a response to a text request - the exact request is cached - //_lastRequestConcluded = (RequestType)trr.RequestType; //Debug.WriteLine($"RESPONSE> {trr.Text}"); } else if (response is DeviceInfoSerialResponse dir) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 1d295601..bb78a61d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1196,7 +1196,16 @@ void OnFileDataReceived(object? sender, string data) _lastRequestConcluded = null; EnqueueRequest(command); - await WaitForConcluded(null, cancellationToken); + if (!await WaitForResult( + () => + { + return contents != string.Empty; + }, + cancellationToken)) + { + CommandTimeoutSeconds = lastTimeout; + return string.Empty; + } CommandTimeoutSeconds = lastTimeout; From d7ae4eab3c6ed8d5850633f01d5c34b66e6f25b9 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 1 Feb 2024 10:55:56 -0800 Subject: [PATCH 139/141] version bump --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 1de59e7f..1db394c8 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0-beta.2 + 2.0.0-beta.3 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index b08591a3..ad2974a0 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.8"; + public const string CLI_VERSION = "2.0.0.9"; } } \ No newline at end of file From 8d17822f545d53d481d208af0c33ce126c0469d3 Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 1 Feb 2024 14:22:55 -0800 Subject: [PATCH 140/141] bump version for release --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 1db394c8..93673fba 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0-beta.3 + 2.0.0 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index ad2974a0..493b93ff 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.9"; + public const string CLI_VERSION = "2.0.0.10"; } } \ No newline at end of file From 80d8143cccdfbee22d9b0f952631219f79ff2f4d Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 1 Feb 2024 14:26:31 -0800 Subject: [PATCH 141/141] Bump nuget version for release --- Source/v2/Meadow.CLI/Meadow.CLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 93673fba..fc9b39dc 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -10,7 +10,7 @@ Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0 + 2.0.0.10 AnyCPU http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI