From abbfc2205c71efc0d9d5a3bed1709243e905739a Mon Sep 17 00:00:00 2001 From: Dominique Louis Date: Thu, 19 Dec 2024 14:24:07 +0000 Subject: [PATCH] 1st pass at FirmwareUpdater class. --- .../Current/Firmware/FirmwareUpdater.cs | 609 ++++++++++++++++++ .../Commands/Current/BaseDeviceCommand.cs | 4 +- .../Current/Firmware/FirmwareWriteCommand.cs | 511 +-------------- Source/v2/Meadow.Cli/Strings.cs | 12 +- Source/v2/Meadow.Dfu/DfuUtils.cs | 188 +++--- .../Connection/MeadowConnectionManager.cs | 55 +- 6 files changed, 785 insertions(+), 594 deletions(-) create mode 100644 Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs 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 000000000..426cbe3ac --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Firmware/FirmwareUpdater.cs @@ -0,0 +1,609 @@ +using System.Runtime.InteropServices; +using Meadow.CLI.Commands.DeviceManagement; +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; +using Meadow.LibUsb; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Current.Firmware; + +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)) + { + 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, $"/{AppManager.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); + + await connection.WaitForMeadowAttach(); + + // reset device + if (connection != null && connection.Device != null) + { + await connection.Device.Reset(); + } + } + + return true; + } + + 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 bool RequiresDfuForEspUpdates(DeviceInfo info) + { + return true; + } + + 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 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, $"/{AppManager.MeadowRootFolder}/update/os/{destinationFilename}"); + } + + return connection; + } + + private async Task WriteEspFiles(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, $"/{AppManager.MeadowRootFolder}/update/os/{Path.GetFileName(file)}"); + } + } + } + + 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.GetDeviceSerialNumber() == 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 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 async Task WriteOsWithDfu(ILibUsbDevice libUsbDevice, string osFile, bool ignoreSerialNumber = false) + { + try + { //validate device + if (string.IsNullOrWhiteSpace(serialNumber)) + { + 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: 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> 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; + } + + /*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, $"/{AppManager.MeadowRootFolder}/update/os/{package.OsWithoutBootloader}"); + } + }*/ +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index a94358c0d..9c7b8d6cc 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) diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index a298c6dc8..ea094696c 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 CliFx.Attributes; +using Meadow.CLI.Commands.Current.Firmware; using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.LibUsb; @@ -30,6 +31,9 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandParameter(0, Description = "Files to write", IsRequired = false)] public FirmwareType[]? FirmwareFileTypes { get; set; } = default!; + [CommandOption("serialnumber", 's', IsRequired = false, Description = "Flash the specified device")] + public string? SerialNumber { get; set; } + private FileManager FileManager { get; } private ISettingsManager Settings { get; } @@ -40,514 +44,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, $"/{AppManager.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); - - await connection.WaitForMeadowAttach(); - - // 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; - } - - 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 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, $"/{AppManager.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, $"/{AppManager.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 firmareUpdater = new FirmwareUpdater(this, Settings, FileManager, ConnectionManager, IndividualFile, FirmwareFileTypes, UseDfu, Version, SerialNumber, Logger, CancellationToken); - var retryCount = 0; - - while (ports.Count == 0) + if (await firmareUpdater.UpdateFirmware()) { - if (retryCount++ > 10) - { - throw new CommandException(Strings.NewMeadowDeviceNotFound); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); + Logger?.LogInformation(Strings.FirmwareUpdatedSuccessfully); } - - 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/Strings.cs b/Source/v2/Meadow.Cli/Strings.cs index 43dbfc186..ea5ee1e15 100644 --- a/Source/v2/Meadow.Cli/Strings.cs +++ b/Source/v2/Meadow.Cli/Strings.cs @@ -71,8 +71,8 @@ public static class Strings public const string AppDeployFailed = "Application deploy failed"; public const string AppDeployedSuccessfully = "Application deployed successfully"; public const string AppTrimFailed = "Application trimming failed"; - public const string WithConfiguration = "with configuration"; - public const string At = "at"; + public const string WithConfiguration = "with configuration"; + public const string At = "at"; public static class Telemetry { @@ -116,4 +116,12 @@ public static class ProjectTemplates public const string UnsupportedOperatingSystem = "Unsupported Operating System"; public const string Enter = "Enter"; public const string Space = "Space"; + + 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 bbec43961..a27d8f7b0 100644 --- a/Source/v2/Meadow.Dfu/DfuUtils.cs +++ b/Source/v2/Meadow.Dfu/DfuUtils.cs @@ -16,6 +16,8 @@ namespace Meadow.CLI.Core.Internals.Dfu; public static class DfuUtils { private static readonly int _osAddress = 0x08000000; + public const string DEFAULT_DFU_VERSION = "0.11"; + private const string DFU_UTIL = "dfu-util"; public enum DfuFlashFormat { @@ -31,6 +33,10 @@ public enum DfuFlashFormat /// Console.WriteLine for CLI - ToDo - remove /// ConsoleOut, + /// + /// No Console Output + /// + None, } private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) @@ -79,38 +85,39 @@ public static async Task FlashFile(string fileName, string? dfuSerialNumbe logger.LogInformation($"Flashing OS with {fileName}"); - var dfuUtilVersion = new Version(GetDfuUtilVersion()); + var dfuUtilVersion = new Version(await GetDfuUtilVersion(logger)); logger.LogDebug("Detected OS: {os}", RuntimeInformation.OSDescription); + var expectedDfuUtilVersion = new Version(DEFAULT_DFU_VERSION); if (dfuUtilVersion == null) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - logger.LogError("dfu-util not found - to install, run: `meadow dfu install` (may require administrator mode)"); + logger.LogError($"{DFU_UTIL} not found - to install, run: `meadow dfu install` (may require administrator mode)"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - logger.LogError("dfu-util not found - to install run: `brew install dfu-util`"); + logger.LogError($"{DFU_UTIL} not found - to install run: `brew install {DFU_UTIL}`"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - logger.LogError("dfu-util not found - install using package manager, for example: `apt install dfu-util` or the equivalent for your Linux distribution"); + logger.LogError($"{DFU_UTIL} not found - install using package manager, for example: `apt install {DFU_UTIL}` or the equivalent for your Linux distribution"); } return false; } - else if (dfuUtilVersion.CompareTo(new Version("0.11")) < 0) + else if (dfuUtilVersion.CompareTo(expectedDfuUtilVersion) < 0) { 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. Expected: {expectedDfuUtilVersion}, Found: {dfuUtilVersion} - 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. Expected: {expectedDfuUtilVersion}, Found: {dfuUtilVersion} - 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. Expected: {expectedDfuUtilVersion}, Found: {dfuUtilVersion} - to update , run: `apt upgrade {DFU_UTIL}` or the equivalent for your Linux distribution"); } else { @@ -135,7 +142,7 @@ public static async Task FlashFile(string fileName, string? dfuSerialNumbe } catch (Exception ex) { - logger.LogError($"There was a problem executing dfu-util: {ex.Message}"); + logger.LogError($"There was a problem executing {DFU_UTIL}: {ex.Message}"); return false; } @@ -144,79 +151,60 @@ public static async Task FlashFile(string fileName, string? dfuSerialNumbe private static async Task RunDfuUtil(string args, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) { - var startInfo = new ProcessStartInfo("dfu-util", args) + try { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = false, - CreateNoWindow = true - }; - using var process = Process.Start(startInfo); - - if (process == null) + var result = await RunProcessCommand(DFU_UTIL, args, + outputLogLine => + { + // Ignore empty output + if (!string.IsNullOrWhiteSpace(outputLogLine) + && format != DfuFlashFormat.None) + { + FormatDfuOutput(outputLogLine, logger, format); + } + }, + errorLogLine => + { + if (!string.IsNullOrWhiteSpace(errorLogLine)) + { + logger?.LogError(errorLogLine); + } + }); + } + catch (Exception ex) { - throw new Exception("Failed to start dfu-util"); + throw new Exception($"{DFU_UTIL} failed. Error: {ex.Message}"); } - - var informationLogger = logger != null - ? Task.Factory.StartNew( - () => - { - var lastProgress = string.Empty; - - while (process.HasExited == false) - { - var logLine = process.StandardOutput.ReadLine(); - // Ignore empty output - if (logLine == null) - continue; - - FormatDfuOutput(logLine, logger, format); - } - }) : Task.CompletedTask; - - var errorLogger = logger != null - ? Task.Factory.StartNew( - () => - { - while (process.HasExited == false) - { - var logLine = process.StandardError.ReadLine(); - logger.LogError(logLine); - } - }) : Task.CompletedTask; - await informationLogger; - await errorLogger; - process.WaitForExit(); } - private static string GetDfuUtilVersion() + private static async Task GetDfuUtilVersion(ILogger? logger) { try { - using (var process = new Process()) - { - process.StartInfo.FileName = "dfu-util"; - process.StartInfo.Arguments = $"--version"; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.Start(); - - var reader = process.StandardOutput; - var output = reader.ReadLine(); - if (output != null && output.StartsWith("dfu-util")) + var version = string.Empty; + + var result = await RunProcessCommand(DFU_UTIL, "--version", + outputLogLine => { - var split = output.Split(new char[] { ' ' }); - if (split.Length == 2) + if (!string.IsNullOrWhiteSpace(outputLogLine) + && outputLogLine.StartsWith(DFU_UTIL)) { - return split[1]; + var split = outputLogLine.Split(new char[] { ' ' }); + if (split.Length == 2) + { + version = split[1]; + } } - } + }, + errorLogLine => + { + if (!string.IsNullOrWhiteSpace(errorLogLine)) + { + logger?.LogError(errorLogLine); + } + }); - process.WaitForExit(); - return string.Empty; - } + return string.IsNullOrWhiteSpace(version) ? string.Empty : version; } catch (Win32Exception ex) { @@ -237,6 +225,44 @@ private static string GetDfuUtilVersion() } } + public static async Task RunProcessCommand(string command, string args, Action? handleOutput = null, Action? handleError = null) + { + var processStartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = new Process { StartInfo = processStartInfo }) + { + process.Start(); + + var outputCompletion = ReadLinesAsync(process.StandardOutput, handleOutput); + var errorCompletion = ReadLinesAsync(process.StandardError, handleError); + + await Task.WhenAll(outputCompletion, errorCompletion, process.WaitForExitAsync()); + + return process.ExitCode; + } + } + + private static async Task ReadLinesAsync(StreamReader reader, Action? handleLine) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (!string.IsNullOrWhiteSpace(line) + && handleLine != null) + { + handleLine(line); + } + } + } + public static async Task InstallDfuUtil( string tempFolder, string dfuUtilVersion = "0.11", @@ -253,7 +279,7 @@ public static async Task InstallDfuUtil( Directory.CreateDirectory(tempFolder); - var downloadUrl = $"https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-{dfuUtilVersion}-binaries.zip"; + var downloadUrl = $"https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/{DFU_UTIL}-{dfuUtilVersion}-binaries.zip"; var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); @@ -261,7 +287,7 @@ public static async Task InstallDfuUtil( if (response.IsSuccessStatusCode == false) { - throw new Exception("Failed to download dfu-util"); + throw new Exception($"Failed to download {DFU_UTIL}"); } using (var stream = await response.Content.ReadAsStreamAsync()) @@ -278,7 +304,7 @@ public static async Task InstallDfuUtil( var is64Bit = Environment.Is64BitOperatingSystem; var dfuUtilExe = new FileInfo( - Path.Combine(tempFolder, is64Bit ? "win64" : "win32", "dfu-util.exe")); + Path.Combine(tempFolder, is64Bit ? "win64" : "win32", $"{DFU_UTIL}.exe")); var libUsbDll = new FileInfo( Path.Combine( @@ -301,4 +327,20 @@ public static async Task InstallDfuUtil( } } } +} + +public static class ProcessExtensions +{ + public static Task WaitForExitAsync(this Process process) + { + var tcs = new TaskCompletionSource(); + + process.EnableRaisingEvents = true; + process.Exited += (sender, args) => + { + tcs.SetResult(process.ExitCode == 0); + }; + + return tcs.Task; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs index 435f54348..440ea7af2 100644 --- a/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Tooling.Core/Connection/MeadowConnectionManager.cs @@ -100,24 +100,23 @@ public MeadowConnectionManager(ISettingsManager settingsManager) return _currentConnection; } - 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,7 +130,7 @@ public static async Task> GetSerialPorts() } } - public static async Task> GetMeadowSerialPortsForOsx() + public static async Task> GetMeadowSerialPortsForOsx(string? serialNumber) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) == false) { @@ -187,6 +186,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,7 +201,7 @@ public static async Task> GetMeadowSerialPortsForOsx() }); } - public static async Task> GetMeadowSerialPortsForLinux() + public static async Task> GetMeadowSerialPortsForLinux(string? serialNumber) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) { @@ -205,13 +210,15 @@ public static async Task> GetMeadowSerialPortsForLinux() return await Task.Run(() => { - const string devicePath = "/dev/serial/by-id"; + const string devicePath = "/dev/serial/by-id/"; var psi = new ProcessStartInfo() { FileName = "ls", Arguments = $"-l {devicePath}", + RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, - RedirectStandardOutput = true + CreateNoWindow = true }; using var proc = Process.Start(psi); @@ -223,7 +230,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 => @@ -232,11 +240,22 @@ 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) { @@ -286,7 +305,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})"); } @@ -304,4 +327,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