From b8bee31356c959e44433873c6ff3faa3bf32cea7 Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Fri, 28 Jun 2024 15:32:21 +0100 Subject: [PATCH] Add meadow provision command --- .github/workflows/dotnet.yml | 2 +- .../Current/Firmware/FirmwareUpdater.cs | 579 ++++++++++++++++++ .../Current/Provision/ProvisionCommand.cs | 327 ++++++++++ .../Current/Provision/ProvisionSettings.cs | 9 + Source/v2/Meadow.CLI/provision.json | 6 + .../Commands/Current/BaseDeviceCommand.cs | 8 +- .../Current/Firmware/FirmwareWriteCommand.cs | 512 +--------------- Source/v2/Meadow.Cli/Meadow.CLI.csproj | 1 + .../Meadow.Cli/Properties/launchSettings.json | 4 + Source/v2/Meadow.Cli/Strings.cs | 40 ++ Source/v2/Meadow.Dfu/DfuUtils.cs | 5 +- Source/v2/Meadow.Firmware/FirmwareWriter.cs | 2 +- .../Connections/SerialConnection.cs | 47 +- Source/v2/Meadow.Hcom/Meadow.HCom.csproj | 2 +- .../Connection/MeadowConnectionManager.cs | 134 ++-- Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs | 2 +- Source/v2/Meadow.UsbLib/LibUsbDevice.cs | 26 +- .../ClassicLibUsbDevice.cs | 83 +-- 18 files changed, 1157 insertions(+), 632 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs create mode 100644 Source/v2/Meadow.CLI/provision.json mode change 100755 => 100644 Source/v2/Meadow.Hcom/Connections/SerialConnection.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 855182ac..f418f3c9 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,7 +1,7 @@ name: Meadow.CLI Packaging env: CLI_RELEASE_VERSION_1: 1.9.4.0 - CLI_RELEASE_VERSION_2: 2.0.17.0 + CLI_RELEASE_VERSION_2: 2.0.54.0 IDE_TOOLS_RELEASE_VERSION: 1.9.4 MEADOW_OS_VERSION: 1.9.0.0 VS_MAC_2019_VERSION: 8.10 diff --git a/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs new file mode 100644 index 00000000..52b40c10 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs @@ -0,0 +1,579 @@ +using System.Runtime.InteropServices; +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; +using Meadow.LibUsb; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public class FirmwareUpdater where T : BaseDeviceCommand +{ + private string? individualFile; + private FirmwareType[]? firmwareFileTypes; + private bool useDfu; + private string? osVersion; + private string? serialNumber; + private readonly MeadowConnectionManager connectionManager; + private readonly ILogger? logger; + private readonly bool provisioningInProgress; + private readonly CancellationToken cancellationToken; + private readonly ISettingsManager settings; + private readonly FileManager fileManager; + + private int lastWriteProgress = 0; + + private BaseDeviceCommand command; + + public event EventHandler<(string message, double percentage)> UpdateProgress = default!; + + public FirmwareUpdater(BaseDeviceCommand command, ISettingsManager settings, FileManager fileManager, MeadowConnectionManager connectionManager, string? individualFile, FirmwareType[]? firmwareFileTypes, bool useDfu, string? osVersion, string? serialNumber, ILogger? logger, CancellationToken cancellationToken) + { + this.command = command; + this.settings = settings; + this.fileManager = fileManager; + this.connectionManager = connectionManager; + this.individualFile = individualFile; + this.firmwareFileTypes = firmwareFileTypes; + this.useDfu = useDfu; + this.osVersion = osVersion; + this.serialNumber = serialNumber; + this.logger = logger; + this.provisioningInProgress = logger == null; + this.cancellationToken = cancellationToken; + } + + public async Task UpdateFirmware() + { + var package = await GetSelectedPackage(); + + if (package == null) + { + return false; + } + + if (individualFile != null) + { + // check the file exists + var fullPath = Path.GetFullPath(individualFile); + if (!File.Exists(fullPath)) + { + throw new CommandException(string.Format(Strings.InvalidFirmwareForSpecifiedPath, fullPath), CommandExitCode.FileNotFound); + } + + // set the file type + firmwareFileTypes = Path.GetFileName(individualFile) switch + { + F7FirmwarePackageCollection.F7FirmwareFiles.OSWithBootloaderFile => new[] { FirmwareType.OS }, + F7FirmwarePackageCollection.F7FirmwareFiles.OsWithoutBootloaderFile => new[] { FirmwareType.OS }, + F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile => new[] { FirmwareType.Runtime }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile => new[] { FirmwareType.ESP }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile => new[] { FirmwareType.ESP }, + F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile => new[] { FirmwareType.ESP }, + _ => throw new CommandException(string.Format(Strings.UnknownSpecifiedFirmwareFile, Path.GetFileName(individualFile))) + }; + + logger?.LogInformation(string.Format($"{Strings.WritingSpecifiedFirmwareFile}...", fullPath)); + } + else if (firmwareFileTypes == null) + { + logger?.LogInformation(string.Format(Strings.WritingAllFirmwareForSpecifiedVersion, package.Version)); + + firmwareFileTypes = new FirmwareType[] + { + FirmwareType.OS, + FirmwareType.Runtime, + FirmwareType.ESP + }; + } + else if (firmwareFileTypes.Length == 1 && firmwareFileTypes[0] == FirmwareType.Runtime) + { //use the "DFU" path when only writing the runtime + useDfu = true; + } + + IMeadowConnection? connection = null; + DeviceInfo? deviceInfo = null; + + if (firmwareFileTypes.Contains(FirmwareType.OS)) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.FlashingOS, 20)); + await WriteOSFiles(connection, deviceInfo, package, useDfu); + } + + if (!string.IsNullOrWhiteSpace(serialNumber)) + { + connection = await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + if (connection != null) + { + if (provisioningInProgress) + { + connection.ConnectionMessage += (o, e) => + { + UpdateProgress?.Invoke(this, (e, 0)); + }; + } + + deviceInfo = await connection.GetDeviceInfo(cancellationToken); + } + } + + if (firmwareFileTypes.Contains(FirmwareType.Runtime) || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.WritingRuntime, 40)); + await WriteRuntimeFiles(connection, deviceInfo, package, individualFile); + } + + if (firmwareFileTypes.Contains(FirmwareType.ESP) + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile + || Path.GetFileName(individualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile) + { + UpdateProgress?.Invoke(this, (Strings.FirmwareUpdater.WritingESP, 60)); + await WriteEspFiles(connection, deviceInfo, package); + } + + // reset device + if (connection != null && connection.Device != null) + { + await connection.Device.Reset(); + } + + return true; + } + + private async Task WriteEspFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + await WriteEsp(connection, deviceInfo, package); + + // reset device + if (connection != null && connection.Device != null) + { + await connection.Device.Reset(); + } + } + + private async Task WriteRuntimeFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package, string? individualFile) + { + if (string.IsNullOrEmpty(individualFile)) + { + connection = await WriteRuntime(connection, deviceInfo, package); + } + else + { + connection = await WriteRuntime(connection, deviceInfo, individualFile, Path.GetFileName(individualFile)); + } + + if (connection == null) + { + throw CommandException.MeadowDeviceNotFound; + } + + await connection.WaitForMeadowAttach(); + } + + private async Task WriteOSFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package, bool useDfu) + { + var osFileWithBootloader = package.GetFullyQualifiedPath(package.OSWithBootloader); + var osFileWithoutBootloader = package.GetFullyQualifiedPath(package.OsWithoutBootloader); + + if (osFileWithBootloader == null && osFileWithoutBootloader == null) + { + throw new CommandException(string.Format(Strings.OsFileNotFoundForSpecifiedVersion, package.Version)); + } + + var provider = new LibUsbProvider(); + var dfuDevice = GetLibUsbDeviceForCurrentEnvironment(provider, serialNumber); + bool ignoreSerial = IgnoreSerialNumberForDfu(provider); + + if (dfuDevice != null) + { + logger?.LogInformation($"{Strings.DfuDeviceDetected} - {Strings.UsingDfuToWriteOs}"); + useDfu = true; + } + else + { + if (useDfu) + { + throw new CommandException(Strings.NoDfuDeviceDetected); + } + + connection = await GetConnectionAndDisableRuntime(); + + deviceInfo = await connection.GetDeviceInfo(cancellationToken); + } + + if (useDfu || dfuDevice != null || osFileWithoutBootloader == null || RequiresDfuForRuntimeUpdates(deviceInfo!)) + { + // 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(); + + await WriteOsWithDfu(dfuDevice!, osFileWithBootloader!, ignoreSerial); + + await Task.Delay(1500); + + connection ??= await FindMeadowConnection(initialPorts); + + await connection.WaitForMeadowAttach(cancellationToken); + } + else + { + await connection!.Device!.WriteFile(osFileWithoutBootloader, $"/{AppTools.MeadowRootFolder}/update/os/{package.OsWithoutBootloader}"); + } + } + + private async Task GetSelectedPackage() + { + await fileManager.Refresh(); + + var collection = fileManager.Firmware["Meadow F7"]; + FirmwarePackage package; + + if (osVersion != null) + { + // make sure the requested version exists + var existing = collection.FirstOrDefault(v => v.Version == osVersion); + + if (existing == null) + { + logger?.LogError(string.Format(Strings.SpecifiedFirmwareVersionNotFound, osVersion)); + return null; + } + package = existing; + } + else + { + osVersion = collection.DefaultPackage?.Version ?? throw new CommandException($"{Strings.NoDefaultVersionSet}. {Strings.UseCommandFirmwareDefault}."); + + package = collection.DefaultPackage; + } + + return package; + } + + private ILibUsbDevice? GetLibUsbDeviceForCurrentEnvironment(LibUsbProvider? provider, string? serialNumber = null) + { + provider ??= new LibUsbProvider(); + + var devices = provider.GetDevicesInBootloaderMode(); + + var meadowsInDFU = devices.Where(device => device.IsMeadow()).ToList(); + + if (meadowsInDFU.Count == 0) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(serialNumber)) + { + return meadowsInDFU.Where(device => device.SerialNumber == serialNumber).FirstOrDefault(); + } + else if (meadowsInDFU.Count == 1 || IgnoreSerialNumberForDfu(provider)) + { //IgnoreSerialNumberForDfu is a macOS-specific hack for Mark's machine + return meadowsInDFU.FirstOrDefault(); + } + + throw new CommandException(Strings.MultipleDfuDevicesFound); + } + + private bool IgnoreSerialNumberForDfu(LibUsbProvider provider) + { //hack check for Mark's Mac + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var devices = provider.GetDevicesInBootloaderMode(); + + if (devices.Count == 2) + { + if (devices[0].SerialNumber.Length > 12 || devices[1].SerialNumber.Length > 12) + { + return true; + } + } + } + + return false; + } + + private async Task GetConnectionAndDisableRuntime(string? route = null) + { + IMeadowConnection connection; + + if (!string.IsNullOrWhiteSpace(route)) + { + connection = await command.GetConnectionForRoute(route, true); + } + else + { + connection = await command.GetCurrentConnection(true); + } + + if (await connection.Device!.IsRuntimeEnabled()) + { + logger?.LogInformation($"{Strings.DisablingRuntime}..."); + await connection.Device.RuntimeDisable(); + } + + lastWriteProgress = 0; + + connection.FileWriteProgress += (s, e) => + { + var p = (int)(e.completed / (double)e.total * 100d); + // don't report < 10% increments (decrease spew on large files) + if (p - lastWriteProgress < 10) { return; } + + lastWriteProgress = p; + + logger?.LogInformation($"{Strings.Writing} {e.fileName}: {p:0}% {(p < 100 ? string.Empty : "\r\n")}"); + }; + 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); + }; + + return connection; + } + + private bool RequiresDfuForRuntimeUpdates(DeviceInfo info) + { + return true; + /* + restore this when we support OtA-style updates again + if (System.Version.TryParse(info.OsVersion, out var version)) + { + return version.Major >= 2; + } + */ + } + + private async Task WriteOsWithDfu(ILibUsbDevice libUsbDevice, string osFile, bool ignoreSerialNumber = false) + { + try + { //validate device + if (string.IsNullOrWhiteSpace(serialNumber)) + { + serialNumber = libUsbDevice.SerialNumber; + } + } + catch + { + throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.UnableToReadSerialNumber} ({Strings.MakeSureDeviceisConnected})"); + } + + try + { + if (ignoreSerialNumber) + { + serialNumber = string.Empty; + } + + await DfuUtils.FlashFile( + osFile, + serialNumber, + logger: logger, + format: provisioningInProgress ? DfuUtils.DfuFlashFormat.None : DfuUtils.DfuFlashFormat.ConsoleOut); + } + catch (ArgumentException) + { + throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.IsDfuUtilInstalled} {Strings.RunMeadowDfuInstall}"); + } + 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"); + + throw new CommandException(Strings.FirmwareUpdater.SwitchingToLibUsbClassic); + } + } + + private async Task FindMeadowConnection(IList portsToIgnore) + { + IMeadowConnection? connection = null; + + var newPorts = await WaitForNewSerialPorts(portsToIgnore); + string newPort = string.Empty; + + if (newPorts == null) + { + throw CommandException.MeadowDeviceNotFound; + } + + if (newPorts.Count == 1) + { + connection = await GetConnectionAndDisableRuntime(newPorts[0]); + newPort = newPorts[0]; + } + else + { + foreach (var port in newPorts) + { + try + { + connection = await GetConnectionAndDisableRuntime(port); + newPort = port; + break; + } + catch + { + throw CommandException.MeadowDeviceNotFound; + } + } + } + + logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); + + await connection!.WaitForMeadowAttach(); + + // configure the route to that port for the user + settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + return connection; + } + + private async Task> WaitForNewSerialPorts(IList? ignorePorts) + { + var ports = await MeadowConnectionManager.GetSerialPorts(); + + var retryCount = 0; + + while (ports.Count == 0) + { + if (retryCount++ > 10) + { + throw new CommandException(Strings.NewMeadowDeviceNotFound); + } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + } + + if (ignorePorts != null) + { + return ports.Except(ignorePorts).ToList(); + } + return ports.ToList(); + } + + private async Task WaitForNewSerialPort(IList? ignorePorts) + { + var ports = await WaitForNewSerialPorts(ignorePorts); + + return ports.FirstOrDefault(); + } + + private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + logger?.LogInformation($"{Environment.NewLine}{Strings.GettingRuntimeFor} {package.Version}..."); + + if (package.Runtime == null) { return null; } + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + return await WriteRuntime(connection, deviceInfo, rtpath, package.Runtime); + } + + private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, string runtimePath, string destinationFilename) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + logger?.LogInformation($"{Environment.NewLine}{Strings.WritingRuntime}..."); + + deviceInfo ??= await connection.GetDeviceInfo(cancellationToken); + + if (deviceInfo == null) + { + throw new CommandException(Strings.UnableToGetDeviceInfo); + } + + if (useDfu || RequiresDfuForRuntimeUpdates(deviceInfo)) + { + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + write_runtime: + if (!await connection!.Device!.WriteRuntime(runtimePath, cancellationToken)) + { + // TODO: implement a retry timeout + logger?.LogInformation($"{Strings.ErrorWritingRuntime} - {Strings.Retrying}"); + goto write_runtime; + } + + connection ??= await command.GetCurrentConnection(true); + + if (connection == null) + { + var newPort = await WaitForNewSerialPort(initialPorts) ?? throw CommandException.MeadowDeviceNotFound; + connection = await command.GetCurrentConnection(true); + + logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); + + // configure the route to that port for the user + settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + } + } + else + { + await connection.Device!.WriteFile(runtimePath, $"/{AppTools.MeadowRootFolder}/update/os/{destinationFilename}"); + } + + return connection; + } + + private async Task WriteEsp(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) + { + connection ??= await GetConnectionAndDisableRuntime(await MeadowConnectionManager.GetRouteFromSerialNumber(serialNumber)); + + if (connection == null) { return; } // couldn't find a connected device + + logger?.LogInformation($"{Environment.NewLine}{Strings.WritingCoprocessorFiles}..."); + + string[] fileList; + + if (individualFile != null) + { + fileList = new string[] { individualFile }; + } + else + { + fileList = new string[] + { + package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; + } + + deviceInfo ??= await connection.GetDeviceInfo(cancellationToken); + + if (deviceInfo == null) { throw new CommandException(Strings.UnableToGetDeviceInfo); } + + if (useDfu || RequiresDfuForEspUpdates(deviceInfo)) + { + await connection.Device!.WriteCoprocessorFiles(fileList, cancellationToken); + } + else + { + foreach (var file in fileList) + { + await connection!.Device!.WriteFile(file, $"/{AppTools.MeadowRootFolder}/update/os/{Path.GetFileName(file)}"); + } + } + } + + private bool RequiresDfuForEspUpdates(DeviceInfo info) + { + return true; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs new file mode 100644 index 00000000..411968fe --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionCommand.cs @@ -0,0 +1,327 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using CliFx.Attributes; +using Meadow.CLI.Commands.DeviceManagement; +using Meadow.Cloud.Client; +using Meadow.LibUsb; +using Meadow.Package; +using Meadow.Software; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Spectre.Console; + +namespace Meadow.CLI.Commands.Provision; + +[Command("provision", Description = Strings.Provision.CommandDescription)] +public class ProvisionCommand : BaseDeviceCommand +{ + public const string DefaultFirmwareVersion = "1.12.2.0"; + private string? appPath; + private string? configuration = "Release"; + + [CommandOption("version", 'v', Description = Strings.Provision.CommandOptionVersion, IsRequired = false)] + public string? FirmwareVersion { get; set; } = DefaultFirmwareVersion; + + [CommandOption("path", 'p', Description = Strings.Provision.CommandOptionPath, IsRequired = false)] + public string? Path { get; set; } = "."; + + private ConcurrentQueue bootloaderDeviceQueue = new ConcurrentQueue(); + + private List selectedDeviceList = default!; + private ISettingsManager settingsManager; + private FileManager fileManager; + private IMeadowCloudClient meadowCloudClient; + private MeadowConnectionManager connectionManager; + private IPackageManager packageManager; + private bool? deployApp = true; + + public ProvisionCommand(ISettingsManager settingsManager, FileManager fileManager, + IMeadowCloudClient meadowCloudClient, IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + this.settingsManager = settingsManager; + this.fileManager = fileManager; + this.meadowCloudClient = meadowCloudClient; + this.connectionManager = connectionManager; + this.packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand() + { + try + { + AnsiConsole.MarkupLine(Strings.Provision.RunningTitle); + + bool refreshDeviceList = false; + do + { + UpdateDeviceList(CancellationToken); + + if (bootloaderDeviceQueue.Count == 0) + { + Logger?.LogError(Strings.Provision.NoDevicesFound); + return; + } + + var multiSelectionPrompt = new MultiSelectionPrompt() + .Title(Strings.Provision.PromptTitle) + .PageSize(15) + .NotRequired() // Can be Blank to exit + .MoreChoicesText($"[grey]{Strings.Provision.MoreChoicesInstructions}[/]") + .InstructionsText(string.Format($"[grey]{Strings.Provision.Instructions}[/]", $"[blue]<{Strings.Space}>[/]", $"[green]<{Strings.Enter}>[/]")) + .UseConverter(x => x); + + foreach (var device in bootloaderDeviceQueue) + { + multiSelectionPrompt.AddChoices(device.SerialNumber); + } + + selectedDeviceList = AnsiConsole.Prompt(multiSelectionPrompt); + + if (selectedDeviceList.Count == 0) + { + AnsiConsole.MarkupLine($"[yellow]{Strings.Provision.NoDeviceSelected}[/]"); + return; + } + + var selectedDeviceTable = new Table(); + selectedDeviceTable.AddColumn(Strings.Provision.ColumnTitle); + + foreach (var device in selectedDeviceList) + { + selectedDeviceTable.AddRow(device); + } + + AnsiConsole.Write(selectedDeviceTable); + + refreshDeviceList = AnsiConsole.Confirm(Strings.Provision.RefreshDeviceList); + } while (!refreshDeviceList); + + string path = System.IO.Path.Combine(Path, "provision.json"); + + if (!string.IsNullOrWhiteSpace(path) + && !File.Exists(path)) + { + deployApp = false; + AnsiConsole.MarkupLine($"[red]{Strings.Provision.FileNotFound}[/]", $"[yellow]{path}[/]"); + } + else + { + Path = path; + } + + if (deployApp.HasValue && deployApp.Value) + { + try + { + var provisionSettings = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(Path!)); + if (provisionSettings == null) + { + throw new Exception($"{Strings.Provision.FailedToReadProvisionFile}."); + } + + // Use the settings from provisionSettings as needed + configuration = provisionSettings.Configuration; + FirmwareVersion = provisionSettings.FirmwareVersion; + deployApp = provisionSettings.DeployApp; + + if (deployApp.HasValue && deployApp.Value) + { + appPath = AppTools.ValidateAndSanitizeAppPath(provisionSettings.AppPath); + + if (!File.Exists(appPath)) + { + throw new FileNotFoundException($"{Strings.Provision.AppDllNotFound}:{appPath}"); + } + + AnsiConsole.MarkupLine(Strings.Provision.TrimmingApp); + await AppTools.TrimApplication(appPath!, packageManager, FirmwareVersion!, configuration, null, null, Console, CancellationToken); + } + } + catch (Exception ex) + { + // Eat the exception and keep going. + deployApp = false; + AnsiConsole.MarkupLine($"[red]{ex.Message}[/]"); + Debug.WriteLine($"{ex.Message + Environment.NewLine + ex.StackTrace}"); + } + } + + if(deployApp.HasValue && !deployApp.Value) + { + AnsiConsole.MarkupLine(Strings.Provision.NoAppDeployment, $"[yellow]{FirmwareVersion}[/]"); + } + + if (string.IsNullOrEmpty(FirmwareVersion)) + { + FirmwareVersion = DefaultFirmwareVersion; + } + + // Install DFU, if it's not already installed. + var dfuInstallCommand = new DfuInstallCommand(settingsManager, LoggerFactory); + await dfuInstallCommand.ExecuteAsync(Console); + + // Make sure we've downloaded the osVersion or default + var firmwareDownloadCommand = new FirmwareDownloadCommand(fileManager, meadowCloudClient, LoggerFactory) + { + Version = FirmwareVersion, + Force = true + }; + await firmwareDownloadCommand.ExecuteAsync(Console); + + + // If we've reached here we're ready to Flash + await FlashingAttachedDevices(); + } + catch (Exception ex) + { + + var message = ex.Message; +#if DEBUG + var stackTrace = ex.StackTrace; + message += Environment.NewLine + stackTrace; +#endif + AnsiConsole.MarkupLine($"[red]{message}[/]"); + } + } + + private void UpdateDeviceList(CancellationToken cancellationToken) + { + var ourDevices = GetValidUsbDevices(); + + if (ourDevices?.Count() > 0) + { + bootloaderDeviceQueue.Clear(); + + foreach (ILibUsbDevice device in ourDevices) + { + if (bootloaderDeviceQueue != null) + { + if (device != null) + { + bootloaderDeviceQueue.Enqueue(device); + } + } + } + } + } + + private IEnumerable? GetValidUsbDevices() + { + try + { + var provider = new LibUsbProvider(); + + var devices = provider.GetDevicesInBootloaderMode(); + + return devices; + } + catch (Exception) + { + return null; + } + } + + public async Task FlashingAttachedDevices() + { + var succeedCount = 0; + var errorList = new List<(string SerialNumber, string Message, string StackTrace)>(); + + await AnsiConsole.Progress() + .AutoRefresh(true) + .HideCompleted(false) + .Columns(new ProgressColumn[] + { + new TaskDescriptionColumn(), // Task description + new ProgressBarColumn(), // Progress bar + new PercentageColumn(), // Percentage + new SpinnerColumn(), // Spinner + }) + .StartAsync(async ctx => + { + foreach (var deviceSerialNumber in selectedDeviceList) + { + var formatedDevice = $"[green]{deviceSerialNumber}[/]"; + var task = ctx.AddTask(formatedDevice, maxValue: 100); + + try + { + var firmareUpdater = new FirmwareUpdater(this, settingsManager, fileManager, this.connectionManager, null, null, true, FirmwareVersion, deviceSerialNumber, null, CancellationToken); + firmareUpdater.UpdateProgress += (o, e) => + { + if (e.percentage > 0) + { + task.Value = e.percentage; + } + task.Description = $"{formatedDevice}: {e.message}"; + }; + + if (!await firmareUpdater.UpdateFirmware()) + { + task.Description = $"{formatedDevice}: [red]{Strings.Provision.UpdateFailed}[/]"; + task.StopTask(); + } + + if (deployApp.HasValue && deployApp.Value) + { + task.Increment(20.00); + task.Description = $"{formatedDevice}: [yellow]{Strings.Provision.DeployingApp}[/]"; + + var route = await MeadowConnectionManager.GetRouteFromSerialNumber(deviceSerialNumber!); + if (!string.IsNullOrWhiteSpace(route)) + { + var connection = await GetConnectionForRoute(route, true); + var appDir = System.IO.Path.GetDirectoryName(appPath); + await AppManager.DeployApplication(packageManager, connection, FirmwareVersion!, appDir!, true, false, null, CancellationToken); + + await connection?.Device?.RuntimeEnable(CancellationToken); + } + } + + task.Value = 100.00; + task.Description = $"{formatedDevice}: [green]{Strings.Provision.UpdateComplete}[/]"; + + task.StopTask(); + + await Task.Delay(2000); // TODO May not be required, futher testing needed + + succeedCount++; + } + catch (Exception ex) + { + task.Description = $"{formatedDevice}: [red]{ex.Message}[/]"; + task.StopTask(); + + if (!string.IsNullOrWhiteSpace(ex.StackTrace)) + { + errorList.Add((deviceSerialNumber, ex.Message, ex.StackTrace)); + } + } + } + }); + + if (succeedCount == selectedDeviceList.Count) + { + AnsiConsole.MarkupLine($"[green]{Strings.Provision.AllDevicesFlashed}[/]"); + } + else + { + AnsiConsole.MarkupLine($"[yellow]{Strings.Provision.IssuesFound}[/]"); + var showErrorMessages = AnsiConsole.Confirm(Strings.Provision.ShowErrorMessages); + if (showErrorMessages) + { + var errorTable = new Table(); + errorTable.AddColumn(Strings.Provision.ErrorSerialNumberColumnTitle); + errorTable.AddColumn(Strings.Provision.ErrorMessageColumnTitle); + errorTable.AddColumn(Strings.Provision.ErrorStackTraceColumnTitle); + + foreach (var error in errorList) + { + errorTable.AddRow(error.SerialNumber, error.Message, error.StackTrace); + } + + AnsiConsole.Write(errorTable); + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs new file mode 100644 index 00000000..46016d42 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Provision/ProvisionSettings.cs @@ -0,0 +1,9 @@ +namespace Meadow.CLI.Commands.Provision; + +public class ProvisionSettings +{ + public string? AppPath { get; set; } + public string? Configuration { get; set; } + public bool? DeployApp { get; set; } + public string? FirmwareVersion { get; set; } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/provision.json b/Source/v2/Meadow.CLI/provision.json new file mode 100644 index 00000000..997e4bd1 --- /dev/null +++ b/Source/v2/Meadow.CLI/provision.json @@ -0,0 +1,6 @@ +{ + "AppPath": "C:\\Users\\willo\\Downloads\\Blinky\\Debug\\netstandard2.1\\App.dll", + "Configuration": "Debug", + "DeployApp": "false", + "FirmwareVersion": "1.12.2.0" +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index a94358c0..58df2c60 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -17,10 +17,10 @@ protected async Task GetCurrentDevice() return (await GetCurrentConnection()).Device ?? throw CommandException.MeadowDeviceNotFound; } - protected Task GetCurrentConnection(bool forceReconnect = false) + internal Task GetCurrentConnection(bool forceReconnect = false) => GetConnection(null, forceReconnect); - protected Task GetConnectionForRoute(string route, bool forceReconnect = false) + internal Task GetConnectionForRoute(string route, bool forceReconnect = false) => GetConnection(route, forceReconnect); private async Task GetConnection(string? route, bool forceReconnect = false) @@ -29,11 +29,11 @@ private async Task GetConnection(string? route, bool forceRec if (route != null) { - connection = ConnectionManager.GetConnectionForRoute(route, forceReconnect); + connection = await MeadowConnectionManager.GetConnectionForRoute(route, forceReconnect); } else { - connection = ConnectionManager.GetCurrentConnection(forceReconnect); + connection = await ConnectionManager.GetCurrentConnection(forceReconnect); } if (connection != null) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index b1d436b9..dd8f9470 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,10 +1,10 @@ -using CliFx.Attributes; +using System.Runtime.InteropServices; +using CliFx.Attributes; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; using Meadow.Software; using Microsoft.Extensions.Logging; -using System.Runtime.InteropServices; namespace Meadow.CLI.Commands.DeviceManagement; @@ -27,6 +27,9 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandOption("file", 'f', IsRequired = false, Description = "Send only the specified file")] public string? IndividualFile { get; set; } + [CommandOption("serialnumber", 's', IsRequired = false, Description = "Flash the specified device")] + public string? SerialNumber { get; set; } + [CommandParameter(0, Description = "Files to write", IsRequired = false)] public FirmwareType[]? FirmwareFileTypes { get; set; } = default!; @@ -40,512 +43,13 @@ public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileMa Settings = settingsManager; } - private int _lastWriteProgress = 0; - - private async Task GetConnectionAndDisableRuntime(string? route = null) - { - IMeadowConnection connection; - - if (route != null) - { - connection = await GetConnectionForRoute(route, true); - } - else - { - connection = await GetCurrentConnection(true); - } - - if (await connection.Device!.IsRuntimeEnabled()) - { - Logger?.LogInformation($"{Strings.DisablingRuntime}..."); - await connection.Device.RuntimeDisable(); - } - - _lastWriteProgress = 0; - - connection.FileWriteProgress += (s, e) => - { - var p = (int)(e.completed / (double)e.total * 100d); - // don't report < 10% increments (decrease spew on large files) - if (p - _lastWriteProgress < 10) { return; } - - _lastWriteProgress = p; - - Logger?.LogInformation($"{Strings.Writing} {e.fileName}: {p:0}% {(p < 100 ? string.Empty : "\r\n")}"); - }; - 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); - }; - - return connection; - } - - private bool RequiresDfuForRuntimeUpdates(DeviceInfo info) - { - return true; - /* - restore this when we support OtA-style updates again - if (System.Version.TryParse(info.OsVersion, out var version)) - { - return version.Major >= 2; - } - */ - } - - private bool RequiresDfuForEspUpdates(DeviceInfo info) - { - return true; - } - protected override async ValueTask ExecuteCommand() { - var package = await GetSelectedPackage(); - - if (package == null) - { - return; - } - - if (IndividualFile != null) - { - // check the file exists - var fullPath = Path.GetFullPath(IndividualFile); - if (!File.Exists(fullPath)) - { - throw new CommandException(string.Format(Strings.InvalidFirmwareForSpecifiedPath, fullPath), CommandExitCode.FileNotFound); - } - - // set the file type - FirmwareFileTypes = Path.GetFileName(IndividualFile) switch - { - F7FirmwarePackageCollection.F7FirmwareFiles.OSWithBootloaderFile => new[] { FirmwareType.OS }, - F7FirmwarePackageCollection.F7FirmwareFiles.OsWithoutBootloaderFile => new[] { FirmwareType.OS }, - F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile => new[] { FirmwareType.Runtime }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile => new[] { FirmwareType.ESP }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile => new[] { FirmwareType.ESP }, - F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile => new[] { FirmwareType.ESP }, - _ => throw new CommandException(string.Format(Strings.UnknownSpecifiedFirmwareFile, Path.GetFileName(IndividualFile))) - }; - - Logger?.LogInformation(string.Format($"{Strings.WritingSpecifiedFirmwareFile}...", fullPath)); - } - else if (FirmwareFileTypes == null) - { - Logger?.LogInformation(string.Format(Strings.WritingAllFirmwareForSpecifiedVersion, package.Version)); - - FirmwareFileTypes = new FirmwareType[] - { - FirmwareType.OS, - FirmwareType.Runtime, - FirmwareType.ESP - }; - } - else if (FirmwareFileTypes.Length == 1 && FirmwareFileTypes[0] == FirmwareType.Runtime) - { //use the "DFU" path when only writing the runtime - UseDfu = true; - } - - IMeadowConnection? connection = null; - DeviceInfo? deviceInfo = null; - - if (FirmwareFileTypes.Contains(FirmwareType.OS)) - { - string? osFileWithBootloader = null; - string? osFileWithoutBootloader = null; - - if (string.IsNullOrWhiteSpace(IndividualFile)) - { - osFileWithBootloader = package.GetFullyQualifiedPath(package.OSWithBootloader); - osFileWithoutBootloader = package.GetFullyQualifiedPath(package.OsWithoutBootloader); - - if (osFileWithBootloader == null && osFileWithoutBootloader == null) - { - throw new CommandException(string.Format(Strings.OsFileNotFoundForSpecifiedVersion, package.Version)); - } - } - else - { - osFileWithBootloader = IndividualFile; - } - - // do we have a dfu device attached, or is DFU specified? - var provider = new LibUsbProvider(); - var dfuDevice = GetLibUsbDeviceForCurrentEnvironment(provider); - bool ignoreSerial = IgnoreSerialNumberForDfu(provider); - - if (dfuDevice != null) - { - Logger?.LogInformation($"{Strings.DfuDeviceDetected} - {Strings.UsingDfuToWriteOs}"); - UseDfu = true; - } - else - { - if (UseDfu) - { - throw new CommandException(Strings.NoDfuDeviceDetected); - } - - connection = await GetConnectionAndDisableRuntime(); - - deviceInfo = await connection.GetDeviceInfo(CancellationToken); - } - - if (UseDfu || dfuDevice != null || osFileWithoutBootloader == null || RequiresDfuForRuntimeUpdates(deviceInfo!)) - { - // 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(); - - try - { - await WriteOsWithDfu(dfuDevice!, osFileWithBootloader!, ignoreSerial); - } - finally - { - dfuDevice?.Dispose(); - } - - await Task.Delay(1500); - - connection = await FindMeadowConnection(initialPorts); - - await connection.WaitForMeadowAttach(); - } - else - { - await connection!.Device!.WriteFile(osFileWithoutBootloader, $"/{AppTools.MeadowRootFolder}/update/os/{package.OsWithoutBootloader}"); - } - } - - if (FirmwareFileTypes.Contains(FirmwareType.Runtime) || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.RuntimeFile) - { - if (string.IsNullOrEmpty(IndividualFile)) - { - connection = await WriteRuntime(connection, deviceInfo, package); - } - else - { - connection = await WriteRuntime(connection, deviceInfo, IndividualFile, Path.GetFileName(IndividualFile)); - } - - if (connection == null) - { - throw CommandException.MeadowDeviceNotFound; - } - - await connection.WaitForMeadowAttach(); - } - - if (FirmwareFileTypes.Contains(FirmwareType.ESP) - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocPartitionTableFile - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocApplicationFile - || Path.GetFileName(IndividualFile) == F7FirmwarePackageCollection.F7FirmwareFiles.CoprocBootloaderFile) - { - connection = await GetConnectionAndDisableRuntime(); - - await WriteEspFiles(connection, deviceInfo, package); - - // reset device - if (connection != null && connection.Device != null) - { - await connection.Device.Reset(); - } - } - - Logger?.LogInformation(Strings.FirmwareUpdatedSuccessfully); - } - - private async Task FindMeadowConnection(IList portsToIgnore) - { - IMeadowConnection? connection = null; - - var newPorts = await WaitForNewSerialPorts(portsToIgnore); - string newPort = string.Empty; - - if (newPorts == null) - { - throw CommandException.MeadowDeviceNotFound; - } + var firmareUpdater = new FirmwareUpdater(this, Settings, FileManager, ConnectionManager, IndividualFile, FirmwareFileTypes, UseDfu, Version, SerialNumber, Logger, CancellationToken); - if (newPorts.Count == 1) - { - connection = await GetConnectionAndDisableRuntime(newPorts[0]); - newPort = newPorts[0]; - } - else + if (await firmareUpdater.UpdateFirmware()) { - foreach (var port in newPorts) - { - try - { - connection = await GetConnectionAndDisableRuntime(port); - newPort = port; - break; - } - catch - { - throw CommandException.MeadowDeviceNotFound; - } - } + Logger?.LogInformation(Strings.FirmwareUpdatedSuccessfully); } - - Logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); - - await connection!.WaitForMeadowAttach(); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - - return connection; - } - - private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) - { - Logger?.LogInformation($"{Environment.NewLine}{Strings.GettingRuntimeFor} {package.Version}..."); - - if (package.Runtime == null) { return null; } - - // get the path to the runtime file - var rtpath = package.GetFullyQualifiedPath(package.Runtime); - - return await WriteRuntime(connection, deviceInfo, rtpath, package.Runtime); - } - - private async Task WriteRuntime(IMeadowConnection? connection, DeviceInfo? deviceInfo, string runtimePath, string destinationFilename) - { - connection ??= await GetConnectionAndDisableRuntime(); - - Logger?.LogInformation($"{Environment.NewLine}{Strings.WritingRuntime}..."); - - deviceInfo ??= await connection.GetDeviceInfo(CancellationToken); - - if (deviceInfo == null) - { - throw new CommandException(Strings.UnableToGetDeviceInfo); - } - - if (UseDfu || RequiresDfuForRuntimeUpdates(deviceInfo)) - { - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - - write_runtime: - if (!await connection!.Device!.WriteRuntime(runtimePath, CancellationToken)) - { - // TODO: implement a retry timeout - Logger?.LogInformation($"{Strings.ErrorWritingRuntime} - {Strings.Retrying}"); - goto write_runtime; - } - - connection = await GetCurrentConnection(true); - - if (connection == null) - { - var newPort = await WaitForNewSerialPort(initialPorts) ?? throw CommandException.MeadowDeviceNotFound; - connection = await GetCurrentConnection(true); - - Logger?.LogInformation($"{Strings.MeadowFoundAt} {newPort}"); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - } - } - else - { - await connection.Device!.WriteFile(runtimePath, $"/{AppTools.MeadowRootFolder}/update/os/{destinationFilename}"); - } - - return connection; - } - - private async Task WriteEspFiles(IMeadowConnection? connection, DeviceInfo? deviceInfo, FirmwarePackage package) - { - connection ??= await GetConnectionAndDisableRuntime(); - - if (connection == null) { return; } // couldn't find a connected device - - Logger?.LogInformation($"{Environment.NewLine}{Strings.WritingCoprocessorFiles}..."); - - string[] fileList; - - if (IndividualFile != null) - { - fileList = new string[] { IndividualFile }; - } - else - { - fileList = new string[] - { - package.GetFullyQualifiedPath(package.CoprocApplication), - package.GetFullyQualifiedPath(package.CoprocBootloader), - package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - - deviceInfo ??= await connection.GetDeviceInfo(CancellationToken); - - if (deviceInfo == null) { throw new CommandException(Strings.UnableToGetDeviceInfo); } - - if (UseDfu || RequiresDfuForEspUpdates(deviceInfo)) - { - await connection.Device!.WriteCoprocessorFiles(fileList, CancellationToken); - } - else - { - foreach (var file in fileList) - { - await connection!.Device!.WriteFile(file, $"/{AppTools.MeadowRootFolder}/update/os/{Path.GetFileName(file)}"); - await Task.Delay(500); - } - } - } - - private ILibUsbDevice? GetLibUsbDeviceForCurrentEnvironment(LibUsbProvider? provider) - { - provider ??= new LibUsbProvider(); - - var devices = provider.GetDevicesInBootloaderMode(); - - var meadowsInDFU = devices.Where(device => device.IsMeadow()).ToList(); - - if (meadowsInDFU.Count == 0) - { - return null; - } - - if (meadowsInDFU.Count == 1 || IgnoreSerialNumberForDfu(provider)) - { //IgnoreSerialNumberForDfu is a macOS-specific hack for Mark's machine - return meadowsInDFU.FirstOrDefault(); - } - - throw new CommandException(Strings.MultipleDfuDevicesFound); - } - - private async Task GetSelectedPackage() - { - await FileManager.Refresh(); - - var collection = FileManager.Firmware["Meadow F7"]; - FirmwarePackage package; - - if (Version != null) - { - // make sure the requested version exists - var existing = collection.FirstOrDefault(v => v.Version == Version); - - if (existing == null) - { - Logger?.LogError(string.Format(Strings.SpecifiedFirmwareVersionNotFound, Version)); - return null; - } - package = existing; - } - else - { - Version = collection.DefaultPackage?.Version ?? throw new CommandException($"{Strings.NoDefaultVersionSet}. {Strings.UseCommandFirmwareDefault}."); - - package = collection.DefaultPackage; - } - - return package; - } - - private async Task WriteOsWithDfu(ILibUsbDevice libUsbDevice, string osFile, bool ignoreSerialNumber = false) - { - string serialNumber; - - try - { //validate device - serialNumber = libUsbDevice.GetDeviceSerialNumber(); - } - catch - { - throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.UnableToReadSerialNumber} ({Strings.MakeSureDeviceisConnected})"); - } - - try - { - if (ignoreSerialNumber) - { - serialNumber = string.Empty; - } - - await DfuUtils.FlashFile( - osFile, - serialNumber, - logger: Logger, - format: DfuUtils.DfuFlashFormat.ConsoleOut); - } - catch (ArgumentException) - { - throw new CommandException($"{Strings.FirmwareWriteFailed} - {Strings.IsDfuUtilInstalled} {Strings.RunMeadowDfuInstall}"); - } - 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"); - - throw new CommandException("This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."); - } - } - - private async Task> WaitForNewSerialPorts(IList? ignorePorts) - { - var ports = await MeadowConnectionManager.GetSerialPorts(); - - var retryCount = 0; - - while (ports.Count == 0) - { - if (retryCount++ > 10) - { - throw new CommandException(Strings.NewMeadowDeviceNotFound); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); - } - - if (ignorePorts != null) - { - return ports.Except(ignorePorts).ToList(); - } - return ports.ToList(); - } - - private async Task WaitForNewSerialPort(IList? ignorePorts) - { - var ports = await WaitForNewSerialPorts(ignorePorts); - - return ports.FirstOrDefault(); - } - - private bool IgnoreSerialNumberForDfu(LibUsbProvider provider) - { //hack check for Mark's Mac - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - var devices = provider.GetDevicesInBootloaderMode(); - - if (devices.Count == 2) - { - if (devices[0].GetDeviceSerialNumber().Length > 12 || devices[1].GetDeviceSerialNumber().Length > 12) - { - return true; - } - } - } - - return false; } } \ 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 3624b1bf..d2209538 100644 --- a/Source/v2/Meadow.Cli/Meadow.CLI.csproj +++ b/Source/v2/Meadow.Cli/Meadow.CLI.csproj @@ -47,6 +47,7 @@ + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 2075af16..217050de 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -279,6 +279,10 @@ "commandName": "Project", "commandLineArgs": "cloud package publish d867bb8b6e56418ba26ebe4e2b3ef6db -c 9d5f9780e6964c22b4ec15c44b886545" }, + "Provision Multiple Devices": { + "commandName": "Project", + "commandLineArgs": "provision" + }, "legacy list ports": { "commandName": "Project", "commandLineArgs": "list ports" diff --git a/Source/v2/Meadow.Cli/Strings.cs b/Source/v2/Meadow.Cli/Strings.cs index 4a0fb946..f6a7b35d 100644 --- a/Source/v2/Meadow.Cli/Strings.cs +++ b/Source/v2/Meadow.Cli/Strings.cs @@ -87,4 +87,44 @@ public static class Telemetry public const string AskToParticipate = "Would you like to participate?"; } + + public const string Enter = "Enter"; + public const string Space = "Space"; + + public static class Provision + { + public const string CommandDescription = "Provision 1 or more devices that are in DFU mode."; + public const string CommandOptionVersion = "Target OS version for devices to be provisioned with"; + public const string CommandOptionPath = "Path to the provision.json file"; + public const string RefreshDeviceList = "Flash devices (y=Flash selected devices, n=Refresh List)?"; + public const string MoreChoicesInstructions = "(Move up and down to reveal more devices)"; + public const string Instructions = "Press {0} to toggle a device, {1} to accept and flash the selected device"; + public const string RunningTitle = "Provisioning"; + public const string PromptTitle = "Devices in Bootloader mode"; + public const string NoDevicesFound = "No devices found in bootloader mode. Rerun this command when at least 1 connected device is in bootloader mode."; + public const string ColumnTitle = "Selected Devices"; + public const string NoDeviceSelected = "No devices selected to provision. Exiting."; + public const string UpdateFailed = "Update failed"; + public const string UpdateComplete = "Update completed"; + public const string AllDevicesFlashed = "All devices updated!"; + public const string FileNotFound = "Provision Settings file (provision.json), not found at location: {0}."; + public const string NoAppDeployment = "Skipping App Deployment and using default version: {0}"; + public const string DeployingApp = "Deploying App"; + public const string TrimmingApp = "Trimming App, before we get started"; + public const string ShowErrorMessages = "Show all error messages (y=Show Messages, n=Exit Immediately)?"; + public const string IssuesFound = "There were issues during the last provision."; + public const string ErrorSerialNumberColumnTitle = "Serial Number"; + public const string ErrorMessageColumnTitle = "Message"; + public const string ErrorStackTraceColumnTitle = "Stack Trace"; + public const string AppDllNotFound = "App.dll Not found at location"; + public const string FailedToReadProvisionFile = "Failed to read provision.json file"; + } + + public static class FirmwareUpdater + { + public const string FlashingOS = "Flashing OS"; + public const string WritingRuntime = "Writing Runtime"; + public const string WritingESP = "Writing ESP"; + public const string SwitchingToLibUsbClassic = "This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Dfu/DfuUtils.cs b/Source/v2/Meadow.Dfu/DfuUtils.cs index b83e0bec..57d704b1 100644 --- a/Source/v2/Meadow.Dfu/DfuUtils.cs +++ b/Source/v2/Meadow.Dfu/DfuUtils.cs @@ -1,17 +1,14 @@ -using System; +using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Meadow.Hcom; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using static System.Net.Mime.MediaTypeNames; namespace Meadow.CLI.Core.Internals.Dfu; diff --git a/Source/v2/Meadow.Firmware/FirmwareWriter.cs b/Source/v2/Meadow.Firmware/FirmwareWriter.cs index 2541c9e9..07ca5c5d 100644 --- a/Source/v2/Meadow.Firmware/FirmwareWriter.cs +++ b/Source/v2/Meadow.Firmware/FirmwareWriter.cs @@ -47,7 +47,7 @@ public Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacy default: throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } - var serialNumber = devices.First().GetDeviceSerialNumber(); + var serialNumber = devices.First().SerialNumber; Debug.WriteLine($"DFU Writing file {osFile}"); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs old mode 100755 new mode 100644 index 1d175e75..a6fadbe8 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -189,46 +189,51 @@ public override void Detach() public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { + CancellationTokenSource? timeoutCts = null; try { // ensure the port is open Open(); + timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds * 2)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken ?? CancellationToken.None); + var ct = linkedCts.Token; + // 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); - _commandEvent.Set(); + EnqueueRequest(command); + + // Wait for a response + var taskCompletionSource = new TaskCompletionSource(); - while (timeout-- > 0) + _ = Task.Run(async () => { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); - - if (count != _messageCount) + while (!ct.IsCancellationRequested) { - dataReceived = true; - break; + if (count != _messageCount) + { + taskCompletionSource.TrySetResult(true); + break; + } + await Task.Delay(500, ct); } - - await Task.Delay(500); - } + if (!taskCompletionSource.Task.IsCompleted) + { + taskCompletionSource.TrySetException(new TimeoutException("Timeout waiting for device response")); + } + }, ct); // 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) + // Create the device instance if the response is received + if (await taskCompletionSource.Task) { Device = new MeadowDevice(this); } @@ -240,6 +245,10 @@ public override void Detach() _logger?.LogError(e, "Failed to connect"); throw; } + finally + { + timeoutCts?.Dispose(); + } } private void CommandManager() diff --git a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj index 6c0525fa..78f63c2b 100644 --- a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj +++ b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj @@ -16,7 +16,7 @@ - + diff --git a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs index c2aa15c3..95b0b610 100644 --- a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs @@ -19,14 +19,17 @@ public class MeadowConnectionManager private static readonly object _lockObject = new(); private readonly ISettingsManager _settingsManager; - private IMeadowConnection? _currentConnection; + private static IMeadowConnection? _currentConnection; + + private static readonly int RETRY_COUNT = 10; + private static readonly int RETRY_DELAY = 500; public MeadowConnectionManager(ISettingsManager settingsManager) { _settingsManager = settingsManager; } - public IMeadowConnection? GetCurrentConnection(bool forceReconnect = false) + public async Task GetCurrentConnection(bool forceReconnect = false) { var route = _settingsManager.GetSetting(SettingsManager.PublicSettings.Route); @@ -35,22 +38,36 @@ public MeadowConnectionManager(ISettingsManager settingsManager) throw new Exception($"No 'route' configuration set.{Environment.NewLine}Use the `meadow config route` command. For example:{Environment.NewLine} > meadow config route COM5"); } - return GetConnectionForRoute(route, forceReconnect); + return await GetConnectionForRoute(route, forceReconnect); } - public IMeadowConnection? GetConnectionForRoute(string route, bool forceReconnect = false) + public static async Task GetConnectionForRoute(string route, bool forceReconnect = false) { // TODO: support connection changing (CLI does this rarely as it creates a new connection with each command) - if (_currentConnection != null && forceReconnect == false) + lock (_lockObject) { - return _currentConnection; + if (_currentConnection != null + && _currentConnection.Name == route + && forceReconnect == false) + { + return _currentConnection; + } + else if (_currentConnection != null) + { + _currentConnection.Detach(); + _currentConnection.Dispose(); + _currentConnection = null; + } } - _currentConnection?.Detach(); - _currentConnection?.Dispose(); if (route == "local") { - return new LocalConnection(); + var newConnection = new LocalConnection(); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; } // try to determine what the route is @@ -74,50 +91,59 @@ public MeadowConnectionManager(ISettingsManager settingsManager) if (uri != null) { - _currentConnection = new TcpConnection(uri); + var newConnection = new TcpConnection(uri); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; } else { - var retryCount = 0; - - get_serial_connection: - try - { - _currentConnection = new SerialConnection(route); - } - catch + for (int retryCount = 0; retryCount <= RETRY_COUNT; retryCount++) { - retryCount++; - if (retryCount > 10) + try + { + var newConnection = new SerialConnection(route); + lock (_lockObject) + { + _currentConnection = newConnection; + } + return _currentConnection; + } + catch { - throw new Exception($"Cannot find port {route}"); + if (retryCount == RETRY_COUNT) + { + throw new Exception($"Cannot create SerialConnection on route: {route}"); + } + + await Task.Delay(RETRY_DELAY); } - Thread.Sleep(500); - goto get_serial_connection; } - } - return _currentConnection; + // This line should never be reached because the loop will either return or throw + throw new Exception("Unexpected error in GetCurrentConnection"); + } } - public static async Task> GetSerialPorts() + public static async Task> GetSerialPorts(string? serialNumber = null) { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return await GetMeadowSerialPortsForLinux(); + return await GetMeadowSerialPortsForLinux(serialNumber); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return await GetMeadowSerialPortsForOsx(); + return await GetMeadowSerialPortsForOsx(serialNumber); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { lock (_lockObject) { - return GetMeadowSerialPortsForWindows(); + return GetMeadowSerialPortsForWindows(serialNumber); } } else @@ -131,9 +157,9 @@ public static async Task> GetSerialPorts() } } - public static async Task> GetMeadowSerialPortsForOsx() + public static async Task> GetMeadowSerialPortsForOsx(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { throw new PlatformNotSupportedException("This method is only supported on macOS"); } @@ -187,6 +213,12 @@ public static async Task> GetMeadowSerialPortsForOsx() var port = line.Substring(startIndex, endIndex - startIndex); + if (!string.IsNullOrWhiteSpace(serialNumber) + && !port.Contains(serialNumber)) + { + continue; + } + ports.Add(port); foundMeadow = false; } @@ -196,16 +228,16 @@ public static async Task> GetMeadowSerialPortsForOsx() }); } - public static async Task> GetMeadowSerialPortsForLinux() + public static async Task> GetMeadowSerialPortsForLinux(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { throw new PlatformNotSupportedException("This method is only supported on Linux"); } return await Task.Run(() => { - const string devicePath = "/dev/serial/by-id"; + const string devicePath = "/dev/serial/by-id/"; var psi = new ProcessStartInfo() { FileName = "ls", @@ -225,7 +257,8 @@ public static async Task> GetMeadowSerialPortsForLinux() return Array.Empty(); } - return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + var wlSerialPorts = output + .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) .Where(x => x.Contains("Wilderness_Labs")) .Select( line => @@ -234,13 +267,24 @@ public static async Task> GetMeadowSerialPortsForLinux() var target = parts[1]; var port = Path.GetFullPath(Path.Combine(devicePath, target)); return port; - }).ToArray(); + }); + + if (string.IsNullOrWhiteSpace(serialNumber)) + { + return wlSerialPorts.ToArray(); + } + else + { + return wlSerialPorts + .Where(line => !string.IsNullOrWhiteSpace(serialNumber) && line.Contains(serialNumber)) + .ToArray(); + } }); } - public static IList GetMeadowSerialPortsForWindows() + public static IList GetMeadowSerialPortsForWindows(string? serialNumber) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new PlatformNotSupportedException("This method is only supported on Windows"); } @@ -288,7 +332,11 @@ public static IList GetMeadowSerialPortsForWindows() // 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]; + + if (!string.IsNullOrWhiteSpace(serialNumber) + && !string.IsNullOrWhiteSpace(splits[2]) + && !splits[2].Contains(serialNumber)) + continue; results.Add($"{port}"); // removed serial number for consistency and will break fallback ({serialNumber})"); } @@ -306,4 +354,10 @@ public static IList GetMeadowSerialPortsForWindows() return ports; } } + + public static async Task GetRouteFromSerialNumber(string? serialNumber) + { + var results = await GetSerialPorts(serialNumber); + return results.FirstOrDefault(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs index 61fb302f..9942fb83 100644 --- a/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs @@ -10,7 +10,7 @@ public interface ILibUsbProvider public interface ILibUsbDevice : IDisposable { - string GetDeviceSerialNumber(); + string SerialNumber { get; } bool IsMeadow(); } \ No newline at end of file diff --git a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs index 96cb39f9..11682562 100644 --- a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs @@ -7,9 +7,11 @@ namespace Meadow.LibUsb; public class LibUsbProvider : ILibUsbProvider { private const int UsbBootLoaderVendorID = 1155; + private const int UsbMeadowVendorID = 11882; internal static UsbContext _context; internal static List? _devices; + static LibUsbProvider() { // only ever create one of these - there's a bug in the LibUsbDotNet library and when this disposes, things go sideways @@ -30,20 +32,13 @@ public List GetDevicesInBootloaderMode() public class LibUsbDevice : ILibUsbDevice { private readonly IUsbDevice _device; + private string? serialNumber; + + public string? SerialNumber => serialNumber; public LibUsbDevice(IUsbDevice usbDevice) { _device = usbDevice; - } - - public void Dispose() - { - _device?.Dispose(); - } - - public string GetDeviceSerialNumber() - { - var serialNumber = string.Empty; _device.Open(); if (_device.IsOpen) @@ -51,17 +46,16 @@ public string GetDeviceSerialNumber() serialNumber = _device.Info?.SerialNumber ?? string.Empty; _device.Close(); } + } - return serialNumber; + public void Dispose() + { + _device?.Dispose(); } public bool IsMeadow() { - if (_device.VendorId != 1155) - { - return false; - } - if (GetDeviceSerialNumber().Length > 12) + if (serialNumber?.Length > 12) { return false; } diff --git a/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs b/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs index 5161e084..b9a30931 100644 --- a/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLibClassic/ClassicLibUsbDevice.cs @@ -10,70 +10,71 @@ public class ClassicLibUsbProvider : ILibUsbProvider { private const string UsbStmName = "STM32 BOOTLOADER"; private const int UsbBootLoaderVendorID = 1155; + private const int UsbMeadowVendorID = 11882; - public List GetDevicesInBootloaderMode() - { - var propName = (Environment.OSVersion.Platform == PlatformID.Win32NT) + private string propName = (Environment.OSVersion.Platform == PlatformID.Win32NT) ? "FriendlyName" : "DeviceDesc"; + public List GetDevicesInBootloaderMode() + { return UsbDevice .AllDevices .Where(d => d.DeviceProperties[propName].ToString() == UsbStmName) .Select(d => new ClassicLibUsbDevice(d)) - .Cast() - .ToList(); + .ToList(); } -} - -public class ClassicLibUsbDevice : ILibUsbDevice -{ - private readonly UsbRegistry _device; - public ClassicLibUsbDevice(UsbRegistry usbDevice) + public class ClassicLibUsbDevice : ILibUsbDevice { - _device = usbDevice; - } + private readonly UsbRegistry _device; + private string? serialNumber; - public void Dispose() - { - _device.Device.Close(); - } + public string? SerialNumber => serialNumber; - public string GetDeviceSerialNumber() - { - if (_device != null && _device.DeviceProperties != null) + public ClassicLibUsbDevice(UsbRegistry usbDevice) { - switch (Environment.OSVersion.Platform) + _device = usbDevice; + + _device.Device.Open(); + if (_device.Device.IsOpen) { - case PlatformID.Win32NT: - var deviceID = _device.DeviceProperties["DeviceID"].ToString(); - if (!string.IsNullOrWhiteSpace(deviceID)) - { - return deviceID.Substring(deviceID.LastIndexOf("\\") + 1); - } - else + if (_device.DeviceProperties != null) + { + switch (Environment.OSVersion.Platform) { - return string.Empty; + case PlatformID.Win32NT: + var deviceID = _device.DeviceProperties["DeviceID"].ToString(); + if (!string.IsNullOrWhiteSpace(deviceID)) + { + serialNumber = deviceID.Substring(deviceID.LastIndexOf("\\") + 1); + } + else + { + serialNumber = string.Empty; + } + break; + default: + serialNumber = _device.DeviceProperties["SerialNumber"].ToString() ?? string.Empty; + break; } - default: - return _device.DeviceProperties["SerialNumber"].ToString() ?? string.Empty; + _device.Device.Close(); + } } } - return string.Empty; - } - - public bool IsMeadow() - { - if (_device.Vid != 1155) + public void Dispose() { - return false; + _device.Device.Close(); } - if (GetDeviceSerialNumber().Length > 12) + + public bool IsMeadow() { - return false; + if (SerialNumber?.Length > 12) + { + return false; + } + return true; } - return true; } } \ No newline at end of file