diff --git a/Meadow.CLI.Core/CloudServices/DeviceRepository.cs b/Meadow.CLI.Core/CloudServices/DeviceRepository.cs index 2192b2b3..f6ff332a 100644 --- a/Meadow.CLI.Core/CloudServices/DeviceRepository.cs +++ b/Meadow.CLI.Core/CloudServices/DeviceRepository.cs @@ -1,18 +1,26 @@ -using Meadow.CLI.Core.Auth; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Meadow.CLI.Core.Identity; +using Microsoft.Extensions.Logging; namespace Meadow.CLI.Core.CloudServices { public class DeviceRepository { + private readonly ILogger _logger; + + public DeviceRepository(ILogger logger) + { + _logger = logger; + } + public async Task<(bool isSuccess, string message)> AddDevice(string serialNumber) { var host = SettingsManager.GetAppSetting("wlApiHost"); - var authToken = await new IdentityManager().GetAccessToken(); + var authToken = await new IdentityManager(_logger).GetAccessToken(); HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); diff --git a/Meadow.CLI.Core/DeviceManagement/AssemblyManager.cs b/Meadow.CLI.Core/DeviceManagement/AssemblyManager.cs index 15df40bd..e0895777 100644 --- a/Meadow.CLI.Core/DeviceManagement/AssemblyManager.cs +++ b/Meadow.CLI.Core/DeviceManagement/AssemblyManager.cs @@ -1,18 +1,17 @@ -using System; -using Mono.Collections.Generic; +using System.Collections.Generic; using System.IO; using Mono.Cecil; -using System.Collections.Generic; +using Mono.Collections.Generic; -namespace MeadowCLI.DeviceManagement +namespace Meadow.CLI.Core.DeviceManagement { //https://github.com/xamarin/xamarin-macios/blob/main/tools/mtouch/Assembly.mtouch.cs#L54 public static class AssemblyManager { - static List dependencyMap = new List(); - static string folderPath; - static string fileName; + private static readonly List dependencyMap = new List(); + private static string? folderPath; + private static string? fileName; public static List GetDependencies(string file, string path) { @@ -29,23 +28,23 @@ public static List GetDependencies(string file, string path) return dependencies; } - static Collection GetAssemblyNameReferences(string fileName, string path = null) + static Collection GetAssemblyNameReferences(string fileName, string? path = null) { if (!string.IsNullOrWhiteSpace(path)) { - fileName = Path.Combine(path, fileName); + fileName = Path.Combine(path!, fileName); } - if (Path.GetExtension(fileName) != ".exe" && Path.GetExtension(fileName) != ".dll") + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") { fileName += ".dll"; } Collection references; - if (File.Exists(fileName) == false) + if(File.Exists(fileName) == false) { - Console.WriteLine($"Could not find {fileName}"); return null; } @@ -65,9 +64,7 @@ static List GetDependencies(Collection references var namedRefs = GetAssemblyNameReferences(ar.Name, folderPath); if (namedRefs == null) - { continue; - } dependencyMap.Add(ar.Name); diff --git a/Meadow.CLI.Core/DeviceManagement/DeploymentManager.cs b/Meadow.CLI.Core/DeviceManagement/DeploymentManager.cs deleted file mode 100644 index 97fbb7b1..00000000 --- a/Meadow.CLI.Core/DeviceManagement/DeploymentManager.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace MeadowCLI.DeviceManagement -{ - /// - /// TODO: put deployment stuff here - /// - /// TODO: consider using a singleton. - /// - public static class DeploymentManager - { - static DeploymentManager() - { - } - - public static Tuple DeployApp(string appOutputPath, MeadowDevice device) - { - bool success = false; - string message = ""; - - // 0) TODO: any init checks? - - // 1) TODO: enumerate all the files in the output path - - // 2) TODO: get a list of the files already present on the device - - // 3) TODO: compare file names and sizes/checksums or whatever, and - // and figure out what we need to deploy - - // 4) TODO: Deploy app files - - // 5) TODO: restart device - - return new Tuple(success, message); - } - } -} diff --git a/Meadow.CLI.Core/DeviceManagement/DeviceInfoException.cs b/Meadow.CLI.Core/DeviceManagement/DeviceInfoException.cs new file mode 100644 index 00000000..c26651bd --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/DeviceInfoException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Meadow.CLI.Core.DeviceManagement +{ + // TODO: Inherit from MeadowDeviceException + public class DeviceInfoException : Exception + { + public DeviceInfoException(Exception? innerException = null) : base("An exception occurred while retrieving the device info", innerException) + {} + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/FileTransferResult.cs b/Meadow.CLI.Core/DeviceManagement/FileTransferResult.cs new file mode 100644 index 00000000..d52165ec --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/FileTransferResult.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public class FileTransferResult + { + public FileTransferResult(long transferTime, long fileSize, uint checksum) + { + TransferTime = transferTime; + FileSize = fileSize; + Checksum = $"{checksum:x08}"; + } + /// + /// Gets the number of milliseconds it took to transfer the file to the device + /// + public long TransferTime {get;} + + /// + /// Get the number of bytes written + /// + public long FileSize {get;} + + /// + /// Get the CRC Checksum of the file + /// + public string Checksum {get;} + + public static FileTransferResult EmptyResult = new FileTransferResult(0, 0, 0); + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/HcomHostRequestType.cs b/Meadow.CLI.Core/DeviceManagement/HcomHostRequestType.cs new file mode 100644 index 00000000..262849d6 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/HcomHostRequestType.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.DeviceManagement +{ + /// + /// Messages sent from meadow to host + /// + public enum HcomHostRequestType : ushort + { + HCOM_HOST_REQUEST_UNDEFINED_REQUEST = 0x00 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED, + + // Simple with some text message + HCOM_HOST_REQUEST_TEXT_REJECTED = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_ACCEPTED = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_CONCLUDED = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_ERROR = 0x04 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_INFORMATION = 0x05 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_LIST_HEADER = 0x06 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_LIST_MEMBER = 0x07 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_CRC_MEMBER = 0x08 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_MONO_STDOUT = 0x09 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_DEVICE_INFO = 0x0A | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_TRACE_MSG = 0x0B | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_RECONNECT = 0x0C | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_TEXT_MONO_STDERR = 0x0d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_FILE_START_OKAY = 0x0e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + HCOM_HOST_REQUEST_FILE_START_FAIL = 0x0f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, + + // Simple with debugger message from Meadow + HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, + HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/HcomMeadowRequestType.cs b/Meadow.CLI.Core/DeviceManagement/HcomMeadowRequestType.cs new file mode 100644 index 00000000..a2fab983 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/HcomMeadowRequestType.cs @@ -0,0 +1,60 @@ +namespace Meadow.CLI.Core.DeviceManagement +{ + /// + /// Messages to be sent to Meadow board from host + /// + public enum HcomMeadowRequestType : ushort + { + HCOM_MDOW_REQUEST_UNDEFINED_REQUEST = 0x00 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED, + + HCOM_MDOW_REQUEST_CREATE_ENTIRE_FLASH_FS = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_FORMAT_FLASH_FILE_SYS = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_END_FILE_TRANSFER = 0x04 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU = 0x05 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_VERIFY_ERASED_FLASH = 0x06 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_PARTITION_FLASH_FS = 0x07 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MOUNT_FLASH_FS = 0x08 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_INITIALIZE_FLASH_FS = 0x09 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_BULK_FLASH_ERASE = 0x0a | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_ENTER_DFU_MODE = 0x0b | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH = 0x0c | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_LIST_PARTITION_FILES = 0x0d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC = 0x0e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_DISABLE = 0x0f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_ENABLE = 0x10 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_RUN_STATE = 0x11 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION = 0x12 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_PART_RENEW_FILE_SYS = 0x13 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST = 0x14 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST = 0x15 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER = 0x16 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS = 0x17 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_RESTART_ESP32 = 0x18 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_FLASH = 0x19 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART = 0x1a | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_NO_TRACE_TO_UART = 0x1b | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME = 0x1c | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END = 0x1d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION = 0x1e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_GET_DEVICE_NAME = 0x1f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES = 0x20 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + + // Only used for testing + HCOM_MDOW_REQUEST_DEVELOPER_1 = 0xf0 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_DEVELOPER_2 = 0xf1 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_DEVELOPER_3 = 0xf2 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_DEVELOPER_4 = 0xf3 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + + HCOM_MDOW_REQUEST_S25FL_QSPI_INIT = 0xf4 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE = 0xf5 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + HCOM_MDOW_REQUEST_S25FL_QSPI_READ = 0xf6 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, + + HCOM_MDOW_REQUEST_START_FILE_TRANSFER = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, + HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, + HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, + + // Simple debugger message to Meadow + HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderOffsets.cs b/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderOffsets.cs new file mode 100644 index 00000000..51d42689 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderOffsets.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public enum HcomProtocolHeaderOffsets + { + HCOM_PROTOCOL_REQUEST_HEADER_SEQ_OFFSET = 0, + HCOM_PROTOCOL_REQUEST_HEADER_VERSION_OFFSET = 2, + HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET = 4, + HCOM_PROTOCOL_REQUEST_HEADER_EXTRA_DATA_OFFSET = 6, + HCOM_PROTOCOL_REQUEST_HEADER_USER_DATA_OFFSET = 8, + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderTypes.cs b/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderTypes.cs new file mode 100644 index 00000000..26c08a93 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/HcomProtocolHeaderTypes.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public enum HcomProtocolHeaderTypes : UInt16 + { + HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED = 0x0000, + // Simple request types, include 4-byte user data + HCOM_PROTOCOL_HEADER_TYPE_SIMPLE = 0x0100, + // File releted request types, includes 4-byte user data (for the + // destination partition id), 4-byte file size, 4-byte checksum and + // variable length destination file name. + HCOM_PROTOCOL_HEADER_TYPE_FILE_START = 0x0200, + // Simple text. + HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT = 0x0300, + // Header followed by binary data. The size of the data can be up to + // HCOM_PROTOCOL_PACKET_MAX_SIZE minus header size + HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY = 0x0400, + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowDataProcessor.cs b/Meadow.CLI.Core/DeviceManagement/MeadowDataProcessor.cs new file mode 100644 index 00000000..30926b7a --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowDataProcessor.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.Internals.MeadowCommunication; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public class MeadowDataProcessor + { + public EventHandler? OnReceiveData; + public Func? ForwardDebuggingData; + } + + public class MeadowMessageEventArgs : EventArgs + { + public string Message { get; private set; } + public MeadowMessageType MessageType { get; private set; } + + public MeadowMessageEventArgs(MeadowMessageType messageType, string message = "") + { + Message = message; + MessageType = messageType; + } + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowDevice.cs b/Meadow.CLI.Core/DeviceManagement/MeadowDevice.cs index 4c0a50ec..b50f162c 100644 --- a/Meadow.CLI.Core/DeviceManagement/MeadowDevice.cs +++ b/Meadow.CLI.Core/DeviceManagement/MeadowDevice.cs @@ -1,41 +1,179 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; using System.Threading.Tasks; +using Meadow.CLI.Core.Exceptions; +using Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; -namespace MeadowCLI.DeviceManagement +namespace Meadow.CLI.Core.DeviceManagement { //is this needed? public class MeadowDeviceException : Exception { - public MeadowDeviceException(string message, Exception innerException) + public MeadowDeviceException(string message, Exception? innerException = null) : base(message, innerException) { } } //a simple model object that represents a meadow device including connection - public abstract class MeadowDevice + public abstract class MeadowDevice : IDisposable { - public MeadowDeviceInfo DeviceInfo { get; protected set; } = new MeadowDeviceInfo(); + private protected TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5); + private protected TimeSpan SlowTimeout = TimeSpan.FromSeconds(60); + public readonly MeadowDataProcessor DataProcessor; + private protected DebuggingServer DebuggingServer; + private protected readonly ILogger Logger; - public List FilesOnDevice { get; protected set; } = new List(); - public List FileCrcs { get; protected set; } = new List(); + protected MeadowDevice(MeadowDataProcessor dataProcessor, ILogger? logger) + { + DataProcessor = dataProcessor; + Logger = logger ?? new NullLogger(); + } + + public MeadowDeviceInfo? DeviceInfo { get; protected set; } + + public IDictionary FilesOnDevice { get; protected set; } = + new Dictionary(); + public abstract Task> GetFilesAndCrcsAsync(int timeoutInMs = 10000, int partition = 0, CancellationToken cancellationToken = default); + public abstract Task WriteFileAsync(string filename, string path, int timeoutInMs = 200000, CancellationToken cancellationToken = default); + public abstract Task DeleteFileAsync(string fileName, uint partition = 0, CancellationToken cancellationToken = default); + public abstract Task EraseFlashAsync(CancellationToken cancellationToken = default); + public abstract Task VerifyErasedFlashAsync(CancellationToken cancellationToken = default); + public abstract Task FormatFileSystemAsync(uint partition = 0, CancellationToken cancellationToken = default); + // TODO: Should this also take a partition parameter? + public abstract Task RenewFileSystemAsync(CancellationToken cancellationToken = default); + public abstract Task UpdateMonoRuntimeAsync(string fileName, string? targetFileName = null, uint partition = 0, CancellationToken cancellationToken = default); + public abstract Task WriteFileToEspFlashAsync(string fileName, string? targetFileName = null, uint partition = 0, string? mcuDestAddress = null, CancellationToken cancellationToken = default); + public abstract Task FlashEspAsync(string sourcePath, CancellationToken cancellationToken = default); + public abstract Task GetDeviceInfoAsync(TimeSpan timeout, CancellationToken cancellationToken = default); + public abstract Task GetDeviceNameAsync(TimeSpan timeout, CancellationToken cancellationToken = default); + public abstract Task GetMonoRunStateAsync(CancellationToken cancellationToken = default); + public abstract Task MonoDisableAsync(CancellationToken cancellationToken = default); + public abstract Task MonoEnableAsync(CancellationToken cancellationToken = default); + public abstract Task ResetMeadowAsync(CancellationToken cancellationToken = default); + public abstract Task MonoFlashAsync(CancellationToken cancellationToken = default); + public abstract Task EnterDfuModeAsync(CancellationToken cancellationToken = default); + public abstract Task NshEnableAsync(CancellationToken cancellationToken = default); + public abstract Task NshDisableAsync(CancellationToken cancellationToken = default); + public abstract Task TraceEnableAsync(CancellationToken cancellationToken = default); + public abstract Task TraceDisableAsync(CancellationToken cancellationToken = default); + public abstract Task QspiWriteAsync(int value, CancellationToken cancellationToken = default); + public abstract Task QspiReadAsync(int value, CancellationToken cancellationToken = default); + public abstract Task QspiInitAsync(int value, CancellationToken cancellationToken = default); + public abstract Task DeployAppAsync(string fileName, bool includePdbs = false, CancellationToken cancellationToken = default); + public abstract Task ForwardVisualStudioDataToMonoAsync(byte[] debuggerData, uint userData, CancellationToken cancellationToken = default); + public abstract Task GetInitialBytesFromFile(string fileName, uint partition = 0, CancellationToken cancellationToken = default); + + // TODO: This is very Meadow Local Device Specific... + public abstract Task RestartEsp32Async(CancellationToken cancellationToken = default); + public abstract Task GetDeviceMacAddressAsync(CancellationToken cancellationToken = default); + + public virtual async Task FlashEspAsync(CancellationToken cancellationToken = default) + { + await WaitForReadyAsync(DefaultTimeout, cancellationToken) + .ConfigureAwait(false); - public abstract Task WriteFile(string filename, string path, int timeoutInMs = 200000); + await MonoDisableAsync(cancellationToken) + .ConfigureAwait(false); - public abstract Task> GetFilesOnDevice(bool refresh = false, int timeoutInMs = 10000); + Trace.Assert(await GetMonoRunStateAsync(cancellationToken).ConfigureAwait(false) == false, + "Meadow was expected to have Mono Disabled"); - public abstract Task<(List files, List crcs)> GetFilesAndCrcs(int timeoutInMs = 10000); + Logger.LogInformation("Flashing ESP"); - public abstract Task GetDeviceInfo(int timeoutInMs = 500); + Logger.LogInformation($"Transferring {DownloadManager.NetworkMeadowCommsFilename}"); - public abstract Task GetDeviceName(int timeoutInMs = 500); + await WriteFileToEspFlashAsync( + Path.Combine( + DownloadManager.FirmwareDownloadsFilePath, + DownloadManager.NetworkMeadowCommsFilename), + mcuDestAddress: "0x10000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); - public abstract Task GetInitialFileData(string fileName, int timeoutInMs); + await Task.Delay(5000, cancellationToken) + .ConfigureAwait(false); - public Task IsFileOnDevice(string filename) + Logger.LogInformation($"Transferring {DownloadManager.NetworkBootloaderFilename}"); + + await WriteFileToEspFlashAsync( + Path.Combine( + DownloadManager.FirmwareDownloadsFilePath, + DownloadManager.NetworkBootloaderFilename), + mcuDestAddress: "0x1000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await Task.Delay(5000, cancellationToken) + .ConfigureAwait(false); + + Logger.LogInformation($"Transferring {DownloadManager.NetworkPartitionTableFilename}"); + + await WriteFileToEspFlashAsync( + Path.Combine( + DownloadManager.FirmwareDownloadsFilePath, + DownloadManager.NetworkPartitionTableFilename), + mcuDestAddress: "0x8000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await Task.Delay(5000, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Wait for the Meadow to respond to commands + /// + /// How long to wait for the meadow to become ready + /// A to cancel the operation + /// A indicating if the Meadow is ready + public virtual async Task WaitForReadyAsync(TimeSpan timeout, + CancellationToken cancellationToken = default) + { + var now = DateTime.UtcNow; + var then = now.Add(timeout); + while (DateTime.UtcNow < then) + { + try + { + var deviceInfo = await GetDeviceInfoAsync(timeout, cancellationToken: cancellationToken); + + if (string.IsNullOrWhiteSpace(deviceInfo) == false) + return; + } + catch (MeadowCommandException meadowCommandException) + { + Logger.LogTrace(meadowCommandException, "Caught exception while waiting for device to be ready"); + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Caught exception while waiting for device to be ready. Retrying."); + } + + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + } + + throw new Exception($"Device not ready after {timeout}ms"); + } + + public virtual Task ForwardMonoDataToVisualStudioAsync(byte[] debuggerData, CancellationToken cancellationToken = default) + { + return DebuggingServer.SendToVisualStudio(debuggerData, cancellationToken); + } + + public ValueTask IsFileOnDevice(string filename) { - return Task.FromResult(FilesOnDevice.Contains(filename)); + return new(FilesOnDevice.ContainsKey(filename)); } + + public abstract bool IsDeviceInitialized(); + + public abstract void Dispose(); } } \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowDeviceInfo.cs b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceInfo.cs index 5201ccaf..b125e23f 100644 --- a/Meadow.CLI.Core/DeviceManagement/MeadowDeviceInfo.cs +++ b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceInfo.cs @@ -1,4 +1,6 @@ -namespace MeadowCLI.DeviceManagement +using System; + +namespace Meadow.CLI.Core.DeviceManagement { public class MeadowDeviceInfo { @@ -12,14 +14,36 @@ public class MeadowDeviceInfo CoProcessor: ESP32, CoProcessor OS Version: 0.1.x\r\n" */ + public MeadowDeviceInfo(string deviceInfoString) + { + RawDeviceInfo = deviceInfoString; + Name = deviceInfoString.Substring(0, deviceInfoString.IndexOf(' ')); + Model = ParseValue("Model: ", deviceInfoString); + MeadowOSVersion = ParseValue("MeadowOS Version: ", deviceInfoString); + Processor = ParseValue("Processor: ", deviceInfoString); + ProcessorId = ParseValue("Processor Id:", deviceInfoString); + SerialNumber = ParseValue("Serial Number: ", deviceInfoString); + CoProcessor = ParseValue("CoProcessor: ", deviceInfoString); + CoProcessorOs = ParseValue("CoProcessor OS Version: ", deviceInfoString); + } + + public string RawDeviceInfo { get; } + public string Name { get; } + public string Model { get; } + public string MeadowOSVersion { get; } + public string Processor { get; } + public string ProcessorId { get; } + public string SerialNumber { get; } + public string CoProcessor { get; } + public string CoProcessorOs { get; } + + public override string ToString() => RawDeviceInfo; - public string Name { get; set; } = "Meadow"; - public string Model { get; set; } = "F7Micro"; - public string MeadowOSVersion { get; set; } - public string Proccessor { get; set; } - public string ProcessorId { get; set; } - public string SerialNumber { get; set; } - public string CoProcessor { get; set; } - public string CoProcessorOs { get; set; } + private static string ParseValue(string key, string source) + { + var start = source.IndexOf(key, StringComparison.Ordinal) + key.Length; + var end = source.IndexOf(',', start); + return source.Substring(start, end - start); + } } -} +} \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManager.cs b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManager.cs index f8bacd16..471df0bf 100644 --- a/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManager.cs +++ b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManager.cs @@ -1,592 +1,340 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Ports; -using System.Linq; using System.Management; -using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Meadow.CLI.Internals.MeadowComms.RecvClasses; -using MeadowCLI.Hcom; -using static MeadowCLI.DeviceManagement.MeadowFileManager; +using LibUsbDotNet.Main; +using Meadow.CLI.Core.Exceptions; +using Meadow.CLI.Core.Internals.Dfu; +using Microsoft.Extensions.Logging; -namespace MeadowCLI.DeviceManagement +namespace Meadow.CLI.Core.DeviceManagement { - /// - /// TODO: put device enumeration and such stuff here. - /// - public static class MeadowDeviceManager + public class MeadowDeviceManager { - internal const ushort DefaultVS2019DebugPort = 4024; // Port used by VS 2019 + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + internal const int MaxAllowableMsgPacketLength = 512; - // Note: While not truly important, it can be noted that size of the s25fl QSPI flash - // chip's "Page" (i.e. the smallest size it can program) is 256 bytes. By making the - // maxmimum data block size an even multiple of 256 we insure that each packet received - // can be immediately written to the s25fl QSPI flash chip. + internal const int MaxEstimatedSizeOfEncodedPayload = + MaxAllowableMsgPacketLength + (MaxAllowableMsgPacketLength / 254) + 8; - internal const int MaxAllowableMsgPacketLength = 512; - internal const int MaxEstimatedSizeOfEncodedPayload = MaxAllowableMsgPacketLength + (MaxAllowableMsgPacketLength / 254) + 8; internal const int ProtocolHeaderSize = 12; - internal const int MaxAllowableMsgPayloadLength = MaxAllowableMsgPacketLength - ProtocolHeaderSize; - static HcomMeadowRequestType _meadowRequestType; - static DebuggingServer debuggingServer; + internal const int MaxAllowableMsgPayloadLength = + MaxAllowableMsgPacketLength - ProtocolHeaderSize; - static readonly string _systemHttpNetDllName = "System.Net.Http.dll"; - - static MeadowDeviceManager() + public MeadowDeviceManager(ILoggerFactory loggerFactory) { - // TODO: populate the list of attached devices - - // TODO: wire up listeners for device plug and unplug + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); } - static Dictionary _connections = new Dictionary(); - - public static async Task GetMeadowForSerialPort(string serialPort) //, bool verbose = true) + public MeadowDevice? GetMeadowForSerialPort(string serialPort, bool verbose = true) { try { - if (_connections.ContainsKey(serialPort)) - { - _connections[serialPort].Dispose(); - _connections.Remove(serialPort); - Thread.Sleep(1000); - } - var meadow = new MeadowSerialDevice(serialPort); + _logger.LogInformation($"Connecting to Meadow on {serialPort}", serialPort); + var meadow = new MeadowSerialDevice(serialPort, _logger); + meadow.Initialize(); - _connections.Add(serialPort, meadow); + return meadow; + } + catch (FileNotFoundException fnfEx) + { + + LogUserError(verbose); + + _logger.LogDebug(fnfEx, "Failed to open Serial Port."); + return null; + } + catch (IOException ioEx) + { + LogUserError(verbose); + + _logger.LogDebug(ioEx, "Failed to open Serial Port."); + return null; + } + catch (UnauthorizedAccessException unAuthEx) when ( + unAuthEx.InnerException is IOException) + { + LogUserError(verbose); + _logger.LogDebug(unAuthEx, "Failed to open Serial Port."); + return null; } catch (Exception ex) { - throw ex; + // TODO: Remove exception catch here and let the caller handle it or wrap it up in our own exception type. + _logger.LogError(ex, "Failed to connect to Meadow on {serialPort}", serialPort); + throw; } } - //we'll move this soon - public static List FindSerialDevices() + private void LogUserError(bool verbose) { - var devices = new List(); - - foreach (var s in SerialPort.GetPortNames()) + if (verbose) { - //limit Mac searches to tty.usb*, Windows, try all COM ports - //on Mac it's pretty quick to test em all so we could remove this check - if (Environment.OSVersion.Platform != PlatformID.Unix || - s.Contains("tty.usb")) - { - devices.Add(s); - } + // TODO: Move message to ResourceManager or other tool for localization + _logger.LogError( + "Failed to open Serial Port. Please ensure you have exclusive access to the serial port and the specified port exists."); } - return devices; } - public static List GetSerialDeviceCaptions() + public async Task FindMeadowBySerialNumber( + string serialNumber, + int maxAttempts = 10, + CancellationToken cancellationToken = default) { - var devices = new List(); - - using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Caption like '%(COM%'")) + var attempts = 0; + while (attempts < maxAttempts) { - var portnames = SerialPort.GetPortNames(); - foreach (var item in searcher.Get()) + string[]? ports; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - devices.Add(item["Caption"].ToString()); + ports = Directory.GetFiles("/dev", "tty.usb*"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + ports = Directory.GetFiles("/dev", "tty.usb*"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + ports = SerialPort.GetPortNames(); + } + else + { + throw new Exception("Unknown operating system."); } - } - return devices; - } - - //providing a numeric (0 = none, 1 = info and 2 = debug) - public static async Task SetTraceLevel(MeadowSerialDevice meadow, int level) - { - if (level < 0 || level > 3) - throw new System.ArgumentOutOfRangeException(nameof(level), "Trace level must be between 0 & 3 inclusive"); - - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL, userData: (uint)level); - } - - public static async Task ResetMeadow(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU, doAcceptedCheck: false, filter: null); - // needs some time to complete restart - Thread.Sleep(1000); - } - - public static async Task EnterDfuMode(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENTER_DFU_MODE); - } - - public static async Task NshEnable(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH, userData: (uint)1); - } - public static async Task MonoDisable(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_DISABLE, MeadowMessageType.SerialReconnect, timeoutMs: 15000); - } + foreach (var port in ports) + { + try + { + var device = GetMeadowForSerialPort(port, false); + if (device == null) + continue; - public static async Task MonoEnable(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_ENABLE, MeadowMessageType.SerialReconnect, timeoutMs: 15000); - } + var deviceInfo = + await device.GetDeviceInfoAsync(TimeSpan.FromSeconds(5), cancellationToken: cancellationToken); - public static async Task MonoRunState(MeadowSerialDevice meadow) - { - await new SendTargetData(meadow).SendSimpleCommand(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_RUN_STATE); - - var tcs = new TaskCompletionSource(); - var result = false; + if (!string.IsNullOrWhiteSpace(deviceInfo) + && deviceInfo!.Contains(serialNumber)) + { + return device; + } - EventHandler handler = (s, e) => - { - if (e.MessageType == MeadowMessageType.Data) - { - if (e.Message == "On reset, Meadow will start MONO and run app.exe") - { - result = true; - tcs.SetResult(true); + device.Dispose(); } - else if (e.Message == "On reset, Meadow will not start MONO, therefore app.exe will not run") + catch (MeadowDeviceException meadowDeviceException) { - result = false; - tcs.SetResult(true); + // eat it for now + _logger.LogDebug( + meadowDeviceException, + "This error can be safely ignored."); } } - }; - - if (meadow.DataProcessor != null) meadow.DataProcessor.OnReceiveData += handler; - - await Task.WhenAny(new Task[] { tcs.Task, Task.Delay(5000) }); - if (meadow.DataProcessor != null) meadow.DataProcessor.OnReceiveData -= handler; - - return result; - } - - public static async Task MonoFlash(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_FLASH, timeoutMs: 200000, filter: e => e.Message.StartsWith("Mono runtime successfully flashed.")); - } - - public static async Task<(bool isSuccessful, string message, MeadowMessageType msgType)> GetDeviceInfo(MeadowSerialDevice meadow, int timeoutMs = 1000) - { - _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION; - await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); - return await WaitForResponseMessage(meadow, p => p.MessageType == MeadowMessageType.DeviceInfo, millisecondDelay: timeoutMs); - } + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); - public static async Task GetDeviceSerialNumber(MeadowSerialDevice meadow, int timeoutMs = 1000) - { - _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION; - await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); - var result = await WaitForResponseMessage(meadow, p => p.MessageType == MeadowMessageType.DeviceInfo, millisecondDelay: timeoutMs); - if (result.Success) - { - return ParseDeviceInfo(result.Message, "Serial Number: ", ","); + attempts++; } - return string.Empty; - } - - private static string ParseDeviceInfo(string deviceInfo, string value, string endChar) - { - var start = deviceInfo.IndexOf(value) + value.Length; - var end = deviceInfo.IndexOf(endChar, start); - return deviceInfo.Substring(start, end-start); - } - - public static async Task<(bool isSuccessful, string message, MeadowMessageType msgType)> GetDeviceName(MeadowSerialDevice meadow, int timeoutMs = 1000) - { - _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_NAME; - await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); - return await WaitForResponseMessage(meadow, p => p.MessageType == MeadowMessageType.DeviceInfo, millisecondDelay: timeoutMs); - } - - public static async Task SetDeveloper1(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER_1, userData: (uint)userData); - } - public static async Task SetDeveloper2(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER_2, userData: (uint)userData); - } - public static async Task SetDeveloper3(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER_3, userData: (uint)userData); - } - - public static async Task SetDeveloper4(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER_4, userData: (uint)userData); - } - - public static async Task TraceDisable(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST); - } - - public static async Task TraceEnable(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST); - } - - public static async Task Uart1Apps(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_UART); - } - - public static async Task Uart1Trace(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART); + throw new DeviceNotFoundException( + $"Could not find a connected Meadow with the serial number {serialNumber}"); } - public static async Task RenewFileSys(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_PART_RENEW_FILE_SYS, MeadowMessageType.SerialReconnect); - } - - public static async Task QspiWrite(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE, userData: (uint)userData); - } - - public static async Task QspiRead(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_READ, userData: (uint)userData); - } - - public static async Task QspiInit(MeadowSerialDevice meadow, int userData) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_INIT, userData: (uint)userData); - } - - // This method is called to sent to Visual Studio debugging to Mono - public static void ForwardVisualStudioDataToMono(byte[] debuggerData, MeadowSerialDevice meadow, int userData) - { - // Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-MDM-Forwarding {debuggerData.Length} bytes to Mono via hcom"); - _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA; - - new SendTargetData(meadow).BuildAndSendSimpleData(debuggerData, _meadowRequestType, (uint)userData); - } - // This method is called to forward from mono debugging to Visual Studio - public static void ForwardMonoDataToVisualStudio(byte[] debuggerData) - { - // Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-MDM-Received {debuggerData.Length} bytes from hcom for VS"); - debuggingServer.SendToVisualStudio(debuggerData); - } - - // Creates a DebuggingServer that can listen on the given port on all network interfaces. - public static Task CreateDebuggingServer(MeadowSerialDevice meadow, int vsDebugPort = 0) - { - if (vsDebugPort == 0) - { - Console.WriteLine($"Without '--VSDebugPort' being specified, will assume Visual Studio 2019 using default port {DefaultVS2019DebugPort}"); - vsDebugPort = DefaultVS2019DebugPort; - } - return CreateDebuggingServer(meadow, new IPEndPoint(IPAddress.Any, vsDebugPort)); - } - - // Enter StartDebugging mode. - public static async Task CreateDebuggingServer(MeadowSerialDevice meadow, IPEndPoint localEndpoint) + //we'll move this soon + public static List FindSerialDevices() { - // Create the DebuggingServer first so we aren't racing for it in ForwardMonoDataToVisualStudio after Meadow restarts - debuggingServer = new DebuggingServer(localEndpoint); - - // Tell meadow to start it's debugging server, after restarting. - _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION; - await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); + var devices = new List(); - // The previous command caused Meadow to restart. Therefore, we must reestablish - // Meadow communication. - var attempts = 0; - retry: - try - { - attempts++; - meadow.AttemptToReconnectToMeadow(); - } catch (Exception ex) when (ex is IOException || ex.InnerException is IOException) + foreach (var s in SerialPort.GetPortNames()) { - if (attempts < 5) - { - await Task.Yield(); - goto retry; - } else + //limit Mac searches to tty.usb*, Windows, try all COM ports + //on Mac it's pretty quick to test em all so we could remove this check + if (Environment.OSVersion.Platform != PlatformID.Unix || s.Contains("tty.usb")) { - throw; + devices.Add(s); } } - return debuggingServer; + return devices; } - public static void EnterEchoMode(MeadowSerialDevice meadow) + public static List GetSerialDeviceCaptions() { - if (meadow == null) - { - Console.WriteLine("No current device"); - return; - } + var devices = new List(); - if (meadow.SerialPort == null && meadow.Socket == null) + using (var searcher = new ManagementObjectSearcher( + "SELECT * FROM Win32_PnPEntity WHERE Caption like '%(COM%'")) { - Console.WriteLine("No current serial port or socket"); - return; + var portnames = SerialPort.GetPortNames(); + foreach (var item in searcher.Get()) + { + devices.Add( + item["Caption"] + .ToString()); + } } - meadow.Initialize(true); - } - - public static async Task Esp32ReadMac(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS); - } - - public static async Task Esp32Restart(MeadowSerialDevice meadow) - { - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESTART_ESP32); + return devices; } - //public static async Task - // GetInitialFileBytes(MeadowSerialDevice meadow, string fileName) - // { - // await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES); - - // _meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES; - // await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); - // return await WaitForResponseMessage(meadow, p => p.MessageType == MeadowMessageType.Data, millisecondDelay: 1000); - - /* - await ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES); - - await new SendTargetData(meadow).SendSimpleCommand(_meadowRequestType); - return await WaitForResponseMessage(meadow, p => p.MessageType == MeadowMessageType.DeviceInfo, millisecondDelay: timeoutMs); - */ - // } - - public static async Task DeployApp(MeadowSerialDevice meadow, string applicationFilePath, bool includeDebugSymbols = true) + public async Task FlashOsAsync(string serialPortName, string binPath, bool skipDfu = false, CancellationToken cancellationToken = default) { - if (!File.Exists(applicationFilePath)) - { - Console.WriteLine($"{applicationFilePath} not found."); - return; - } - - FileInfo fi = new FileInfo(applicationFilePath); - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + var dfuAttempts = 0; + + string serialNumber; + if (skipDfu) { - // for some strange reason, System.Net.Http.dll doesn't get copied to the output folder in VS. - // so, we need to copy it over from the meadow assemblies nuget. - CopySystemNetHttpDll(fi.DirectoryName); - } - - var deviceFile = await meadow.GetFilesAndCrcs(); - var extensions = new List { ".exe", ".bmp", ".jpg", ".jpeg", ".json", ".xml", ".yml", ".txt" }; - - var paths = Directory.EnumerateFiles(fi.DirectoryName, "*.*", SearchOption.TopDirectoryOnly) - .Where(s => extensions.Contains(new FileInfo(s).Extension)); - - var files = new List(); - var crcs = new List(); - void AddFile(string file, bool lookForPDB) - { - using (FileStream fs = File.Open(file, FileMode.Open)) - { - var len = (int)fs.Length; - var bytes = new byte[len]; - - fs.Read(bytes, 0, len); - - //0x - var crc = CrcTools.Crc32part(bytes, len, 0);// 0x04C11DB7); - - //Console.WriteLine($"{file} crc is {crc}"); - files.Add(Path.GetFileName(file)); - crcs.Add(crc); - } - if (lookForPDB) + _logger.LogInformation("Skipping DFU flash step."); + using var device = GetMeadowForSerialPort(serialPortName, false); + if (device == null) { - var pdbFile = Path.ChangeExtension(file, "pdb"); - if (File.Exists(pdbFile)) - AddFile(pdbFile, false); + _logger.LogWarning("Cannot find Meadow on {port}", serialPortName); + return; } - } - foreach (var file in paths) - { - AddFile(file, includeDebugSymbols); - } + var deviceInfo = await device.GetDeviceInfoAsync(TimeSpan.FromSeconds(60), cancellationToken) + .ConfigureAwait(false); - var dependences = AssemblyManager.GetDependencies(fi.Name, fi.DirectoryName); - - //crawl dependences - foreach (var file in dependences) - { - AddFile(Path.Combine(fi.DirectoryName, file), includeDebugSymbols); + var f = new MeadowDeviceInfo(deviceInfo); + serialNumber = f.SerialNumber; } - - // delete unused files - foreach (var file in deviceFile.files) + else { - if (files.Contains(file) == false) + UsbRegistry dfuDevice; + while (true) { - await meadow.DeleteFile(file).ConfigureAwait(false); - Console.WriteLine($"Removing file: {file}"); - } - } + try + { + try + { + dfuDevice = DfuUtils.GetDevice(); + break; + } + catch (MultipleDfuDevicesException) + { + // This is bad, we can't just blindly flash with multiple devices, let the user know + throw; + } + catch (DeviceNotFoundException) + { + // eat it. + } - // write new files - for (int i = 0; i < files.Count; i++) - { - if (deviceFile.crcs.Contains(crcs[i])) - { - Console.WriteLine($"Skipping file: {files[i]}"); - continue; - } + // No DFU device found, lets try to set the meadow to DFU mode. + using var device = GetMeadowForSerialPort(serialPortName, false); - if (!File.Exists(Path.Combine(fi.DirectoryName, files[i]))) - { - Console.WriteLine($"{files[i]} not found"); - continue; - } + if (device != null) + { + _logger.LogInformation("Entering DFU Mode"); + await device.EnterDfuModeAsync(cancellationToken) + .ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogDebug( + "An exception occurred while switching device to DFU Mode. Exception: {0}", + ex); + } - await meadow.WriteFile(files[i], fi.DirectoryName); - Console.WriteLine($"Writing file: {files[i]}"); - } + switch (dfuAttempts) + { + case 5: + _logger.LogInformation( + "Having trouble putting Meadow in DFU Mode, please press RST button on Meadow and press enter to try again"); + + Console.ReadKey(); + break; + case 10: + _logger.LogInformation( + "Having trouble putting Meadow in DFU Mode, please hold BOOT button, press RST button and release BOOT button on Meadow and press enter to try again"); + + Console.ReadKey(); + break; + case > 15: + throw new Exception( + "Unable to place device in DFU mode, please disconnect the Meadow, hold the BOOT button, reconnect the Meadow, release the BOOT button and try again."); + } - Console.WriteLine($"{fi.Name} deploy complete"); - } + // Lets give the device a little time to settle in and get picked up + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); - public static async Task ProcessCommand(MeadowSerialDevice meadow, HcomMeadowRequestType requestType, - MeadowMessageType responseMessageType = MeadowMessageType.Concluded, uint userData = 0, bool doAcceptedCheck = true, int timeoutMs = 10000) - { - await ProcessCommand(meadow, requestType, e => e.MessageType == responseMessageType, userData, doAcceptedCheck, timeoutMs); - } + dfuAttempts++; + } - public static async Task ProcessCommand(MeadowSerialDevice meadow, HcomMeadowRequestType requestType, - Predicate filter, uint userData = 0, bool doAcceptedCheck = true, int timeoutMs = 10000) - { - await new SendTargetData(meadow).SendSimpleCommand(requestType, userData, doAcceptedCheck); - var result = await WaitForResponseMessage(meadow, filter, timeoutMs); - if (!result.Success) - { - throw new MeadowDeviceManagerException(requestType); - } - } + // Get the serial number so that later we can pick the right device if the system has multiple meadow plugged in + serialNumber = DfuUtils.GetDeviceSerial(dfuDevice); - /// - /// - /// - /// - /// - /// - /// - public static async Task<(bool Success, string Message, MeadowMessageType MessageType)> - WaitForResponseMessage( - MeadowSerialDevice meadow, - Predicate filter, - int millisecondDelay = 10000) - { - // if there's no filter, - if (filter == null) { - return (true, string.Empty, MeadowMessageType.ErrOutput); + _logger.LogInformation("Device in DFU Mode, flashing OS"); + await DfuUtils.FlashOsAsync(device: dfuDevice, logger: _logger); + _logger.LogInformation("Device Flashed."); } - var tcs = new TaskCompletionSource(); - var result = false; - var message = string.Empty; - var messageType = MeadowMessageType.ErrOutput; - - // anonmyous handler so we can unsubscribe - EventHandler handler = (s, e) => + try { - if (filter(e)) + using var device = await FindMeadowBySerialNumber( + serialNumber, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (device == null) { - message = e?.Message; - messageType = e.MessageType; - result = true; - tcs.SetResult(true); + _logger.LogWarning("Unable to find Meadow after DFU Flash."); + return; } - }; - - // wire up the handler - if (meadow.DataProcessor != null) - { - meadow.DataProcessor.OnReceiveData += handler; - } - // wait for it to finish - await Task.WhenAny(new Task[] { tcs.Task, Task.Delay(millisecondDelay) }); + await device.UpdateMonoRuntimeAsync(binPath, cancellationToken: cancellationToken); - // cleanup the handler - if (meadow.DataProcessor != null) - { - meadow.DataProcessor.OnReceiveData -= handler; - } + // Again, verify that Mono is disabled + Trace.Assert(await device.GetMonoRunStateAsync(cancellationToken).ConfigureAwait(false) == false, + "Meadow was expected to have Mono Disabled"); - // return the result - return (result, message, messageType); - } + _logger.LogInformation("Updating ESP"); + await device.FlashEspAsync(cancellationToken) + .ConfigureAwait(false); - private static void CopySystemNetHttpDll(string targetDir) - { - try - { - var bclNugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "wildernesslabs.meadow.assemblies"); + // Reset the meadow again to ensure flash worked. + await device.ResetMeadowAsync(cancellationToken) + .ConfigureAwait(false); - if (Directory.Exists(bclNugetPath)) + _logger.LogInformation("Enabling Mono and Resetting"); + while (await device.GetMonoRunStateAsync(cancellationToken).ConfigureAwait(false) == false) { - List versions = new List(); + await device.MonoEnableAsync(cancellationToken); + } - var versionFolders = Directory.EnumerateDirectories(bclNugetPath); - foreach (var versionFolder in versionFolders) - { - var di = new DirectoryInfo(versionFolder); - Version outVersion; - if (Version.TryParse(di.Name, out outVersion)) - { - versions.Add(outVersion); - } - } + await Task.Delay(2000, cancellationToken) + .ConfigureAwait(false); - if (versions.Any()) - { - versions.Sort(); + // TODO: Verify that the device info returns the expected version + var deviceInfoString = await device + .GetDeviceInfoAsync(TimeSpan.FromSeconds(5), cancellationToken) + .ConfigureAwait(false); - var sourcePath = Path.Combine(bclNugetPath, versions.Last().ToString(), "lib", "net472"); - if (Directory.Exists(sourcePath)) - { - if (File.Exists(Path.Combine(sourcePath, _systemHttpNetDllName))) - { - File.Copy(Path.Combine(sourcePath, _systemHttpNetDllName), Path.Combine(targetDir, _systemHttpNetDllName)); - } - } - } + if (string.IsNullOrWhiteSpace(deviceInfoString)) + { + throw new Exception("Unable to retrieve device info."); } + + var deviceInfo = new MeadowDeviceInfo(deviceInfoString); + _logger.LogInformation( + $"Updated Meadow to OS: {deviceInfo.MeadowOSVersion} ESP: {deviceInfo.CoProcessorOs}"); } - catch (Exception) + catch (Exception ex) { - // eat this for now + _logger.LogError(ex, "Error flashing OS to Meadow"); } } } - - public class MeadowDeviceManagerException : Exception - { - public MeadowDeviceManagerException(HcomMeadowRequestType hcomMeadowRequestType) - { - HcomMeadowRequestType = hcomMeadowRequestType; - } - - public HcomMeadowRequestType HcomMeadowRequestType { get; set; } - - public override string ToString() - { - return $"A {GetType()} exception has occurred while making a {nameof(HcomMeadowRequestType)} of {HcomMeadowRequestType}."; - } - } } \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManagerException.cs b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManagerException.cs new file mode 100644 index 00000000..39101702 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowDeviceManagerException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public class MeadowDeviceManagerException : Exception + { + public MeadowDeviceManagerException(HcomMeadowRequestType hcomMeadowRequestType, Exception? innerException) : base(null, innerException) + { + HcomMeadowRequestType = hcomMeadowRequestType; + } + + public HcomMeadowRequestType HcomMeadowRequestType { get; set; } + + public override string ToString() + { + return $"A {GetType()} exception has occurred while making a {nameof(HcomMeadowRequestType)} of {HcomMeadowRequestType}."; + } + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowFileManager.cs b/Meadow.CLI.Core/DeviceManagement/MeadowFileManager.cs deleted file mode 100644 index 9a4243a9..00000000 --- a/Meadow.CLI.Core/DeviceManagement/MeadowFileManager.cs +++ /dev/null @@ -1,426 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Meadow.CLI; -using MeadowCLI.Hcom; - -namespace MeadowCLI.DeviceManagement -{ - public static class MeadowFileManager - { - static HcomMeadowRequestType meadowRequestType; - - static MeadowFileManager() { } - - public static async Task WriteFileToFlash(MeadowSerialDevice meadow, string fileName, string targetFileName = null, - int partition = 0) - { - meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER; - - if (string.IsNullOrWhiteSpace(targetFileName)) - { - targetFileName = Path.GetFileName(fileName); - } - - // For the STM32F7 on meadow, we need source file and destination file names. - string[] csvArray = fileName.Split(','); - if (csvArray.Length == 1) - { - await Task.WhenAll( - Task.Run(() => TransmitFileInfoToExtFlash(meadow, meadowRequestType, fileName, targetFileName, partition, 0, false, true)), - MeadowDeviceManager.WaitForResponseMessage(meadow, x => x.MessageType == MeadowMessageType.Concluded)); - - // No CSV, just the source file name. So we'll assume the targetFileName is correct - //TransmitFileInfoToExtFlash(meadow, meadowRequestType, fileName, targetFileName, partition, 0, false, true); - return true; - } - else - { - // At this point, the fileName field should contain a CSV string containing the source - // and destionation file names, always in an even number. - if (csvArray.Length % 2 != 0) - { - Console.WriteLine("Please provide a CSV input with file names \"source, destination, source, destination\""); - return false; - } - - for (int i = 0; i < csvArray.Length; i += 2) - { - // Send files one-by-one - bool lastFile = i == csvArray.Length - 2 ? true : false; - TransmitFileInfoToExtFlash(meadow, meadowRequestType, csvArray[i].Trim(), csvArray[i + 1].Trim(), - partition, 0, false, lastFile); - } - } - return false; - } - - public static async Task DeleteFile(MeadowSerialDevice meadow, string fileName, int partition = 0) - { - await Task.WhenAll( - Task.Run(() => TransmitFileInfoToExtFlash(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME, fileName, fileName, partition, 0, true)), - MeadowDeviceManager.WaitForResponseMessage(meadow, x => x.MessageType == MeadowMessageType.Concluded)); - } - - public static async Task MonoUpdateRt(MeadowSerialDevice meadow, string fileName, string targetFileName = null, - int partition = 0) - { - Console.WriteLine("Updating runtime..."); - - if (string.IsNullOrWhiteSpace(targetFileName)) - { - targetFileName = Path.GetFileName(fileName); - } - - await Task.WhenAll( - Task.Run(() => TransmitFileInfoToExtFlash(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, fileName, targetFileName, partition, 0, false, true)), - MeadowDeviceManager.WaitForResponseMessage(meadow, x => x.MessageType == MeadowMessageType.Concluded, 300000)); - } - - public static async Task EraseFlash(MeadowSerialDevice meadow) - { - // not sure why this responds with a SerialReconnect message - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_BULK_FLASH_ERASE, MeadowMessageType.SerialReconnect, timeoutMs: 200000); - } - - public static async Task VerifyErasedFlash(MeadowSerialDevice meadow) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_VERIFY_ERASED_FLASH, timeoutMs: 30000); - } - - public static async Task PartitionFileSystem(MeadowSerialDevice meadow, int numberOfPartitions = 2) - { - if (numberOfPartitions < 1 || numberOfPartitions > 8) - { - throw new IndexOutOfRangeException("Number of partitions must be between 1 & 8 inclusive"); - } - - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_PARTITION_FLASH_FS, userData: (uint)numberOfPartitions); - } - - public static async Task MountFileSystem(MeadowSerialDevice meadow, int partition) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_MOUNT_FLASH_FS, userData: (uint)partition); - } - - public static async Task InitializeFileSystem(MeadowSerialDevice meadow, int partition) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_INITIALIZE_FLASH_FS, userData: (uint)partition); - } - - public static async Task CreateFileSystem(MeadowSerialDevice meadow) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_CREATE_ENTIRE_FLASH_FS); - } - - public static async Task FormatFileSystem(MeadowSerialDevice meadow, int partition) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_FORMAT_FLASH_FILE_SYS, userData: (uint)partition); - } - - public static async Task ListFiles(MeadowSerialDevice meadow, int partition = 0) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_LIST_PARTITION_FILES, userData: (uint)partition); - } - - public static async Task ListFilesAndCrcs(MeadowSerialDevice meadow, int partition = 0) - { - await MeadowDeviceManager.ProcessCommand(meadow, HcomMeadowRequestType.HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC, userData: (uint)partition, timeoutMs: 30000); - } - - /// - /// Writes a file to the ESP's flash. - /// - /// - /// the name of the file on this host PC - /// the name of the file on the F7 - /// - /// - /// - public static async Task WriteFileToEspFlash(MeadowSerialDevice meadow, string fileName, - string targetFileName = null, int partition = 0, string mcuDestAddr = null) - { - meadowRequestType = HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER; - - // For the ESP32 on the meadow, we don't need the target file name, we just need the - // MCU's destination address and the file's binary. - // Assume if no mcuDestAddr that the fileName is a CSV with both file names and Mcu Addr - if (mcuDestAddr != null) - { - // Since the mcuDestAddr is used we'll assume the fileName field just contains - // a single file. - if (string.IsNullOrWhiteSpace(targetFileName)) - { - // While not used by the ESP32 it cost nothing to send it and can help - // with debugging - targetFileName = Path.GetFileName(fileName); - } - - // Convert mcuDestAddr from a string to a 32-bit unsigned int, but first - // insure it starts with 0x - uint mcuAddr = 0; - if (mcuDestAddr.StartsWith("0x") || mcuDestAddr.StartsWith("0X")) - { - mcuAddr = uint.Parse(mcuDestAddr.Substring(2), System.Globalization.NumberStyles.HexNumber); - } - else - { - Console.WriteLine($"The '--McuDestAddr' argument must be followed with an address in the form '0x1800'"); - return; - } - - await Task.WhenAll( - Task.Run(() => TransmitFileInfoToExtFlash(meadow, meadowRequestType, fileName, targetFileName, partition, mcuAddr, false, true)), - MeadowDeviceManager.WaitForResponseMessage(meadow, x => x.MessageType == MeadowMessageType.Concluded)); - } - else - { - // At this point, the fileName field should contain a CSV string containing the destination - // addresses followed by file's location within the host's file system. - // E.g. "0x8000, C:\Blink\partition-table.bin, 0x1000, C:\Blink\bootloader.bin, 0x10000, C:\Blink\blink.bin" - string[] fileElement = fileName.Split(','); - if (fileElement.Length % 2 != 0) - { - Console.WriteLine("Please provide a CSV input with \"address, fileName, address, fileName\""); - return; - } - - uint mcuAddr; - for (int i = 0; i < fileElement.Length; i += 2) - { - // Trim any white space from this mcu addr and file name - fileElement[i] = fileElement[i].Trim(); - fileElement[i + 1] = fileElement[i + 1].Trim(); - - if (fileElement[i].StartsWith("0x") || fileElement[i].StartsWith("0X")) - { - // Fill in the Mcu Addr - mcuAddr = uint.Parse(fileElement[i].Substring(2), System.Globalization.NumberStyles.HexNumber); - } - else - { - Console.WriteLine("Please provide a CSV input with addresses like 0x1234"); - return; - } - // Meadow.CLI --Esp32WriteFile --SerialPort Com26 --File - // "0x8000, C:\Download\Esp32\Hello\partition-table.bin, 0x1000, C:\Download\Esp32\Hello\bootloader.bin, 0x10000, C:\Download\Esp32\Hello\hello-world.bin" - // File Path and Name - targetFileName = Path.GetFileName(fileElement[i + 1]); - bool lastFile = i == fileElement.Length - 2 ? true : false; - - // this may need need to be awaited? - TransmitFileInfoToExtFlash(meadow, meadowRequestType, fileElement[i + 1], targetFileName, partition, mcuAddr, false, lastFile); - } - } - } - - public static async Task FlashEsp(MeadowSerialDevice device, string sourcePath) - { - Console.WriteLine($"Transferring {DownloadManager.NetworkMeadowCommsFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(sourcePath, DownloadManager.NetworkMeadowCommsFilename), mcuDestAddr: "0x10000"); - await Task.Delay(1000); - - Console.WriteLine($"Transferring {DownloadManager.NetworkBootloaderFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(sourcePath, DownloadManager.NetworkBootloaderFilename), mcuDestAddr: "0x1000"); - await Task.Delay(1000); - - Console.WriteLine($"Transferring {DownloadManager.NetworkPartitionTableFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(sourcePath, DownloadManager.NetworkPartitionTableFilename), mcuDestAddr: "0x8000"); - await Task.Delay(1000); - } - - public static async Task GetInitialBytesFromFile(MeadowSerialDevice meadow, string fileName, int partition = 0) - { - Console.WriteLine($"Getting initial bytes from {fileName}..."); - Byte[] encodedFileName = System.Text.Encoding.UTF8.GetBytes(fileName); - - await Task.WhenAll( - Task.Run(() => new SendTargetData(meadow).BuildAndSendSimpleData(encodedFileName, - HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES, 0)), - MeadowDeviceManager.WaitForResponseMessage(meadow, x => x.MessageType == MeadowMessageType.Concluded, 5000)); - } - - private static void TransmitFileInfoToExtFlash(MeadowSerialDevice meadow, - HcomMeadowRequestType requestType, string sourceFileName, - string targetFileName, int partition, uint mcuAddr, - bool useSourceAsTarget, bool lastInSeries = false) - { - var sw = new Stopwatch(); - try - { - if(string.IsNullOrWhiteSpace(sourceFileName)) - { - Console.WriteLine($"No source file name provided"); - return; - } - - var sendTargetData = new SendTargetData(meadow, false); - - //---------------------------------------------- - if (useSourceAsTarget == true) - { - // No data packets, no end-of-file message and no mcu address - // Currently only used by delete - sendTargetData.BuildAndSendFileRelatedCommand(requestType, - (uint)partition, 0, 0, 0, string.Empty, sourceFileName); - return; - } - - var fi = new FileInfo(sourceFileName); - if (!fi.Exists) - { - Console.WriteLine($"Can't find source file '{fi.FullName}'"); - return; - } - - // If ESP32 file we must also send the MD5 has of the file - string md5Hash = string.Empty; - if (mcuAddr != 0) - { - using (var md5 = MD5.Create()) - { - using (var stream = File.OpenRead(sourceFileName)) - { - var hash = md5.ComputeHash(stream); - md5Hash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - - // Open, read and close the data file - var fileBytes = File.ReadAllBytes(sourceFileName); - var fileCrc32 = CrcTools.Crc32part(fileBytes, fileBytes.Length, 0); - var fileLength = fileBytes.Length; - - sw.Start(); - sw.Restart(); - - // Now send the Start, Data packets and End - sendTargetData.SendTheEntireFile(meadow, requestType, targetFileName, - (uint)partition, fileBytes, mcuAddr, fileCrc32, md5Hash, lastInSeries); - - sw.Stop(); - - if (sendTargetData.Verbose) Console.WriteLine($"It took {sw.ElapsedMilliseconds:N0} millisec to send {fileLength:N0} bytes. FileCrc:{fileCrc32:x08}"); - } - catch (Exception ex) - { - Debug.WriteLine($"TransmitFileInfoToExtFlash threw :{ex}"); - throw; - } - } - - public enum HcomProtocolHeaderTypes : ushort - { - HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED = 0x0000, - // Simple request types, include 4-byte user data - HCOM_PROTOCOL_HEADER_TYPE_SIMPLE = 0x0100, - // File releted request types, includes 4-byte user data (for the - // destination partition id), 4-byte file size, 4-byte checksum and - // variable length destination file name. - HCOM_PROTOCOL_HEADER_TYPE_FILE_START = 0x0200, - // Simple text. - HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT = 0x0300, - // Header followed by binary data. The size of the data can be up to - // HCOM_PROTOCOL_PACKET_MAX_SIZE minus header size - HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY = 0x0400, - } - - public enum HcomProtocolHeaderOffsets - { - HCOM_PROTOCOL_REQUEST_HEADER_SEQ_OFFSET = 0, - HCOM_PROTOCOL_REQUEST_HEADER_VERSION_OFFSET = 2, - HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET = 4, - HCOM_PROTOCOL_REQUEST_HEADER_EXTRA_DATA_OFFSET = 6, - HCOM_PROTOCOL_REQUEST_HEADER_USER_DATA_OFFSET = 8, - } - - // Messages to be sent to Meadow board from host - public enum HcomMeadowRequestType : ushort - { - HCOM_MDOW_REQUEST_UNDEFINED_REQUEST = 0x00 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED, - - HCOM_MDOW_REQUEST_CREATE_ENTIRE_FLASH_FS = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_FORMAT_FLASH_FILE_SYS = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_END_FILE_TRANSFER = 0x04 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU = 0x05 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_VERIFY_ERASED_FLASH = 0x06 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_PARTITION_FLASH_FS = 0x07 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MOUNT_FLASH_FS = 0x08 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_INITIALIZE_FLASH_FS = 0x09 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_BULK_FLASH_ERASE = 0x0a | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_ENTER_DFU_MODE = 0x0b | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH = 0x0c | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_LIST_PARTITION_FILES = 0x0d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC = 0x0e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_DISABLE = 0x0f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_ENABLE = 0x10 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_RUN_STATE = 0x11 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION = 0x12 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_PART_RENEW_FILE_SYS = 0x13 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST = 0x14 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST = 0x15 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER = 0x16 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS = 0x17 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_RESTART_ESP32 = 0x18 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_FLASH = 0x19 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART = 0x1a | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_NO_TRACE_TO_UART = 0x1b | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME = 0x1c | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END = 0x1d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION = 0x1e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_GET_DEVICE_NAME = 0x1f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES = 0x20 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - - // Only used for testing - HCOM_MDOW_REQUEST_DEVELOPER_1 = 0xf0 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_DEVELOPER_2 = 0xf1 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_DEVELOPER_3 = 0xf2 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_DEVELOPER_4 = 0xf3 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - - HCOM_MDOW_REQUEST_S25FL_QSPI_INIT = 0xf4 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE = 0xf5 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - HCOM_MDOW_REQUEST_S25FL_QSPI_READ = 0xf6 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE, - - HCOM_MDOW_REQUEST_START_FILE_TRANSFER = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, - HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, - HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_FILE_START, - - // Simple debugger message to Meadow - HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, - } - - // Messages sent from meadow to host - public enum HcomHostRequestType : ushort - { - HCOM_HOST_REQUEST_UNDEFINED_REQUEST = 0x00 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_UNDEFINED, - - // Simple with some text message - HCOM_HOST_REQUEST_TEXT_REJECTED = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_ACCEPTED = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_CONCLUDED = 0x03 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_ERROR = 0x04 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_INFORMATION = 0x05 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_LIST_HEADER = 0x06 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_LIST_MEMBER = 0x07 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_CRC_MEMBER = 0x08 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_MONO_STDOUT = 0x09 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_DEVICE_INFO = 0x0A | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_TRACE_MSG = 0x0B | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_RECONNECT = 0x0C | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_TEXT_MONO_STDERR = 0x0d | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_FILE_START_OKAY = 0x0e | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - HCOM_HOST_REQUEST_FILE_START_FAIL = 0x0f | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_TEXT, - - // Simple with debugger message from Meadow - HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA = 0x01 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, - HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES = 0x02 | HcomProtocolHeaderTypes.HCOM_PROTOCOL_HEADER_TYPE_SIMPLE_BINARY, - } - } -} \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowLocalDevice.cs b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDevice.cs new file mode 100644 index 00000000..866dd254 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDevice.cs @@ -0,0 +1,323 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Meadow.CLI.Core.Internals.MeadowCommunication; + +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public abstract partial class MeadowLocalDevice : MeadowDevice + { + protected MeadowLocalDevice(MeadowDataProcessor dataProcessor, ILogger? logger = null) + : base(dataProcessor, logger) + { + } + + public abstract Task WriteAsync(byte[] encodedBytes, + int encodedToSend, + CancellationToken cancellationToken = default); + + public abstract bool Initialize(CancellationToken cancellationToken = default); + + //device Id information is processed when the message is received + //this will request the device Id and return true it was set successfully + public override async Task GetDeviceInfoAsync( + TimeSpan timeout, + CancellationToken cancellationToken = default) + { + Initialize(cancellationToken); + + var command = new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION) + .WithTimeout(timeout) + .WithResponseType(MeadowMessageType.DeviceInfo) + .Build(); + + + try + { + var commandResponse = + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + if (commandResponse.IsSuccess) + return commandResponse.Message; + + throw new DeviceInfoException(); + } + catch (MeadowDeviceManagerException mdmEx) + { + throw new DeviceInfoException(mdmEx); + } + } + + //device name is processed when the message is received + //this will request the device name and return true it was successfully + public override async Task GetDeviceNameAsync( + TimeSpan timeout, + CancellationToken cancellationToken = default) + { + var command = new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION) + .WithTimeout(timeout) + .WithResponseType(MeadowMessageType.DeviceInfo) + .Build(); + + try + { + var commandResponse = + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + ; + + if (commandResponse.IsSuccess) + return commandResponse.Message; + + throw new DeviceInfoException(); + } + catch (MeadowDeviceManagerException mdmEx) + { + throw new DeviceInfoException(mdmEx); + } + } + + public override async Task GetMonoRunStateAsync( + CancellationToken cancellationToken = default) + { + Logger.LogDebug("Sending Mono Run State Request"); + + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_RUN_STATE) + .WithResponseType(MeadowMessageType.Data) + .Build(); + + var commandResponse = + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + var result = false; + switch (commandResponse.Message) + { + case "On reset, Meadow will start MONO and run app.exe": + case "Mono is enabled": + result = true; + break; + case "On reset, Meadow will not start MONO, therefore app.exe will not run": + case "Mono is disabled": + result = false; + break; + } + + Logger.LogDebug("Mono Run State: {runState}", result ? "enabled" : "disabled"); + return result; + } + + public override async Task MonoDisableAsync(CancellationToken cancellationToken = default) + { + var endTime = DateTime.UtcNow.Add(TimeSpan.FromSeconds(60)); + bool monoRunState; + while ((monoRunState = await GetMonoRunStateAsync(cancellationToken) + .ConfigureAwait(false)) + && endTime > DateTime.UtcNow) + { + Logger.LogDebug("Sending Mono Disable Request"); + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_DISABLE) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + Logger.LogDebug("Waiting for Meadow to cycle"); + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + + Logger.LogDebug("Re-initialize the device"); + Initialize(cancellationToken); + + Logger.LogDebug("Waiting for the Meadow to be ready"); + await WaitForReadyAsync(DefaultTimeout, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + if (monoRunState) + throw new Exception("Failed to stop mono."); + } + + public override async Task MonoEnableAsync(CancellationToken cancellationToken = default) + { + var endTime = DateTime.UtcNow.Add(TimeSpan.FromSeconds(60)); + bool monoRunState; + while ((monoRunState = await GetMonoRunStateAsync(cancellationToken).ConfigureAwait(false)) == false + && endTime > DateTime.UtcNow) + { + Logger.LogDebug("Sending Mono Enable Request"); + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_ENABLE) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + Logger.LogDebug("Waiting for Meadow to cycle"); + await Task.Delay(500, cancellationToken) + .ConfigureAwait(false); + + Logger.LogDebug("Re-initialize the device"); + Initialize(cancellationToken); + + Logger.LogDebug("Waiting for the Meadow to be ready"); + await WaitForReadyAsync(DefaultTimeout, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + if (!monoRunState) + throw new Exception("Failed to enable mono."); + } + + public override Task MonoFlashAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_FLASH) + .WithCompletionFilter( + e => e.Message.StartsWith("Mono runtime successfully flashed.")) + .WithTimeout(TimeSpan.FromMinutes(5)) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override async Task ResetMeadowAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + // Give the meadow a little time to cycle + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + + Initialize(cancellationToken); + + await WaitForReadyAsync(DefaultTimeout, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public override Task EnterDfuModeAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENTER_DFU_MODE) + .WithCompletionResponseType(MeadowMessageType.Accepted) + .WithResponseFilter(x => true) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task NshEnableAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) + .WithUserData(1) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + // TODO: Is sending a 0 to HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH correct? + public override Task NshDisableAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) + .WithUserData(0) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task TraceEnableAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task TraceDisableAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task QspiWriteAsync(int value, + CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE) + .WithUserData((uint)value) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task QspiReadAsync(int value, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_READ) + .WithUserData((uint)value) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task QspiInitAsync(int value, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_INIT) + .WithUserData((uint)value) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task RestartEsp32Async(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESTART_ESP32) + .WithCompletionResponseType(MeadowMessageType.Concluded) + .Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override async Task GetDeviceMacAddressAsync( + CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS).Build(); + + var commandResponse = + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + return commandResponse.Message; + } + } +} \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceComms.cs b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceComms.cs new file mode 100644 index 00000000..df7835c9 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceComms.cs @@ -0,0 +1,452 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.DeviceManagement.Tools; +using Meadow.CLI.Core.Exceptions; +using Meadow.CLI.Core.Internals.MeadowCommunication; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public partial class MeadowLocalDevice + { + uint _packetCrc32; + + public async Task SendTheEntireFile(FileCommand command, + bool lastInSeries, + CancellationToken cancellationToken) + { + _packetCrc32 = 0; + _lastProgress = 0; + + Logger.LogDebug("Sending {filename} to device", command.DestinationFileName); + try + { + var response = await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + if (response.MessageType == MeadowMessageType.DownloadStartFail) + { + throw new MeadowCommandException( + "Meadow rejected download request with ", + response); + } + + switch (command.RequestType) + { + // if it's an ESP start file transfer and the download started ok. + case HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER + when response.MessageType == MeadowMessageType.DownloadStartOkay: + Logger.LogDebug("ESP32 download request accepted"); + break; + // if it's an ESP file transfer start and it failed to start + case HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER + when response.MessageType == MeadowMessageType.DownloadStartFail: + Logger.LogDebug("ESP32 download request rejected"); + throw new MeadowCommandException( + "Halting download due to an error while preparing Meadow for download", + response); + case HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER + when response.MessageType != MeadowMessageType.DownloadStartOkay: + throw response.MessageType switch + { + MeadowMessageType.DownloadStartFail => new MeadowCommandException( + "Halting download due to an error while preparing Meadow for download", + response), + MeadowMessageType.Concluded => new MeadowCommandException( + "Halting download due to an unexpectedly Meadow 'Concluded' received prematurely", + response), + _ => new MeadowCommandException( + $"Halting download due to an unexpected Meadow message type {response.MessageType} received", + response) + }; + } + + var fileBufOffset = 0; + ushort sequenceNumber = 1; + + Logger.LogInformation("Starting File Transfer..."); + while (fileBufOffset <= command.FileSize - 1) // equal would mean past the end + { + int numBytesToSend; + if (fileBufOffset + MeadowDeviceManager.MaxAllowableMsgPacketLength + > command.FileSize - 1) + { + numBytesToSend = + command.FileSize - fileBufOffset; // almost done, last packet + } + else + { + numBytesToSend = MeadowDeviceManager.MaxAllowableMsgPacketLength; + } + + if (command.FileBytes == null) + { + throw new MeadowCommandException("File bytes are missing for file command"); + } + + await BuildAndSendDataPacketRequest( + command.FileBytes, + fileBufOffset, + numBytesToSend, + sequenceNumber, + cancellationToken) + .ConfigureAwait(false); + + var progress = (decimal)fileBufOffset / command.FileSize; + WriteProgress(progress); + + fileBufOffset += numBytesToSend; + + sequenceNumber++; + } + + // echo the device responses + //await Task.Delay(250, cancellationToken); // if we're too fast, we'll finish and the device will still echo a little + + //-------------------------------------------------------------- + // Build and send the correct trailer + // TODO: Move this into the Command object + var trailerCommand = command.RequestType switch + { + HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER => + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER) + .WithUserData(lastInSeries ? 1U : 0U) + .Build(), + HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME => + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END) + .WithUserData(lastInSeries ? 1U : 0U) + .Build(), + HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER => + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER) + .WithUserData(lastInSeries ? 1U : 0U) + .Build(), + _ => throw new ArgumentOutOfRangeException( + nameof(command.RequestType), + "Cannot build trailer for unknown command") + }; + + await SendCommandAsync(trailerCommand, cancellationToken) + .ConfigureAwait(false); + + + // bufferOffset should point to the byte after the last byte + Debug.Assert(fileBufOffset == command.FileSize); + Logger.LogTrace( + "Total bytes sent {count} in {packetCount} packets. PacketCRC:{_crc}", + fileBufOffset, + sequenceNumber, + $"{_packetCrc32:x08}"); + + Logger.LogInformation( + "Transfer Complete, wrote {count} bytes to Meadow", + fileBufOffset); + } + catch (Exception except) + { + Logger.LogError(except, "Exception sending command to Meadow"); + throw; + } + } + + private int _lastProgress = 0; + + private void WriteProgress(decimal i) + { + var intProgress = Convert.ToInt32(i * 100); + if (intProgress <= _lastProgress || intProgress % 5 != 0) return; + + Logger.LogInformation("Operation Progress: {progress:P0}", i); + _lastProgress = intProgress; + } + + internal async Task SendAcknowledgedSimpleCommand( + Command command, + CancellationToken cancellationToken) + { + Logger.LogTrace("Sending command {requestType}", command.RequestType); + var tcs = new TaskCompletionSource(); + var received = false; + + void Handler(object s, MeadowMessageEventArgs e) + { + Logger.LogTrace( + "Received MessageType: {messageType} Message: {message}", + e.MessageType, + string.IsNullOrWhiteSpace(e.Message) ? "[empty]" : e.Message); + + if (e.MessageType != MeadowMessageType.Accepted) return; + + received = true; + tcs.SetResult(true); + } + + Logger.LogTrace("Attaching data received handler"); + DataProcessor.OnReceiveData += Handler; + + await SendCommandAsync(command, cancellationToken) + .ConfigureAwait(false); + + try + { + using var cts = new CancellationTokenSource(10_000); + cts.Token.Register(() => tcs.TrySetCanceled()); + await tcs.Task.ConfigureAwait(false); + } + catch (TaskCanceledException e) + { + throw new MeadowCommandException( + "Command timeout waiting for response.", + innerException: e); + } + finally + { + Logger.LogTrace("Removing data received handler"); + DataProcessor.OnReceiveData -= Handler; + } + + if (!received) + { + throw new MeadowCommandException("Command not accepted."); + } + + return received; + } + + //========================================================================== + // Prepare a data packet for sending + private async Task BuildAndSendDataPacketRequest(byte[] messageBytes, + int messageOffset, + int messageSize, + ushort sequenceNumber, + CancellationToken cancellationToken) + { + try + { + // Need to prepend the sequence number to the packet + var transmitSize = messageSize + sizeof(ushort); + byte[] fullMsg = new byte[transmitSize]; + + byte[] seqBytes = BitConverter.GetBytes(sequenceNumber); + Array.Copy(seqBytes, fullMsg, sizeof(ushort)); + Array.Copy(messageBytes, messageOffset, fullMsg, sizeof(ushort), messageSize); + + await EncodeAndSendPacket(fullMsg, 0, transmitSize, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception except) + { + Console.WriteLine($"An exception was caught: {except}"); + throw; + } + } + + internal async Task SendCommandAsync(Command command, CancellationToken cancellationToken) + { + // Populate the header + var messageBytes = command.ToMessageBytes(); + await EncodeAndSendPacket(messageBytes, 0, messageBytes.Length, cancellationToken) + .ConfigureAwait(false); + } + + private async Task EncodeAndSendPacket(byte[] messageBytes, + int messageOffset, + int messageSize, + CancellationToken cancellationToken) + { + try + { + Logger.LogTrace("Sending packet of {messageSize} bytes", messageSize); + // For testing calculate the crc including the sequence number + _packetCrc32 = CrcTools.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + + // Add 2, first to account for start delimiter and second for end + byte[] encodedBytes = + new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload + 2]; + + // Skip first byte so it can be a start delimiter + int encodedToSend = CobsTools.CobsEncoding( + messageBytes, + messageOffset, + messageSize, + ref encodedBytes, + 1); + + // Verify COBS - any delimiters left? Skip first byte + for (int i = 1; i < encodedToSend; i++) + { + if (encodedBytes[i] == 0x00) + { + throw new InvalidProgramException( + "All zeros should have been removed. " + + $"There's one at offset of {i}"); + } + } + + // Terminate packet with delimiter so packet boundaries can be more easily found + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + + Logger.LogTrace("Encoded packet successfully"); + try + { + await WriteAsync(encodedBytes, encodedToSend, cancellationToken) + .ConfigureAwait(false); + } + catch (InvalidOperationException ioe) // Port not opened + { + Logger.LogError(ioe, "Write but port not opened"); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + Logger.LogError(aore, "Write buffer, offset and count don't line up"); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + Logger.LogError(ae, "Write offset plus count > buffer length"); + throw; + } + catch (TimeoutException te) // Took too long to send + { + Logger.LogError(te, "Write took too long to send"); + throw; + } + } + catch (Exception except) + { + Logger.LogTrace(except, "EncodeAndSendPacket threw"); + throw; + } + } + + private protected async Task SendCommandAndWaitForResponseAsync( + Command command, + CancellationToken cancellationToken = default, + [CallerMemberName] string? caller = null) + { + Logger.LogTrace($"{caller} is sending {command.RequestType}"); + + await SendCommandAsync(command, cancellationToken) + .ConfigureAwait(false); + + var (isSuccess, message, messageType) = + await WaitForResponseMessageAsync(command, cancellationToken) + .ConfigureAwait(false); + + Logger.LogTrace( + "Returning to {caller} with {success} {message}", + caller, + isSuccess, + string.IsNullOrWhiteSpace(message) ? "[empty]" : message); + + return new CommandResponse(isSuccess, message, messageType); + } + + private protected async Task<(bool Success, string? Message, MeadowMessageType MessageType)> + WaitForResponseMessageAsync(Command command, + CancellationToken cancellationToken = default, + [CallerMemberName] string? caller = null) + { + Logger.LogTrace( + "{caller} is waiting {seconds} for response.", + caller, + command.Timeout.TotalSeconds); + + var tcs = new TaskCompletionSource(); + var result = false; + var message = string.Empty; + var messageType = MeadowMessageType.ErrOutput; + + void ResponseHandler(object s, MeadowMessageEventArgs e) + { + Logger.LogTrace( + "Received MessageType: {messageType} Message: {message}", + e.MessageType, + string.IsNullOrWhiteSpace(e.Message) ? "[empty]" : e.Message); + + if (!command.ResponsePredicate(e)) return; + Logger.LogTrace("Message matched response filter"); + message = e.Message; + messageType = e.MessageType; + result = true; + } + + void CompletionHandler(object s, MeadowMessageEventArgs e) + { + Logger.LogTrace( + "Received MessageType: {messageType} Message: {message}", + e.MessageType, + string.IsNullOrWhiteSpace(e.Message) ? "[empty]" : e.Message); + + if (command.CompletionPredicate(e)) + { + Logger.LogTrace("Setting result complete"); + tcs.SetResult(true); + } + } + + Logger.LogTrace("Attaching response handler(s)"); + if (command.ResponseHandler != null) + { + DataProcessor.OnReceiveData += command.ResponseHandler; + } + + DataProcessor.OnReceiveData += ResponseHandler; + Logger.LogTrace("Attaching completion handler(s)"); + DataProcessor.OnReceiveData += CompletionHandler; + + try + { + using var timeoutCancellationTokenSource = + new CancellationTokenSource(command.Timeout); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCancellationTokenSource.Token); + + timeoutCancellationTokenSource.Token.Register(() => tcs.TrySetCanceled()); + await tcs.Task.ConfigureAwait(false); + if (cts.IsCancellationRequested) + throw new TimeoutException("Timeout while waiting for meadow"); + } + catch (TaskCanceledException e) + { + throw new MeadowCommandException( + "Command timeout waiting for response.", + innerException: e); + } + finally + { + Logger.LogTrace("Removing handlers"); + DataProcessor.OnReceiveData -= CompletionHandler; + DataProcessor.OnReceiveData -= ResponseHandler; + if (command.ResponseHandler != null) + { + DataProcessor.OnReceiveData -= command.ResponseHandler; + } + } + + if (result) + { + Logger.LogTrace( + "Returning to {caller} with {message}", + caller, + string.IsNullOrWhiteSpace(message) ? "[empty]" : message); + + return (result, message, messageType); + } + + throw new MeadowCommandException(message); + } + } + + public class NotConnectedException : Exception + { + } +} \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceFileManager.cs b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceFileManager.cs new file mode 100644 index 00000000..511b7288 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowLocalDeviceFileManager.cs @@ -0,0 +1,593 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.DeviceManagement.Tools; +using Meadow.CLI.Core.Internals.MeadowCommunication; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public abstract partial class MeadowLocalDevice + { + private protected static readonly string SystemHttpNetDllName = "System.Net.Http.dll"; + + public override async Task> GetFilesAndCrcsAsync( + int timeoutInMs = 10000, + int partition = 0, + CancellationToken cancellationToken = default) + { + var started = false; + + EventHandler handler = (s, e) => + { + if (e.MessageType == MeadowMessageType.FileListTitle) + { + FilesOnDevice.Clear(); + started = true; + } + else if (started == false) + { + //ignore everything until we've started a new file list request + return; + } + + if (e.MessageType == MeadowMessageType.FileListCrcMember) + { + SetFileAndCrcsFromMessage(e.Message); + } + }; + + var command = new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC) + .WithResponseHandler(handler) + .WithUserData((uint)partition).Build(); + + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + + return FilesOnDevice; + } + + /// + /// Write a file to the Meadow + /// + /// The name of the file + /// The path to the file + /// The amount of time to wait to write the file + /// The to cancel the operation + /// + public override async Task WriteFileAsync(string filename, + string path, + int timeoutInMs = 200000, + CancellationToken cancellationToken = + default) + { + if (IsDeviceInitialized() == false) + { + throw new Exception("Device is not initialized"); + } + + var sourceFileName = Path.Combine(path, filename); + var fi = new FileInfo(sourceFileName); + if (!fi.Exists) + { + throw new FileNotFoundException("Cannot find source file", fi.FullName); + } + + // If ESP32 file we must also send the MD5 has of the file + using var md5 = MD5.Create(); + var fileBytes = await File.ReadAllBytesAsync(sourceFileName, cancellationToken); + var hash = md5.ComputeHash(fileBytes); + string md5Hash = BitConverter.ToString(hash) + .Replace("-", "") + .ToLowerInvariant(); + var fileCrc32 = CrcTools.Crc32part(fileBytes, fileBytes.Length, 0); + + var command = await new FileCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER) + .WithSourceFileName(sourceFileName) + .WithDestinationFileName(filename) + .WithTimeout(TimeSpan.FromMilliseconds(timeoutInMs)) + .WithPartition(0) + .BuildAsync(); + + var sw = Stopwatch.StartNew(); + await SendTheEntireFile(command, true, cancellationToken) + .ConfigureAwait(false); + sw.Stop(); + return new FileTransferResult(sw.ElapsedMilliseconds, fileBytes.Length, fileCrc32); + } + + public override async Task DeleteFileAsync(string fileName, + uint partition = 0, + CancellationToken cancellationToken = default) + { + var command = + await new FileCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME) + .WithDestinationFileName(fileName) + .WithPartition(partition) + .WithResponseType(MeadowMessageType.Concluded) + .WithCompletionResponseType(MeadowMessageType.Concluded) + .BuildAsync(); + + await SendCommandAndWaitForResponseAsync(command, cancellationToken) + .ConfigureAwait(false); + } + + public override Task EraseFlashAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_BULK_FLASH_ERASE) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect).WithTimeout(TimeSpan.FromMinutes(5)).Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task VerifyErasedFlashAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_VERIFY_ERASED_FLASH) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect).WithTimeout(TimeSpan.FromMinutes(5)).Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task FormatFileSystemAsync(uint partition = 0, + CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_FORMAT_FLASH_FILE_SYS) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect).WithTimeout(TimeSpan.FromMinutes(5)).WithUserData(partition).Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override Task RenewFileSystemAsync(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_PART_RENEW_FILE_SYS) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect).WithTimeout(TimeSpan.FromMinutes(5)).Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + + public override async Task UpdateMonoRuntimeAsync(string fileName, + string? targetFileName = null, + uint partition = 0, + CancellationToken cancellationToken = + default) + { + Logger.LogInformation("Starting Mono Runtime Update"); + Logger.LogInformation("Waiting for Meadow to be ready"); + await WaitForReadyAsync(DefaultTimeout, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + Logger.LogDebug("Calling Mono Disable"); + await MonoDisableAsync(cancellationToken) + .ConfigureAwait(false); + + Trace.Assert( + await GetMonoRunStateAsync(cancellationToken) + .ConfigureAwait(false) + == false, + "Meadow was expected to have Mono Disabled"); + + Logger.LogInformation("Updating Mono Runtime"); + + var sourceFilename = fileName; + if (string.IsNullOrWhiteSpace(sourceFilename)) + { + // check local override + sourceFilename = Path.Combine( + Directory.GetCurrentDirectory(), + DownloadManager.RuntimeFilename); + + if (File.Exists(sourceFilename)) + { + Logger.LogInformation( + $"Using current directory '{DownloadManager.RuntimeFilename}'"); + } + else + { + sourceFilename = Path.Combine( + DownloadManager.FirmwareDownloadsFilePath, + DownloadManager.RuntimeFilename); + + if (File.Exists(sourceFilename)) + { + Logger.LogInformation("FileName not specified, using latest download."); + } + else + { + Logger.LogInformation( + "Unable to locate a runtime file. Either provide a path or download one."); + + return; // KeepConsoleOpen? + } + } + } + + if (!File.Exists(sourceFilename)) + { + Logger.LogInformation($"File '{sourceFilename}' not found"); + return; + } + + if (string.IsNullOrWhiteSpace(targetFileName)) + { + targetFileName = Path.GetFileName(sourceFilename); + } + + Logger.LogDebug("Sending Mono Update Runtime Request"); + var command = await new FileCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME) + .WithPartition(partition) + .WithDestinationFileName(targetFileName) + .WithSourceFileName(sourceFilename) + .BuildAsync() + .ConfigureAwait(false); + + await SendTheEntireFile(command, true, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFileToEspFlashAsync(string fileName, + string? targetFileName = null, + uint partition = 0, + string? mcuDestAddress = null, + CancellationToken cancellationToken = + default) + { + // For the ESP32 on the meadow, we don't need the target file name, we just need the + // MCU's destination address and the file's binary. + // Assume if no mcuDestAddress that the fileName is a CSV with both file names and Mcu Address + if (mcuDestAddress != null) + { + // Since the mcuDestAddr is used we'll assume the fileName field just contains + // a single file. + if (string.IsNullOrWhiteSpace(targetFileName)) + { + // While not used by the ESP32 it cost nothing to send it and can help + // with debugging + targetFileName = Path.GetFileName(fileName); + } + + // Convert mcuDestAddress from a string to a 32-bit unsigned int, but first + // insure it starts with 0x + uint mcuAddress = 0; + if (mcuDestAddress.StartsWith("0x") || mcuDestAddress.StartsWith("0X")) + { + mcuAddress = uint.Parse( + mcuDestAddress.Substring(2), + System.Globalization.NumberStyles.HexNumber); + } + else + { + Console.WriteLine( + $"The '--McuDestAddress' argument must be followed with an address in the form '0x1800'"); + + return; + } + var command = await new FileCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER) + .WithPartition(partition) + .WithDestinationFileName(targetFileName) + .WithMcuAddress(mcuAddress) + .WithSourceFileName(fileName) + .BuildAsync() + .ConfigureAwait(false); + + await SendTheEntireFile(command, true, cancellationToken).ConfigureAwait(false); + } + else + { + // At this point, the fileName field should contain a CSV string containing the destination + // addresses followed by file's location within the host's file system. + // E.g. "0x8000, C:\Blink\partition-table.bin, 0x1000, C:\Blink\bootloader.bin, 0x10000, C:\Blink\blink.bin" + string[] fileElement = fileName.Split(','); + if (fileElement.Length % 2 != 0) + { + Console.WriteLine( + "Please provide a CSV input with \"address, fileName, address, fileName\""); + + return; + } + + uint mcuAddress; + for (int i = 0; i < fileElement.Length; i += 2) + { + // Trim any white space from this mcu addr and file name + fileElement[i] = fileElement[i] + .Trim(); + + fileElement[i + 1] = fileElement[i + 1] + .Trim(); + + if (fileElement[i] + .StartsWith("0x") + || fileElement[i] + .StartsWith("0X")) + { + // Fill in the Mcu Address + mcuAddress = uint.Parse(fileElement[i][2..], + System.Globalization.NumberStyles.HexNumber); + } + else + { + Console.WriteLine("Please provide a CSV input with addresses like 0x1234"); + return; + } + + // Meadow.CLI --Esp32WriteFile --SerialPort Com26 --File + // "0x8000, C:\Download\Esp32\Hello\partition-table.bin, 0x1000, C:\Download\Esp32\Hello\bootloader.bin, 0x10000, C:\Download\Esp32\Hello\hello-world.bin" + // File Path and Name + targetFileName = Path.GetFileName(fileElement[i + 1]); + bool lastFile = i == fileElement.Length - 2; + + // this may need need to be awaited? + var command = await new FileCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER) + .WithPartition(partition) + .WithDestinationFileName(targetFileName) + .WithMcuAddress(mcuAddress) + .WithSourceFileName(fileElement[i+1]) + .BuildAsync() + .ConfigureAwait(false); + + await SendTheEntireFile(command, lastFile, cancellationToken) + .ConfigureAwait(false); + } + } + } + + public override async Task FlashEspAsync(string sourcePath, + CancellationToken cancellationToken = default) + { + Logger.LogInformation($"Transferring {DownloadManager.NetworkMeadowCommsFilename}"); + await WriteFileToEspFlashAsync( + Path.Combine(sourcePath, DownloadManager.NetworkMeadowCommsFilename), + mcuDestAddress: "0x10000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + + Logger.LogInformation($"Transferring {DownloadManager.NetworkBootloaderFilename}"); + await WriteFileToEspFlashAsync( + Path.Combine(sourcePath, DownloadManager.NetworkBootloaderFilename), + mcuDestAddress: "0x1000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + + Logger.LogInformation($"Transferring {DownloadManager.NetworkPartitionTableFilename}"); + await WriteFileToEspFlashAsync( + Path.Combine(sourcePath, DownloadManager.NetworkPartitionTableFilename), + mcuDestAddress: "0x8000", + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + } + + public override async Task GetInitialBytesFromFile( + string fileName, + uint partition = 0, + CancellationToken cancellationToken = default) + { + Logger.LogDebug("Getting initial bytes from {fileName}", fileName); + var encodedFileName = System.Text.Encoding.UTF8.GetBytes(fileName); + + var command = new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES) + .WithResponseType(MeadowMessageType.InitialFileData) + .WithData(encodedFileName) + .Build(); + + var commandResponse = await SendCommandAndWaitForResponseAsync(command, cancellationToken); + + if (!commandResponse.IsSuccess) + { + Logger.LogWarning("No bytes found for file."); + } + + return commandResponse.Message; + } + + public override Task ForwardVisualStudioDataToMonoAsync(byte[] debuggerData, + uint userData, + CancellationToken cancellationToken = default) + { + var command = new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA) + .WithData(debuggerData) + .WithResponseFilter(x => true) + .WithCompletionFilter(x => true) + .WithUserData(userData).Build(); + + return SendCommandAndWaitForResponseAsync(command, cancellationToken); + } + + public override async Task DeployAppAsync(string applicationFilePath, + bool includePdbs = false, + CancellationToken cancellationToken = default) + { + if (!File.Exists(applicationFilePath)) + { + Console.WriteLine($"{applicationFilePath} not found."); + return; + } + + var fi = new FileInfo(applicationFilePath); + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + // for some strange reason, System.Net.Http.dll doesn't get copied to the output folder in VS. + // so, we need to copy it over from the meadow assemblies nuget. + CopySystemNetHttpDll(fi.DirectoryName); + } + + var deviceFiles = await GetFilesAndCrcsAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + foreach (var (filename, crc) in deviceFiles) + { + Logger.LogInformation("Found {file} (CRC: {crc})", filename, crc); + } + + var extensions = new List + { ".exe", ".bmp", ".jpg", ".jpeg", ".json", ".xml", ".yml", ".txt" }; + + var paths = Directory.EnumerateFiles( + fi.DirectoryName, + "*.*", + SearchOption.TopDirectoryOnly) + .Where(s => extensions.Contains(new FileInfo(s).Extension)); + + var files = new Dictionary(); + + async Task AddFile(string file, bool includePdbs) + { + await using FileStream fs = File.Open(file, FileMode.Open); + var len = (int)fs.Length; + var bytes = new byte[len]; + + await fs.ReadAsync(bytes, 0, len, cancellationToken); + + //0x + var crc = CrcTools.Crc32part(bytes, len, 0); // 0x04C11DB7); + + Logger.LogDebug("{file} crc is {crc:X8}", file, crc); + files.Add(Path.GetFileName(file), crc); + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(file, "pdb"); + if (File.Exists(pdbFile)) + await AddFile(pdbFile, false) + .ConfigureAwait(false); + } + } + + foreach (var file in paths) + { + await AddFile(file, includePdbs) + .ConfigureAwait(false); + } + + var dependencies = AssemblyManager.GetDependencies(fi.Name, fi.DirectoryName); + + //crawl dependencies + foreach (var file in dependencies) + { + await AddFile(Path.Combine(fi.DirectoryName, file), includePdbs); + } + + // delete unused files + foreach (var file in deviceFiles.Keys) + { + if (files.ContainsKey(file) == false) + { + await DeleteFileAsync(file, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + Logger.LogInformation("Removing file: {file}", file); + } + } + + // write new files + foreach(var file in files) + { + if (deviceFiles.ContainsKey(file.Key) && deviceFiles[file.Key] == file.Value) + { + Logger.LogInformation("Skipping file (hash match): {file}", file.Key); + continue; + } + + if (!File.Exists(Path.Combine(fi.DirectoryName, file.Key))) + { + Logger.LogInformation("{file} not found", file.Key); + continue; + } + + Logger.LogInformation("Writing file: {file}", file.Key); + await WriteFileAsync( + file.Key, + fi.DirectoryName, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + Logger.LogInformation("Wrote file: {file}", file.Key); + } + + Logger.LogInformation("{file} deploy complete", fi.Name); + } + + private protected void CopySystemNetHttpDll(string targetDir) + { + try + { + var bclNugetPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", + "packages", + "wildernesslabs.meadow.assemblies"); + + if (Directory.Exists(bclNugetPath)) + { + List versions = new List(); + + var versionFolders = Directory.EnumerateDirectories(bclNugetPath); + foreach (var versionFolder in versionFolders) + { + var di = new DirectoryInfo(versionFolder); + if (Version.TryParse(di.Name, out Version outVersion)) + { + versions.Add(outVersion); + } + } + + if (versions.Any()) + { + versions.Sort(); + + var sourcePath = Path.Combine( + bclNugetPath, + versions.Last() + .ToString(), + "lib", + "net472"); + + if (Directory.Exists(sourcePath)) + { + if (File.Exists(Path.Combine(sourcePath, SystemHttpNetDllName))) + { + File.Copy( + Path.Combine(sourcePath, SystemHttpNetDllName), + Path.Combine(targetDir, SystemHttpNetDllName)); + } + } + } + } + } + catch (Exception) + { + // eat this for now + } + } + + private void SetFileAndCrcsFromMessage(string fileListMember) + { + int fileNameStart = fileListMember.LastIndexOf('/') + 1; + int crcStart = fileListMember.IndexOf('[') + 1; + if (fileNameStart == 0 && crcStart == 0) + return; // No files found + + Debug.Assert(crcStart > fileNameStart); + + var file = fileListMember.Substring(fileNameStart, crcStart - fileNameStart - 2); + var crc = Convert.ToUInt32(fileListMember.Substring(crcStart, 10), 16); + FilesOnDevice.Add(file.Trim(), crc); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowSerialDevice.cs b/Meadow.CLI.Core/DeviceManagement/MeadowSerialDevice.cs index ece7b117..3091abc4 100644 --- a/Meadow.CLI.Core/DeviceManagement/MeadowSerialDevice.cs +++ b/Meadow.CLI.Core/DeviceManagement/MeadowSerialDevice.cs @@ -1,575 +1,109 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.IO.Ports; -using System.Net; -using System.Net.Sockets; +using System.IO.Ports; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Meadow.CLI; -using MeadowCLI.Hcom; +using Meadow.CLI.Core.Internals.MeadowCommunication; +using Microsoft.Extensions.Logging; -namespace MeadowCLI.DeviceManagement +namespace Meadow.CLI.Core.DeviceManagement { - //a simple model object that represents a meadow device including connection - public class MeadowSerialDevice : MeadowDevice, IDisposable + public partial class MeadowSerialDevice : MeadowLocalDevice { - public EventHandler OnMeadowMessage; - - public bool Verbose { get; protected set; } - public bool LocalEcho { get; set; } = true; - + private readonly string _serialPortName; public SerialPort SerialPort { get; private set; } - public Socket Socket { get; private set; } - - private string serialPortName; - public string PortName => SerialPort == null ? serialPortName : SerialPort.PortName; - public MeadowSerialDataProcessor DataProcessor { get; private set; } - - public MeadowSerialDevice(string serialPortName, bool verbose = true) + public MeadowSerialDevice(string serialPortName, ILogger? logger = null) + : this(serialPortName, OpenSerialPort(serialPortName), logger) { - this.serialPortName = serialPortName; - Verbose = verbose; - } - - public static string[] GetAvailableSerialPorts() - { - return SerialPort.GetPortNames(); - } - - public static bool TryCreateIPEndPoint(string address, - out IPEndPoint endpoint) - { - if (string.IsNullOrEmpty(address)) - { - address = string.Empty; - } - address = address.Replace("localhost", "127.0.0.1"); - endpoint = null; - - string[] ep = address.Split(':'); - if (ep.Length != 2) - return false; - - if (!IPAddress.TryParse(ep[0], out IPAddress ip)) - return false; - - int port; - if (!int.TryParse(ep[1], NumberStyles.None, NumberFormatInfo.CurrentInfo, out port)) - return false; - - endpoint = new IPEndPoint(ip, port); - return true; } - public bool Initialize(bool listen = true) + private MeadowSerialDevice(string serialPortName, + SerialPort serialPort, + ILogger? logger = null) + : base(new MeadowSerialDataProcessor(serialPort, logger), logger) { - if (TryCreateIPEndPoint(serialPortName, out IPEndPoint endpoint)) - { - Socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try - { - Socket.Connect(endpoint); - } - catch (SocketException) - { - Console.WriteLine("Could not connect to socket, aborting..."); - Environment.Exit(1); - } - } - else - { - if (SerialPort != null) - { - SerialPort.Close(); // note: exception in ReadAsync - SerialPort = null; - } - - if (OpenSerialPort(serialPortName) == false) - return false; - } - - if (listen == true) - { - ListenForSerialData(); - } - return true; - } - - public async Task DeleteFile(string filename, int timeoutInMs = 10000) - { - if (SerialPort == null) - { - throw new Exception("SerialPort not intialized"); - } - - bool result = false; - - var timeOutTask = Task.Delay(timeoutInMs); - - EventHandler handler = null; - - var tcs = new TaskCompletionSource(); - - handler = (s, e) => - { - if (e.Message.StartsWith("Delete success")) - { - result = true; - tcs.SetResult(true); - } - }; - DataProcessor.OnReceiveData += handler; - - await MeadowFileManager.DeleteFile(this, filename); - - await Task.WhenAny(new Task[] { timeOutTask, tcs.Task }); - DataProcessor.OnReceiveData -= handler; - - return result; + SerialPort = serialPort; + _serialPortName = serialPortName; } - public override async Task WriteFile(string filename, string path, int timeoutInMs = 200000) //200s + public sealed override bool IsDeviceInitialized() { - if (SerialPort == null) - { - throw new Exception("SerialPort not intialized"); - } - - bool result = false; - - var timeOutTask = Task.Delay(timeoutInMs); - - EventHandler handler = null; - - var tcs = new TaskCompletionSource(); - - handler = (s, e) => - { - if (e.MessageType == MeadowMessageType.Concluded) - { - result = true; - tcs.SetResult(true); - } - }; - DataProcessor.OnReceiveData += handler; - - await MeadowFileManager.WriteFileToFlash(this, Path.Combine(path, filename), filename); - - await Task.WhenAny(new Task[] { timeOutTask, tcs.Task }); - DataProcessor.OnReceiveData -= handler; - - return result; + return SerialPort != null; } - public override async Task<(List files, List crcs)> GetFilesAndCrcs(int timeoutInMs = 60000) + public override void Dispose() { - if (SerialPort == null) - { - throw new Exception("SerialPort not intialized"); - } - - var timeOutTask = Task.Delay(timeoutInMs); - - EventHandler handler = null; - - var tcs = new TaskCompletionSource(); - var started = false; - - handler = (s, e) => - { - Console.WriteLine($"Msg: {e.MessageType}"); - - if (e.MessageType == MeadowMessageType.FileListTitle) - { - FilesOnDevice.Clear(); - FileCrcs.Clear(); - started = true; - } - else if (started == false) - { //ignore everything until we've started a new file list request - return; - } - - if (e.MessageType == MeadowMessageType.FileListCrcMember) - { - SetFileAndCrcsFromMessage(e.Message); - } - - if (e.MessageType == MeadowMessageType.Concluded) - { - tcs.TrySetResult(true); - } - }; - DataProcessor.OnReceiveData += handler; - - await MeadowFileManager.ListFilesAndCrcs(this).ConfigureAwait(false); - - await Task.WhenAny(new Task[] { timeOutTask, tcs.Task }); - DataProcessor.OnReceiveData -= handler; - - return (FilesOnDevice, FileCrcs); + Logger.LogTrace("Disposing SerialPort"); + SerialPort.Dispose(); } - public override async Task> GetFilesOnDevice(bool refresh = true, int timeoutInMs = 30000) + public override async Task WriteAsync(byte[] encodedBytes, int encodedToSend, CancellationToken cancellationToken = default) { if (SerialPort == null) - { - throw new Exception("SerialPort not intialized"); - } - - if (FilesOnDevice.Count == 0 || refresh == true) - { - var timeOutTask = Task.Delay(timeoutInMs); - - EventHandler handler = null; - - var tcs = new TaskCompletionSource(); - bool started = false; - - handler = (s, e) => - { - if (e.MessageType == MeadowMessageType.FileListTitle) - { - FilesOnDevice.Clear(); - FileCrcs.Clear(); - - started = true; - } - else if (started == false) - { //ignore everything until we've started a new file list request - return; - } - - if (e.MessageType == MeadowMessageType.FileListMember) - { - SetFileNameFromMessage(e.Message); - } - - if (e.MessageType == MeadowMessageType.Concluded) - { - tcs.SetResult(true); - } - }; - DataProcessor.OnReceiveData += handler; - - await MeadowFileManager.ListFiles(this); - - await Task.WhenAny(new Task[] { timeOutTask, tcs.Task }); - DataProcessor.OnReceiveData -= handler; - } + throw new NotConnectedException(); - return FilesOnDevice; - } - - public override async Task GetInitialFileData(string filename, int timeoutInMs = 1000) - { - var timeOutTask = Task.Delay(timeoutInMs); - - EventHandler handler = null; - - var tcs = new TaskCompletionSource(); - bool started = false; - - string msg = string.Empty; - - handler = (s, e) => - { - if (e.MessageType == MeadowMessageType.InitialFileData) - { - msg = e.Message; - } - - }; - - DataProcessor.OnReceiveData += handler; - - // await MeadowFileManager.ListFiles(this); - - await MeadowFileManager.GetInitialBytesFromFile(this, filename); - - await Task.WhenAny(new Task[] { timeOutTask, tcs.Task }); - DataProcessor.OnReceiveData -= handler; - - return msg; - } - - //device Id information is processed when the message is received - //this will request the device Id and return true it was set successfully - public override async Task GetDeviceInfo(int timeoutInMs = 1000) - { - var result = await MeadowDeviceManager.GetDeviceInfo(this, timeoutInMs); - if (!result.isSuccessful) + if (SerialPort.IsOpen == false) { - throw new DeviceInfoException(); + Logger.LogDebug("Port is not open, attempting reconnect."); + await AttemptToReconnectToMeadow(cancellationToken); } - } - //device name is processed when the message is received - //this will request the device name and return true it was successfully - public override async Task GetDeviceName(int timeoutInMs = 1000) - { - var result = await MeadowDeviceManager.GetDeviceName(this, timeoutInMs); - if (!result.isSuccessful) - { - throw new DeviceInfoException(); - } + SerialPort.Write(encodedBytes, 0, encodedToSend); } - //putting this here for now ..... - bool OpenSerialPort(string portName) + public override bool Initialize(CancellationToken cancellationToken = default) { - try - { // Create a new SerialPort object with default settings - var port = new SerialPort - { - PortName = portName, - BaudRate = 115200, // This value is ignored when using ACM - Parity = Parity.None, - DataBits = 8, - StopBits = StopBits.One, - Handshake = Handshake.None, - - // Set the read/write timeouts - ReadTimeout = 5000, - WriteTimeout = 5000 - }; - - port.Open(); - - //improves perf on Windows? - port.BaseStream.ReadTimeout = 0; - - SerialPort = port; - } - catch (Exception ex) + if (!SerialPort.IsOpen) { - throw ex; + SerialPort.Open(); + SerialPort.BaseStream.ReadTimeout = 0; } - return true; - } - - internal void ListenForSerialData() - { - if (Socket != null) - { - DataProcessor = new MeadowSerialDataProcessor(Socket); - - DataProcessor.OnReceiveData += DataReceived; - } - else if (SerialPort != null) - { - DataProcessor = new MeadowSerialDataProcessor(SerialPort); - DataProcessor.OnReceiveData += DataReceived; - } + return SerialPort.IsOpen; } - void DataReceived(object sender, MeadowMessageEventArgs args) + private static SerialPort OpenSerialPort(string portName) { - OnMeadowMessage?.Invoke(this, args); - - switch (args.MessageType) - { - case MeadowMessageType.Data: - ConsoleOutIf(LocalEcho, "Data: " + args.Message); - break; - case MeadowMessageType.MeadowTrace: - ConsoleOut("Trace: " + args.Message); - break; - case MeadowMessageType.FileListTitle: - ConsoleOut("File List: "); - break; - case MeadowMessageType.FileListMember: - ConsoleOut(args.Message); - break; - case MeadowMessageType.FileListCrcMember: - ConsoleOut(args.Message); - break; - case MeadowMessageType.DeviceInfo: - //ToDo move this - SetDeviceIdFromMessage(args.Message); - ConsoleOut("ID: " + args.Message); - break; - case MeadowMessageType.SerialReconnect: - AttemptToReconnectToMeadow(); - break; - // The next 2 types received text straight from mono' stdout / stderr - // via hcom and may not be packetized at the end of a lines. - case MeadowMessageType.ErrOutput: - ParseAndOutputStdioText(args.Message, "Err: "); - break; - case MeadowMessageType.AppOutput: - ParseAndOutputStdioText(args.Message, "App: "); - break; + // Create a new SerialPort object with default settings + var port = new SerialPort + { + PortName = portName, + BaudRate = 115200, // This value is ignored when using ACM + Parity = Parity.None, + DataBits = 8, + StopBits = StopBits.One, + Handshake = Handshake.None, - case MeadowMessageType.DownloadStartFail: - break; - case MeadowMessageType.DownloadStartOkay: - break; - } - } + // Set the read/write timeouts + ReadTimeout = 5000, + WriteTimeout = 5000 + }; - // Previously this code assumed that each received block of text should - // begin with 'App :' and end with a new line. - // However, when the App sends a lot of text quickly the packet's boundaries - // can have no relation to the original App.exe author's intended when - // using Console.Write or Console.WriteLine. - // This code creates new lines much more closly to the intent of the - // original App.exe author. It looks for the new line as an indication - // that a new line is needed, because some packets have not new line. - private void ParseAndOutputStdioText(string message, string leadInText) - { - // Note: There may have already been a part of a line received and - // displayed (i.e. our screen cursor may be at the end of a text - // that doesn't represent a full line. So we may need to finish - // the line and then add our new line. - // - // Break the data into whatever lines we can - // If a normal line is received it will be split into 2 array - // entries. The first all the text and the second empty and - // ignored - string[] oneLine = message.Split('\n'); - for (int i = 0; i < oneLine.Length; i++) - { - // The last oneLine array entry is a special case. If it's null or - // empty then the last character received was a single '\n' and - // we can ignore it - if (i == oneLine.Length - 1) - { - // Last oneLine array entry. - if (!string.IsNullOrEmpty(oneLine[i])) - { - // There's text on this line but no '\n' - ConsoleOutNoEol(leadInText); - ConsoleOutNoEol(oneLine[i]); - } - } - else - { - // Most typical lines have new line at end - ConsoleOutNoEol(leadInText); - ConsoleOut(oneLine[i]); - } - } + return port; } - internal bool AttemptToReconnectToMeadow() + internal async Task AttemptToReconnectToMeadow(CancellationToken cancellationToken = default) { - int delayCount = 20; // 10 seconds + var delayCount = 20; // 10 seconds while (true) { - System.Threading.Thread.Sleep(500); + await Task.Delay(500, cancellationToken) + .ConfigureAwait(false); + + var portOpened = Initialize(cancellationToken); - bool portOpened = Initialize(true); if (portOpened) { - Console.WriteLine("Device successfully reconnected"); - Thread.Sleep(2000); + Logger.LogDebug("Device successfully reconnected"); + await Task.Delay(2000, cancellationToken) + .ConfigureAwait(false); + return true; } - if (delayCount-- == 0) - { - throw new NotConnectedException(); - } + if (delayCount-- == 0) + throw new NotConnectedException(); } } - - void SetFileAndCrcsFromMessage(string fileListMember) - { - ConsoleOut($"SetFileAndCrcsFromMessage {fileListMember}"); - - int fileNameStart = fileListMember.LastIndexOf('/') + 1; - int crcStart = fileListMember.IndexOf('[') + 1; - if (fileNameStart == 0 && crcStart == 0) - { return; } // No files found - - Debug.Assert(crcStart > fileNameStart); - - var file = fileListMember.Substring(fileNameStart, crcStart - fileNameStart - 2); - FilesOnDevice.Add(file.Trim()); - - var crc = Convert.ToUInt32(fileListMember.Substring(crcStart, 10), 16); - FileCrcs.Add(crc); - } - - void SetFileNameFromMessage(string fileListMember) - { - int fileNameStart = fileListMember.LastIndexOf('/') + 1; - int crcStart = fileListMember.IndexOf('[') + 1; - if (fileNameStart == 0 && crcStart == 0) - { return; } // No files found - - Debug.Assert(crcStart == 0); - - var file = fileListMember.Substring(fileNameStart, fileListMember.Length - fileNameStart); - FilesOnDevice.Add(file.Trim()); - } - - /*"Meadow by Wilderness Labs, - Model: F7Micro, - MeadowOS Version: 0.1.0, - Processor: STM32F777IIK6, - Processor Id: 1d-00-29-00-12-51-36-30-33-33-37-33, - Serial Number: 3360335A3036, - CoProcessor: ESP32, - CoProcessor OS Version: 0.1.x\r\n" - */ - void SetDeviceIdFromMessage(string message) - { - var info = message.Split(','); - - if (info.Length < 8) - { return; } - - DeviceInfo.Name = info[0]; - DeviceInfo.Model = info[1]; - DeviceInfo.MeadowOSVersion = info[2]; - DeviceInfo.Proccessor = info[3]; - DeviceInfo.ProcessorId = info[4]; - DeviceInfo.SerialNumber = info[5]; - DeviceInfo.CoProcessor = info[6]; - DeviceInfo.CoProcessorOs = info[7]; - } - - void ConsoleOutIf(bool condition, string msg) - { - if(condition) - { - Console.WriteLine(msg); - } - } - - void ConsoleOut(string msg) - { - if (Verbose == false) - { - return; - } - - Console.WriteLine(msg); - } - - void ConsoleOutNoEol(string msg) - { - if (Verbose == false) - { - return; - } - - Console.Write(msg); - } - - public void Dispose() - { - this.SerialPort?.Dispose(); - } } - - public class DeviceInfoException : Exception { } } \ No newline at end of file diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowSocketDevice.cs b/Meadow.CLI.Core/DeviceManagement/MeadowSocketDevice.cs new file mode 100644 index 00000000..48e53891 --- /dev/null +++ b/Meadow.CLI.Core/DeviceManagement/MeadowSocketDevice.cs @@ -0,0 +1,81 @@ +using System; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.Internals.MeadowCommunication; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.DeviceManagement +{ + public class MeadowSocketDevice : MeadowLocalDevice + { + private readonly AddressFamily _addressFamily; + public readonly Socket Socket; + + public MeadowSocketDevice(Socket socket, ILogger? logger = null) + : base(new MeadowSerialDataProcessor(socket), logger) + { + Socket = socket; + } + + public override bool IsDeviceInitialized() + { + throw new NotImplementedException(); + } + + public override void Dispose() + { + Socket.Dispose(); + } + + public override async Task WriteAsync(byte[] encodedBytes, int encodedToSend, CancellationToken cancellationToken) + { + await Task.Yield(); + Socket.Send(encodedBytes, encodedToSend, + SocketFlags.None); + } + + public override bool Initialize(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + //Socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + //try + //{ + // Socket.Connect(endpoint); + //} + //catch (SocketException) + //{ + // Console.WriteLine("Could not connect to socket, aborting..."); + // Environment.Exit(1); + //} + } + + private static bool TryCreateIPEndPoint(string address, + out IPEndPoint? endpoint) + { + if (string.IsNullOrEmpty(address)) + { + address = string.Empty; + } + address = address.Replace("localhost", "127.0.0.1"); + endpoint = null; + + string[] ep = address.Split(':'); + if (ep.Length != 2) + return false; + + if (!IPAddress.TryParse(ep[0], out IPAddress ip)) + return false; + + int port; + if (!int.TryParse(ep[1], NumberStyles.None, NumberFormatInfo.CurrentInfo, out port)) + return false; + + endpoint = new IPEndPoint(ip, port); + return true; + } + } +} diff --git a/Meadow.CLI.Core/DeviceManagement/MeadowUsbDevice.cs b/Meadow.CLI.Core/DeviceManagement/MeadowUsbDevice.cs deleted file mode 100644 index 564fbea0..00000000 --- a/Meadow.CLI.Core/DeviceManagement/MeadowUsbDevice.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -namespace Meadow.CLI.DeviceManagement -{ - public class MeadowUsbDevice - { - // or some shit - public string Serial { get; set; } - // - public string UsbDeviceName { get; set; } - - public override string ToString() - { - return UsbDeviceName + "::" + Serial; - } - } -} diff --git a/Meadow.CLI.Core/DeviceManagement/UsbDeviceManager.cs b/Meadow.CLI.Core/DeviceManagement/UsbDeviceManager.cs deleted file mode 100644 index 696964ae..00000000 --- a/Meadow.CLI.Core/DeviceManagement/UsbDeviceManager.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using LibUsbDotNet; -using LibUsbDotNet.Info; -using LibUsbDotNet.Main; - -namespace Meadow.CLI.DeviceManagement -{ - public class UsbDeviceManager : IDisposable - { - // Thread-safe singleton - private static Lazy instance = new Lazy(() => new UsbDeviceManager()); - public static UsbDeviceManager Instance => instance.Value; - - // propers - public ObservableCollection Devices = new ObservableCollection(); - public int PollingIntervalInSeconds { get; set; } = 1; - - // state - protected bool _listeningForDevices = false; - protected CancellationTokenSource _listenCancel = new CancellationTokenSource(); - - // internals - protected ushort _vendorID = 0x483; // STMicro - protected ushort _productID = 0xdf11; // STM32F7 chip - - private UsbDeviceManager() - { - - this.PopulateInitialDeviceList(); - - this.StartListeningForDevices(); - } - - private void PopulateInitialDeviceList() - { - Debug.WriteLine("PopulateInitialDeviceList"); - - // devices - var ds = GetDevices(_vendorID, _productID); - - foreach (var d in ds) - { - Debug.WriteLine($"Found device: {d.Info.ProductString}, by {d.Info.ManufacturerString}, serial: {d.Info.SerialString}"); - Debug.WriteLine($" VendordID: 0x{d.Info.Descriptor.VendorID.ToString("x4")}, ProductID: 0x{d.Info.Descriptor.ProductID.ToString("x4")}"); - - Devices.Add(new MeadowUsbDevice() { Serial = d.Info.SerialString, UsbDeviceName = d.Info.ProductString }); - } - } - - protected object _deviceListLock; - protected void UpdateDeviceList() - { - Debug.WriteLine("UpdateDeviceList()"); - - //// thread safety. - // BUGBUG: this causes a bug where devices don't update. - //lock (_deviceListLock) { - - // get a list of devices - var ds = GetDevices(_vendorID, _productID); - - // remove any missing - // TODO: there's got to be a way better way to do this. someone - // please clean up my terrible code. - List devicesToRemove = new List(); - foreach (var d in Devices) - { - if (!ds.Exists((x) => x.Info.SerialString == d.Serial)) - { - devicesToRemove.Add(d); - } - } - foreach (var d in devicesToRemove) - { - Devices.Remove(d); - } - - // add any new ones - List newDevices = new List(); - foreach (var d in ds) - { - if (!DevicesContains(d.Info.SerialString)) - { - // add to the collection - Devices.Add(new MeadowUsbDevice() { Serial = d.Info.SerialString, UsbDeviceName = d.Info.ProductString }); - } - } - //} - - } - // probably a better way to do this, but .Exists() doesn't - // seem to exist for ObservableCollection. - protected bool DevicesContains(string serial) - { - foreach (var d in Devices) - { - if (d.Serial == serial) - { - return true; - } - } - return false; - } - - /// - /// Gets all USB devices that match the vendor id and product id passed in. - /// - /// - /// - /// - private List GetDevices(ushort vendorIdFilter, ushort productIdFilter) - { - List matchingDevices = new List(); - - // get all the devices in the USB Registry - UsbRegDeviceList devices = UsbDevice.AllDevices; - - // loop through all the devices - foreach (UsbRegistry usbRegistry in devices) - { - - // try and open the device to get info - if (usbRegistry.Open(out UsbDevice device)) - { - - // Filters - // string BS because of [this](https://github.com/LibUsbDotNet/LibUsbDotNet/issues/91) bug. - ushort vendorID = ushort.Parse(device.Info.Descriptor.VendorID.ToString("x"), System.Globalization.NumberStyles.AllowHexSpecifier); - ushort productID = ushort.Parse(device.Info.Descriptor.ProductID.ToString("x"), System.Globalization.NumberStyles.AllowHexSpecifier); - if (vendorIdFilter != 0 && vendorID != vendorIdFilter) - { - continue; - } - if (productIdFilter != 0 && productID != productIdFilter) - { - continue; - } - - // Check for the DFU descriptor in the - - // get the configs - for (int iConfig = 0; iConfig < device.Configs.Count; iConfig++) - { - UsbConfigInfo configInfo = device.Configs[iConfig]; - - // get the interfaces - ReadOnlyCollection interfaceList = configInfo.InterfaceInfoList; - - // loop through the interfaces - for (int iInterface = 0; iInterface < interfaceList.Count; iInterface++) - { - // shortcut - UsbInterfaceInfo interfaceInfo = interfaceList[iInterface]; - - // if it's a DFU device, we want to grab the DFU descriptor - // have to string compare because 0xfe isn't defined in `ClassCodeType` - if (interfaceInfo.Descriptor.Class.ToString("x").ToLower() != "fe" || interfaceInfo.Descriptor.SubClass != 0x1) - { - // interface doesn't support DFU - } - - // we should also be getting the DFU descriptor - // which describes the DFU parameters like speed and - // flash size. However, it's missing from LibUsbDotNet - // the Dfu descriptor is supposed to be 0x21 - //// get the custom descriptor - //var dfuDescriptor = interfaceInfo.CustomDescriptors[0x21]; - //if (dfuDescriptor != null) { - // // add the matching device - // matchingDevices.Add(device); - //} - } - } - - // add the matching device - matchingDevices.Add(device); - - // cleanup - device.Close(); - } - } - - return matchingDevices; - } - - /// - /// Used for debug, enumerates all USB devices and their info to the console. - /// - public void ConsoleOutUsbInfo() - { - UsbRegDeviceList devices = UsbDevice.AllDevices; - - Debug.WriteLine($"Device Count: {devices.Count}"); - - // loop through all the devices in the registry - foreach (UsbRegistry usbRegistry in devices) - { - - // try and open the device to get info - if (usbRegistry.Open(out UsbDevice device)) - { - //Debug.WriteLine($"Device.Info: {device.Info.ToString()}"); - - Debug.WriteLine("-----------------------------------------------"); - Debug.WriteLine($"Found device: {device.Info.ProductString}, by {device.Info.ManufacturerString}, serial: {device.Info.SerialString}"); - Debug.WriteLine($" VendordID: 0x{device.Info.Descriptor.VendorID.ToString("x4")}, ProductID: 0x{device.Info.Descriptor.ProductID.ToString("x4")}"); - Debug.WriteLine($" Config count: {device.Configs.Count}"); - - for (int iConfig = 0; iConfig < device.Configs.Count; iConfig++) - { - UsbConfigInfo configInfo = device.Configs[iConfig]; - - // get the interfaces - ReadOnlyCollection interfaceList = configInfo.InterfaceInfoList; - - - - // loop through the interfaces - for (int iInterface = 0; iInterface < interfaceList.Count; iInterface++) - { - UsbInterfaceInfo interfaceInfo = interfaceList[iInterface]; - - Debug.WriteLine($" Found Interface: {interfaceInfo.InterfaceString}, w/following descriptors: {{"); - Debug.WriteLine($" Descriptor Type: {interfaceInfo.Descriptor.DescriptorType}"); - Debug.WriteLine($" Interface ID: 0x{interfaceInfo.Descriptor.InterfaceID.ToString("x")}"); - Debug.WriteLine($" Alternate ID: 0x{interfaceInfo.Descriptor.AlternateID.ToString("x")}"); - Debug.WriteLine($" Class: 0x{interfaceInfo.Descriptor.Class.ToString("x")}"); - Debug.WriteLine($" SubClass: 0x{interfaceInfo.Descriptor.SubClass.ToString("x")}"); - Debug.WriteLine($" Protocol: 0x{interfaceInfo.Descriptor.Protocol.ToString("x")}"); - Debug.WriteLine($" String Index: {interfaceInfo.Descriptor.StringIndex}"); - Debug.WriteLine($" }}"); - - if (interfaceInfo.Descriptor.Class.ToString("x").ToLower() != "fe" || interfaceInfo.Descriptor.SubClass != 0x1) - { - Debug.WriteLine("Not a DFU device"); - } - else - { - Debug.WriteLine("DFU Device"); - } - - // TODO: we really should be looking for the DFU descriptor: - // (note this code comes from our binding of LibUsb in DFU-sharp, so the API is different. - //// get the descriptor for the interface - //var dfu_descriptor = FindDescriptor( - // interface_descriptor.Extra, - // interface_descriptor.Extra_length, - // (byte)Consts.USB_DT_DFU); - - - //foreach (var cd in interfaceInfo.CustomDescriptors) { - // Debug.WriteLine($"Custom Descriptor: { System.Text.Encoding.ASCII.GetChars(cd).ToString() }"); - //} - - // get the endpoints - ReadOnlyCollection endpointList = interfaceInfo.EndpointInfoList; - for (int iEndpoint = 0; iEndpoint < endpointList.Count; iEndpoint++) - { - Debug.WriteLine($"endpointList[{ iEndpoint}]: {endpointList[iEndpoint].ToString()}"); - } - } - } - - device.Close(); - Debug.WriteLine("-----------------------------------------------"); - } - } - //UsbDevice.Exit(); - - } - - /// - /// Begins polling for device connect and disconnects. Polling interval - /// is controlled via `PollingIntervalInSeconds` - /// - /// - public Task StartListeningForDevices() - { - Debug.WriteLine("StartListeningForDevices()"); - - // if already listening, ignore the call to start - if (_listeningForDevices) { return new Task(() => { }); } - - // spin up a new task - Task t = new Task(() => - { - // state - _listeningForDevices = true; - - // loop while _listening is on - while (_listeningForDevices) - { - - // check for cancel; this is probably redundant because - // we're checking for _listening - // TODO: someone should review. - if (_listenCancel.IsCancellationRequested) - { - _listeningForDevices = false; - return; - } - // update our devices - UpdateDeviceList(); - // wait for a bit - Thread.Sleep(PollingIntervalInSeconds * 1000); - } - }); - t.Start(); - - return t; - } - - /// - /// Stops device disconnect/connect polling. - /// - public void StopListeningForDevices() - { - _listenCancel.Cancel(); - _listeningForDevices = false; - } - - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // if we don't exit, it leaves a thread open, but calling it here - // causes a sigsev - //UsbDevice.Exit(); - // TODO: dispose managed state (managed objects). - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - this.StopListeningForDevices(); - // if we don't exit, it leaves a thread open, but calling it here - // sometimes causes a sigsev - UsbDevice.Exit(); - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - ~UsbDeviceManager() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(false); - } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } -} diff --git a/Meadow.CLI.Core/DownloadManager.cs b/Meadow.CLI.Core/DownloadManager.cs index 348eec22..247686ac 100644 --- a/Meadow.CLI.Core/DownloadManager.cs +++ b/Meadow.CLI.Core/DownloadManager.cs @@ -2,41 +2,69 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Net; using System.Net.Http; -using System.Threading.Tasks; using System.Reflection; using System.Text.Json; -using Meadow.CLI.Core; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; -namespace Meadow.CLI +namespace Meadow.CLI.Core { public class DownloadManager { - readonly string _versionCheckUrl = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/latest.json"; - string _versionCheckFile { get { return new Uri(_versionCheckUrl).Segments.Last(); } } - - public static readonly string FirmwareDownloadsFilePath = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "Firmware"); - public static readonly string WildernessLabsTemp = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "temp"); - public static readonly string OSFilename = "Meadow.OS.bin"; + readonly string _versionCheckUrl = + "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/latest.json"; + + string VersionCheckFile => new Uri(_versionCheckUrl).Segments.Last(); + + public static readonly string FirmwareDownloadsFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WildernessLabs", + "Firmware"); + + public static readonly string WildernessLabsTemp = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WildernessLabs", + "temp"); + + public static readonly string OsFilename = "Meadow.OS.bin"; public static readonly string RuntimeFilename = "Meadow.OS.Runtime.bin"; public static readonly string NetworkBootloaderFilename = "bootloader.bin"; public static readonly string NetworkMeadowCommsFilename = "MeadowComms.bin"; public static readonly string NetworkPartitionTableFilename = "partition-table.bin"; - public static readonly string UpdateCommand = "dotnet tool update WildernessLabs.Meadow.CLI --global"; - public async Task DownloadLatest() + public static readonly string UpdateCommand = + "dotnet tool update WildernessLabs.Meadow.CLI --global"; + + private static readonly HttpClient Client = new HttpClient(); + private readonly ILogger _logger; + + public DownloadManager(ILogger logger) + { + _logger = logger; + } + + public async Task DownloadLatestAsync() { - HttpClient httpClient = new HttpClient(); - var payload = await httpClient.GetStringAsync(_versionCheckUrl); + var payload = await Client.GetStringAsync(_versionCheckUrl); var release = JsonSerializer.Deserialize(payload); - var appVersion = Assembly.GetEntryAssembly().GetCustomAttribute().Version; + if (release == null) + { + var ex = new Exception("Unable to identify release."); + _logger.LogError(ex, "Unable to identify release. Payload: {payload}", payload); + throw ex; + } + + var appVersion = Assembly.GetEntryAssembly()! + .GetCustomAttribute() + .Version; if (release.MinCLIVersion.ToVersion() > appVersion.ToVersion()) { - Console.WriteLine($"Installing OS version {release.Version} requires the latest CLI. To update, run: {UpdateCommand}"); + _logger.LogInformation( + $"Installing OS version {release.Version} requires the latest CLI. To update, run: {UpdateCommand}"); + return; } @@ -44,63 +72,86 @@ public async Task DownloadLatest() { Directory.Delete(FirmwareDownloadsFilePath, true); } + Directory.CreateDirectory(FirmwareDownloadsFilePath); await DownloadFile(new Uri(release.DownloadURL)); await DownloadFile(new Uri(release.NetworkDownloadURL)); - Console.WriteLine($"Download and extracted OS version {release.Version} to:\r\n{FirmwareDownloadsFilePath}"); + _logger.LogInformation( + $"Download and extracted OS version {release.Version} to: {FirmwareDownloadsFilePath}"); } - public void InstallDfuUtil(bool is64bit = true) + public async Task InstallDfuUtilAsync(bool is64Bit = true, + CancellationToken cancellationToken = default) { try { - Console.WriteLine("Installing dfu-util..."); + _logger.LogInformation("Installing dfu-util..."); if (Directory.Exists(WildernessLabsTemp)) { Directory.Delete(WildernessLabsTemp, true); } + Directory.CreateDirectory(WildernessLabsTemp); - WebClient client = new WebClient(); - var downloadUrl = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-0.10-binaries.zip"; - var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/") + 1); - client.DownloadFile(downloadUrl, Path.Combine(WildernessLabsTemp, downloadFileName)); - ZipFile.ExtractToDirectory(Path.Combine(WildernessLabsTemp, downloadFileName), WildernessLabsTemp); + const string downloadUrl = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-0.10-binaries.zip"; + + var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); + var response = await Client.GetAsync(downloadUrl, cancellationToken) + .ConfigureAwait(false); + + if (response.IsSuccessStatusCode == false) + throw new Exception("Failed to download dfu-util"); + + using var fs = File.OpenWrite(Path.Combine(WildernessLabsTemp, downloadFileName)); + await response.Content.CopyToAsync(fs) + .ConfigureAwait(false); - var dfuutilexe = new FileInfo(Path.Combine(WildernessLabsTemp, is64bit ? "win64" : "win32", "dfu-util.exe")); - var libusbdll = new FileInfo(Path.Combine(WildernessLabsTemp, is64bit ? "win64" : "win32", "libusb-1.0.dll")); + ZipFile.ExtractToDirectory( + Path.Combine(WildernessLabsTemp, downloadFileName), + WildernessLabsTemp); - var targetDir = is64bit ? - Environment.GetFolderPath(Environment.SpecialFolder.System) : - Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); + var dfuUtilExe = new FileInfo( + Path.Combine(WildernessLabsTemp, is64Bit ? "win64" : "win32", "dfu-util.exe")); - File.Copy(dfuutilexe.FullName, Path.Combine(targetDir, dfuutilexe.Name), true); - File.Copy(libusbdll.FullName, Path.Combine(targetDir, libusbdll.Name), true); + var libUsbDll = new FileInfo( + Path.Combine( + WildernessLabsTemp, + is64Bit ? "win64" : "win32", + "libusb-1.0.dll")); + + var targetDir = is64Bit + ? Environment.GetFolderPath(Environment.SpecialFolder.System) + : Environment.GetFolderPath( + Environment.SpecialFolder.SystemX86); + + File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); + File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); // clean up from previous version - if (File.Exists(Path.Combine(@"C:\Windows\System", dfuutilexe.Name))) + var dfuPath = Path.Combine(@"C:\Windows\System", dfuUtilExe.Name); + var libUsbPath = Path.Combine(@"C:\Windows\System", libUsbDll.Name); + if (File.Exists(dfuPath)) { - File.Delete(Path.Combine(@"C:\Windows\System", dfuutilexe.Name)); + File.Delete(dfuPath); } - if (File.Exists(Path.Combine(@"C:\Windows\System", libusbdll.Name))) + + if (File.Exists(libUsbPath)) { - File.Delete(Path.Combine(@"C:\Windows\System", libusbdll.Name)); + File.Delete(libUsbPath); } - Console.WriteLine("dfu-util 0.10 installed"); + + _logger.LogInformation("dfu-util 0.10 installed"); } catch (Exception ex) { - if (ex.Message.Contains("Access to the path")) - { - Console.WriteLine($"{ex.Message}{Environment.NewLine}Run terminal as administrator and try again."); - } - else - { - Console.WriteLine($"Unexpected error: {ex.Message}"); - } + _logger.LogError( + ex, + ex.Message.Contains("Access to the path") + ? $"Run terminal as administrator and try again." + : "Unexpected error"); } finally { @@ -111,36 +162,74 @@ public void InstallDfuUtil(bool is64bit = true) } } - public async Task<(bool updateExists, string latestVersion, string currentVersion)> CheckForUpdates() + public async Task<(bool updateExists, string latestVersion, string currentVersion)> + CheckForUpdatesAsync() { try { var packageId = "WildernessLabs.Meadow.CLI"; - var appVersion = Assembly.GetEntryAssembly().GetCustomAttribute().Version; - HttpClient client = new HttpClient(); - var json = await client.GetStringAsync($"https://api.nuget.org/v3-flatcontainer/{packageId}/index.json"); + var appVersion = Assembly.GetEntryAssembly()! + .GetCustomAttribute() + .Version; + + var json = await Client.GetStringAsync( + $"https://api.nuget.org/v3-flatcontainer/{packageId}/index.json"); + var result = JsonSerializer.Deserialize(json); - if (!string.IsNullOrEmpty(result?.versions?.LastOrDefault())) + if (!string.IsNullOrEmpty(result?.Versions.LastOrDefault())) { - var latest = result.versions.Last(); + var latest = result!.Versions!.Last(); return (latest.ToVersion() > appVersion.ToVersion(), latest, appVersion); } } catch (Exception ex) { + _logger.LogError(ex, "Error checking for updates"); } + return (false, string.Empty, string.Empty); } - async Task DownloadFile(Uri uri) + private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) { - var fileName = uri.Segments.ToList().Last(); + _logger.LogDebug("Downloading latest firmware"); + using var firmwareRequest = new HttpRequestMessage(HttpMethod.Get, uri); + using var firmwareResponse = await Client.SendAsync(firmwareRequest, cancellationToken) + .ConfigureAwait(false); + + firmwareResponse.EnsureSuccessStatusCode(); + + var downloadFileName = Path.GetTempFileName(); + _logger.LogDebug("Copying firmware to temp file {filename}", downloadFileName); + using (var firmwareFile = File.OpenWrite(downloadFileName)) + { + await firmwareResponse.Content.CopyToAsync(firmwareFile) + .ConfigureAwait(false); + } + + _logger.LogDebug("Downloading latest version file"); + using var versionRequest = new HttpRequestMessage(HttpMethod.Get, _versionCheckUrl); + using var versionResponse = await Client.SendAsync(versionRequest, cancellationToken) + .ConfigureAwait(false); + + versionResponse.EnsureSuccessStatusCode(); + + var versionFileName = Path.Combine(FirmwareDownloadsFilePath, VersionCheckFile); + + _logger.LogDebug("Copying version file to {filename}", versionFileName); + using (var versionFile = + File.OpenWrite(versionFileName)) + { + + await versionResponse.Content.CopyToAsync(versionFile) + .ConfigureAwait(false); + } - WebClient webClient = new WebClient(); - webClient.DownloadFile(uri, Path.Combine(FirmwareDownloadsFilePath, fileName)); - webClient.DownloadFile(_versionCheckUrl, Path.Combine(FirmwareDownloadsFilePath, _versionCheckFile)); - ZipFile.ExtractToDirectory(Path.Combine(FirmwareDownloadsFilePath, fileName), FirmwareDownloadsFilePath); + _logger.LogDebug("Extracting firmware to {path}", FirmwareDownloadsFilePath); + ZipFile.ExtractToDirectory( + downloadFileName, + FirmwareDownloadsFilePath); } } -} +} \ No newline at end of file diff --git a/Meadow.CLI.Core/Exceptions/DeviceNotFoundException.cs b/Meadow.CLI.Core/Exceptions/DeviceNotFoundException.cs new file mode 100644 index 00000000..1f1db0b8 --- /dev/null +++ b/Meadow.CLI.Core/Exceptions/DeviceNotFoundException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Meadow.CLI.Core.Exceptions +{ + public class DeviceNotFoundException : Exception + { + public DeviceNotFoundException(string message, Exception? innerException = null) + : base(message, innerException) + { + + } + } +} diff --git a/Meadow.CLI.Core/Exceptions/MeadowCommandException.cs b/Meadow.CLI.Core/Exceptions/MeadowCommandException.cs new file mode 100644 index 00000000..72b9c1db --- /dev/null +++ b/Meadow.CLI.Core/Exceptions/MeadowCommandException.cs @@ -0,0 +1,19 @@ +using System; +using Meadow.CLI.Core.DeviceManagement; +using Meadow.CLI.Core.Internals.MeadowCommunication; + +namespace Meadow.CLI.Core.Exceptions +{ + public class MeadowCommandException : MeadowDeviceException + { + public MeadowCommandException(string message, CommandResponse? commandResponse = null, Exception? innerException = null) + : base(message, innerException) + { + MeadowMessage = commandResponse?.Message; + MessageType = commandResponse?.MessageType; + } + + public MeadowMessageType? MessageType { get; private set; } + public string? MeadowMessage { get; private set; } + } +} diff --git a/Meadow.CLI.Core/Exceptions/MultipleDfuDevicesException.cs b/Meadow.CLI.Core/Exceptions/MultipleDfuDevicesException.cs new file mode 100644 index 00000000..35910f18 --- /dev/null +++ b/Meadow.CLI.Core/Exceptions/MultipleDfuDevicesException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Meadow.CLI.Core.Exceptions +{ + public class MultipleDfuDevicesException : DeviceNotFoundException + { + public MultipleDfuDevicesException(string message, Exception? innerException = null) + : base(message, innerException) + { + } + } +} diff --git a/Meadow.CLI.Core/Identity/IdentityManager.cs b/Meadow.CLI.Core/Identity/IdentityManager.cs index b39c1b2c..c20109a3 100644 --- a/Meadow.CLI.Core/Identity/IdentityManager.cs +++ b/Meadow.CLI.Core/Identity/IdentityManager.cs @@ -1,28 +1,36 @@ -using CredentialManagement; -using IdentityModel.OidcClient; -using Microsoft.IdentityModel.Logging; -using System; +using System; using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; +using CredentialManagement; +using IdentityModel.OidcClient; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Logging; -namespace Meadow.CLI.Core.Auth +namespace Meadow.CLI.Core.Identity { public class IdentityManager { - public readonly string WLRefreshCredentialName = "WL:Identity:Refresh"; + public readonly string WlRefreshCredentialName = "WL:Identity:Refresh"; readonly string authority = "https://identity.wildernesslabs.co"; readonly string redirectUri = "http://localhost:8877/"; readonly string postAuthRedirectUri = "https://www.wildernesslabs.co"; readonly string clientId = "0oa3axsuyupb7J6E15d6"; + private readonly ILogger _logger; + + public IdentityManager(ILogger logger) + { + _logger = logger; + } /// /// Kick off login /// /// - public async Task LoginAsync() + public async Task LoginAsync(CancellationToken cancellationToken = default) { try { @@ -34,7 +42,7 @@ public async Task LoginAsync() http.Start(); // generated login url with PKCE - var state = await client.PrepareLoginAsync(); + var state = await client.PrepareLoginAsync(cancellationToken: cancellationToken); OpenBrowser(state.StartUrl); @@ -44,23 +52,23 @@ public async Task LoginAsync() context.Response.AddHeader("Location", postAuthRedirectUri); context.Response.Close(); - var result = await client.ProcessResponseAsync(raw, state); + var result = await client.ProcessResponseAsync(raw, state, cancellationToken: cancellationToken); if (result.IsError) { - Console.WriteLine(result.Error); + _logger.LogError(result.Error); } else { var email = result.User.Claims.SingleOrDefault(x => x.Type == "email")?.Value; - if (string.IsNullOrEmpty(email)) + if (string.IsNullOrWhiteSpace(email)) { - Console.WriteLine("Unable to get email address"); + _logger.LogWarning("Unable to get email address"); } else { // saving only the refresh token since the access token is too large - SaveCredential(WLRefreshCredentialName, email, result.RefreshToken); + SaveCredential(WlRefreshCredentialName, email!, result.RefreshToken); } } return !result.IsError; @@ -68,27 +76,27 @@ public async Task LoginAsync() } catch(Exception ex) { - Console.WriteLine(ex.Message); + _logger.LogError(ex, "An error occurred"); return false; } } public void Logout() { - DeleteCredential(WLRefreshCredentialName); + DeleteCredential(WlRefreshCredentialName); } /// /// Get access token through a token refresh /// /// - public async Task GetAccessToken() + public async Task GetAccessToken(CancellationToken cancellationToken = default) { string refreshToken = string.Empty; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - refreshToken = GetCredentials(WLRefreshCredentialName).password; + refreshToken = GetCredentials(WlRefreshCredentialName).password; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -96,14 +104,14 @@ public async Task GetAccessToken() } else { - Console.WriteLine("Unsupported OS detected."); + _logger.LogWarning("Unsupported OS detected."); throw new NotSupportedException(); } if (!string.IsNullOrEmpty(refreshToken)) { var client = GetOidcClient(); - var result = await client.RefreshTokenAsync(refreshToken); + var result = await client.RefreshTokenAsync(refreshToken, cancellationToken: cancellationToken); return result.AccessToken; } else @@ -141,7 +149,7 @@ public async Task GetAccessToken() } else { - Console.WriteLine("Unsupported OS detected."); + _logger.LogWarning("Unsupported OS detected."); throw new NotSupportedException(); } } @@ -178,7 +186,7 @@ public void DeleteCredential(string credentialName) } else { - Console.WriteLine("Unsupported OS detected."); + _logger.LogWarning("Unsupported OS detected."); throw new NotSupportedException(); } } @@ -209,7 +217,7 @@ public bool SaveCredential(string credentialName, string username, string passwo } else { - Console.WriteLine("Unsupported OS detected."); + _logger.LogWarning("Unsupported OS detected."); throw new NotSupportedException(); } } @@ -238,7 +246,7 @@ private void OpenBrowser(string url) } else { - Console.WriteLine("Unsupported OS detected."); + _logger.LogWarning("Unsupported OS detected."); throw new NotSupportedException(); } } diff --git a/Meadow.CLI.Core/Identity/Keychain.cs b/Meadow.CLI.Core/Identity/Keychain.cs index 569dce92..d9300c6c 100644 --- a/Meadow.CLI.Core/Identity/Keychain.cs +++ b/Meadow.CLI.Core/Identity/Keychain.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Meadow.CLI.Core.Auth +namespace Meadow.CLI.Core.Identity { static class Keychain { diff --git a/Meadow.CLI.Core/Internals/Dfu/DfuUpload.cs b/Meadow.CLI.Core/Internals/Dfu/DfuUpload.cs deleted file mode 100644 index 5f50212a..00000000 --- a/Meadow.CLI.Core/Internals/Dfu/DfuUpload.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using LibUsbDotNet; -using Meadow.CLI; - -namespace MeadowCLI -{ - public static class DfuUpload - { - private const int ERROR_FILE_NOT_FOUND = 2; - private const int ERROR_PATH_NOT_FOUND = 3; - - static int _osAddress = 0x08000000; - static string _usbStmName = "STM32 BOOTLOADER"; - - public static void FlashOS(string filename = "") - { - var allDevices = UsbDevice.AllDevices; - if (allDevices.Count(x => x.Name == _usbStmName) > 1) - { - Console.WriteLine("More than one DFU device found, please connect only one and try again."); - return; - } - - var device = UsbDevice.AllDevices.SingleOrDefault(x => x.Name == _usbStmName); - if (device == null) - { - Console.WriteLine("Connect a device in bootloader mode. If the device is in bootloader mode, please update the device driver. See instructions at https://wldrn.es/usbdriver"); - return; - } - else - { - // if filename isn't specified fallback to download path - if (string.IsNullOrEmpty(filename)) - { - filename = Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.OSFilename); - } - - if (!File.Exists(filename)) - { - Console.WriteLine("Please specify valid --File or --Download latest"); - return; - } - else - { - Console.WriteLine($"Flashing OS with {filename}"); - } - - string serial = string.Empty; - - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - var deviceID = device.DeviceProperties["DeviceID"].ToString(); - serial = deviceID.Substring(deviceID.LastIndexOf("\\") + 1); - } - else - { - serial = device.DeviceProperties["SerialNumber"].ToString(); - } - - var dfuUtilVersion = GetDfuUtilVersion(); - - if (string.IsNullOrEmpty(dfuUtilVersion)) - { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - Console.WriteLine("dfu-util not found. To install, run in administrator mode: meadow --InstallDfuUtil"); - } - else - { - Console.WriteLine("dfu-util not found. To install run: brew install dfu-util"); - } - return; - } - else if (dfuUtilVersion != "0.10") - { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - Console.WriteLine("dfu-util update required. To install, run in administrator mode: meadow --InstallDfuUtil"); - } - else - { - Console.WriteLine("dfu-util update required. To install, run: brew upgrade dfu-util"); - } - return; - } - - try - { - using (var process = new Process()) - { - process.StartInfo.FileName = "dfu-util"; - process.StartInfo.Arguments = $"-a 0 -S {serial} -D \"{filename}\" -s {_osAddress}:leave"; - process.StartInfo.UseShellExecute = false; - process.Start(); - process.WaitForExit(); - } - } - catch (Exception ex) - { - Console.WriteLine($"There was a problem executing dfu-util: {ex.Message}"); - return; - } - } - } - - private static string GetDfuUtilVersion() - { - 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; - string output = reader.ReadLine(); - if (output.StartsWith("dfu-util")) - { - var split = output.Split(new char[] { ' ' }); - if (split.Length == 2) - { - return split[1]; - } - } - - process.WaitForExit(); - return string.Empty; - } - } - catch (Win32Exception ex) - { - if (ex.NativeErrorCode == ERROR_FILE_NOT_FOUND || ex.NativeErrorCode == ERROR_PATH_NOT_FOUND) - { - return string.Empty; - } - - throw ex; - } - } - } -} diff --git a/Meadow.CLI.Core/Internals/Dfu/DfuUtils.cs b/Meadow.CLI.Core/Internals/Dfu/DfuUtils.cs new file mode 100644 index 00000000..f52c3d63 --- /dev/null +++ b/Meadow.CLI.Core/Internals/Dfu/DfuUtils.cs @@ -0,0 +1,223 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using LibUsbDotNet; +using LibUsbDotNet.Main; +using Meadow.CLI.Core.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Meadow.CLI.Core.Internals.Dfu +{ + public static class DfuUtils + { + static int _osAddress = 0x08000000; + static string _usbStmName = "STM32 BOOTLOADER"; + + public static bool CheckForValidDevice() + { + try + { + GetDevice(); + return true; + } + catch (Exception) + { + return false; + } + } + + public static UsbRegistry GetDevice() + { + var allDevices = UsbDevice.AllDevices; + if (allDevices.Count(x => x.Name == _usbStmName) > 1) + { + throw new MultipleDfuDevicesException("More than one DFU device found, please connect only one and try again."); + + } + + var device = UsbDevice.AllDevices.SingleOrDefault(x => x.Name == _usbStmName); + if (device == null) + { + throw new DeviceNotFoundException("Device not found. Connect a device in bootloader mode. If the device is in bootloader mode, please update the device driver. See instructions at https://wldrn.es/usbdriver"); + } + + return device; + } + + public static string GetDeviceSerial(UsbRegistry device) + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + { + var deviceID = device.DeviceProperties["DeviceID"].ToString(); + return deviceID.Substring(deviceID.LastIndexOf("\\") + 1); + } + default: + return device.DeviceProperties["SerialNumber"].ToString(); + } + } + + public static async Task FlashOsAsync(string filename = "", UsbRegistry? device = null, ILogger? logger = null) + { + logger ??= NullLogger.Instance; + device ??= GetDevice(); + + // if filename isn't specified fallback to download path + if (string.IsNullOrEmpty(filename)) + { + filename = Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.OsFilename); + } + + if (!File.Exists(filename)) + { + logger.LogError("Please specify valid --File or --Download latest"); + return; + } + else + { + logger.LogInformation($"Flashing OS with {filename}"); + } + + var serial = GetDeviceSerial(device); + + var dfuUtilVersion = GetDfuUtilVersion(); + + if (string.IsNullOrEmpty(dfuUtilVersion)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + logger.LogError("dfu-util not found. To install, run in administrator mode: meadow --InstallDfuUtil"); + } + else + { + logger.LogError("dfu-util not found. To install run: brew install dfu-util"); + } + return; + } + else if (dfuUtilVersion != "0.10") + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + logger.LogError("dfu-util update required. To install, run in administrator mode: meadow --InstallDfuUtil"); + } + else + { + logger.LogError("dfu-util update required. To install, run: brew upgrade dfu-util"); + } + return; + } + + try + { + var startInfo = new ProcessStartInfo( + "dfu-util", + $"-a 0 -S {serial} -D \"{filename}\" -s {_osAddress}:leave") + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = false + }; + using var process = Process.Start(startInfo); + if (process == null) + throw new Exception("Failed to start dfu-util"); + + var informationLogger = logger != null + ? Task.Factory.StartNew( + () => + { + while (process.HasExited == false) + { + var logLine = process.StandardOutput.ReadLine(); + if (logLine == null) + { + logger.LogInformation(string.Empty); + } + else if (logLine.Contains("%")) + { + var operation = logLine.Substring( + 0, + logLine.IndexOf( + "\t", + StringComparison.Ordinal)).Trim(); + var progressBarEnd = logLine.IndexOf( + "]", + StringComparison.Ordinal) + 1; + var progress = logLine.Substring(progressBarEnd, logLine.IndexOf("%", StringComparison.Ordinal) - progressBarEnd + 1).TrimStart(); + Console.SetCursorPosition(0, Console.CursorTop); + if (progress != "100%") + Console.Write($"{operation} {progress}"); + } + else + { + logger.LogInformation(logLine); + } + } + }) : 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(); + } + catch (Exception ex) + { + logger.LogError($"There was a problem executing dfu-util: {ex.Message}"); + return; + } + } + + private static string GetDfuUtilVersion() + { + 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; + string output = reader.ReadLine(); + if (output.StartsWith("dfu-util")) + { + var split = output.Split(new char[] { ' ' }); + if (split.Length == 2) + { + return split[1]; + } + } + + process.WaitForExit(); + return string.Empty; + } + } + catch (Exception ex) + { + if (ex.Message.Contains("cannot find") || ex.Message.Contains("No such file or directory")) + { + return string.Empty; + } + else + { + throw ex; + } + } + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/DebuggingServer.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/DebuggingServer.cs deleted file mode 100644 index 1b817540..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/DebuggingServer.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using MeadowCLI.DeviceManagement; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - // This TCP server directly interacts with Visual Studio debugging. - // What it receives from Visual Studio it forwards to Meadow. - // What it receives from Meadow it forwards to Visual Studio. - public class DebuggingServer - { - // VS 2019 - 4024 - // VS 2017 - 4022 - // VS 2015 - 4020 - public IPEndPoint LocalEndpoint { get; private set; } - ActiveClient activeClient; - int activeClientCount = 0; - - List buffers = new List(); - - // Constructor - public DebuggingServer(IPEndPoint localEndpoint) - { - LocalEndpoint = localEndpoint; - } - - public async void StartListening(MeadowSerialDevice meadow) - { - try - { - TcpListener tcpListener = new TcpListener(LocalEndpoint); - tcpListener.Start(); - LocalEndpoint = (IPEndPoint)tcpListener.LocalEndpoint; - Console.WriteLine("Listening for Visual Studio to connect"); - - while(true) - { - // Wait for client to connect - TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync(); - OnConnect(meadow, tcpClient); - } - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - } - - public void Connect(MeadowSerialDevice meadow) - { - TcpClient tcpClient = new TcpClient(); - tcpClient.Connect(LocalEndpoint); - OnConnect(meadow, tcpClient); - } - - void OnConnect(MeadowSerialDevice meadow, TcpClient tcpClient) - { - try - { - Console.WriteLine("Visual Studio has connected"); - if (activeClientCount > 0) - { - Debug.Assert(activeClientCount == 1); - Debug.Assert(activeClient != null); - CloseActiveClient(); - } - - activeClient = new ActiveClient(this, tcpClient); - lock (buffers) - { - foreach (var buffer in buffers) - activeClient.SendToVisualStudio(buffer); - buffers.Clear(); - } - activeClient.ReceiveVSDebug(meadow); - activeClientCount++; - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - } - - internal void CloseActiveClient() - { - activeClient.Close(); - activeClient = null; - activeClientCount = 0; - lock (buffers) - buffers.Clear(); - } - - public void SendToVisualStudio(byte[] byteData) - { - if (activeClient is ActiveClient ac) - { - ac.SendToVisualStudio(byteData); - return; - } - - // Buffer the data until VS connects - lock (buffers) - buffers.Add(byteData); - } - - // Imbedded class - private class ActiveClient - { - DebuggingServer debuggingServer; - TcpClient tcpClient; - NetworkStream networkStream; - bool okayToRun; - - // Constructor - internal ActiveClient(DebuggingServer _debuggingServer, TcpClient _tcpClient) - { - debuggingServer = _debuggingServer; - okayToRun = true; - tcpClient = _tcpClient; - networkStream = tcpClient.GetStream(); - } - - internal void Close() - { - Console.WriteLine("ActiveClient:Close active client"); - okayToRun = false; - tcpClient.Close(); // Closes NetworkStream too - } - - internal async void ReceiveVSDebug(MeadowSerialDevice meadow) - { - // Console.WriteLine("ActiveClient:Start receiving from VS"); - try - { - // Receive from Visual Studio and send to Meadow - await Task.Run(async () => - { - var recvdBuffer = new byte[490]; - var meadowBuffer = Array.Empty(); - while (tcpClient.Connected && okayToRun) - { - int bytesRead; - - read: - bytesRead = await networkStream.ReadAsync(recvdBuffer, 0, recvdBuffer.Length); - if (bytesRead == 0 || !okayToRun) - break; - - var destIndex = meadowBuffer.Length; - Array.Resize(ref meadowBuffer, destIndex + bytesRead); - Array.Copy(recvdBuffer, 0, meadowBuffer, destIndex, bytesRead); - - // Ensure we read all the data in this message before passing it along - if (networkStream.DataAvailable) - goto read; - - // Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-Received {bytesRead} bytes from VS will forward to HCOM"); - - // Forward to Meadow - MeadowDeviceManager.ForwardVisualStudioDataToMono(meadowBuffer, meadow, 0); - //Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-Forwarded {bytesRead} from VS to Meadow"); - meadowBuffer = Array.Empty(); - } - }); - } - catch (System.IO.IOException ioe) - { - // VS client probably died - Console.WriteLine(ioe.ToString()); - debuggingServer.CloseActiveClient(); - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - debuggingServer.CloseActiveClient(); - if (okayToRun) - throw; - } - } - - public async void SendToVisualStudio(byte[] byteData) - { - //Console.WriteLine($"Forwarding {byteData.Length} bytes to VS"); - try - { - // Receive from Meadow and send to Visual Studio - if (!tcpClient.Connected) - { - Console.WriteLine($"Send attempt is not possible, Visual Studio not connected"); - return; - } - - await networkStream.WriteAsync(byteData, 0, byteData.Length); - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - if (okayToRun) - throw; - } - } - } - } -} \ No newline at end of file diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvFactory.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvFactory.cs deleted file mode 100644 index 91173222..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvFactory.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MeadowCLI.DeviceManagement; -using static MeadowCLI.DeviceManagement.MeadowFileManager; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - public abstract class RecvMessageFactory - { - public abstract IReceivedMessage Create(byte[] recvdMsg, int recvdMsgLength); - } - - public class RecvFactoryManager - { - private readonly Dictionary _factories; - public RecvFactoryManager() - { - // A factory for each received unique request type - _factories = new Dictionary - { - {HcomHostRequestType.HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA, new RecvSimpleBinaryFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES, new RecvSimpleBinaryFactory() }, - - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_REJECTED, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ACCEPTED, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CONCLUDED, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ERROR, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_INFORMATION, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_TRACE_MSG, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_RECONNECT, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_OKAY, new RecvSimpleTextFactory() }, - {HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_FAIL, new RecvSimpleTextFactory() }, - }; - } - - public IReceivedMessage CreateProcessor(byte[] recvdMsg, int receivedMsgLen) - { - HcomHostRequestType rqstType = HcomHostRequestType.HCOM_HOST_REQUEST_UNDEFINED_REQUEST; - try - { - rqstType = FindRequestTypeValue(recvdMsg); - RecvMessageFactory factory = _factories[rqstType]; - return factory.Create(recvdMsg, receivedMsgLen); - } - catch (KeyNotFoundException) - { - Console.WriteLine($"An unknown request value of '0x{rqstType:x}' was received."); - return null; - } - catch (Exception ex) - { - // I saw a few time, that this exception was being thrown. It was caused by - // corrupted data being processed. - Console.WriteLine($"Request type was: 0x{rqstType:x}. Exception: {ex.Message}"); - return null; - } - } - - HcomHostRequestType FindRequestTypeValue(byte[] recvdMsg) - { - int RequestTypeOffset = (int)HcomProtocolHeaderOffsets.HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET; - return (HcomHostRequestType) Convert.ToUInt16(recvdMsg[RequestTypeOffset] + (recvdMsg[RequestTypeOffset + 1] << 8)); - } - } -} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvHeader.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvHeader.cs deleted file mode 100644 index 17da581b..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvHeader.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Runtime.InteropServices; -using static MeadowCLI.DeviceManagement.MeadowFileManager; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - public interface IReceivedMessage - { - // Each derived class needs these - bool Execute(byte[] recvdMsg, int receivedMsgLen); - string ToString(); - - // These are in RecvHeader - ushort SeqNumber { get; } - ushort VersionNumber { get; } - ushort RequestType { get; } - ushort ExtraData { get; } - uint UserData { get; } - int HeaderLength { get; } - - byte[] MessageData { get; } - } - - public class RecvHeader : IReceivedMessage - { - // Header 12 bytes for Header plus message data - public ushort SeqNumber { get; private set; } - public ushort VersionNumber { get; private set; } - public ushort RequestType { get; private set; } - public ushort ExtraData { get; private set; } - public uint UserData { get; private set; } - public int HeaderLength { get ; private set; } - - public byte[] MessageData { get; private set; } - public int MessageDataLength { get; private set; } - - - public RecvHeader(byte[] recvdMsg, int recvdMsgLength) - { - // Recover the sequence number which must proceed all messages. - SeqNumber = Convert.ToUInt16(recvdMsg[HeaderLength] + (recvdMsg[HeaderLength + 1] << 8)); - HeaderLength += sizeof(ushort); - - VersionNumber = Convert.ToUInt16(recvdMsg[HeaderLength] + (recvdMsg[HeaderLength + 1] << 8)); - HeaderLength += sizeof(ushort); - - RequestType = Convert.ToUInt16(recvdMsg[HeaderLength] + (recvdMsg[HeaderLength + 1] << 8)); - HeaderLength += sizeof(ushort); - - ExtraData = Convert.ToUInt16(recvdMsg[HeaderLength] + (recvdMsg[HeaderLength + 1] << 8)); - HeaderLength += sizeof(ushort); - - UserData = Convert.ToUInt32(recvdMsg[HeaderLength] + (recvdMsg[HeaderLength + 1] << 8) + - (recvdMsg[HeaderLength + 2] << 16) + (recvdMsg[HeaderLength + 3] << 24)); - HeaderLength += sizeof(UInt32); - - MessageDataLength = recvdMsgLength - HeaderLength; - if(MessageDataLength > 0) - { - MessageData = new byte[MessageDataLength]; - Array.Copy(recvdMsg, HeaderLength, MessageData, 0, MessageDataLength); - } - else - { - MessageData = null; - } - } - - public virtual bool Execute(byte[] recvdMsg, int receivedMsgLen) - { - return true; - } - } -} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleBinary.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleBinary.cs deleted file mode 100644 index da8dc3b6..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleBinary.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - // Factory class - public class RecvSimpleBinaryFactory : RecvMessageFactory - { - public override IReceivedMessage Create(byte[] recvdMsg, int recvdMsgLength) => new RecvSimpleBinary(recvdMsg, recvdMsgLength); - } - - // Concrete class - internal class RecvSimpleBinary : RecvHeader - { - public RecvSimpleBinary(byte[] recvdMsg, int recvdMsgLength) : base(recvdMsg, recvdMsgLength) - { - } - - public override bool Execute(byte[] recvdMsg, int recvdMsgLen) - { - try - { - return true; - } - catch (Exception ex) - { - Console.WriteLine($"Exception:{ex.Message}"); - return false; - } - } - } -} - diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleMsg.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleMsg.cs deleted file mode 100644 index 62e3f13d..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleMsg.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - // Factory class - public class RecvSimpleMsgFactory : RecvMessageFactory - { - public override IReceivedMessage Create(byte[] recvdMsg, int recvdMsgLength) => new RecvSimpleMsg(recvdMsg, recvdMsgLength); - } - - // Concrete class - internal class RecvSimpleMsg : RecvHeader - { - public RecvSimpleMsg(byte[] recvdMsg, int recvdMsgLength) : base(recvdMsg, recvdMsgLength) - { - } - - public override bool Execute(byte[] recvdMsg, int recvdMsgLen) - { - try - { - return true; - } - catch (Exception ex) - { - Console.WriteLine($"Exception:{ex.Message}"); - return false; - } - } - } -} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleText.cs b/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleText.cs deleted file mode 100644 index 4759e024..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/RecvClasses/RecvSimpleText.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Text; - -namespace Meadow.CLI.Internals.MeadowComms.RecvClasses -{ - // Factory class - public class RecvSimpleTextFactory : RecvMessageFactory - { - public override IReceivedMessage Create(byte[] recvdMsg, int recvdMsgLength) => new RecvSimpleText(recvdMsg, recvdMsgLength); - } - - // Concrete class - internal class RecvSimpleText : RecvHeader - { - public RecvSimpleText(byte[] recvdMsg, int recvdMsgLength) : base(recvdMsg, recvdMsgLength) - { - } - - public override bool Execute(byte[] recvdMsg, int recvdMsgLen) - { - try - { - if (recvdMsg.Length == HeaderLength) - { - throw new ArgumentException("Received RecvSimpleText with no text data"); - } - return true; - } - catch (Exception ex) - { - Console.WriteLine($"Exception:{ex.Message}"); - return false; - } - } - public override string ToString() - { - return (MessageDataLength > 0) ? ASCIIEncoding.ASCII.GetString(MessageData) : string.Empty; - } - } -} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/SendTargetData.cs b/Meadow.CLI.Core/Internals/MeadowComms/SendTargetData.cs deleted file mode 100644 index f80d0d2b..00000000 --- a/Meadow.CLI.Core/Internals/MeadowComms/SendTargetData.cs +++ /dev/null @@ -1,501 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO.Ports; -using System.Text; -using static MeadowCLI.DeviceManagement.MeadowFileManager; -using MeadowCLI.DeviceManagement; -using System.Threading.Tasks; -using Meadow.CLI; -using System.Threading; - -namespace MeadowCLI.Hcom -{ - public class SendTargetData - { - const int HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH = 12; - const int HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH = 32; - const int HCOM_PROTOCOL_COMMAND_SEQ_NUMBER = 0; - const ushort HCOM_PROTOCOL_EXTRA_DATA_DEFAULT_VALUE = 0x0000; // Currently not used field - - //questioning if this class should send or just create the message - MeadowSerialDevice _device; //refactor this .... - - uint _packetCrc32; - - //========================================================================== - // Constructor - public SendTargetData(MeadowSerialDevice device, bool verbose = true) - { - _device = device; - this.Verbose = verbose; - } - - //========================================================================== - public bool Verbose { get; protected set; } - - /// - /// Build and send the Start, Data packets and the End - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public void SendTheEntireFile( - MeadowSerialDevice meadow, - HcomMeadowRequestType requestType, - string destFileName, - uint partitionId, - byte[] fileBytes, - uint mcuAddr, - uint payloadCrc32, - string md5Hash, - bool lastInSeries) - { - _packetCrc32 = 0; - - try - { - // Build and send the header - BuildAndSendFileRelatedCommand(requestType, partitionId, - (uint)fileBytes.Length, payloadCrc32, mcuAddr, md5Hash, - destFileName); - - int ResponseWaitTime; - if (requestType == HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER) - { - ResponseWaitTime = 30000; // 30 seconds because ESP32 Startup can take longer - Console.Write($"Erasing ESP32 Flash..."); - } - else - { - ResponseWaitTime = 10000; // 10 seconds is the default - } - - //==== Wait for response from Meadow - // create our message filter. - Predicate filter = p => ( - p.MessageType == MeadowMessageType.Concluded || - p.MessageType == MeadowMessageType.DownloadStartOkay || - p.MessageType == MeadowMessageType.DownloadStartFail); - // await the response - var result = MeadowDeviceManager.WaitForResponseMessage(meadow, - filter, ResponseWaitTime).GetAwaiter().GetResult(); - - // if it failed, bail out - if (!result.Success) { return; } - - // if it's an ESP start file transfer and the download started ok. - if (requestType == HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER - && - result.MessageType == MeadowMessageType.DownloadStartOkay) { - Console.WriteLine($"done"); - } - // if it's an ESP file transfer statrt and it failed to start - else if (requestType == HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER && - result.MessageType == MeadowMessageType.DownloadStartFail) { - Console.WriteLine("failed."); - } - - // if the download didn't start ok. - if (result.MessageType != MeadowMessageType.DownloadStartOkay) - { - if (result.MessageType == MeadowMessageType.DownloadStartFail) { - Console.WriteLine("Halting download due to an error while preparing Meadow for download"); - } else if (result.MessageType == MeadowMessageType.Concluded) { - Console.WriteLine("Halting download due to an unexpectedly Meadow 'Concluded' received prematurely"); - } else { - Console.WriteLine($"Halting download due to an unexpected Meadow message type {result.MessageType} received"); - } - // bail out - return; - } - - // 22 May 21 Peter - not sure how the following prevented the 'semaphore timeout' error. - // With the addition of file start handshaking we are notified when the file start has - // completed so the following Sleep is probably not necessary. Leaving code just incase - // it's needed by someone else. - // - // if (requestType == HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER) - // { - // // For the ESP32 file download, the proceeding command will erase - // // the ESP32 on chip flash memory before we can download. If the - // // file is large enough, the time to erase the flash will prevent - // // data from being downloaded and the 'semaphore timeout' error - // // will cause the CLI to disconnect. - - // if ((uint)fileBytes.Length > 1024 * 200) - // { - // // Using 6 ms / kbyte - // int eraseDelay = (6 * fileBytes.Length) / 1000; - // // Console.WriteLine($"Large file download delay:{eraseDelay} mSec"); - // System.Threading.Thread.Sleep(eraseDelay); - // } - // Console.WriteLine("done."); - // } - - // Since the Start was Successful we can sent the data - // Build and send the file as a group of data packets - int fileBufOffset = 0; - int numbToSend; - ushort sequenceNumber = 1; - - // don't echo the device responses - _device.LocalEcho = false; - - WriteProgress(-1); - while (fileBufOffset <= fileBytes.Length - 1) // equal would mean past the end - { - if ((fileBufOffset + MeadowDeviceManager.MaxAllowableMsgPacketLength) > (fileBytes.Length - 1)) - { - numbToSend = fileBytes.Length - fileBufOffset; // almost done, last packet - } - else - { - numbToSend = MeadowDeviceManager.MaxAllowableMsgPacketLength; - } - - // Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-Sending file data packet {sequenceNumber}"); // TESTING - BuildAndSendDataPacketRequest(fileBytes, fileBufOffset, numbToSend, sequenceNumber); - - var progress = fileBufOffset * 100 / fileBytes.Length; - WriteProgress(progress); - - fileBufOffset += numbToSend; - - sequenceNumber++; - } - WriteProgress(101); - - // echo the device responses - Thread.Sleep(250); // if we're too fast, we'll finish and the device will still echo a little - _device.LocalEcho = true; - - //-------------------------------------------------------------- - // Build and send the correct File End request - switch (requestType) - { - // Provide the correct message end depending on the reason the file - // is being downloaded to the F7 file system. - case HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER: - BuildAndSendSimpleCommand(HcomMeadowRequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, - lastInSeries ? (uint)1 : (uint)0); // set UserData - break; - case HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME: - BuildAndSendSimpleCommand(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, - lastInSeries ? (uint)1 : (uint)0); // set UserData - break; - case HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER: - BuildAndSendSimpleCommand(HcomMeadowRequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, - lastInSeries ? (uint)1 : (uint)0); // set UserData - break; - default: - Console.WriteLine($"File end Meadow request type of {requestType} not defined"); - break; - } - - // bufferOffset should point to the byte after the last byte - Debug.Assert(fileBufOffset == fileBytes.Length); - if (Verbose) Console.WriteLine($"Total bytes sent {fileBufOffset:N0} in {sequenceNumber:N0} packets. PacketCRC:{_packetCrc32:x08}"); - } - catch (Exception except) - { - Debug.WriteLine("{DateTime.Now:HH:mm:ss.fff}-Exception sending to Meadow:{0}", except); - throw; - } - } - - private void WriteProgress(int i) - { - // 50 characters - 2% each - if (i < 0) - { - Console.Write($"[ ]"); - } - else if(i > 100) - { - Console.WriteLine($"\r[==================================================]"); - } - else - { - var p = i / 2; - //Console.WriteLine($"{i} | {p}"); - Console.Write($"\r[{new string('=', p)}{new string(' ', 50 - p)}]"); - } - } - //========================================================================== - internal async Task SendSimpleCommand(HcomMeadowRequestType requestType, uint userData = 0, bool doAcceptedCheck = true) - { - var tcs = new TaskCompletionSource(); - var received = false; - - if (!doAcceptedCheck) - { - BuildAndSendSimpleCommand(requestType, userData); - return true; - } - - EventHandler handler = (s, e) => - { - if (e.MessageType == MeadowMessageType.Accepted) - { - received = true; - tcs.SetResult(true); - } - }; - - if (_device.DataProcessor != null) _device.DataProcessor.OnReceiveData += handler; - - BuildAndSendSimpleCommand(requestType, userData); - - await Task.WhenAny(new Task[] { tcs.Task, Task.Delay(10000) }); - - if (_device.DataProcessor != null) _device.DataProcessor.OnReceiveData -= handler; - - if (!received) - { - throw new Exception("Command not accepted."); - } - return received; - } - - //========================================================================== - // Prepare a data packet for sending - private void BuildAndSendDataPacketRequest(byte[] messageBytes, int messageOffset, - int messageSize, ushort seqNumb) - { - try - { - // Need to prepend the sequence number to the packet - int xmitSize = messageSize + sizeof(ushort); - byte[] fullMsg = new byte[xmitSize]; - - byte[] seqBytes = BitConverter.GetBytes(seqNumb); - Array.Copy(seqBytes, fullMsg, sizeof(ushort)); - Array.Copy(messageBytes, messageOffset, fullMsg, sizeof(ushort), messageSize); - - EncodeAndSendPacket(fullMsg, 0, xmitSize); - } - catch (Exception except) - { - Console.WriteLine($"An exception was caught: {except}"); - throw; - } - } - - //========================================================================== - // Build and send a "simple" message with data - // Added for Visual Studio Debugging but used by others - internal void BuildAndSendSimpleData(byte[] additionalData, HcomMeadowRequestType requestType, uint userData) - { - int totalMsgLength = additionalData.Length + HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH; - var messageBytes = new byte[totalMsgLength]; - - // Populate the header - BuildMeadowBoundSimpleCommand(requestType, userData, ref messageBytes); - - // Copy the payload into the message - Array.Copy(additionalData, 0, messageBytes, - HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH, additionalData.Length); - - EncodeAndSendPacket(messageBytes, 0, totalMsgLength); - } - - //========================================================================== - // Build and send a "simple" message with only a header - internal void BuildAndSendSimpleCommand(HcomMeadowRequestType requestType, uint userData) - { - var messageBytes = new byte[HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH]; - - // Populate the header - BuildMeadowBoundSimpleCommand(requestType, userData, ref messageBytes); - EncodeAndSendPacket(messageBytes, 0, HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH); - } - - //========================================================================== - /// - /// Convenience method to build the core part of the command packet. - /// - /// - /// - /// The actual message that got gets built. - /// The size of the message - private int BuildMeadowBoundSimpleCommand( - HcomMeadowRequestType requestType, - uint userData, - ref byte[] messageBytes) - { - // Note: Could use the StructLayout attribute to build - int offset = 0; - - // Two byte seq numb - Array.Copy(BitConverter.GetBytes((ushort)HCOM_PROTOCOL_COMMAND_SEQ_NUMBER), 0, - messageBytes, offset, sizeof(ushort)); - offset += sizeof(ushort); - - // Protocol version - Array.Copy(BitConverter.GetBytes(Constants.HCOM_PROTOCOL_CURRENT_VERSION_NUMBER), 0, messageBytes, offset, sizeof(ushort)); - offset += sizeof(ushort); - - // Command type (2 bytes) - Array.Copy(BitConverter.GetBytes((ushort)requestType), 0, messageBytes, offset, sizeof(ushort)); - offset += sizeof(ushort); - - // Extra Data - Array.Copy(BitConverter.GetBytes((ushort)HCOM_PROTOCOL_EXTRA_DATA_DEFAULT_VALUE), 0, messageBytes, offset, sizeof(ushort)); - offset += sizeof(ushort); - - // User Data - Array.Copy(BitConverter.GetBytes((uint)userData), 0, messageBytes, offset, sizeof(uint)); - offset += sizeof(uint); - - return offset; - } - - //========================================================================== - /// - /// Builds up a file command from the parameters and then sends it off to - /// the Meadow device. - /// - /// The type of request to build. - /// - /// - /// - /// - /// - /// - internal void BuildAndSendFileRelatedCommand( - HcomMeadowRequestType requestType, - uint userData, - uint fileSize, - uint fileCheckSum, - uint mcuAddr, - string md5Hash, - string destFileName) - { - // Future: Try to use the StructLayout attribute - Debug.Assert(md5Hash.Length == 0 || md5Hash.Length == HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH); - - // Allocate the correctly size message buffers - byte[] targetFileName = Encoding.UTF8.GetBytes(destFileName); // Using UTF-8 works for ASCII but should be Unicode in nuttx - byte[] md5HashBytes = Encoding.UTF8.GetBytes(md5Hash); - int optionalDataLength = sizeof(uint) + sizeof(uint) + sizeof(uint) + - HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH + targetFileName.Length; - byte[] messageBytes = new byte[HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH + optionalDataLength]; - - // Add the required header - int offset = BuildMeadowBoundSimpleCommand(requestType, userData, ref messageBytes); - - // File Size - Array.Copy(BitConverter.GetBytes(fileSize), 0, messageBytes, offset, sizeof(uint)); - offset += sizeof(uint); - - // CRC32 checksum or delete file partition number - Array.Copy(BitConverter.GetBytes(fileCheckSum), 0, messageBytes, offset, sizeof(uint)); - offset += sizeof(uint); - - // MCU address for this file. Used for ESP32 file downloads - Array.Copy(BitConverter.GetBytes(mcuAddr), 0, messageBytes, offset, sizeof(uint)); - offset += sizeof(uint); - - // Include ESP32 MD5 hash if it's needed - if (string.IsNullOrEmpty(md5Hash)) - Array.Clear(messageBytes, offset, HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH); - else - Array.Copy(md5HashBytes, 0, messageBytes, offset, HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH); - offset += HCOM_PROTOCOL_REQUEST_MD5_HASH_LENGTH; - - // Destination File Name - Array.Copy(targetFileName, 0, messageBytes, offset, targetFileName.Length); - offset += targetFileName.Length; - - Debug.Assert(offset == optionalDataLength + HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH); - EncodeAndSendPacket(messageBytes, 0, offset); - } - - //========================================================================== - // Last stop before transmitting information - private void EncodeAndSendPacket(byte[] messageBytes, int messageOffset, int messageSize) - { - try - { - // For testing calculate the crc including the sequence number - _packetCrc32 = CrcTools.Crc32part(messageBytes, messageSize, 0, _packetCrc32); - - // Add 2, first to account for start delimiter and second for end - byte[] encodedBytes = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload + 2]; - // Skip first byte so it can be a start delimiter - int encodedToSend = CobsTools.CobsEncoding(messageBytes, messageOffset, messageSize, ref encodedBytes, 1); - - // Verify COBS - any delimiters left? Skip first byte - for (int i = 1; i < encodedToSend; i++) - { - if (encodedBytes[i] == 0x00) - { - throw new InvalidProgramException("All zeros should have been removed. " + - $"There's one at offset of {i}"); - } - } - - // Terminate packet with delimiter so packet boundaries can be more easily found - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; - - try - { - if (_device.Socket != null) - { - _device.Socket.Send(encodedBytes, encodedToSend, - System.Net.Sockets.SocketFlags.None); - } - else - { - if (_device.SerialPort == null) - throw new NotConnectedException(); - - if (!_device.SerialPort.IsOpen) - { - _device.AttemptToReconnectToMeadow(); - } - - _device.SerialPort.Write(encodedBytes, 0, encodedToSend); - } - - } - catch (InvalidOperationException ioe) // Port not opened - { - Console.WriteLine("Write but port not opened. Exception: {0}", ioe); - throw; - } - catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer - { - Console.WriteLine("Write buffer, offset and count don't line up. Exception: {0}", aore); - throw; - } - catch (ArgumentException ae) // offset plus count > buffer length - { - Console.WriteLine("Write offset plus count > buffer length. Exception: {0}", ae); - throw; - } - catch (TimeoutException te) // Took too long to send - { - Console.WriteLine("Write took too long to send. Exception: {0}", te); - throw; - } - } - catch (Exception except) - { - Debug.WriteLine($"EncodeAndSendPacket threw: {except}"); - throw; - } - } - } - - public class NotConnectedException : Exception { } -} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/Command.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/Command.cs new file mode 100644 index 00000000..337c13b9 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/Command.cs @@ -0,0 +1,105 @@ +using System; +using Meadow.CLI.Core.DeviceManagement; +using MeadowCLI; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + public class Command + { + private protected const int HcomProtocolCommandRequiredHeaderLength = 12; + private protected const int HcomProtocolCommandSeqNumber = 0; + private protected const ushort HcomProtocolExtraDataDefaultValue = 0x0000; + private protected const int HcomProtocolRequestMd5HashLength = 32; + + public Command(HcomMeadowRequestType requestType, TimeSpan timeout, uint userData, byte[]? data, Predicate responsePredicate, Predicate completionPredicate, EventHandler? responseHandler) + { + RequestType = requestType; + Timeout = timeout; + UserData = userData; + Data = data; + ResponsePredicate = responsePredicate; + CompletionPredicate = completionPredicate; + ResponseHandler = responseHandler; + } + + public HcomMeadowRequestType RequestType { get; protected set; } + public uint UserData { get; protected set; } + public TimeSpan Timeout { get; protected set; } + public byte[]? Data { get; protected set; } + public Predicate ResponsePredicate { get; protected set; } + public Predicate CompletionPredicate { get; protected set; } + public EventHandler? ResponseHandler { get; protected set; } + + protected int ToMessageBytes(ref byte[] messageBytes) + { + int offset = 0; + + // Two byte seq numb + Array.Copy( + BitConverter.GetBytes((ushort)HcomProtocolCommandSeqNumber), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // Protocol version + Array.Copy( + BitConverter.GetBytes(Constants.HCOM_PROTOCOL_CURRENT_VERSION_NUMBER), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // Command type (2 bytes) + Array.Copy( + BitConverter.GetBytes((ushort)RequestType), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // Extra Data + Array.Copy( + BitConverter.GetBytes(HcomProtocolExtraDataDefaultValue), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // User Data + Array.Copy(BitConverter.GetBytes(UserData), 0, messageBytes, offset, sizeof(uint)); + offset += sizeof(uint); + + if (Data != null) + { + Array.Copy( + Data, + 0, + messageBytes, + HcomProtocolCommandRequiredHeaderLength, + Data.Length); + + offset += Data.Length; + } + + return offset; + } + + public virtual byte[] ToMessageBytes() + { + var messageSize = HcomProtocolCommandRequiredHeaderLength + (Data?.Length).GetValueOrDefault(0); + var messageBytes = new byte[messageSize]; + // Note: Could use the StructLayout attribute to build + ToMessageBytes(ref messageBytes); + return messageBytes; + } + } +} \ No newline at end of file diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/CommandResponse.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/CommandResponse.cs new file mode 100644 index 00000000..7ce23899 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/CommandResponse.cs @@ -0,0 +1,16 @@ +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + public class CommandResponse + { + public CommandResponse(bool isSuccess, string? message, MeadowMessageType messageType) + { + IsSuccess = isSuccess; + Message = message; + MessageType = messageType; + } + + public bool IsSuccess { get; } + public string? Message { get; } + public MeadowMessageType MessageType { get;} + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommand.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommand.cs new file mode 100644 index 00000000..11ff06de --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommand.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.Text; +using Meadow.CLI.Core.DeviceManagement; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + public class FileCommand : Command + { + internal FileCommand(HcomMeadowRequestType requestType, + TimeSpan timeout, + string sourceFileName, + string destinationFileName, + string? md5Hash, + uint crc32, + int fileSize, + uint partition, + uint mcuAddress, + byte[]? fileBytes, + Predicate responseHandler, + Predicate completionHandler) + : base(requestType, timeout, partition, null, responseHandler, completionHandler, null) + { + SourceFileName = sourceFileName; + DestinationFileName = destinationFileName; + Md5Hash = md5Hash??string.Empty; + Crc32 = crc32; + FileSize = fileSize; + Partition = partition; + McuAddress = mcuAddress; + FileBytes = fileBytes; + } + + public string DestinationFileName { get; protected set; } + public string SourceFileName { get; protected set; } + public string Md5Hash { get; protected set; } + public uint Crc32 { get; protected set; } + public int FileSize { get; protected set; } + public uint Partition { get; protected set; } + public uint McuAddress { get; protected set; } + public byte[]? FileBytes { get; protected set; } + + public override byte[] ToMessageBytes() + { + // Allocate the correctly size message buffers + byte[] targetFileName = Encoding.UTF8.GetBytes(DestinationFileName); // Using UTF-8 works for ASCII but should be Unicode in nuttx + + byte[] md5HashBytes = Encoding.UTF8.GetBytes(Md5Hash); + int optionalDataLength = sizeof(uint) + + sizeof(uint) + + sizeof(uint) + + HcomProtocolRequestMd5HashLength + + targetFileName.Length; + + byte[] messageBytes = new byte[HcomProtocolCommandRequiredHeaderLength + optionalDataLength]; + + var offset = base.ToMessageBytes(ref messageBytes); + Array.Copy(BitConverter.GetBytes(FileSize), 0, messageBytes, offset, sizeof(uint)); + offset += sizeof(uint); + + // CRC32 checksum or delete file partition number + Array.Copy(BitConverter.GetBytes(Crc32), 0, messageBytes, offset, sizeof(uint)); + offset += sizeof(uint); + + // MCU address for this file. Used for ESP32 file downloads + Array.Copy(BitConverter.GetBytes(McuAddress), 0, messageBytes, offset, sizeof(uint)); + offset += sizeof(uint); + + // Include ESP32 MD5 hash if it's needed + if (string.IsNullOrEmpty(Md5Hash)) + Array.Clear(messageBytes, offset, HcomProtocolRequestMd5HashLength); + else + Array.Copy(md5HashBytes, 0, messageBytes, offset, HcomProtocolRequestMd5HashLength); + + offset += HcomProtocolRequestMd5HashLength; + + // Destination File Name + Array.Copy(targetFileName, 0, messageBytes, offset, targetFileName.Length); + offset += targetFileName.Length; + + Debug.Assert(offset == optionalDataLength + HcomProtocolCommandRequiredHeaderLength); + + return messageBytes; + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommandBuilder.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommandBuilder.cs new file mode 100644 index 00000000..3d36abfe --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/FileCommandBuilder.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Meadow.CLI.Core.DeviceManagement; +using Meadow.CLI.Core.DeviceManagement.Tools; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + public class FileCommandBuilder + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + private readonly Dictionary> _predicates = + new Dictionary>() + { + { + HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, + p => p.MessageType == MeadowMessageType.Concluded + || p.MessageType == MeadowMessageType.DownloadStartOkay + || p.MessageType == MeadowMessageType.DownloadStartFail + }, + { + HcomMeadowRequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + p => p.MessageType == MeadowMessageType.Concluded + || p.MessageType == MeadowMessageType.DownloadStartOkay + || p.MessageType == MeadowMessageType.DownloadStartFail + }, + { + HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, + p => p.MessageType == MeadowMessageType.Concluded + || p.MessageType == MeadowMessageType.DownloadStartOkay + || p.MessageType == MeadowMessageType.DownloadStartFail + } + }; + + public FileCommandBuilder(HcomMeadowRequestType requestType) + { + RequestType = requestType; + if (_predicates.ContainsKey(RequestType)) + { + CompletionPredicate = _predicates[RequestType]; + ResponsePredicate = _predicates[RequestType]; + } + } + + private protected MeadowMessageType? ResponseMessageType; + private protected MeadowMessageType? CompletionMessageType; + + public HcomMeadowRequestType RequestType { get; protected set; } + public uint UserData { get; protected set; } + public TimeSpan? Timeout { get; protected set; } + public byte[]? Data { get; protected set; } + public string? DestinationFileName { get; protected set; } + public string? SourceFileName { get; protected set; } + public string? Md5Hash { get; protected set; } + public uint Crc32 { get; protected set; } + public int FileSize { get; protected set; } + public uint Partition { get; protected set; } + public uint McuAddress { get; protected set; } + public byte[]? FileBytes { get; protected set; } + public Predicate? ResponsePredicate { get; protected set; } + public Predicate? CompletionPredicate { get; protected set; } + + public FileCommandBuilder WithDestinationFileName(string destinationFileName) + { + DestinationFileName = destinationFileName; + return this; + } + + public FileCommandBuilder WithSourceFileName(string sourceFileName) + { + SourceFileName = sourceFileName; + return this; + } + + public FileCommandBuilder WithMd5Hash(string md5Hash) + { + Md5Hash = md5Hash; + return this; + } + + public FileCommandBuilder WithCrc32(uint crc32) + { + Crc32 = crc32; + return this; + } + + public FileCommandBuilder WithPartition(uint partition) + { + Partition = partition; + return this; + } + + public FileCommandBuilder WithMcuAddress(uint mcuAddress) + { + McuAddress = mcuAddress; + return this; + } + + public FileCommandBuilder WithFileBytes(byte[] fileBytes) + { + FileBytes = fileBytes; + return this; + } + + public FileCommandBuilder WithTimeout(TimeSpan timeout) + { + Timeout = timeout; + return this; + } + + public FileCommandBuilder WithResponseType(MeadowMessageType responseMessageType) + { + ResponseMessageType = responseMessageType; + return this; + } + + public FileCommandBuilder WithCompletionResponseType(MeadowMessageType completionMessageType) + { + CompletionMessageType = completionMessageType; + return this; + } + + public FileCommandBuilder WithResponseFilter(Predicate predicate) + { + ResponsePredicate = predicate; + return this; + } + + public FileCommandBuilder WithCompletionFilter(Predicate predicate) + { + CompletionPredicate = predicate; + return this; + } + + public async Task BuildAsync() + { + if (RequestType != HcomMeadowRequestType.HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME) + { + if (string.IsNullOrWhiteSpace(SourceFileName)) + { + throw new ArgumentNullException(SourceFileName); + } + + if (FileBytes == null) + { + var fi = new FileInfo(SourceFileName); + if (!fi.Exists) + { + throw new FileNotFoundException("Cannot find source file", fi.FullName); + } + + FileBytes = await File.ReadAllBytesAsync(SourceFileName) + .ConfigureAwait(false); + } + + FileSize = FileBytes.Length; + if (Md5Hash == null) + { + // Calculate the file hashes + using var md5 = MD5.Create(); + var hash = md5.ComputeHash(FileBytes); + if (McuAddress != 0) + { + Md5Hash = BitConverter.ToString(hash) + .Replace("-", "") + .ToLowerInvariant(); + } + } + + if (Crc32 == 0) + { + Crc32 = CrcTools.Crc32part(FileBytes, FileBytes.Length, 0); + } + } + else + { + SourceFileName ??= DestinationFileName; + } + + DestinationFileName ??= Path.GetFileName(SourceFileName); + + if (ResponsePredicate == null) + { + if (ResponseMessageType != null) + ResponsePredicate = e => e.MessageType == ResponseMessageType; + else ResponsePredicate = e => e.MessageType == MeadowMessageType.Concluded; + } + + if (CompletionPredicate == null) + { + if (CompletionMessageType != null) + CompletionPredicate = e => e.MessageType == CompletionMessageType; + else CompletionPredicate = e => e.MessageType == MeadowMessageType.Concluded; + } + + return new FileCommand( + RequestType, + Timeout ?? DefaultTimeout, + SourceFileName, + DestinationFileName, + Md5Hash, + Crc32, + FileSize, + Partition, + McuAddress, + FileBytes, + ResponsePredicate, + CompletionPredicate); + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowComms/HostComBuffer.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/HostComBuffer.cs similarity index 93% rename from Meadow.CLI.Core/Internals/MeadowComms/HostComBuffer.cs rename to Meadow.CLI.Core/Internals/MeadowCommunication/HostComBuffer.cs index 54775ed9..b10ff05a 100644 --- a/Meadow.CLI.Core/Internals/MeadowComms/HostComBuffer.cs +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/HostComBuffer.cs @@ -1,172 +1,170 @@ -using MeadowCLI.DeviceManagement; -using System; -using System.Diagnostics; - -namespace MeadowCLI.Hcom -{ - //============================================================== - // Circular Buffer - // This circular buffer was written for the Meadow F7. Unlike the classic - // version, this version has a byte array as input (received data) and - // returns a byte array consisting of everything from the head to the - // first byte whose value is 0. It's designed to work with the COBS - // encoding scheme. - public enum HcomBufferReturn - { - // Leaving in this form to match 'C' coding style - HCOM_CIR_BUF_INIT_OK, - HCOM_CIR_BUF_INIT_FAILED, - - HCOM_CIR_BUF_GET_FOUND_MSG, - HCOM_CIR_BUF_GET_NONE_FOUND, - HCOM_CIR_BUF_GET_BUF_NO_ROOM, - - HCOM_CIR_BUF_ADD_SUCCESS, - HCOM_CIR_BUF_ADD_WONT_FIT, - HCOM_CIR_BUF_ADD_BAD_ARG - } - - public class HostCommBuffer - { - byte[] hcomCircularBuffer; - - int bottom; // bottom of buffer - int top; // top end of buffer - int head; - int tail; - - readonly object bufferLock = new object(); - - //------------------------------------------------------------------------------ - public HcomBufferReturn Init(int totalCapacity) - { - hcomCircularBuffer = new byte[totalCapacity]; - bottom = 0; - top = bottom + totalCapacity; - head = bottom; - tail = bottom; - - return HcomBufferReturn.HCOM_CIR_BUF_INIT_OK; - } - - //------------------------------------------------------------------------------ - private int GetAvailableSpace() - { - // We leave one free byte so the head and tail are equal only if - // empty not when full. Full means 1 unused byte. - if (head < tail) - return tail - head - 1; - else - return ((top - bottom) - (head - tail)) - 1; - } - - //------------------------------------------------------------------------------ - // Adds 1 to n bytes to the head of the circular buffer - public HcomBufferReturn AddBytes(byte[] newBytes, int bytesOffset, int bytesToAdd) - { - lock (bufferLock) - { - if (bytesToAdd == 0) - return HcomBufferReturn.HCOM_CIR_BUF_ADD_BAD_ARG; - - if (GetAvailableSpace() < bytesToAdd) - return HcomBufferReturn.HCOM_CIR_BUF_ADD_WONT_FIT; - - int newHead = head + bytesToAdd; - - if (newHead < top) - { - // Simple case (no wrap around) - Array.Copy(newBytes, bytesOffset, hcomCircularBuffer, head, bytesToAdd); - head = newHead; - } - else - { - // Wrap around - fill up head-top space and use bottom space too - int spaceFreeOnTop = top - head; - // Fill top - Array.Copy(newBytes, bytesOffset, hcomCircularBuffer, head, spaceFreeOnTop); - // Fill bottom - Array.Copy(newBytes, spaceFreeOnTop + bytesOffset, hcomCircularBuffer, - 0, bytesToAdd - spaceFreeOnTop); - - head = bottom + bytesToAdd - spaceFreeOnTop; - } - return HcomBufferReturn.HCOM_CIR_BUF_ADD_SUCCESS; - } - } - - //------------------------------------------------------------------------------ - // Caller must supply packetBuffer and the buffers size - // Note: These message are longer because they are encoded - public HcomBufferReturn GetNextPacket(byte[] packetBuffer, - int packetBufferSize, out int packetLength) - { - lock (bufferLock) - { - int foundOffset; - int sizeFoundTop; - - packetLength = 0; - if (head == tail) - return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; - - // Scan the buffer looking for the delimiter 0x00 - if (head > tail) - { - // Simple case (no wrap around) - foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, tail, head - tail); - - if (foundOffset == -1) - return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; - } - else - { - foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, tail, top - tail); - } - - if (foundOffset != -1) - { - // Found the delimiter, message in one contiguous block - sizeFoundTop = foundOffset - tail + 1; - - if (sizeFoundTop > packetBufferSize) - { - packetLength = sizeFoundTop; - Console.WriteLine($"1. Need buffer with {packetLength} bytes, max:{MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload}"); - //Debug.Assert(false); - return HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM; - } - - Array.Copy(hcomCircularBuffer, tail, packetBuffer, 0, sizeFoundTop); - tail = foundOffset + 1; - packetLength = sizeFoundTop; - return HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG; - } - - // Continue looking for the delimiter from the bottom up since we got here - // because the delimiter was not found while scanning - foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, bottom, head - bottom); - if (foundOffset == -1) - return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; - - sizeFoundTop = top - tail; - int sizeFoundBottom = foundOffset - bottom + 1; - if (sizeFoundBottom + sizeFoundTop > packetBufferSize) - { - packetLength = sizeFoundTop; - Console.WriteLine($"2. Need buffer with {packetLength} bytes, max:{MeadowDeviceManager.MaxAllowableMsgPayloadLength}"); - //Debug.Assert(false); - return HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM; - } - - Array.Copy(hcomCircularBuffer, tail, packetBuffer, 0, sizeFoundTop); - Array.Copy(hcomCircularBuffer, bottom, packetBuffer, sizeFoundTop, sizeFoundBottom); - - tail = foundOffset + 1; - packetLength = sizeFoundTop + sizeFoundBottom; - return HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG; - } - } - } +using System; +using Meadow.CLI.Core.DeviceManagement; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + //============================================================== + // Circular Buffer + // This circular buffer was written for the Meadow F7. Unlike the classic + // version, this version has a byte array as input (received data) and + // returns a byte array consisting of everything from the head to the + // first byte whose value is 0. It's designed to work with the COBS + // encoding scheme. + public enum HcomBufferReturn + { + // Leaving in this form to match 'C' coding style + HCOM_CIR_BUF_INIT_OK, + HCOM_CIR_BUF_INIT_FAILED, + + HCOM_CIR_BUF_GET_FOUND_MSG, + HCOM_CIR_BUF_GET_NONE_FOUND, + HCOM_CIR_BUF_GET_BUF_NO_ROOM, + + HCOM_CIR_BUF_ADD_SUCCESS, + HCOM_CIR_BUF_ADD_WONT_FIT, + HCOM_CIR_BUF_ADD_BAD_ARG + } + + public class HostCommBuffer + { + byte[] hcomCircularBuffer; + + int bottom; // bottom of buffer + int top; // top end of buffer + int head; + int tail; + + readonly object bufferLock = new object(); + + //------------------------------------------------------------------------------ + public HcomBufferReturn Init(int totalCapacity) + { + hcomCircularBuffer = new byte[totalCapacity]; + bottom = 0; + top = bottom + totalCapacity; + head = bottom; + tail = bottom; + + return HcomBufferReturn.HCOM_CIR_BUF_INIT_OK; + } + + //------------------------------------------------------------------------------ + private int GetAvailableSpace() + { + // We leave one free byte so the head and tail are equal only if + // empty not when full. Full means 1 unused byte. + if (head < tail) + return tail - head - 1; + else + return ((top - bottom) - (head - tail)) - 1; + } + + //------------------------------------------------------------------------------ + // Adds 1 to n bytes to the head of the circular buffer + public HcomBufferReturn AddBytes(byte[] newBytes, int bytesOffset, int bytesToAdd) + { + lock (bufferLock) + { + if (bytesToAdd == 0) + return HcomBufferReturn.HCOM_CIR_BUF_ADD_BAD_ARG; + + if (GetAvailableSpace() < bytesToAdd) + return HcomBufferReturn.HCOM_CIR_BUF_ADD_WONT_FIT; + + int newHead = head + bytesToAdd; + + if (newHead < top) + { + // Simple case (no wrap around) + Array.Copy(newBytes, bytesOffset, hcomCircularBuffer, head, bytesToAdd); + head = newHead; + } + else + { + // Wrap around - fill up head-top space and use bottom space too + int spaceFreeOnTop = top - head; + // Fill top + Array.Copy(newBytes, bytesOffset, hcomCircularBuffer, head, spaceFreeOnTop); + // Fill bottom + Array.Copy(newBytes, spaceFreeOnTop + bytesOffset, hcomCircularBuffer, + 0, bytesToAdd - spaceFreeOnTop); + + head = bottom + bytesToAdd - spaceFreeOnTop; + } + return HcomBufferReturn.HCOM_CIR_BUF_ADD_SUCCESS; + } + } + + //------------------------------------------------------------------------------ + // Caller must supply packetBuffer and the buffers size + public HcomBufferReturn GetNextPacket(byte[] packetBuffer, + int packetBufferSize, out int packetLength) + { + lock (bufferLock) + { + int foundOffset; + int sizeFoundTop; + + packetLength = 0; + if (head == tail) + return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; + + // Scan the buffer looking for the delimiter 0x00 + if (head > tail) + { + // Simple case (no wrap around) + foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, tail, head - tail); + + if (foundOffset == -1) + return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; + } + else + { + foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, tail, top - tail); + } + + if (foundOffset != -1) + { + // Found the delimiter, message in one contiguous block + sizeFoundTop = foundOffset - tail + 1; + + if (sizeFoundTop > packetBufferSize) + { + packetLength = sizeFoundTop; + Console.WriteLine($"1. Need buffer with {packetLength} bytes, max:{MeadowDeviceManager.MaxAllowableMsgPayloadLength}"); + //Debug.Assert(false); + return HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM; + } + + Array.Copy(hcomCircularBuffer, tail, packetBuffer, 0, sizeFoundTop); + tail = foundOffset + 1; + packetLength = sizeFoundTop; + return HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG; + } + + // Continue looking for the delimiter from the bottom up since we got here + // because the delimiter was not found while scanning + foundOffset = Array.IndexOf(hcomCircularBuffer, (byte)0x00, bottom, head - bottom); + if (foundOffset == -1) + return HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND; + + sizeFoundTop = top - tail; + int sizeFoundBottom = foundOffset - bottom + 1; + if (sizeFoundBottom + sizeFoundTop > packetBufferSize) + { + packetLength = sizeFoundTop; + Console.WriteLine($"2. Need buffer with {packetLength} bytes, max:{MeadowDeviceManager.MaxAllowableMsgPayloadLength}"); + //Debug.Assert(false); + return HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM; + } + + Array.Copy(hcomCircularBuffer, tail, packetBuffer, 0, sizeFoundTop); + Array.Copy(hcomCircularBuffer, bottom, packetBuffer, sizeFoundTop, sizeFoundBottom); + + tail = foundOffset + 1; + packetLength = sizeFoundTop + sizeFoundBottom; + return HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG; + } + } + } } \ No newline at end of file diff --git a/Meadow.CLI.Core/Internals/MeadowComms/MeadowSerialDataProcessor.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/MeadowSerialDataProcessor.cs similarity index 61% rename from Meadow.CLI.Core/Internals/MeadowComms/MeadowSerialDataProcessor.cs rename to Meadow.CLI.Core/Internals/MeadowCommunication/MeadowSerialDataProcessor.cs index ba7a71c0..cbfef2a1 100644 --- a/Meadow.CLI.Core/Internals/MeadowComms/MeadowSerialDataProcessor.cs +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/MeadowSerialDataProcessor.cs @@ -1,407 +1,422 @@ -using System; -using System.Diagnostics; -using System.IO.Ports; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Meadow.CLI.Internals.MeadowComms.RecvClasses; -using MeadowCLI.DeviceManagement; -using static MeadowCLI.DeviceManagement.MeadowFileManager; - -namespace MeadowCLI.Hcom -{ - // For data received due to a CLI request these provide a secondary - // type of identification. The primary being the protocol request value - public enum MeadowMessageType - { - AppOutput, - ErrOutput, - DeviceInfo, - FileListTitle, - FileListMember, - FileListCrcMember, - Data, - InitialFileData, - MeadowTrace, - SerialReconnect, - Accepted, - Concluded, - DownloadStartOkay, - DownloadStartFail, - } - - public class MeadowMessageEventArgs : EventArgs - { - public string Message { get; private set; } - public MeadowMessageType MessageType { get; private set; } - - public MeadowMessageEventArgs(MeadowMessageType messageType, string message = "") - { - Message = message; - MessageType = messageType; - } - } - - public class MeadowSerialDataProcessor - { - //collapse to one and use enum - public EventHandler OnReceiveData; - HostCommBuffer _hostCommBuffer; - RecvFactoryManager _recvFactoryManager; - readonly SerialPort serialPort; - readonly Socket socket; - - // It seems that the .Net SerialPort class is not all it could be. - // To acheive reliable operation some SerialPort class methods must - // not be used. When receiving, the BaseStream must be used. - // http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport - - //------------------------------------------------------------- - // Constructor - public MeadowSerialDataProcessor() - { - _recvFactoryManager = new RecvFactoryManager(); - _hostCommBuffer = new HostCommBuffer(); - _hostCommBuffer.Init(MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload * 4); - - } - - public MeadowSerialDataProcessor(SerialPort serialPort) : this() - { - this.serialPort = serialPort; - var t = ReadSerialPortAsync(); - } - - public MeadowSerialDataProcessor(Socket socket) : this() - { - this.socket = socket; - var t = ReadSocketAsync(); - } - - //------------------------------------------------------------- - // All received data handled here - private async Task ReadSocketAsync() - { - byte[] buffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; - - try - { - while (true) - { - var segment = new ArraySegment(buffer); - var receivedLength = await socket.ReceiveAsync(segment, SocketFlags.None).ConfigureAwait(false); - - AddAndProcessData(buffer, receivedLength); - - await Task.Delay(50).ConfigureAwait(false); - } - } - catch (ThreadAbortException) - { - //ignoring for now until we wire cancelation ... - //this blocks the thread abort exception when the console app closes - } - catch (InvalidOperationException) - { - // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info - } - catch (Exception ex) - { - ConsoleOut($"Exception: {ex} may mean the target connection dropped"); - } - } - - //------------------------------------------------------------- - // All received data handled here - private async Task ReadSerialPortAsync() - { - byte[] buffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; - - try - { - while (true) - { - if (!serialPort.IsOpen) break; - - var byteCount = Math.Min(serialPort.BytesToRead, buffer.Length); - - if (byteCount > 0) - { - var receivedLength = await serialPort.BaseStream.ReadAsync(buffer, 0, byteCount).ConfigureAwait(false); - AddAndProcessData(buffer, receivedLength); - } - await Task.Delay(50).ConfigureAwait(false); - } - } - catch (ThreadAbortException) - { - //ignoring for now until we wire cancelation ... - //this blocks the thread abort exception when the console app closes - } - catch (InvalidOperationException) - { - // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info - } - catch (Exception ex) - { - ConsoleOut($"Exception: {ex} may mean the target connection dropped"); - } - } - - void AddAndProcessData(byte[] buffer, int availableBytes) - { - HcomBufferReturn result; - while (true) - { - // Add these bytes to the circular buffer - result = _hostCommBuffer.AddBytes(buffer, 0, availableBytes); - if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_SUCCESS) - { - break; - } - else if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_WONT_FIT) - { - // Wasn't possible to put these bytes in the buffer. We need to - // process a few packets and then retry to add this data - result = PullAndProcessAllPackets(); - if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG || - result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND) - continue; // There should be room now for the failed add - - if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM) - { - // The buffer to receive the message is too small? Probably - // corrupted data in buffer. - Debug.Assert(false); - } - } - else if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_BAD_ARG) - { - // Something wrong with implemenation - Debug.Assert(false); - } - else - { - // Undefined return value???? - Debug.Assert(false); - } - } - - result = PullAndProcessAllPackets(); - - // Any other response is an error - Debug.Assert(result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG || - result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND); - } - - HcomBufferReturn PullAndProcessAllPackets() - { - byte[] packetBuffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; - byte[] decodedBuffer = new byte[MeadowDeviceManager.MaxAllowableMsgPacketLength]; - int packetLength; - HcomBufferReturn result; - - while (true) - { - result = _hostCommBuffer.GetNextPacket(packetBuffer, - MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload, out packetLength); - if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND) - break; // We've emptied buffer of all messages - - if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM) - { - // The buffer to receive the message is too small! Perhaps - // corrupted data in buffer. - // I don't know why but without the following 2 lines the Debug.Assert will - // assert eventhough the following line is not executed? - Console.WriteLine($"Need a buffer with {packetLength} bytes, not {MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload}"); - Thread.Sleep(1); - Debug.Assert(false); - } - - // Only other possible outcome is success - Debug.Assert(result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG); - - // It's possible that we may find a series of 0x00 values in the buffer. - // This is because when the sender is blocked (because this code isn't - // running) it will attempt to send a single 0x00 before the full message. - // This allows it to test for a connection. When the connection is - // unblocked this 0x00 is sent and gets put into the buffer along with - // any others that were queued along the usb serial pipe line. - if (packetLength == 1) - { - //ConsoleOut("Throwing out 0x00 from buffer"); - continue; - } - - int decodedSize = CobsTools.CobsDecoding(packetBuffer, --packetLength, ref decodedBuffer); - - // If a message is too short it is ignored - if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) - continue; - - Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); - - // Process the received packet - if (decodedSize > 0) - { - bool procResult = ParseAndProcessReceivedPacket(decodedBuffer, decodedSize); - if (procResult) - continue; // See if there's another packet ready - } - break; // processing errors exit - } - return result; - } - - bool ParseAndProcessReceivedPacket(byte[] receivedMsg, int receivedMsgLen) - { - try - { - IReceivedMessage processor = _recvFactoryManager.CreateProcessor(receivedMsg, receivedMsgLen); - if (processor == null) - { - return false; - } - - if (processor.Execute(receivedMsg, receivedMsgLen)) - { - switch (processor.RequestType) - { - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_UNDEFINED_REQUEST: - ConsoleOut("Request Undefined"); // TESTING - break; - - // This set are responses to request issued by this application - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_REJECTED: - ConsoleOut("Request Rejected"); // TESTING - if (!string.IsNullOrEmpty(processor.ToString())) - { - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, processor.ToString())); - } - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: - // ConsoleOut($"{DateTime.Now:HH:mm:ss.fff}-Request Accepted"); // TESTING - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Accepted)); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: - // ConsoleOut($"{DateTime.Now:HH:mm:ss.fff}-Request Concluded"); // TESTING - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Concluded)); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ERROR: - ConsoleOut("Request Error"); // TESTING - if (!string.IsNullOrEmpty(processor.ToString())) - { - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, processor.ToString())); - } - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_INFORMATION: - //ConsoleOut("protocol-Request Information"); // TESTING - if (!string.IsNullOrEmpty(processor.ToString())) - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER: - //ConsoleOut("protocol-Request File List Header received"); // TESTING - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListTitle, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER: - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListMember, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER: - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListCrcMember, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT: - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.AppOutput, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR: - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.ErrOutput, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DeviceInfo, processor.ToString())); - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_TRACE_MSG: - if (!string.IsNullOrEmpty(processor.ToString())) - { - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.MeadowTrace, processor.ToString())); - } - break; - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_RECONNECT: - Thread.Sleep(2000); // need to give the device a couple seconds - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.SerialReconnect, null)); - break; - - // Debug message from Meadow for Visual Studio - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA: - //ConsoleOut($"Debugging message from Meadow for Visual Studio"); // TESTING - MeadowDeviceManager.ForwardMonoDataToVisualStudio(processor.MessageData); - break; - - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_OKAY: - // ConsoleOut("protocol-File Start OKAY received"); // TESTING - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DownloadStartOkay)); - break; - - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_FAIL: - // ConsoleOut("protocol-File Start FAIL received"); // TESTING - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DownloadStartFail)); - break; - - case (ushort)HcomHostRequestType.HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES: +using System; +using System.Diagnostics; +using System.IO.Ports; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.DeviceManagement; +using Meadow.CLI.Core.DeviceManagement.Tools; +using Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + // For data received due to a CLI request these provide a secondary + // type of identification. The primary being the protocol request value + public enum MeadowMessageType + { + AppOutput, + ErrOutput, + DeviceInfo, + FileListTitle, + FileListMember, + FileListCrcMember, + Data, + InitialFileData, + MeadowTrace, + SerialReconnect, + Accepted, + Concluded, + DownloadStartOkay, + DownloadStartFail, + } + + public class MeadowSerialDataProcessor : MeadowDataProcessor, IDisposable + { + private readonly ILogger _logger; + //collapse to one and use enum + private readonly SerialPort _serialPort; + readonly Socket _socket; + private readonly Task _dataProcessorTask; + + private readonly HostCommBuffer _hostCommBuffer; + private readonly ReceiveMessageFactoryManager _receiveMessageFactoryManager; + private readonly CancellationTokenSource _cts; + + // It seems that the .Net SerialPort class is not all it could be. + // To acheive reliable operation some SerialPort class methods must + // not be used. When receiving, the BaseStream must be used. + // http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport + + //------------------------------------------------------------- + // Constructor + private MeadowSerialDataProcessor(ILogger logger) + { + _cts = new CancellationTokenSource(); + _receiveMessageFactoryManager = new ReceiveMessageFactoryManager(logger); + _hostCommBuffer = new HostCommBuffer(); + _hostCommBuffer.Init(MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload * 4); + _logger = logger; + } + + public MeadowSerialDataProcessor(SerialPort serialPort, ILogger? logger = null) : this(logger ?? new NullLogger()) + { + _serialPort = serialPort; + _dataProcessorTask = Task.Factory.StartNew(ReadSerialPortAsync, TaskCreationOptions.LongRunning); + } + + public MeadowSerialDataProcessor(Socket socket, ILogger? logger = null) : this(logger ?? new NullLogger()) + { + this._socket = socket; + _dataProcessorTask = Task.Factory.StartNew(ReadSocketAsync, TaskCreationOptions.LongRunning); + } + + //------------------------------------------------------------- + // All received data handled here + private async Task ReadSocketAsync() + { + byte[] buffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; + + try + { + while (true) + { + var segment = new ArraySegment(buffer); + var receivedLength = await _socket.ReceiveAsync(segment, SocketFlags.None).ConfigureAwait(false); + + await AddAndProcessData(buffer, receivedLength, _cts.Token).ConfigureAwait(false); + + await Task.Delay(50).ConfigureAwait(false); + } + } + catch (ThreadAbortException) + { + //ignoring for now until we wire cancellation ... + //this blocks the thread abort exception when the console app closes + } + catch (InvalidOperationException) + { + // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info + } + catch (Exception ex) + { + _logger.LogTrace($"Exception: {ex} may mean the target connection dropped"); + } + } + + //------------------------------------------------------------- + // All received data handled here + private async Task ReadSerialPortAsync() + { + byte[] buffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; + + try + { + while (true) + { + if (!_serialPort.IsOpen) + { + await Task.Delay(500) + .ConfigureAwait(false); + continue; + } + + var byteCount = Math.Min(_serialPort.BytesToRead, buffer.Length); + + if (byteCount > 0) + { + var receivedLength = await _serialPort.BaseStream.ReadAsync(buffer, 0, byteCount).ConfigureAwait(false); + await AddAndProcessData(buffer, receivedLength, _cts.Token).ConfigureAwait(false); + } + await Task.Delay(50).ConfigureAwait(false); + } + } + catch (ThreadAbortException) + { + //ignoring for now until we wire cancellation ... + //this blocks the thread abort exception when the console app closes + } + catch (InvalidOperationException) + { + // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info + } + catch (Exception ex) + { + _logger.LogTrace($"Exception: {ex} may mean the target connection dropped"); + } + } + + private async Task AddAndProcessData(byte[] buffer, int availableBytes, CancellationToken cancellationToken) + { + HcomBufferReturn result; + + while (true) + { + // Add these bytes to the circular buffer + result = _hostCommBuffer.AddBytes(buffer, 0, availableBytes); + if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_SUCCESS) + { + break; + } + else if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_WONT_FIT) + { + // Wasn't possible to put these bytes in the buffer. We need to + // process a few packets and then retry to add this data + result = await PullAndProcessAllPackets(cancellationToken).ConfigureAwait(false); + if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG || + result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND) + continue; // There should be room now for the failed add + + if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM) + { + // The buffer to receive the message is too small? Probably + // corrupted data in buffer. + Debug.Assert(false); + } + } + else if (result == HcomBufferReturn.HCOM_CIR_BUF_ADD_BAD_ARG) + { + // Something wrong with implementation + Debug.Assert(false); + } + else + { + // Undefined return value???? + Debug.Assert(false); + } + } + + result = await PullAndProcessAllPackets(cancellationToken).ConfigureAwait(false); + + // Any other response is an error + Debug.Assert(result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG || + result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND); + } + + private async Task PullAndProcessAllPackets(CancellationToken cancellationToken) + { + byte[] packetBuffer = new byte[MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload]; + byte[] decodedBuffer = new byte[MeadowDeviceManager.MaxAllowableMsgPacketLength]; + int packetLength; + HcomBufferReturn result; + + while (true) + { + result = _hostCommBuffer.GetNextPacket(packetBuffer, MeadowDeviceManager.MaxAllowableMsgPacketLength, out packetLength); + if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_NONE_FOUND) + break; // We've emptied buffer of all messages + + if (result == HcomBufferReturn.HCOM_CIR_BUF_GET_BUF_NO_ROOM) + { + // The buffer to receive the message is too small! Perhaps + // corrupted data in buffer. + // I don't know why but without the following 2 lines the Debug.Assert will + // assert eventhough the following line is not executed? + Console.WriteLine($"Need a buffer with {packetLength} bytes, not {MeadowDeviceManager.MaxEstimatedSizeOfEncodedPayload}"); + Thread.Sleep(1); + Debug.Assert(false); + } + + // Only other possible outcome is success + Debug.Assert(result == HcomBufferReturn.HCOM_CIR_BUF_GET_FOUND_MSG); + + // It's possible that we may find a series of 0x00 values in the buffer. + // This is because when the sender is blocked (because this code isn't + // running) it will attempt to send a single 0x00 before the full message. + // This allows it to test for a connection. When the connection is + // unblocked this 0x00 is sent and gets put into the buffer along with + // any others that were queued along the usb serial pipe line. + if (packetLength == 1) + { + //_logger.LogTrace("Throwing out 0x00 from buffer"); + continue; + } + + int decodedSize = CobsTools.CobsDecoding(packetBuffer, --packetLength, ref decodedBuffer); + + // If a message is too short it is ignored + if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + continue; + + Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + + // Process the received packet + if (decodedSize > 0) + { + bool procResult = await ParseAndProcessReceivedPacket(decodedBuffer, decodedSize, cancellationToken).ConfigureAwait(false); + if (procResult) + continue; // See if there's another packet ready + } + break; // processing errors exit + } + return result; + } + + private async Task ParseAndProcessReceivedPacket(byte[] receivedMsg, int receivedMsgLen, CancellationToken cancellationToken) + { + try + { + var processor = _receiveMessageFactoryManager.CreateProcessor(receivedMsg, receivedMsgLen); + if (processor == null) + return false; + + if (processor.Execute(receivedMsg, receivedMsgLen)) + { + var requestType = (HcomHostRequestType)processor.RequestType; + _logger.LogTrace("Received message {messageType}", requestType); + switch (requestType) + { + case HcomHostRequestType.HCOM_HOST_REQUEST_UNDEFINED_REQUEST: + _logger.LogTrace("Request Undefined"); // TESTING + break; + + // This set are responses to request issued by this application + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_REJECTED: + _logger.LogTrace("Request Rejected"); // TESTING + if (!string.IsNullOrEmpty(processor.ToString())) + { + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, processor.ToString())); + } + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: + _logger.LogTrace("Request Accepted"); // TESTING + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Accepted)); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: + _logger.LogTrace("Request Concluded"); // TESTING + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Concluded)); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ERROR: + _logger.LogTrace("Request Error"); // TESTING + if (!string.IsNullOrEmpty(processor.ToString())) + { + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, processor.ToString())); + } + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_INFORMATION: + { + var msg = processor.ToString(); + _logger.LogTrace("Received request text information: {message}", msg); + if (!string.IsNullOrEmpty(processor.ToString())) + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.Data, msg)); + + break; + } + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER: + _logger.LogTrace("protocol-Request File List Header received"); // TESTING + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListTitle, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListMember, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.FileListCrcMember, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.AppOutput, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.ErrOutput, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DeviceInfo, processor.ToString())); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_TRACE_MSG: + if (!string.IsNullOrEmpty(processor.ToString())) + { + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.MeadowTrace, processor.ToString())); + } + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_RECONNECT: + await Task.Delay(2000, cancellationToken).ConfigureAwait(false); // need to give the device a couple seconds + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.SerialReconnect, null)); + break; + + // Debug message from Meadow for Visual Studio + case HcomHostRequestType.HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA: + _logger.LogTrace($"Debugging message from Meadow for Visual Studio"); // TESTING + // TODO: Refactor to expose this without needing a MeadowDevice + if (ForwardDebuggingData != null) + await ForwardDebuggingData(processor.MessageData, cancellationToken) + .ConfigureAwait(false); + + //_device.ForwardMonoDataToVisualStudio(processor.MessageData); + break; + case HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_OKAY: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DownloadStartOkay)); + break; + + case HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_FAIL: + OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.DownloadStartFail)); + break; + + case HcomHostRequestType.HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES: + { // Just length and hex-hex-hex.... - // Console.WriteLine($"Received {processor.MessageData.Length} bytes. They look like this: {Environment.NewLine}{BitConverter.ToString(processor.MessageData)}"); - - var msg = System.Text.Encoding.UTF8.GetString(processor.MessageData); - - OnReceiveData?.Invoke(this, new MeadowMessageEventArgs(MeadowMessageType.InitialFileData, msg)); - break; - } - return true; - } - else - { - return false; - } - } - catch (Exception ex) - { - ConsoleOut($"Exception: {ex}"); - return false; - } - } - - void ConsoleOut(string msg) - { -#if DEBUG - Console.WriteLine(msg); -#endif - } - - /* - // Save for testing in case we suspect data corruption of text - // The protocol requires the first 12 bytes to be the header. The first 2 are 0x00, - // the next 10 are binary. After this the rest are ASCII text or binary. - // Test the message and if it fails it's trashed. - if(decodedBuffer[0] != 0x00 || decodedBuffer[1] != 0x00) - { - ConsoleOut("Corrupted message, first 2 bytes not 0x00"); - continue; - } - - int buffOffset; - for(buffOffset = MeadowDeviceManager.HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH; - buffOffset < decodedSize; - buffOffset++) - { - if(decodedBuffer[buffOffset] < 0x20 || decodedBuffer[buffOffset] > 0x7e) - { - ConsoleOut($"Corrupted message, non-ascii at offset:{buffOffset} value:{decodedBuffer[buffOffset]}"); - break; - } - } - - // Throw away if we found non ASCII where only text should be - if (buffOffset < decodedSize) - continue; - */ - } + // Console.WriteLine($"Received {processor.MessageData.Length} bytes. They look like this: {Environment.NewLine}{BitConverter.ToString(processor.MessageData)}"); + + var msg = System.Text.Encoding.UTF8.GetString(processor.MessageData); + + OnReceiveData?.Invoke( + this, + new MeadowMessageEventArgs(MeadowMessageType.InitialFileData, msg)); + + break; + } + } + return true; + } + else + { + return false; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "An error occurred parsing a received packet"); + return false; + } + } + + /* + // Save for testing in case we suspect data corruption of text + // The protocol requires the first 12 bytes to be the header. The first 2 are 0x00, + // the next 10 are binary. After this the rest are ASCII text or binary. + // Test the message and if it fails it's trashed. + if(decodedBuffer[0] != 0x00 || decodedBuffer[1] != 0x00) + { + _logger.LogTrace("Corrupted message, first 2 bytes not 0x00"); + continue; + } + + int buffOffset; + for(buffOffset = MeadowDeviceManager.HCOM_PROTOCOL_COMMAND_REQUIRED_HEADER_LENGTH; + buffOffset < decodedSize; + buffOffset++) + { + if(decodedBuffer[buffOffset] < 0x20 || decodedBuffer[buffOffset] > 0x7e) + { + _logger.LogTrace($"Corrupted message, non-ascii at offset:{buffOffset} value:{decodedBuffer[buffOffset]}"); + break; + } + } + + // Throw away if we found non ASCII where only text should be + if (buffOffset < decodedSize) + continue; + */ + public void Dispose() + { + try + { + _cts.Cancel(); + _dataProcessorTask?.Dispose(); + } + catch (Exception ex) + { + _logger.LogTrace(ex, "Exception during disposal"); + } + } + } } \ No newline at end of file diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/DebuggingServer.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/DebuggingServer.cs new file mode 100644 index 00000000..bf584c6d --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/DebuggingServer.cs @@ -0,0 +1,289 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + // This TCP server directly interacts with Visual Studio debugging. + // What it receives from Visual Studio it forwards to Meadow. + // What it receives from Meadow it forwards to Visual Studio. + public class DebuggingServer : IDisposable + { + // VS 2019 - 4024 + // VS 2017 - 4022 + // VS 2015 - 4020 + public IPEndPoint LocalEndpoint { get; private set; } + + private readonly ILogger _logger; + private readonly MeadowDevice _meadow; + private ActiveClient? _activeClient; + private int _activeClientCount = 0; + + // Constructor + public DebuggingServer(MeadowDevice meadow, IPEndPoint localEndpoint, ILogger logger) + { + LocalEndpoint = localEndpoint; + _meadow = meadow; + _logger = logger; + } + + public async Task StartListeningAsync() + { + try + { + var tcpListener = new TcpListener(LocalEndpoint); + tcpListener.Start(); + LocalEndpoint = (IPEndPoint)tcpListener.LocalEndpoint; + _logger.LogInformation("Listening for Visual Studio to connect on {address}:{port}", LocalEndpoint.Address, LocalEndpoint.Port); + + while (true) + { + // Wait for client to connect + TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync(); + OnConnect(tcpClient); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while listening for debugging connections"); + } + } + + void OnConnect(TcpClient tcpClient) + { + try + { + _logger.LogInformation("Visual Studio has connected"); + lock (this) + { + if (_activeClientCount > 0 && _activeClient?.Disposed == false) + { + Debug.Assert(_activeClientCount == 1); + Debug.Assert(_activeClient != null); + CloseActiveClient(); + } + + _activeClient = new ActiveClient(_meadow, tcpClient, _logger); + _activeClientCount++; + } + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + internal void CloseActiveClient() + { + _activeClient?.Dispose(); + _activeClient = null; + _activeClientCount = 0; + } + + public Task SendToVisualStudio(byte[] byteData, CancellationToken cancellationToken) + { + return _activeClient != null ? _activeClient.SendToVisualStudio(byteData, cancellationToken) : Task.CompletedTask; + } + + public void Dispose() + { + _activeClient?.Dispose(); + } + + // Embedded class + private class ActiveClient : IDisposable + { + private readonly MeadowDevice _meadow; + private readonly TcpClient _tcpClient; + private readonly NetworkStream _networkStream; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + //private readonly Task _receiverTask; + //private readonly Task _pusherTask; + private readonly Task _receiveVsDebugTask; + private readonly ILogger _logger; + public bool Disposed = false; + + // Constructor + internal ActiveClient(MeadowDevice meadow, TcpClient tcpClient, ILogger logger) + { + _logger = logger; + _meadow = meadow; + _tcpClient = tcpClient; + _networkStream = tcpClient.GetStream(); + _logger.LogDebug("Starting receive task"); + _receiveVsDebugTask = Task.Factory.StartNew( + ReceiveVsDebug, + TaskCreationOptions.LongRunning); + //var pipe = new Pipe(); + //_receiverTask = Task.Factory.StartNew(() => + // ReadDataFromMeadow(_networkStream, pipe.Writer, _cts.Token), + // TaskCreationOptions.LongRunning); + + //_pusherTask = Task.Factory.StartNew(() => PushDataToVs(pipe.Reader, _cts.Token), TaskCreationOptions.LongRunning); + } + + // TODO: Finish implementing pipe + private async Task ReadDataFromMeadow(Stream stream, PipeWriter writer, CancellationToken cancellationToken) + { + const int minimumBufferSize = 1024; + while (!cancellationToken.IsCancellationRequested) + { + // Allocate at least 512 bytes from the PipeWriter + var memory = writer.GetMemory(minimumBufferSize); + try + { + var bytesRead = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + if (bytesRead == 0) + { + break; + } + // Tell the PipeWriter how much was read from the Socket + writer.Advance(bytesRead); + } + catch (Exception ex) + { + _logger.LogError(ex, "foo"); + break; + } + + // Make the data available to the PipeReader + var result = await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + + if (result.IsCompleted) + { + break; + } + } + } + + private async Task PushDataToVs(PipeReader reader, CancellationToken cancellationToken) + { + while (true) + { + var result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + + var buffer = result.Buffer; + SequencePosition? position = null; + + do + { + // Look for a EOL in the buffer + + if (position != null) + { + //await _meadow.ForwardVisualStudioDataToMonoAsync(buffer) + + // Skip the line + the \n character (basically position) + buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); + } + } + while (position != null); + + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.Start, buffer.End); + + // Stop reading if there's no more data coming + if (result.IsCompleted) + { + break; + } + } + + // Mark the PipeReader as complete + await reader.CompleteAsync().ConfigureAwait(false); + } + + private async Task ReceiveVsDebug() + { + _logger.LogDebug("Start receiving from VS"); + try + { + // Receive from Visual Studio and send to Meadow + var ct = _cts.Token; + while (_tcpClient.Connected && !_cts.IsCancellationRequested) + { + var meadowBuffer = Array.Empty(); + while (_networkStream.DataAvailable) + { + var receivedBuffer = ArrayPool.Shared.Rent(1024); + var bytesRead = await _networkStream + .ReadAsync(receivedBuffer, 0, receivedBuffer.Length, ct) + .ConfigureAwait(false); + + if (bytesRead == 0 || !_cts.IsCancellationRequested) + continue; + + var destIndex = meadowBuffer.Length; + Array.Resize(ref meadowBuffer, destIndex + bytesRead); + Array.Copy(receivedBuffer, 0, meadowBuffer, destIndex, bytesRead); + } + + _logger.LogTrace("Received {count} bytes from VS will forward to HCOM", meadowBuffer.Length); + + + // Forward to Meadow + await _meadow.ForwardVisualStudioDataToMonoAsync(meadowBuffer, 0, ct) + .ConfigureAwait(false); + + _logger.LogTrace("Forwarded {count} bytes from VS to HCOM", meadowBuffer.Length); + } + } + catch (IOException ioe) + { + // VS client probably died + _logger.LogError(ioe, "Error forwarding data to Mono"); + } + catch (Exception e) + { + _logger.LogError(e, "Error forwarding data to Mono"); + } + } + + public async Task SendToVisualStudio(byte[] byteData, CancellationToken cancellationToken) + { + _logger.LogTrace("Forwarding {count} bytes to VS", byteData.Length); + try + { + // Receive from Meadow and send to Visual Studio + if (!_tcpClient.Connected) + { + _logger.LogDebug("Cannot forward data, Visual Studio is not connected"); + return; + } + + await _networkStream.WriteAsync(byteData, 0, byteData.Length, cancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "Error sending data to Visual Studio"); + if (_cts.IsCancellationRequested) + throw; + } + } + + public void Dispose() + { + lock (_tcpClient) + { + if (Disposed) + return; + _tcpClient.Dispose(); + _networkStream.Dispose(); + _cts.Dispose(); + //_receiverTask?.Dispose(); + //_pusherTask?.Dispose(); + _receiveVsDebugTask?.Dispose(); + Disposed = true; + } + } + } + } +} \ No newline at end of file diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/IReceivedMessage.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/IReceivedMessage.cs new file mode 100644 index 00000000..a3ae6714 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/IReceivedMessage.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public interface IReceivedMessage + { + // Each derived class needs these + bool Execute(byte[] receivedMessage, int receivedMessageLen); + string ToString(); + + // These are in RecvHeader + ushort SeqNumber { get; } + ushort VersionNumber { get; } + ushort RequestType { get; } + ushort ExtraData { get; } + uint UserData { get; } + int HeaderLength { get; } + + byte[]? MessageData { get; } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveHeader.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveHeader.cs new file mode 100644 index 00000000..1a0a0261 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveHeader.cs @@ -0,0 +1,55 @@ +using System; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public class ReceiveHeader : IReceivedMessage + { + // Header 12 bytes for Header plus message data + public ushort SeqNumber { get; } + public ushort VersionNumber { get; } + public ushort RequestType { get; } + public ushort ExtraData { get; } + public uint UserData { get; } + public int HeaderLength { get ; } + + public byte[]? MessageData { get; } + public int MessageDataLength { get; } + + + public ReceiveHeader(byte[] receivedMessage, int receivedMessageLength) + { + // Recover the sequence number which must proceed all messages. + SeqNumber = Convert.ToUInt16(receivedMessage[HeaderLength] + (receivedMessage[HeaderLength + 1] << 8)); + HeaderLength += sizeof(ushort); + + VersionNumber = Convert.ToUInt16(receivedMessage[HeaderLength] + (receivedMessage[HeaderLength + 1] << 8)); + HeaderLength += sizeof(ushort); + + RequestType = Convert.ToUInt16(receivedMessage[HeaderLength] + (receivedMessage[HeaderLength + 1] << 8)); + HeaderLength += sizeof(ushort); + + ExtraData = Convert.ToUInt16(receivedMessage[HeaderLength] + (receivedMessage[HeaderLength + 1] << 8)); + HeaderLength += sizeof(ushort); + + UserData = Convert.ToUInt32(receivedMessage[HeaderLength] + (receivedMessage[HeaderLength + 1] << 8) + + (receivedMessage[HeaderLength + 2] << 16) + (receivedMessage[HeaderLength + 3] << 24)); + HeaderLength += sizeof(uint); + + MessageDataLength = receivedMessageLength - HeaderLength; + if(MessageDataLength > 0) + { + MessageData = new byte[MessageDataLength]; + Array.Copy(receivedMessage, HeaderLength, MessageData, 0, MessageDataLength); + } + else + { + MessageData = null; + } + } + + public virtual bool Execute(byte[] receivedMessage, int receivedMessageLen) + { + return true; + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactory.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactory.cs new file mode 100644 index 00000000..4781546f --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactory.cs @@ -0,0 +1,7 @@ +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public abstract class ReceiveMessageFactory + { + public abstract IReceivedMessage Create(byte[] receivedMessage, int receivedMessageLength); + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactoryManager.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactoryManager.cs new file mode 100644 index 00000000..0056a31a --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveMessageFactoryManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +using Meadow.CLI.Core.DeviceManagement; + +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public class ReceiveMessageFactoryManager + { + private readonly Dictionary _factories; + private readonly ILogger _logger; + public ReceiveMessageFactoryManager(ILogger logger) + { + _logger = logger; + _factories = new Dictionary + { + {HcomHostRequestType.HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA, new ReceiveSimpleBinaryFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_GET_INITIAL_FILE_BYTES, new ReceiveSimpleBinaryFactory() }, + + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_REJECTED, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ACCEPTED, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CONCLUDED, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_ERROR, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_INFORMATION, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_TRACE_MSG, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_RECONNECT, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_OKAY, new ReceiveSimpleTextFactory() }, + {HcomHostRequestType.HCOM_HOST_REQUEST_FILE_START_FAIL, new ReceiveSimpleTextFactory() }, + }; + } + + public IReceivedMessage? CreateProcessor(byte[] receivedMessage, int receivedMsgLen) + { + var requestType = HcomHostRequestType.HCOM_HOST_REQUEST_UNDEFINED_REQUEST; + try + { + requestType = FindRequestTypeValue(receivedMessage); + ReceiveMessageFactory factory = _factories[requestType]; + return factory.Create(receivedMessage, receivedMsgLen); + } + catch (KeyNotFoundException) + { + _logger.LogWarning($"An unknown request value of '0x{requestType:x}' was received."); + return null; + } + catch (Exception ex) + { + // I saw a few time, that this exception was being thrown. It was caused by + // corrupted data being processed. + _logger.LogWarning(ex, $"Request type was: 0x{requestType:x}"); + return null; + } + } + + private static HcomHostRequestType FindRequestTypeValue(IReadOnlyList receivedMessage) + { + const int requestTypeOffset = (int)HcomProtocolHeaderOffsets.HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET; + return (HcomHostRequestType)Convert.ToUInt16(receivedMessage[requestTypeOffset] + (receivedMessage[requestTypeOffset + 1] << 8)); + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinary.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinary.cs new file mode 100644 index 00000000..bdcb3d16 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinary.cs @@ -0,0 +1,25 @@ +using System; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + internal class ReceiveSimpleBinary : ReceiveHeader + { + public ReceiveSimpleBinary(byte[] receivedMessage, int receivedMessageLength) : base(receivedMessage, receivedMessageLength) + { + } + + public override bool Execute(byte[] receivedMessage, int receivedMessageLen) + { + try + { + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Exception:{ex.Message}"); + return false; + } + } + } +} + diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinaryFactory.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinaryFactory.cs new file mode 100644 index 00000000..3bf51ad8 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleBinaryFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public class ReceiveSimpleBinaryFactory : ReceiveMessageFactory + { + public override IReceivedMessage Create(byte[] receivedMessage, int receivedMessageLength) => new ReceiveSimpleBinary(receivedMessage, receivedMessageLength); + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessage.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessage.cs new file mode 100644 index 00000000..9ae3e57b --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessage.cs @@ -0,0 +1,24 @@ +using System; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + internal class ReceiveSimpleMessage : ReceiveHeader + { + public ReceiveSimpleMessage(byte[] receivedMessage, int receivedMessageLength) : base(receivedMessage, receivedMessageLength) + { + } + + public override bool Execute(byte[] receivedMessage, int receivedMessageLen) + { + try + { + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Exception:{ex.Message}"); + return false; + } + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessageFactory.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessageFactory.cs new file mode 100644 index 00000000..a7c0b721 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleMessageFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public class ReceiveSimpleMessageFactory : ReceiveMessageFactory + { + public override IReceivedMessage Create(byte[] receivedMessage, int receivedMessageLength) => new ReceiveSimpleMessage(receivedMessage, receivedMessageLength); + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleText.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleText.cs new file mode 100644 index 00000000..b98bcff2 --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleText.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + internal class ReceiveSimpleText : ReceiveHeader + { + public ReceiveSimpleText(byte[] receivedMessage, int receivedMessageLength) : base(receivedMessage, receivedMessageLength) + { + } + + public override bool Execute(byte[] receivedMessage, int receivedMessageLen) + { + try + { + if (receivedMessage.Length == HeaderLength) + { + throw new ArgumentException($"Received {nameof(ReceiveSimpleText)} with no text data"); + } + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Exception:{ex.Message}"); + return false; + } + } + public override string ToString() + { + return (MessageDataLength > 0) ? ASCIIEncoding.ASCII.GetString(MessageData) : string.Empty; + } + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleTextFactory.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleTextFactory.cs new file mode 100644 index 00000000..e9cde41c --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/ReceiveClasses/ReceiveSimpleTextFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses +{ + public class ReceiveSimpleTextFactory : ReceiveMessageFactory + { + public override IReceivedMessage Create(byte[] receivedMessage, int receivedMessageLength) => new ReceiveSimpleText(receivedMessage, receivedMessageLength); + } +} diff --git a/Meadow.CLI.Core/Internals/MeadowCommunication/SimpleCommandBuilder.cs b/Meadow.CLI.Core/Internals/MeadowCommunication/SimpleCommandBuilder.cs new file mode 100644 index 00000000..34d25cfb --- /dev/null +++ b/Meadow.CLI.Core/Internals/MeadowCommunication/SimpleCommandBuilder.cs @@ -0,0 +1,92 @@ +using System; +using Meadow.CLI.Core.DeviceManagement; + +namespace Meadow.CLI.Core.Internals.MeadowCommunication +{ + public class SimpleCommandBuilder + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + public SimpleCommandBuilder(HcomMeadowRequestType requestType) + { + RequestType = requestType; + } + + private protected MeadowMessageType? ResponseMessageType; + private protected MeadowMessageType? CompletionMessageType; + + private protected HcomMeadowRequestType RequestType { get; set; } + private protected uint UserData { get; set; } + private protected TimeSpan? Timeout { get; set; } + private protected byte[]? Data { get; set; } + private protected Predicate? ResponsePredicate { get; set; } + private protected Predicate? CompletionPredicate { get; set; } + private protected EventHandler? ResponseHandler { get; set; } + + public SimpleCommandBuilder WithTimeout(TimeSpan timeout) + { + Timeout = timeout; + return this; + } + + public SimpleCommandBuilder WithUserData(uint userData) + { + UserData = userData; + return this; + } + + public SimpleCommandBuilder WithData(byte[] data) + { + Data = data; + return this; + } + + public SimpleCommandBuilder WithResponseType(MeadowMessageType responseMessageType) + { + ResponseMessageType = responseMessageType; + return this; + } + + public SimpleCommandBuilder WithCompletionResponseType(MeadowMessageType completionMessageType) + { + CompletionMessageType = completionMessageType; + return this; + } + + public SimpleCommandBuilder WithResponseFilter(Predicate predicate) + { + ResponsePredicate = predicate; + return this; + } + + public SimpleCommandBuilder WithCompletionFilter(Predicate predicate) + { + CompletionPredicate = predicate; + return this; + } + + public SimpleCommandBuilder WithResponseHandler(EventHandler handler) + { + ResponseHandler = handler; + return this; + } + + public Command Build() + { + if (ResponsePredicate == null) + { + if (ResponseMessageType != null) + ResponsePredicate = e => e.MessageType == ResponseMessageType; + else ResponsePredicate = e => e.MessageType == MeadowMessageType.Concluded; + } + + if (CompletionPredicate == null) + { + if (CompletionMessageType != null) + CompletionPredicate = e => e.MessageType == CompletionMessageType; + else CompletionPredicate = e => e.MessageType == MeadowMessageType.Concluded; + } + + return new Command(RequestType, Timeout ?? DefaultTimeout, UserData, Data, ResponsePredicate, CompletionPredicate, ResponseHandler); + } + } +} diff --git a/Meadow.CLI.Core/Internals/Tools/CobsTools.cs b/Meadow.CLI.Core/Internals/Tools/CobsTools.cs index 927474ea..7b778bb9 100644 --- a/Meadow.CLI.Core/Internals/Tools/CobsTools.cs +++ b/Meadow.CLI.Core/Internals/Tools/CobsTools.cs @@ -1,5 +1,4 @@ -using System; -namespace MeadowCLI.Hcom +namespace Meadow.CLI.Core.DeviceManagement.Tools { public static class CobsTools { diff --git a/Meadow.CLI.Core/Internals/Tools/CrcTools.cs b/Meadow.CLI.Core/Internals/Tools/CrcTools.cs index bde71ce5..1a44433b 100644 --- a/Meadow.CLI.Core/Internals/Tools/CrcTools.cs +++ b/Meadow.CLI.Core/Internals/Tools/CrcTools.cs @@ -1,4 +1,4 @@ -namespace MeadowCLI.Hcom +namespace Meadow.CLI.Core.DeviceManagement.Tools { public static class CrcTools { diff --git a/Meadow.CLI.Core/Internals/Tools/ModuleWeaver.cs b/Meadow.CLI.Core/Internals/Tools/ModuleWeaver.cs index 5ce60154..32e2f090 100644 --- a/Meadow.CLI.Core/Internals/Tools/ModuleWeaver.cs +++ b/Meadow.CLI.Core/Internals/Tools/ModuleWeaver.cs @@ -6,15 +6,15 @@ using System.Text; using Mono.Cecil; -namespace MeadowCLI +namespace Meadow.CLI.Core.Internals.Tools { public class WeaverCRC { public Action LogDebug { get; set; } public Action LogInfo { get; set; } - public ModuleDefinition ModuleDefinition { get; set; } - public IAssemblyResolver AssemblyResolver { get; set; } + public ModuleDefinition? ModuleDefinition { get; set; } + public IAssemblyResolver? AssemblyResolver { get; set; } public WeaverCRC() { diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.csproj index b11a0064..abedd7c7 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -1,13 +1,15 @@  - netstandard2.0 + netstandard2.1 AnyCPU true MeadowCLIKey.snk false false false + latest + enable @@ -33,8 +35,10 @@ + + diff --git a/Meadow.CLI.Core/PackageVersions.cs b/Meadow.CLI.Core/PackageVersions.cs index ffabd778..e83a639f 100644 --- a/Meadow.CLI.Core/PackageVersions.cs +++ b/Meadow.CLI.Core/PackageVersions.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text.Json.Serialization; -namespace Meadow.CLI +namespace Meadow.CLI.Core { public class PackageVersions { - public string[] versions { get; set; } + [JsonPropertyName("versions")] + public string[] Versions { get; set; } } } diff --git a/Meadow.CLI.Core/Settings.cs b/Meadow.CLI.Core/Settings.cs index f7050bb3..25333ba3 100644 --- a/Meadow.CLI.Core/Settings.cs +++ b/Meadow.CLI.Core/Settings.cs @@ -4,13 +4,12 @@ using System.IO; using System.Linq; using System.Text.Json; -using System.Text.Json.Serialization; -namespace Meadow.CLI +namespace Meadow.CLI.Core { public static class SettingsManager { - readonly static string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "clisettings.json"); + private static readonly string Path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "clisettings.json"); public static string GetSetting(Setting setting) { @@ -34,14 +33,14 @@ public static void SaveSetting(Setting setting, string value) settings.Add(setting.ToString(), value); } - FileInfo fi = new FileInfo(path); + FileInfo fi = new FileInfo(Path); if (!Directory.Exists(fi.Directory.FullName)) { Directory.CreateDirectory(fi.Directory.FullName); } var json = JsonSerializer.Serialize(settings); - File.WriteAllText(path, json); + File.WriteAllText(Path, json); } public static string GetAppSetting(string name) @@ -58,18 +57,16 @@ public static string GetAppSetting(string name) private static Settings GetSettings() { - Settings settings; - - if (File.Exists(path)) + if (File.Exists(Path)) { - var json = File.ReadAllText(path); - settings = JsonSerializer.Deserialize(json); + var json = File.ReadAllText(Path); + var settings = JsonSerializer.Deserialize(json); + return settings ?? new Settings(); } else { - settings = new Settings(); + return new Settings(); } - return settings; } } diff --git a/Meadow.CLI.Test/ConnectionTest.cs b/Meadow.CLI.Test/ConnectionTest.cs index 06525d6f..e4f195d8 100644 --- a/Meadow.CLI.Test/ConnectionTest.cs +++ b/Meadow.CLI.Test/ConnectionTest.cs @@ -1,6 +1,8 @@ using System.IO; +using System.Threading; using System.Threading.Tasks; -using MeadowCLI.DeviceManagement; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; namespace Meadow.CLI.Test @@ -8,7 +10,7 @@ namespace Meadow.CLI.Test [TestFixture] public class ConnectionTest { - string port = "COM5"; + string port = "COM19"; public readonly string osFilename = "Meadow.OS.bin"; public readonly string runtimeFilename = "Meadow.OS.Runtime.bin"; public readonly string networkBootloaderFilename = "bootloader.bin"; @@ -19,41 +21,37 @@ public class ConnectionTest // All tests are run expecting the device to already be DFU flash with OS. [Test] - public async Task FlashOSTest() + public async Task FlashOsTest() { - //DfuUpload.FlashOS(Path.Combine(fixturesPath.FullName, osFilename)); - - using (var meadow = await MeadowDeviceManager.GetMeadowForSerialPort(port)) - { - Assert.IsNotNull(meadow, "Initial connection"); - await MeadowDeviceManager.MonoDisable(meadow); - var isEnabled = await MeadowDeviceManager.MonoRunState(meadow); - // try to disable one more time - if (isEnabled) - { - await MeadowDeviceManager.MonoDisable(meadow); - isEnabled = await MeadowDeviceManager.MonoRunState(meadow); - } - Assert.IsFalse(isEnabled, "Disable mono"); - } - - using (var meadow = await MeadowDeviceManager.GetMeadowForSerialPort(port)) - { - await MeadowFileManager.MonoUpdateRt(meadow, Path.Combine(fixturesPath.FullName, runtimeFilename)); - } + var cts = new CancellationTokenSource(); + var deviceManager = new MeadowDeviceManager(NullLoggerFactory.Instance); + await deviceManager.FlashOsAsync(port, string.Empty, cancellationToken: cts.Token); + } - using (var meadow = await MeadowDeviceManager.GetMeadowForSerialPort(port)) - { - await MeadowFileManager.FlashEsp(meadow, fixturesPath.FullName); - } + [Test] + public async Task MonoDisableTest() + { + var cts = new CancellationTokenSource(); + var deviceManager = new MeadowDeviceManager(NullLoggerFactory.Instance); + using var meadow = deviceManager.GetMeadowForSerialPort(port); + Assert.IsNotNull(meadow, "Initial connection"); + + await meadow.MonoDisableAsync(cts.Token); + var monoEnabled = await meadow.GetMonoRunStateAsync(cts.Token); + Assert.False(monoEnabled, "monoEnabled"); + } - using (var meadow = await MeadowDeviceManager.GetMeadowForSerialPort(port)) - { - Assert.IsNotNull(meadow); - await MeadowDeviceManager.MonoEnable(meadow); - var isEnabled = await MeadowDeviceManager.MonoRunState(meadow); - Assert.IsTrue(isEnabled); - } + [Test] + public async Task MonoEnableTest() + { + var cts = new CancellationTokenSource(); + var deviceManager = new MeadowDeviceManager(NullLoggerFactory.Instance); + using var meadow = deviceManager.GetMeadowForSerialPort(port); + Assert.IsNotNull(meadow, "Initial connection"); + + await meadow.MonoEnableAsync(cts.Token); + var monoEnabled = await meadow.GetMonoRunStateAsync(cts.Token); + Assert.True(monoEnabled, "monoEnabled"); } } } diff --git a/Meadow.CLI.Test/IdentityManagerTest.cs b/Meadow.CLI.Test/IdentityManagerTest.cs index f4ff386e..b712176b 100644 --- a/Meadow.CLI.Test/IdentityManagerTest.cs +++ b/Meadow.CLI.Test/IdentityManagerTest.cs @@ -1,5 +1,6 @@ using System; -using Meadow.CLI.Core.Auth; +using Meadow.CLI.Core.Identity; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; namespace Meadow.CLI.Test @@ -10,11 +11,11 @@ public class IdentityManagerTest [Test] public void CredentialStoreTest() { - string name = $"cli-test-{Guid.NewGuid().ToString("N")}"; + string name = $"cli-test-{Guid.NewGuid():N}"; string username = Guid.NewGuid().ToString("N"); string password = Guid.NewGuid().ToString("N"); - IdentityManager identityManager = new IdentityManager(); + IdentityManager identityManager = new IdentityManager(NullLogger.Instance); var saveResult = identityManager.SaveCredential(name, username, password); Assert.IsTrue(saveResult); diff --git a/Meadow.CLI/CliFxConsoleLoggerProvider.cs b/Meadow.CLI/CliFxConsoleLoggerProvider.cs new file mode 100644 index 00000000..80497377 --- /dev/null +++ b/Meadow.CLI/CliFxConsoleLoggerProvider.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using CliFx.Infrastructure; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI +{ + public class CliFxConsoleLogger : ILogger + { + private readonly CliFxConsoleLoggerProviderConfig _config; + private readonly string _name; + private readonly IConsole _console; + + public CliFxConsoleLogger( + IConsole console, + string name, + CliFxConsoleLoggerProviderConfig config) => + (_console, _name, _config) = (console, name, config); + + + public void Log(LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + _console.Output.WriteLine(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) => + logLevel >= _config.LogLevel; + + public IDisposable BeginScope(TState state) => default; + } + + public sealed class CliFxConsoleLoggerProvider : ILoggerProvider + { + private readonly CliFxConsoleLoggerProviderConfig _config; + private readonly IConsole _console; + private readonly ConcurrentDictionary _loggers = + new ConcurrentDictionary(); + + public CliFxConsoleLoggerProvider(CliFxConsoleLoggerProviderConfig config, IConsole console) => + (_config, _console) = (config, console); + + public ILogger CreateLogger(string categoryName) => + _loggers.GetOrAdd(categoryName, name => new CliFxConsoleLogger(_console, name, _config)); + + public void Dispose() => _loggers.Clear(); + } + + public class CliFxConsoleLoggerProviderConfig + { + public LogLevel LogLevel {get; init; } + } +} diff --git a/Meadow.CLI/Commands/App/DeployAppCommand.cs b/Meadow.CLI/Commands/App/DeployAppCommand.cs new file mode 100644 index 00000000..f41e6143 --- /dev/null +++ b/Meadow.CLI/Commands/App/DeployAppCommand.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.App +{ + [Command("app deploy", Description = "Deploy the specified app to the Meadow")] + public class DeployAppCommand : MeadowSerialCommand + { + [CommandOption( + "file", + 'f', + Description = "The path to the application to deploy to the app", + IsRequired = true)] + public string File { get; init; } + + [CommandOption("includePdbs", 'i', Description = "Include the PDB files on deploy to enable debugging", IsRequired = false)] + public bool IncludePdbs { get; init; } + + public DeployAppCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.MonoDisableAsync(cancellationToken) + .ConfigureAwait(false); + await Meadow.DeployAppAsync(File, IncludePdbs, cancellationToken) + .ConfigureAwait(false); + await Meadow.MonoEnableAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Cloud/LoginCommand.cs b/Meadow.CLI/Commands/Cloud/LoginCommand.cs new file mode 100644 index 00000000..3a3e8f01 --- /dev/null +++ b/Meadow.CLI/Commands/Cloud/LoginCommand.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Cloud +{ + [Command("cloud login", Description = "Log into the Meadow Service")] + public class LoginCommand : ICommand + { + private readonly ILogger _logger; + + public LoginCommand(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + var identityManager = new IdentityManager(_logger); + var loginResult = await identityManager.LoginAsync(cancellationToken) + .ConfigureAwait(false); + + if (loginResult) + { + var cred = identityManager.GetCredentials(identityManager.WlRefreshCredentialName); + _logger.LogInformation($"Signed in as {cred.username}"); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Cloud/LogoutCommand.cs b/Meadow.CLI/Commands/Cloud/LogoutCommand.cs new file mode 100644 index 00000000..09877463 --- /dev/null +++ b/Meadow.CLI/Commands/Cloud/LogoutCommand.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Cloud +{ + [Command("cloud logout", Description = "Logout of the Meadow Service")] + public class LogoutCommand : ICommand + { + private readonly ILogger _logger; + + public LogoutCommand(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + await Task.Yield(); + var identityManager = new IdentityManager(_logger); + identityManager.Logout(); + + _logger.LogInformation($"Signed out of Meadow Service"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs b/Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs new file mode 100644 index 00000000..bcb0285b --- /dev/null +++ b/Meadow.CLI/Commands/DeviceManagement/FlashEspCommand.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement +{ + [Command("flash esp", Description = "Flash the ESP co-processor")] + public class FlashEspCommand : MeadowSerialCommand + { + public FlashEspCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.FlashEspAsync(cancellationToken) + .ConfigureAwait(false); + + await Meadow.ResetMeadowAsync(cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs b/Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs new file mode 100644 index 00000000..5db734bd --- /dev/null +++ b/Meadow.CLI/Commands/DeviceManagement/FlashOsCommand.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement +{ + [Command("flash os", Description = "Update the OS on the Meadow Board")] + public class FlashOsCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public FlashOsCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = loggerFactory.CreateLogger(); + } + + [CommandOption("BinPath", 'b', Description = "Path to the Meadow OS binary")] + public string BinPath { get; init; } + + [CommandOption("skipDfu",'d', Description = "Skip DFU flash.")] + public bool SkipDfu { get; init; } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + Meadow?.Dispose(); + + await MeadowDeviceManager.FlashOsAsync(SerialPortName, BinPath, SkipDfu, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs new file mode 100644 index 00000000..93643a19 --- /dev/null +++ b/Meadow.CLI/Commands/DeviceManagement/GetDeviceInfoCommand.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement +{ + [Command("device info", Description = "Get the device info")] + public class GetDeviceInfoCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public GetDeviceInfoCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + var cancellationToken = console.RegisterCancellationHandler(); + + var deviceInfoString = await Meadow.GetDeviceInfoAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(deviceInfoString)) + throw new Exception("Unable to retrieve device info"); + var deviceInfo = new MeadowDeviceInfo(deviceInfoString); + _logger.LogInformation(deviceInfo.ToString()); + } + } +} diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs new file mode 100644 index 00000000..0520da27 --- /dev/null +++ b/Meadow.CLI/Commands/DeviceManagement/GetDeviceMacAddressCommand.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement +{ + [Command("device mac", Description = "Read the ESP32's MAC address")] + public class GetDeviceMacAddressCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public GetDeviceMacAddressCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + var macAddress = await Meadow.GetDeviceMacAddressAsync(cancellationToken).ConfigureAwait(false); + if (macAddress == null) + { + _logger.LogInformation("Unable to retrieve device mac address"); + } + else + { + _logger.LogInformation("Device MAC: {macAddress}", macAddress); + } + } + } +} diff --git a/Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs b/Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs new file mode 100644 index 00000000..93f93a9a --- /dev/null +++ b/Meadow.CLI/Commands/DeviceManagement/GetDeviceNameCommand.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement +{ + [Command("device name", Description = "Get the name of the Meadow")] + public class GetDeviceNameCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public GetDeviceNameCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + var deviceName = await Meadow.GetDeviceNameAsync(TimeSpan.FromSeconds(5), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Device Name: {deviceName}"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs b/Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs new file mode 100644 index 00000000..9b527aa0 --- /dev/null +++ b/Meadow.CLI/Commands/Esp32/RestartEsp32Command.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Esp32 +{ + [Command("esp32 restart", Description = "Restart the ESP32")] + public class RestartEsp32Command : MeadowSerialCommand + { + private readonly ILogger _logger; + + public RestartEsp32Command(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.RestartEsp32Async(cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs b/Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs new file mode 100644 index 00000000..fb533302 --- /dev/null +++ b/Meadow.CLI/Commands/Esp32/WriteEsp32FileCommand.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Esp32 +{ + [Command("esp32 file write", Description = "Write files to the ESP File System")] + public class WriteEsp32FileCommand : MeadowSerialCommand + { + [CommandOption( + "file", + 'f', + Description = "The file to write to the Meadow's ESP32 File System", + IsRequired = true)] + public string Filename { get; init; } + + [CommandOption( + "targetFile", + 't', + Description = "The filename to use on the Meadow's ESP32 File System")] + public string TargetFilename { get; init; } + + [CommandOption("McuDestAddress", Description = "Where file is stored in MCU's internal flash e.g. 0x10000", IsRequired = true)] + public string McuDestAddress { get; init; } + + private readonly ILogger _logger; + + public WriteEsp32FileCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + var targetFileName = string.IsNullOrWhiteSpace(TargetFilename) + ? GetTargetFileName() + : TargetFilename; + + _logger.LogInformation( + $"Writing {Filename} as {targetFileName} to ESP32"); + + _logger.LogDebug("Translated {filename} to {targetFileName}", Filename, targetFileName); + + Trace.Assert( + string.IsNullOrWhiteSpace(targetFileName) == false, + "string.IsNullOrWhiteSpace(targetFileName)"); + + if (!File.Exists(Filename)) + { + _logger.LogInformation("Cannot find {filename}", Filename); + } + else + { + await Meadow.WriteFileToEspFlashAsync(Filename, targetFileName, (uint)0, McuDestAddress, cancellationToken) + .ConfigureAwait(false); + + _logger.LogDebug("File written successfully"); + } + } + + private string GetTargetFileName() + { + return new FileInfo(Filename).Name; + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs b/Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs new file mode 100644 index 00000000..9fc5df35 --- /dev/null +++ b/Meadow.CLI/Commands/FileSystem/FormatFileSystemCommand.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.FileSystem +{ + [Command("filesystem format", Description = "Format a File System on the Meadow Board")] + public class FormatFileSystemCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public FormatFileSystemCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Formatting file system on partition {Partition}"); + + await Meadow.FormatFileSystemAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs b/Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs new file mode 100644 index 00000000..352959fb --- /dev/null +++ b/Meadow.CLI/Commands/FileSystem/RenewFileSystemCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.FileSystem +{ + [Command("filesystem renew", Description = "Create a File System on the Meadow Board")] + public class RenewFileSystemCommand : MeadowSerialCommand + { + public RenewFileSystemCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await console.Output.WriteLineAsync("Renewing file system on the Meadow."); + + await Meadow.RenewFileSystemAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Files/DeleteFileCommand.cs b/Meadow.CLI/Commands/Files/DeleteFileCommand.cs new file mode 100644 index 00000000..3bf02af2 --- /dev/null +++ b/Meadow.CLI/Commands/Files/DeleteFileCommand.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Files +{ + [Command("files delete", Description = "Delete files from the Meadow File System")] + public class DeleteFileCommand : MeadowSerialCommand + { + [CommandOption( + "files", + 'f', + Description = "The file(s) to delete from the Meadow Files System", + IsRequired = true)] + public IList Files { get; init; } + +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public uint Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public DeleteFileCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + foreach (var file in Files.Where(file => string.IsNullOrWhiteSpace(file) == false)) + { + if (!string.IsNullOrEmpty(file)) + { + _logger.LogInformation($"Deleting {file} from partition {Partition}"); + + await Meadow.DeleteFileAsync(file, Partition, cancellationToken) + .ConfigureAwait(false); + } + } + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs b/Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs new file mode 100644 index 00000000..dcf5416b --- /dev/null +++ b/Meadow.CLI/Commands/Files/InitialFileBytesCommand.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Files +{ + [Command("files initial bytes", Description = "Get the initial bytes from a file")] + public class InitialFileBytesCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to list the files")] +#endif + public uint Partition { get; init; } = 0; + + [CommandOption( + "file", + 'f', + Description = "The file to get the bytes from", + IsRequired = true)] + public string Filename { get; init; } + + private readonly ILogger _logger; + + public InitialFileBytesCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Getting files on partition {Partition}"); + + var res = await Meadow.GetInitialBytesFromFile(Filename, Partition, cancellationToken); + if (res != null) + { + _logger.LogInformation("Read {size} bytes from {filename}: {bytes}", + res.Length, + Filename, + res); + } + else + { + _logger.LogInformation("Failed to retrieve bytes from {filename}", Filename); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Files/ListFilesCommand.cs b/Meadow.CLI/Commands/Files/ListFilesCommand.cs new file mode 100644 index 00000000..b2b0aa82 --- /dev/null +++ b/Meadow.CLI/Commands/Files/ListFilesCommand.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Files +{ + [Command("files list", Description = "List files in the on-board filesystem")] + public class ListFilesCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to list the files")] +#endif + public int Partition { get; init; } = 0; + + [CommandOption("includeCrcs", 'i', Description = "Include the CRCs of the files")] + public bool IncludeCrcs { get; init; } + + private readonly ILogger _logger; + + public ListFilesCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Getting files on partition {Partition}"); + + var files = await Meadow.GetFilesAndCrcsAsync( + Partition, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (files.Any()) + { + + var longestFileName = files.Keys.Select(x => x.Length) + .OrderByDescending(x => x) + .FirstOrDefault(); + + if (IncludeCrcs) + { + foreach (var file in files) + _logger.LogInformation( + $"{file.Key.PadRight(longestFileName)}\t{file.Value:x8}"); + } + else + { + foreach (var file in files) + _logger.LogInformation($"{file.Key}"); + } + } + else + { + _logger.LogInformation("No files found."); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Files/WriteFileCommand.cs b/Meadow.CLI/Commands/Files/WriteFileCommand.cs new file mode 100644 index 00000000..bf274543 --- /dev/null +++ b/Meadow.CLI/Commands/Files/WriteFileCommand.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Files +{ + [Command("files write", Description = "Write files to the Meadow File System")] + public class WritesFileCommand : MeadowSerialCommand + { + [CommandOption( + "files", + 'f', + Description = "The file(s) to write to the Meadow Files System", + IsRequired = true)] + public IList Files { get; init; } + + [CommandOption( + "targetFiles", + 't', + Description = "The filename(s) to use on the Meadow File System")] + public IList TargetFileNames { get; init; } = Array.Empty(); + +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public WritesFileCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogDebug( + $"{Files.Count} files and {TargetFileNames.Count} target files specified."); + + if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) + { + _logger.LogInformation( + $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); + + return; + } + + for (var i = 0; i < Files.Count; i++) + { + var targetFileName = GetTargetFileName(i); + _logger.LogDebug($"Translated {Files[i]} to {targetFileName}"); + + Trace.Assert( + string.IsNullOrWhiteSpace(targetFileName) == false, + "string.IsNullOrWhiteSpace(targetFileName)"); + + if (!File.Exists(Files[i])) + { + _logger.LogInformation($"Cannot find {Files[i]}"); + } + else + { + _logger.LogInformation( + $"Writing {Files[i]} as {targetFileName} to partition {Partition}"); + + var result = await Meadow.WriteFileAsync(Files[i], targetFileName, Partition, cancellationToken) + .ConfigureAwait(false); + + _logger.LogDebug($"File written successfully? {result}"); + } + } + } + + private string GetTargetFileName(int i) + { + if (TargetFileNames.Any() + && TargetFileNames.Count >= i + && string.IsNullOrWhiteSpace(TargetFileNames[i]) == false) + { + return TargetFileNames[i]; + } + + return new FileInfo(Files[i]).Name; + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/MeadowCommand.cs b/Meadow.CLI/Commands/MeadowCommand.cs new file mode 100644 index 00000000..18af76ba --- /dev/null +++ b/Meadow.CLI/Commands/MeadowCommand.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands +{ + public abstract class MeadowCommand : ICommand + { + private protected ILoggerFactory LoggerFactory; + + private protected MeadowCommand(ILoggerFactory loggerFactory) + { + LoggerFactory = loggerFactory; + } + + [CommandOption('v', Description = "Log verbosity")] + public string[] Verbosity { get; init; } + + public abstract ValueTask ExecuteAsync(IConsole console); + } +} diff --git a/Meadow.CLI/Commands/MeadowSerialCommand.cs b/Meadow.CLI/Commands/MeadowSerialCommand.cs new file mode 100644 index 00000000..5411e206 --- /dev/null +++ b/Meadow.CLI/Commands/MeadowSerialCommand.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands +{ + public abstract class MeadowSerialCommand : ICommand, IDisposable + { + private protected ILoggerFactory LoggerFactory; + private protected MeadowDeviceManager MeadowDeviceManager; + private protected MeadowDevice Meadow; + + private protected MeadowSerialCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + { + LoggerFactory = loggerFactory; + MeadowDeviceManager = meadowDeviceManager; + } + + [CommandOption('v', Description = "Log verbosity")] + public string[] Verbosity { get; init; } + + [CommandOption("port", 's', Description = "Meadow COM port")] + public string SerialPortName + { + get => GetSerialPort(); + set => SetSerialPort(value); + } + + [CommandOption("listen", 'k', Description = "Keep port open to listen for output")] + public bool Listen {get; init;} + + private string _serialPort; + + private string GetSerialPort() + { + if (string.IsNullOrWhiteSpace(_serialPort)) + { + // TODO: VALIDATE THE INPUT HERE, INPUT IS UNVALIDATED + var port = SettingsManager.GetSetting(Setting.PORT); + if (!string.IsNullOrWhiteSpace(port)) + SerialPortName = port.Trim(); + } + + return _serialPort; + } + + private void SetSerialPort(string value) + { + _serialPort = value; + SettingsManager.SaveSetting(Setting.PORT, _serialPort); + } + + public virtual ValueTask ExecuteAsync(IConsole console) + { + Meadow = MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName); + if (Meadow == null) + { + LoggerFactory.CreateLogger().LogCritical("Unable to find Meadow."); + Environment.Exit(-1); + } + return ValueTask.CompletedTask; + } + + public void Dispose() + { + LoggerFactory?.Dispose(); + Meadow?.Dispose(); + } + } +} diff --git a/Meadow.CLI/Commands/Mono/MonoDisableCommand.cs b/Meadow.CLI/Commands/Mono/MonoDisableCommand.cs new file mode 100644 index 00000000..919814c2 --- /dev/null +++ b/Meadow.CLI/Commands/Mono/MonoDisableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Mono +{ + [Command("mono disable", Description = "Sets mono to NOT run on the Meadow board then resets it.")] + public class MonoDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public MonoDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.MonoDisableAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Mono Disabled Successfully"); + } + } +} diff --git a/Meadow.CLI/Commands/Mono/MonoEnableCommand.cs b/Meadow.CLI/Commands/Mono/MonoEnableCommand.cs new file mode 100644 index 00000000..d2ff6fae --- /dev/null +++ b/Meadow.CLI/Commands/Mono/MonoEnableCommand.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Mono +{ + [Command("mono enable", Description = "Sets mono to run on the Meadow board and then resets it.")] + public class MonoEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public MonoEnableCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.MonoEnableAsync(cancellationToken) + .ConfigureAwait(false); + _logger.LogInformation("Mono Enabled Successfully"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Mono/MonoFlashCommand.cs b/Meadow.CLI/Commands/Mono/MonoFlashCommand.cs new file mode 100644 index 00000000..bdc6389e --- /dev/null +++ b/Meadow.CLI/Commands/Mono/MonoFlashCommand.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Mono +{ + [Command("mono flash", Description = "Uploads the mono runtime file to the Meadow device. Does NOT move it into place.")] + public class MonoFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public MonoFlashCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.MonoFlashAsync(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Mono Flashed Successfully"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs b/Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs new file mode 100644 index 00000000..ce04844a --- /dev/null +++ b/Meadow.CLI/Commands/Mono/MonoRunStateCommand.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Mono +{ + [Command("mono state", Description = "Returns whether or not mono is enabled or disabled on the Meadow device.")] + public class MonoRunStateCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public MonoRunStateCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + var runState = await Meadow.GetMonoRunStateAsync(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Mono Run State: {(runState ? "Enabled" : "Disabled")}"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs b/Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs new file mode 100644 index 00000000..f43cf43c --- /dev/null +++ b/Meadow.CLI/Commands/Mono/MonoUpdateRuntimeCommand.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Mono +{ + [Command("mono update rt", Description = "Uploads the mono runtime files to the Meadow device and moves them into place.")] + public class MonoUpdateRuntimeCommand : MeadowSerialCommand + { + [CommandOption("filename",'f', Description = "The local name of the mono runtime file. Default is empty.")] + public string Filename {get; init;} + + private readonly ILogger _logger; + + public MonoUpdateRuntimeCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.UpdateMonoRuntimeAsync(Filename, cancellationToken: cancellationToken); + + await Meadow.ResetMeadowAsync(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation("Mono Flashed Successfully"); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Nsh/NshDisableCommand.cs b/Meadow.CLI/Commands/Nsh/NshDisableCommand.cs new file mode 100644 index 00000000..84380660 --- /dev/null +++ b/Meadow.CLI/Commands/Nsh/NshDisableCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Nsh +{ + [Command("nsh disable", Description = "Disables NSH on the Meadow device.")] + public class NshDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public NshDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.NshDisableAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Nsh/NshEnableCommand.cs b/Meadow.CLI/Commands/Nsh/NshEnableCommand.cs new file mode 100644 index 00000000..21be368a --- /dev/null +++ b/Meadow.CLI/Commands/Nsh/NshEnableCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Nsh +{ + [Command("nsh enable", Description = "Enables NSH on the Meadow device.")] + public class NshEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public NshEnableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.NshEnableAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Qspi/QspiInitCommand.cs b/Meadow.CLI/Commands/Qspi/QspiInitCommand.cs new file mode 100644 index 00000000..3530e454 --- /dev/null +++ b/Meadow.CLI/Commands/Qspi/QspiInitCommand.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Qspi +{ + [Command("qspi write", Description = "Write a QSPI value to the Meadow")] + public class QspiInitCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to initialize", IsRequired = true)] + public int Value {get; init;} + + public QspiInitCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.QspiInitAsync(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Qspi/QspiReadCommand.cs b/Meadow.CLI/Commands/Qspi/QspiReadCommand.cs new file mode 100644 index 00000000..2b5101ce --- /dev/null +++ b/Meadow.CLI/Commands/Qspi/QspiReadCommand.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Qspi +{ + [Command("qspi read", Description = "Read a QSPI value from the Meadow")] + public class QspiReadCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to read", IsRequired = true)] + public int Value {get; init;} + + public QspiReadCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.QspiReadAsync(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs b/Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs new file mode 100644 index 00000000..8b1121cd --- /dev/null +++ b/Meadow.CLI/Commands/Qspi/QspiWriteCommand.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Qspi +{ + [Command("qspi write", Description = "Write a QSPI value to the Meadow")] + public class QspiWriteCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to write", IsRequired = true)] + public int Value {get; init;} + + public QspiWriteCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.QspiWriteAsync(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Storage/EraseFlashCommand.cs b/Meadow.CLI/Commands/Storage/EraseFlashCommand.cs new file mode 100644 index 00000000..6067594d --- /dev/null +++ b/Meadow.CLI/Commands/Storage/EraseFlashCommand.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Storage +{ + [Command("flash erase", Description = "Erase the flash on the Meadow Board")] + public class EraseFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public EraseFlashCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation("Erasing flash."); + await Meadow.EraseFlashAsync(cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs b/Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs new file mode 100644 index 00000000..cc69494c --- /dev/null +++ b/Meadow.CLI/Commands/Storage/VerifyFlashCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Storage +{ + [Command("flash verify", Description = "Verify the contents of the flash were deleted.")] + public class VerifyFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public VerifyFlashCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation("Verifying flash"); + await Meadow.VerifyErasedFlashAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/TraceCommands/TraceDisableCommand.cs b/Meadow.CLI/Commands/TraceCommands/TraceDisableCommand.cs new file mode 100644 index 00000000..25afb18c --- /dev/null +++ b/Meadow.CLI/Commands/TraceCommands/TraceDisableCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.TraceCommands +{ + [Command("trace disable", Description = "Disable Trace Logging on the Meadow")] + public class TraceDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public TraceDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.TraceDisableAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/TraceCommands/TraceEnableCommand.cs b/Meadow.CLI/Commands/TraceCommands/TraceEnableCommand.cs new file mode 100644 index 00000000..ffa944a7 --- /dev/null +++ b/Meadow.CLI/Commands/TraceCommands/TraceEnableCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.TraceCommands +{ + [Command("trace enable", Description = "Enable trace logging on the Meadow")] + public class TraceEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public TraceEnableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + await Meadow.TraceEnableAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Utility/DebugCommand.cs b/Meadow.CLI/Commands/Utility/DebugCommand.cs new file mode 100644 index 00000000..181c75d5 --- /dev/null +++ b/Meadow.CLI/Commands/Utility/DebugCommand.cs @@ -0,0 +1,41 @@ +using System.Net; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.DeviceManagement; +using Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Utility +{ + [Command("debug", Description = "Debug a Meadow Application")] + public class DebugCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public DebugCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = loggerFactory.CreateLogger(); + } + + // VS 2019 - 4024 + // VS 2017 - 4022 + // VS 2015 - 4020 + [CommandOption("DebugPort", 'p', Description = "The port to run the debug server on")] + public int Port { get; init; } = 4024; + + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + // TODO: This is a terrible hack to link the DataProcessor to the Device + Meadow.DataProcessor.ForwardDebuggingData = Meadow.ForwardMonoDataToVisualStudioAsync; + var endpoint = new IPEndPoint(IPAddress.Loopback, Port); + var server = new DebuggingServer(Meadow, endpoint, _logger); + await server.StartListeningAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CLI/Commands/Utility/DownloadOsCommand.cs b/Meadow.CLI/Commands/Utility/DownloadOsCommand.cs new file mode 100644 index 00000000..3007426d --- /dev/null +++ b/Meadow.CLI/Commands/Utility/DownloadOsCommand.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Utility +{ + [Command("download os", Description = "Downloads the latest Meadow.OS to the host PC.")] + public class DownloadOsCommand : MeadowCommand + { + private readonly ILogger _logger; + public DownloadOsCommand(ILoggerFactory loggerFactory) : base(loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + var downloadManager = new DownloadManager(_logger); + await downloadManager.DownloadLatestAsync().ConfigureAwait(false); + } + } +} diff --git a/Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs b/Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs new file mode 100644 index 00000000..91470f29 --- /dev/null +++ b/Meadow.CLI/Commands/Utility/InstallDfuUtilCommand.cs @@ -0,0 +1,48 @@ +using System; +using System.Runtime.Versioning; +using System.Security.Principal; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.Utility +{ + [Command("install dfu-util", Description = "Install the DfuUtil utility")] + public class InstallDfuUtilCommand : MeadowCommand + { + private readonly ILogger _logger; + public InstallDfuUtilCommand(ILoggerFactory loggerFactory) :base(loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + if (OperatingSystem.IsWindows() && IsAdministrator()) + { + var downloadManager = new DownloadManager(_logger); + await downloadManager.InstallDfuUtilAsync(Environment.Is64BitOperatingSystem, cancellationToken); + } + else if (OperatingSystem.IsMacOS()) + { + _logger.LogInformation("To install on macOS, run: brew install dfu-util"); + } else if (OperatingSystem.IsLinux()) + { + _logger.LogInformation( + "To install on Linux, use the package manager to install the dfu-util package"); + } + } + + [SupportedOSPlatform("windows")] + private static bool IsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/Meadow.CLI/Meadow.CLI.csproj b/Meadow.CLI/Meadow.CLI.csproj index d76c2723..23a9186e 100644 --- a/Meadow.CLI/Meadow.CLI.csproj +++ b/Meadow.CLI/Meadow.CLI.csproj @@ -36,9 +36,15 @@ - - + + + + + + + + diff --git a/Meadow.CLI/Options.cs b/Meadow.CLI/Options.cs deleted file mode 100644 index 50a38ed6..00000000 --- a/Meadow.CLI/Options.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Collections.Generic; -using CommandLine; - -namespace MeadowCLI -{ - public class Options - { - [Option("FlashOS", Required = false, HelpText = "DFU flash Meadow OS.")] - public bool FlashOS { get; set; } - [Option('w', longName: "WriteFile", Required = false, Min = 1, Max = 2, HelpText = "Write one or multiple external files to Meadow's internal flash.\n --WriteFile file[,file,...] [targetFileName[,targetFileName,...]]")] - public IEnumerable WriteFile { get; set; } - [Option('d', longName: "DeleteFile", Required = false, Min = 1, Max = 1, HelpText = "Delete one or multiple files in Meadow's internal flash. /n --DeleteFile file[,file,...]")] - public IEnumerable DeleteFile { get; set; } - [Option(longName: "EraseFlash", Required = false, HelpText = "Delete all content in Meadow flash")] - public bool EraseFlash { get; set; } - [Option(longName: "VerifyErasedFlash", Required = false, HelpText = "Verify the contents of the flash were deleted")] - public bool VerifyErasedFlash { get; set; } - [Option(longName: "PartitionFileSystem", Required = false, HelpText = "Partition Meadow's internal flash")] - public bool PartitionFileSystem { get; set; } - [Option(longName: "MountFileSystem", Required = false, HelpText = "Mount file system in Meadow's internal flash")] - public bool MountFileSystem { get; set; } - [Option(longName: "InitializeFileSystem", Required = false, HelpText = "Initialize file system in Meadow's internal flash")] - public bool InitFileSystem { get; set; } - [Option(longName: "CreateFileSystem", Required = false, HelpText = "Create a new file system in Meadow's internal flash")] - public bool CreateFileSystem { get; set; } - [Option(longName: "FormatFileSystem", Required = false, HelpText = "Format file system in Meadow's internal flash")] - public bool FormatFileSystem { get; set; } - [Option(longName: "ClearCache", Required = false, HelpText = "Clears the CLI's state cache")] - public bool ClearCache { get; set; } - - [Option(longName: "SetDeveloper1", Required = false, HelpText = "Set developer1 (0 to 4,294,967,295)")] - public bool SetDeveloper1 { get; set; } - [Option(longName: "SetDeveloper2", Required = false, HelpText = "Set developer2 (0 to 4,294,967,295)")] - public bool SetDeveloper2 { get; set; } - [Option(longName: "SetDeveloper3", Required = false, HelpText = "Set developer3 (0 to 4,294,967,295)")] - public bool SetDeveloper3 { get; set; } - [Option(longName: "SetDeveloper4", Required = false, HelpText = "Set developer4 (0 to 4,294,967,295)")] - public bool SetDeveloper4 { get; set; } - [Option(longName: "SetTraceLevel", Required = false, HelpText = "Change the debug trace level (0 - 3)")] - public bool SetTraceLevel { get; set; } - [Option('r', longName: "ResetMeadow", Required = false, HelpText = "Reset the MCU on Meadow")] - public bool ResetMeadow { get; set; } - [Option(longName: "EnterDfuMode", Required = false, HelpText = "Put Meadow in DFU mode - Not implemented")] - public bool EnterDfuMode { get; set; } - [Option(longName: "NshEnable", Required = false, HelpText = "Enable NSH")] - public bool NshEnable { get; set; } - [Option(longName: "MonoDisable", Required = false, HelpText = "Disable mono from running")] - public bool MonoDisable { get; set; } - [Option(longName: "MonoEnable", Required = false, HelpText = "Enable mono so it can run")] - public bool MonoEnable { get; set; } - [Option(longName: "MonoRunState", Required = false, HelpText = "Reads mono startup state")] - public bool MonoRunState { get; set; } - [Option(longName: "MonoFlash", Required = false, HelpText = "Flashes Mono runtime to the external flash")] - public bool MonoFlash { get; set; } - [Option(longName: "MonoUpdateRt", Required = false, HelpText = "Download runtime files and flashes Mono runtime to flash")] - public bool MonoUpdateRt { get; set; } - [Option(longName: "GetDeviceInfo", Required = false, HelpText = "Reads device information")] - public bool GetDeviceInfo { get; set; } - [Option(longName: "GetDeviceName", Required = false, HelpText = "Reads device name")] - public bool GetDeviceName { get; set; } - - [Option(longName: "ListFiles", Required = false, HelpText = "List all files in Meadow partition")] - public bool ListFiles { get; set; } - [Option(longName: "ListFilesAndCrcs", Required = false, HelpText = "List all files and CRCs in a Meadow partition")] - public bool ListFilesAndCrcs { get; set; } - - [Option(longName: "ListPorts", Required = false, HelpText = "List all available local serial ports")] - public bool ListPorts { get; set; } - [Option('s', longName: "SerialPort", Required = false, HelpText = "Specify the serial port used by Meadow")] - public string SerialPort { get; set; } - [Option('f', longName: "File", Default = null, Required = false, HelpText = "Local file to send to Meadow")] - public string FileName { get; set; } - [Option(longName: "TargetFileName", Default = null, Required = false, HelpText = "Filename to be written to Meadow (can be different from source name")] - public string TargetFileName { get; set; } - [Option('p', "Partition", Default = 0, Required = false, HelpText = "Destination partition on Meadow")] - public int Partition { get; set; } - [Option('n', "NumberOfPartitions", Default = 1, Required = false, HelpText = "The number of partitions to create on Meadow")] - public int NumberOfPartitions { get; set; } - [Option('t', "TraceLevel", Default = 1, Required = false, HelpText = "Change the amount of debug information provided by the OS")] - public int TraceLevel { get; set; } - [Option(longName: "DeveloperValue", Default = 0, Required = false, HelpText = "Change the developer numeric user data value")] - public int DeveloperValue { get; set; } - - [Option(longName: "RenewFileSys", Required = false, HelpText = "Recreate the Meadow File System")] - public bool RenewFileSys { get; set; } - [Option('k', longName: "KeepAlive", Required = false, HelpText = "Keeps MeadowCLI from terminating after sending")] - public bool KeepAlive { get; set; } - [Option(longName: "TraceDisable", Required = false, HelpText = "Prevent Meadow from sending internal trace messages (default)")] - public bool TraceDisable { get; set; } - [Option(longName: "TraceEnable", Required = false, HelpText = "Request Meadow to send internal trace messages")] - public bool TraceEnable { get; set; } - [Option(longName: "Uart1Apps", Required = false, HelpText = "Use Uart1 for .NET apps")] - public bool Uart1Apps { get; set; } - [Option(longName: "Uart1Trace", Required = false, HelpText = "Use Uart1 for outputting Meadow trace messages")] - public bool Uart1Trace { get; set; } - [Option(longName: "QspiWrite", Required = false, HelpText = "Set developer1 (0 to 4,294,967,295)")] - public bool QspiWrite { get; set; } - [Option(longName: "QspiRead", Required = false, HelpText = "Set developer2 (0 to 4,294,967,295)")] - public bool QspiRead { get; set; } - [Option(longName: "QspiInit", Required = false, HelpText = "Set developer3 (0 to 4,294,967,295)")] - public bool QspiInit { get; set; } - - [Option(longName: "StartDebugging", Required = false, HelpText = "Enables remote debug of Meadow app.exe")] - public bool StartDebugging { get; set; } - [Option(longName: "VSDebugPort", Default = 0, Required = false, HelpText = "TCP/IP debugging port, Visual Studio 2019 uses 4024")] - public int VSDebugPort { get; set; } - - [Option(longName: "Esp32WriteFile", Required = false, HelpText = "Write an external file to ESP32's internal flash")] - public bool Esp32WriteFile { get; set; } - [Option(longName: "McuDestAddr", Required = false, HelpText = "Where file is stored in MCU's internal flash e.g. 0x10000")] - public string McuDestAddr { get; set; } - [Option(longName: "Esp32ReadMac", Required = false, HelpText = "Read the ESP32's MAC address")] - public bool Esp32ReadMac { get; set; } - [Option(longName: "Esp32Restart", Required = false, HelpText = "Restart the ESP32")] - public bool Esp32Restart { get; set; } - [Option(longName: "DeployApp", Required = false, HelpText = "Deploy app and dependencies")] - public bool DeployApp { get; set; } - [Option(longName: "Download", Required = false, HelpText = "Download latest OS, runtime, and networking files.")] - public bool DownloadLatest { get; set; } - [Option(longName: "FlashEsp", Required = false, HelpText = "Flash ESP 32 Coprocessor.")] - public bool FlashEsp { get; set; } - [Option(longName: "InstallDfuUtil", Required = false, HelpText = "Download and install dfu-util.")] - public bool InstallDfuUtil { get; set; } - [Option(longName: "Login", Required = false, HelpText = "Authenticate CLI")] - public bool Login { get; set; } - [Option(longName: "Logout", Required = false, HelpText = "Log out of CLI")] - public bool Logout { get; set; } - [Option(longName: "RegisterDevice", Required = false, HelpText = "Add device to cloud account")] - public bool RegisterDevice { get; set; } - [Option(longName: "GetInitialFileBytes", Required = false, HelpText = "Read the initial bytes of a file, up to 500")] - public bool GetInitialFileBytes { get; set; } - } -} diff --git a/Meadow.CLI/Program.cs b/Meadow.CLI/Program.cs index d26d3b7e..6b447634 100644 --- a/Meadow.CLI/Program.cs +++ b/Meadow.CLI/Program.cs @@ -1,585 +1,70 @@ -using CommandLine; -using System; -using MeadowCLI.DeviceManagement; -using System.IO; -using System.Threading.Tasks; -using Meadow.CLI; +using System; +using System.Diagnostics; using System.Linq; -using Meadow.CLI.Core.Auth; -using Meadow.CLI.Core.CloudServices; - -namespace MeadowCLI +using System.Threading.Tasks; +using CliFx; +using Meadow.CLI.Commands; +using Meadow.CLI.Core.DeviceManagement; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; + +namespace Meadow.CLI { - class Program + public class Program { - - [Flags] - enum CompletionBehavior - { - Success = 0x00, - RequestFailed = 1 << 0, - ExitConsole = 1 << 2, - KeepConsoleOpen = 1 << 3 - } - - static void Main(string[] args) + public static async Task Main(string[] args) { - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - }; - - CompletionBehavior behavior = CompletionBehavior.Success; + var logLevel = LogEventLevel.Information; + var logModifier = args.FirstOrDefault(a => a.Contains("-v")) + ?.Count(x => x == 'v') ?? 0; - DownloadManager downloadManager = new DownloadManager(); - var check = downloadManager.CheckForUpdates().Result; - if (check.updateExists) + logLevel -= logModifier; + if (logLevel < 0) { - Console.WriteLine($"CLI version {check.latestVersion} is available. To update, run: {DownloadManager.UpdateCommand}"); + logLevel = 0; } - if (args.Length == 0) - { - args = new string[] { "--help" }; - } - - var parser = new Parser(settings => { - settings.CaseSensitive = false; - settings.AutoVersion = false; // needed to supercede the built in --version command - settings.HelpWriter = Console.Out; - settings.IgnoreUnknownArguments = true; - }); - - parser.ParseArguments(args) - .WithParsed(options => - { - if (options.ListPorts) + var outputTemplate = logLevel == LogEventLevel.Verbose + ? "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}" + : "{Message:lj}{NewLine}{Exception}"; + Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose() + .WriteTo.Console( + logLevel, + outputTemplate) + .CreateLogger(); + Console.WriteLine($"Using log level {logLevel}"); + var services = new ServiceCollection(); + services.AddLogging( + builder => { - Console.WriteLine("Available serial ports\n----------------------"); - - var ports = MeadowSerialDevice.GetAvailableSerialPorts(); - if (ports == null || ports.Length == 0) - { - Console.WriteLine("\t "); - } - else - { - foreach (var p in ports) - { - Console.WriteLine($"\t{p}"); - } - } - Console.WriteLine($"\n"); - } - else - { - if (options.DownloadLatest) - { - downloadManager.DownloadLatest().Wait(); - } - else if (options.FlashOS) - { - DfuUpload.FlashOS(options.FileName); - } - else if (options.Login) - { - IdentityManager identityManager = new IdentityManager(); - var result = identityManager.LoginAsync().Result; - if (result) - { - var cred = identityManager.GetCredentials(identityManager.WLRefreshCredentialName); - Console.WriteLine($"Signed in as {cred.username}"); - } - } - else if (options.Logout) - { - IdentityManager identityManager = new IdentityManager(); - identityManager.Logout(); - } - else if (options.InstallDfuUtil) - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - { - Console.WriteLine("To install on macOS, run: brew install dfu-util"); - } - else - { - downloadManager.InstallDfuUtil(Environment.Is64BitOperatingSystem); - } - } - else if (args[0].Equals("--version", StringComparison.InvariantCultureIgnoreCase)) - { - Console.WriteLine($"Current version: {check.currentVersion}"); - } - else - { - SyncArgsCache(options); - try - { - ProcessHcom(options).Wait(); - } - catch (Exception ex) - { - Console.WriteLine($"An unexpected error occurred: {ex?.InnerException?.Message}"); - } - } - - } - }); - - //if (System.Diagnostics.Debugger.IsAttached) - //{ - // behavior = CompletionBehavior.KeepConsoleOpen; - //} - - Environment.Exit(0); + builder.AddSerilog(Log.Logger, dispose:true); + }); + + services.AddSingleton(); + AddCommandsAsServices(services); + var serviceProvider = services.BuildServiceProvider(); + return await new CliApplicationBuilder().AddCommandsFromThisAssembly() + .UseTypeActivator(serviceProvider.GetService) + .Build() + .RunAsync(); } - static void SyncArgsCache(Options options) - { - var port = SettingsManager.GetSetting(Setting.PORT); - if (string.IsNullOrEmpty(options.SerialPort) && !string.IsNullOrEmpty(port)) - { - options.SerialPort = port; - } - else if (!string.IsNullOrEmpty(options.SerialPort)) - { - SettingsManager.SaveSetting(Setting.PORT, options.SerialPort); - } - } - - //Probably rename - - static async Task ProcessHcom(Options options) + private static void AddCommandsAsServices(IServiceCollection services) { - if (string.IsNullOrEmpty(options.SerialPort)) - { - Console.WriteLine("Please specify a --SerialPort"); - return; - } + var assembly = System.Reflection.Assembly.GetAssembly(typeof(Program)); + Trace.Assert(assembly != null); + var types = assembly.GetTypes(); - MeadowSerialDevice device = null; - try - { - Console.WriteLine($"Opening port '{options.SerialPort}'"); - device = await MeadowDeviceManager.GetMeadowForSerialPort(options.SerialPort); - } - catch(Exception ex) - { - Console.WriteLine($"Error connecting to device: {ex.Message}"); - return; - } + var commands = types.Where( + x => x.IsAssignableTo(typeof(MeadowSerialCommand)) + || x.IsAssignableTo(typeof(MeadowCommand))) + .Where(x => !x.IsAbstract); - using (device) + foreach (var command in commands) { - // verify that the port was actually connected - if (device.Socket == null && device.SerialPort == null) - { - Console.WriteLine($"Port is unavailable."); - return; - } - - try - { - if (options.WriteFile.Any()) - { - string[] parameters = options.WriteFile.ToArray(); - - string[] files = parameters[0].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - string[] targetFileNames; - - if (parameters.Length == 1) - { - targetFileNames = new string[files.Length]; - } - else - { - targetFileNames = parameters[1].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - if (files.Length != targetFileNames.Length) - { - Console.WriteLine($"Number of files to write ({files.Length}) does not match the number of target file names ({targetFileNames.Length})."); - } - else - { - for (int i = 0; i < files.Length; i++) - { - string targetFileName = targetFileNames[i]; - - if (String.IsNullOrEmpty(targetFileName)) - { - targetFileName = null; - } - - if (!File.Exists(files[i])) - { - Console.WriteLine($"Cannot find {files[i]}"); - } - else - { - if (string.IsNullOrEmpty(targetFileName)) - { -#if USE_PARTITIONS - Console.WriteLine($"Writing {files[i]} to partition {options.Partition}"); -#else - Console.WriteLine($"Writing {files[i]}"); -#endif - } - else - { -#if USE_PARTITIONS - Console.WriteLine($"Writing {files[i]} as {targetFileName} to partition {options.Partition}"); -#else - Console.WriteLine($"Writing {files[i]} as {targetFileName}"); -#endif - } - - await MeadowFileManager.WriteFileToFlash(device, files[i], targetFileName, options.Partition).ConfigureAwait(false); - } - } - } - } - else if (options.DeleteFile.Any()) - { - string[] parameters = options.DeleteFile.ToArray(); - string[] files = parameters[0].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - foreach (string file in files) - { - if (!String.IsNullOrEmpty(file)) - { -#if USE_PARTITIONS - Console.WriteLine($"Deleting {file} from partion {options.Partition}"); -#else - Console.WriteLine($"Deleting {file}"); -#endif - await MeadowFileManager.DeleteFile(device, file, options.Partition); - } - } - } - else if (options.EraseFlash) - { - Console.WriteLine("Erasing flash"); - await MeadowFileManager.EraseFlash(device); - } - else if (options.VerifyErasedFlash) - { - Console.WriteLine("Verifying flash is erased"); - await MeadowFileManager.VerifyErasedFlash(device); - } - else if (options.PartitionFileSystem) - { - Console.WriteLine($"Partioning file system into {options.NumberOfPartitions} partition(s)"); - await MeadowFileManager.PartitionFileSystem(device, options.NumberOfPartitions); - } - else if (options.MountFileSystem) - { -#if USE_PARTITIONS - Console.WriteLine($"Mounting partition {options.Partition}"); -#else - Console.WriteLine("Mounting file system"); -#endif - await MeadowFileManager.MountFileSystem(device, options.Partition); - } - else if (options.InitFileSystem) - { -#if USE_PARTITIONS - Console.WriteLine($"Intializing filesystem in partition {options.Partition}"); -#else - Console.WriteLine("Intializing filesystem"); -#endif - await MeadowFileManager.InitializeFileSystem(device, options.Partition); - } - else if (options.CreateFileSystem) //should this have a partition??? - { - Console.WriteLine($"Creating file system"); - await MeadowFileManager.CreateFileSystem(device); - } - else if (options.FormatFileSystem) - { -#if USE_PARTITIONS - Console.WriteLine($"Format file system on partition {options.Partition}"); -#else - Console.WriteLine("Format file system"); -#endif - await MeadowFileManager.FormatFileSystem(device, options.Partition); - } - else if (options.ListFiles) - { -#if USE_PARTITIONS - Console.WriteLine($"Getting list of files on partition {options.Partition}"); -#else - Console.WriteLine($"Getting list of files"); -#endif - await MeadowFileManager.ListFiles(device, options.Partition); - } - else if (options.ListFilesAndCrcs) - { -#if USE_PARTITIONS - Console.WriteLine($"Getting list of files and CRCs on partition {options.Partition}"); -#else - Console.WriteLine("Getting list of files and CRCs"); -#endif - await MeadowFileManager.ListFilesAndCrcs(device, options.Partition); - } - else if (options.SetTraceLevel) - { - Console.WriteLine($"Setting trace level to {options.TraceLevel}"); - await MeadowDeviceManager.SetTraceLevel(device, options.TraceLevel); - } - else if (options.SetDeveloper1) - { - Console.WriteLine($"Setting developer level to {options.DeveloperValue}"); - await MeadowDeviceManager.SetDeveloper1(device, options.DeveloperValue); - } - else if (options.SetDeveloper2) - { - Console.WriteLine($"Setting developer level to {options.DeveloperValue}"); - await MeadowDeviceManager.SetDeveloper2(device, options.DeveloperValue); - } - else if (options.SetDeveloper3) - { - Console.WriteLine($"Setting developer level to {options.DeveloperValue}"); - await MeadowDeviceManager.SetDeveloper3(device, options.DeveloperValue); - } - else if (options.SetDeveloper4) - { - Console.WriteLine($"Setting developer level to {options.DeveloperValue}"); - await MeadowDeviceManager.SetDeveloper4(device, options.DeveloperValue); - } - else if (options.NshEnable) - { - Console.WriteLine($"Enable Nsh"); - await MeadowDeviceManager.NshEnable(device); - } - else if (options.MonoDisable) - { - await MeadowDeviceManager.MonoDisable(device); - } - else if (options.MonoEnable) - { - await MeadowDeviceManager.MonoEnable(device); - } - else if (options.MonoRunState) - { - await MeadowDeviceManager.MonoRunState(device); - } - else if (options.MonoFlash) - { - await MeadowDeviceManager.MonoFlash(device); - } - else if (options.MonoUpdateRt) - { - string sourcefilename = options.FileName; - if (string.IsNullOrWhiteSpace(sourcefilename)) - { - // check local override - sourcefilename = Path.Combine(Directory.GetCurrentDirectory(), DownloadManager.RuntimeFilename); - if (File.Exists(sourcefilename)) - { - Console.WriteLine($"Using current directory '{DownloadManager.RuntimeFilename}'"); - - } - else - { - sourcefilename = Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.RuntimeFilename); - if (File.Exists(sourcefilename)) - { - Console.WriteLine("FileName not specified, using latest download."); - } - else - { - Console.WriteLine("Unable to locate a runtime file. Either provide a path or download one."); - return; // KeepConsoleOpen? - } - } - } - - if (!File.Exists(sourcefilename)) - { - Console.WriteLine($"File '{sourcefilename}' not found"); - return; // KeepConsoleOpen? - } - - await MeadowFileManager.MonoUpdateRt(device, sourcefilename, options.TargetFileName, options.Partition); - } - else if (options.GetDeviceInfo) - { - await MeadowDeviceManager.GetDeviceInfo(device); - } - else if (options.GetDeviceName) - { - await MeadowDeviceManager.GetDeviceName(device); - } - else if (options.ResetMeadow) - { - Console.WriteLine("Resetting Meadow"); - await MeadowDeviceManager.ResetMeadow(device); - } - else if (options.EnterDfuMode) - { - Console.WriteLine("Entering Dfu mode"); - await MeadowDeviceManager.EnterDfuMode(device); - } - else if (options.TraceDisable) - { - Console.WriteLine("Disabling Meadow trace messages"); - await MeadowDeviceManager.TraceDisable(device); - } - else if (options.TraceEnable) - { - Console.WriteLine("Enabling Meadow trace messages"); - await MeadowDeviceManager.TraceEnable(device); - } - else if (options.Uart1Apps) - { - Console.WriteLine("Use Uart1 for .NET Apps"); - await MeadowDeviceManager.Uart1Apps(device); - } - else if (options.Uart1Trace) - { - Console.WriteLine("Use Uart1 for outputting Meadow trace messages"); - await MeadowDeviceManager.Uart1Trace(device); - } - else if (options.RenewFileSys) - { - Console.WriteLine("Recreate a new file system on Meadow"); - await MeadowDeviceManager.RenewFileSys(device); - } - else if (options.QspiWrite) - { - Console.WriteLine($"Executing QSPI Flash Write using {options.DeveloperValue}"); - await MeadowDeviceManager.QspiWrite(device, options.DeveloperValue); - } - else if (options.QspiRead) - { - Console.WriteLine($"Executing QSPI Flash Read using {options.DeveloperValue}"); - await MeadowDeviceManager.QspiRead(device, options.DeveloperValue); - } - else if (options.QspiInit) - { - Console.WriteLine($"Executing QSPI Flash Initialization using {options.DeveloperValue}"); - await MeadowDeviceManager.QspiInit(device, options.DeveloperValue); - } - else if (options.StartDebugging) - { - (await MeadowDeviceManager.CreateDebuggingServer(device, options.VSDebugPort)).StartListening(device); - Console.WriteLine($"Ready for Visual Studio debugging"); - options.KeepAlive = true; - } - else if (options.Esp32WriteFile) - { - if (string.IsNullOrEmpty(options.FileName)) - { - Console.WriteLine($"option --Esp32WriteFile requires option --File (the local file you wish to write)"); - } - else - { - if (string.IsNullOrEmpty(options.TargetFileName)) - { - Console.WriteLine($"Writing {options.FileName} to ESP32"); - } - else - { - Console.WriteLine($"Writing {options.FileName} as {options.TargetFileName}"); - } - await MeadowFileManager.WriteFileToEspFlash(device, - options.FileName, options.TargetFileName, options.Partition, options.McuDestAddr); - } - } - else if (options.FlashEsp) - { - Console.WriteLine($"Transferring {DownloadManager.NetworkMeadowCommsFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.NetworkMeadowCommsFilename), mcuDestAddr: "0x10000"); - // BC 2021.05.23 - Updated to 5seconds otherwise we're getting the concluded message - // when the next file tries to upload, causing an error. - await Task.Delay(5000); - - Console.WriteLine($"Transferring {DownloadManager.NetworkBootloaderFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.NetworkBootloaderFilename), mcuDestAddr: "0x1000"); - await Task.Delay(1000); - - Console.WriteLine($"Transferring {DownloadManager.NetworkPartitionTableFilename}"); - await MeadowFileManager.WriteFileToEspFlash(device, - Path.Combine(DownloadManager.FirmwareDownloadsFilePath, DownloadManager.NetworkPartitionTableFilename), mcuDestAddr: "0x8000"); - await Task.Delay(2000); - } - else if (options.Esp32ReadMac) - { - await MeadowDeviceManager.Esp32ReadMac(device); - } - else if (options.Esp32Restart) - { - await MeadowDeviceManager.Esp32Restart(device); - } - else if (options.DeployApp && !string.IsNullOrEmpty(options.FileName)) - { - await MeadowDeviceManager.DeployApp(device, options.FileName); - } - else if (options.GetInitialFileBytes) - { - if (string.IsNullOrEmpty(options.FileName)) - { - Console.WriteLine($"option --GetInitialFileBytes requires option --File (the Meadow file you wish to read)"); - } - else - { - await MeadowFileManager.GetInitialBytesFromFile(device, options.FileName); - } - } - else if (options.RegisterDevice) - { - var sn = await MeadowDeviceManager.GetDeviceSerialNumber(device); - - if (string.IsNullOrEmpty(sn)) - { - Console.WriteLine("Could not get device serial number. Reconnect device and try again."); - return; - } - - Console.WriteLine($"Registering device {sn}"); - - DeviceRepository repository = new DeviceRepository(); - var result = await repository.AddDevice(sn); - if (result.isSuccess) - { - Console.WriteLine("Device registration complete"); - } - else - { - Console.WriteLine(result.message); - } - } - - if (options.KeepAlive) - { - Console.Read(); - } - } - catch (IOException ex) - { - if (ex.Message.Contains("semaphore")) - { - if (ex.Message.Contains("semaphore")) - { - Console.WriteLine("Timeout communicating with Meadow"); - } - else - { - Console.WriteLine($"Unexpected error occurred: {ex.Message}"); - } - return; // KeepConsoleOpen? - } - } - catch (Exception ex) - { - Console.WriteLine($"Unexpected error occurred: {ex.Message}"); - return; // KeepConsoleOpen? - } + services.AddTransient(command); } } } -} +} \ No newline at end of file diff --git a/Meadow.CLI/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json index a0fd8faa..8acdd2b0 100644 --- a/Meadow.CLI/Properties/launchSettings.json +++ b/Meadow.CLI/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Download": { "commandName": "Project", - "commandLineArgs": "--Download" + "commandLineArgs": "device info" }, "Login": { "commandName": "Project", @@ -85,4 +85,4 @@ "commandLineArgs": "--Help" } } -} +} \ No newline at end of file diff --git a/Meadow.CommandLine/CliFxConsoleLoggerProvider.cs b/Meadow.CommandLine/CliFxConsoleLoggerProvider.cs new file mode 100644 index 00000000..76e6d233 --- /dev/null +++ b/Meadow.CommandLine/CliFxConsoleLoggerProvider.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using CliFx.Infrastructure; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine +{ + public class CliFxConsoleLogger : ILogger + { + private readonly CliFxConsoleLoggerProviderConfig _config; + private readonly string _name; + private readonly IConsole _console; + + public CliFxConsoleLogger( + IConsole console, + string name, + CliFxConsoleLoggerProviderConfig config) => + (_console, _name, _config) = (console, name, config); + + + public void Log(LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + _console.Output.WriteLine(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) => + logLevel >= _config.LogLevel; + + public IDisposable BeginScope(TState state) => default; + } + + public sealed class CliFxConsoleLoggerProvider : ILoggerProvider + { + private readonly CliFxConsoleLoggerProviderConfig _config; + private readonly IConsole _console; + private readonly ConcurrentDictionary _loggers = + new ConcurrentDictionary(); + + public CliFxConsoleLoggerProvider(CliFxConsoleLoggerProviderConfig config, IConsole console) => + (_config, _console) = (config, console); + + public ILogger CreateLogger(string categoryName) => + _loggers.GetOrAdd(categoryName, name => new CliFxConsoleLogger(_console, name, _config)); + + public void Dispose() => _loggers.Clear(); + } + + public class CliFxConsoleLoggerProviderConfig + { + public LogLevel LogLevel {get; init; } + } +} diff --git a/Meadow.CommandLine/Commands/Cloud/LoginCommand.cs b/Meadow.CommandLine/Commands/Cloud/LoginCommand.cs new file mode 100644 index 00000000..ce7303f8 --- /dev/null +++ b/Meadow.CommandLine/Commands/Cloud/LoginCommand.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Auth; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Cloud +{ + [Command("cloud login", Description = "Log into the Meadow Service")] + public class LoginCommand : ICommand + { + private readonly ILogger _logger; + + public LoginCommand(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + var identityManager = new IdentityManager(); + var loginResult = await identityManager.LoginAsync(cancellationToken) + .ConfigureAwait(false); + + if (loginResult) + { + var cred = identityManager.GetCredentials(identityManager.WLRefreshCredentialName); + _logger.LogInformation($"Signed in as {cred.username}"); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Cloud/LogoutCommand.cs b/Meadow.CommandLine/Commands/Cloud/LogoutCommand.cs new file mode 100644 index 00000000..715e585f --- /dev/null +++ b/Meadow.CommandLine/Commands/Cloud/LogoutCommand.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Auth; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Cloud +{ + [Command("cloud logout", Description = "Logout of the Meadow Service")] + public class LogoutCommand : ICommand + { + private readonly ILogger _logger; + + public LogoutCommand(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + var identityManager = new IdentityManager(); + identityManager.Logout(); + + _logger.LogInformation($"Signed out of Meadow Service"); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/DeviceManagement/FlashEspCommand.cs b/Meadow.CommandLine/Commands/DeviceManagement/FlashEspCommand.cs new file mode 100644 index 00000000..f8272e2d --- /dev/null +++ b/Meadow.CommandLine/Commands/DeviceManagement/FlashEspCommand.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.DeviceManagement +{ + [Command("flash esp", Description = "Flash the ESP co-processor")] + public class FlashEspCommand : MeadowSerialCommand + { + public FlashEspCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.FlashEsp(cancellationToken) + .ConfigureAwait(false); + + await device.ResetMeadow(cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/DeviceManagement/FlashOsCommand.cs b/Meadow.CommandLine/Commands/DeviceManagement/FlashOsCommand.cs new file mode 100644 index 00000000..db77e59c --- /dev/null +++ b/Meadow.CommandLine/Commands/DeviceManagement/FlashOsCommand.cs @@ -0,0 +1,166 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using LibUsbDotNet.Main; +using Meadow.CLI.Core; +using Meadow.CLI.Core.Internals.Dfu; +using MeadowCLI; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.DeviceManagement +{ + [Command("flash os", Description = "Update the OS on the Meadow Board")] + public class FlashOsCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public FlashOsCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = loggerFactory.CreateLogger(); + } + + [CommandOption("BinPath", 'b', Description = "Path to the Meadow OS binary")] + public string BinPath { get; init; } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + var dfuAttempts = 0; + UsbRegistry dfuDevice = null; + while (true) + { + try + { + try + { + dfuDevice = DfuUtils.GetDevice(); + break; + } + catch (MultipleDfuDevicesException) + { + // This is bad, we can't just blindly flash with multiple devices, let the user know + throw; + } + catch (DeviceNotFoundException) + { + // eat it. + } + + // No DFU device found, lets try to set the meadow to DFU mode. + using var device = await MeadowDeviceManager.GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken); + + _logger.LogInformation("Entering DFU Mode"); + await device.EnterDfuMode(cancellationToken) + .ConfigureAwait(false); + } + catch (FileNotFoundException) + { + _logger.LogError("Failed to find Serial Port."); + } + catch (Exception ex) + { + _logger.LogDebug( + "An exception occurred while switching device to DFU Mode. Exception: {0}", + ex); + } + + switch (dfuAttempts) + { + case 5: + _logger.LogInformation( + "Having trouble putting Meadow in DFU Mode, please press RST button on Meadow and press enter to try again"); + + await console.Input.ReadLineAsync(); + break; + case 10: + _logger.LogInformation( + "Having trouble putting Meadow in DFU Mode, please hold BOOT button, press RST button and release BOOT button on Meadow and press enter to try again"); + + await console.Input.ReadLineAsync(); + break; + case > 15: + throw new Exception( + "Unable to place device in DFU mode, please disconnect the Meadow, hold the BOOT button, reconnect the Meadow, release the BOOT button and try again."); + } + + // Lets give the device a little time to settle in and get picked up + await Task.Delay(1000, cancellationToken) + .ConfigureAwait(false); + + dfuAttempts++; + } + + // Get the serial number so that later we can pick the right device if the system has multiple meadow plugged in + var serialNumber = DfuUtils.GetDeviceSerial(dfuDevice); + + _logger.LogInformation("Device in DFU Mode, flashing OS"); + await DfuUtils.FlashOsAsync(device: dfuDevice, logger: _logger); + _logger.LogInformation("Device Flashed."); + + try + { + using var device = await MeadowDeviceManager.FindMeadowBySerialNumber( + serialNumber, + cancellationToken: + cancellationToken) + .ConfigureAwait(false); + + Trace.Assert(device != null, "device != null"); + + await device.UpdateMonoRuntime(BinPath, cancellationToken: cancellationToken); + + // Again, verify that Mono is disabled + Trace.Assert( + await device.GetMonoRunState(cancellationToken) + .ConfigureAwait(false), + "Meadow was expected to have Mono Disabled"); + + _logger.LogInformation("Flashing ESP"); + await device.FlashEsp(cancellationToken) + .ConfigureAwait(false); + + // Reset the meadow again to ensure flash worked. + await device.ResetMeadow(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation("Enabling Mono and Resetting."); + while (await device.GetMonoRunState(cancellationToken) + .ConfigureAwait(false) + == false) + { + await device.MonoEnable(cancellationToken); + } + + // TODO: Verify that the device info returns the expected version + var deviceInfoString = await device + .GetDeviceInfo(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(deviceInfoString)) + { + throw new Exception("Unable to retrieve device info."); + } + + var deviceInfo = new MeadowDeviceInfo(deviceInfoString); + _logger.LogInformation( + $"Updated Meadow to OS: {deviceInfo.MeadowOSVersion} ESP: {deviceInfo.CoProcessorOs}"); + + Environment.Exit(0); + } + catch (Exception ex) + { + console.Output.WriteLine(ex); + Environment.Exit(-1); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceInfoCommand.cs b/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceInfoCommand.cs new file mode 100644 index 00000000..23a115ff --- /dev/null +++ b/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceInfoCommand.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.DeviceManagement +{ + [Command("device info", Description = "Get the device info")] + public class GetDeviceInfoCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public GetDeviceInfoCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + var deviceInfoString = await device.GetDeviceInfo(cancellationToken: cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(deviceInfoString)) + throw new Exception("Unable to retrieve device info"); + var deviceInfo = new MeadowDeviceInfo(deviceInfoString); + _logger.LogInformation(deviceInfo.ToString()); + } + } +} diff --git a/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceNameCommand.cs b/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceNameCommand.cs new file mode 100644 index 00000000..f825321a --- /dev/null +++ b/Meadow.CommandLine/Commands/DeviceManagement/GetDeviceNameCommand.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.DeviceManagement +{ + [Command("device name", Description = "Get the name of the Meadow")] + public class GetDeviceNameCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public GetDeviceNameCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + var deviceName = await device.GetDeviceName(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Device Name: {deviceName}"); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/CreateFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/CreateFileSystemCommand.cs new file mode 100644 index 00000000..83a810dd --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/CreateFileSystemCommand.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem create", Description = "Create a File System on the Meadow Board")] + public class CreateFileSystemCommand : MeadowSerialCommand + { + public CreateFileSystemCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + await console.Output.WriteLineAsync("Creating a file system on the Meadow."); + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken) + .ConfigureAwait(false); + + await device.CreateFileSystem(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/FormatFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/FormatFileSystemCommand.cs new file mode 100644 index 00000000..c5999bd9 --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/FormatFileSystemCommand.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem format", Description = "Format a File System on the Meadow Board")] + public class FormatFileSystemCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public FormatFileSystemCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Formatting file system on partition {Partition}"); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.FormatFileSystem(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/InitializeFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/InitializeFileSystemCommand.cs new file mode 100644 index 00000000..50bb828b --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/InitializeFileSystemCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem init", Description = "Mount the File System on the Meadow Board")] + public class InitializeFileSystemCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + public InitializeFileSystemCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Initializing filesystem in partition {Partition}"); + + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken) + .ConfigureAwait(false); + + await device.InitializeFileSystem(Partition, cancellationToken).ConfigureAwait(false); + } + + + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/MountFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/MountFileSystemCommand.cs new file mode 100644 index 00000000..497d1eb3 --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/MountFileSystemCommand.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem mount", Description = "Mount the File System on the Meadow Board")] + public class MountFileSystemCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public MountFileSystemCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Mounting partition {Partition}"); + + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken) + .ConfigureAwait(false); + + await device.MountFileSystem(Partition, cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/PartitionFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/PartitionFileSystemCommand.cs new file mode 100644 index 00000000..2a0f813b --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/PartitionFileSystemCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem partition", Description = "Create a File System on the Meadow Board")] + public class PartitionFileSystemCommand : MeadowSerialCommand + { + +#if USE_PARTITIONS + [CommandOption("NumberOfPartitions", 'p', Description = "The number of partitions to create on the Meadow")] +#endif + public int NumberOfPartitions { get; init; } = 1; + + private readonly ILogger _logger; + public PartitionFileSystemCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation( + $"Partitioning filesystem into {NumberOfPartitions} partition(s)"); + + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken) + .ConfigureAwait(false); + + await device.PartitionFileSystem(NumberOfPartitions, cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/FileSystem/RenewFileSystemCommand.cs b/Meadow.CommandLine/Commands/FileSystem/RenewFileSystemCommand.cs new file mode 100644 index 00000000..b3f81395 --- /dev/null +++ b/Meadow.CommandLine/Commands/FileSystem/RenewFileSystemCommand.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.FileSystem +{ + [Command("filesystem renew", Description = "Create a File System on the Meadow Board")] + public class RenewFileSystemCommand : MeadowSerialCommand + { + public RenewFileSystemCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + await console.Output.WriteLineAsync("Renewing file system on the Meadow."); + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken) + .ConfigureAwait(false); + + await device.RenewFileSystem(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Files/DeleteFileCommand.cs b/Meadow.CommandLine/Commands/Files/DeleteFileCommand.cs new file mode 100644 index 00000000..63d20ecd --- /dev/null +++ b/Meadow.CommandLine/Commands/Files/DeleteFileCommand.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Files +{ + [Command("files delete", Description = "Delete files from the Meadow File System")] + public class DeleteFileCommand : MeadowSerialCommand + { + [CommandOption( + "files", + 'f', + Description = "The file(s) to delete from the Meadow Files System", + IsRequired = true)] + public IList Files { get; init; } + +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public DeleteFileCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + foreach (var file in Files.Where(file => string.IsNullOrWhiteSpace(file) == false)) + { + if (!string.IsNullOrEmpty(file)) + { + _logger.LogInformation($"Deleting {file} from partition {Partition}"); + + await device.DeleteFile(file, Partition, cancellationToken) + .ConfigureAwait(false); + } + } + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Files/ListFilesCommand.cs b/Meadow.CommandLine/Commands/Files/ListFilesCommand.cs new file mode 100644 index 00000000..ae2f55a1 --- /dev/null +++ b/Meadow.CommandLine/Commands/Files/ListFilesCommand.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Files +{ + [Command("files list", Description = "List files in the on-board filesystem")] + public class ListFilesCommand : MeadowSerialCommand + { +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to list the files")] +#endif + public int Partition { get; init; } = 0; + + [CommandOption("includeCrcs", 'i', Description = "Include the CRCs of the files")] + public bool IncludeCrcs { get; init; } + + private readonly ILogger _logger; + + public ListFilesCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation($"Getting files on partition {Partition}"); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + var files = await device.GetFilesAndCrcs( + Partition, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + var longestFileName = files.Keys.Select(x => x.Length).OrderByDescending(x => x) + .FirstOrDefault(); + + if (IncludeCrcs) + { + foreach (var file in files) + _logger.LogInformation($"{file.Key.PadRight(longestFileName)}\t{file.Value:x8}"); + } + else + { + foreach (var file in files) + _logger.LogInformation($"{file.Key}"); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Files/WriteFileCommand.cs b/Meadow.CommandLine/Commands/Files/WriteFileCommand.cs new file mode 100644 index 00000000..9cffbe62 --- /dev/null +++ b/Meadow.CommandLine/Commands/Files/WriteFileCommand.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Files +{ + [Command("files write", Description = "Write files to the Meadow File System")] + public class WritesFileCommand : MeadowSerialCommand + { + [CommandOption( + "files", + 'f', + Description = "The file(s) to write to the Meadow Files System", + IsRequired = true)] + public IList Files { get; init; } + + [CommandOption( + "targetFiles", + 't', + Description = "The filename(s) to use on the Meadow File System")] + public IList TargetFileNames { get; init; } = Array.Empty(); + +#if USE_PARTITIONS + [CommandOption("Partition", 'p', Description = "The partition to write to on the Meadow")] +#endif + public int Partition { get; init; } = 0; + + private readonly ILogger _logger; + + public WritesFileCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + _logger.LogDebug( + $"{Files.Count} files and {TargetFileNames.Count} target files specified."); + + if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) + { + _logger.LogInformation( + $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); + + return; + } + + for (var i = 0; i < Files.Count; i++) + { + var targetFileName = GetTargetFileName(i); + _logger.LogDebug($"Translated {Files[i]} to {targetFileName}"); + + Trace.Assert( + string.IsNullOrWhiteSpace(targetFileName) == false, + "string.IsNullOrWhiteSpace(targetFileName)"); + + if (!File.Exists(Files[i])) + { + _logger.LogInformation($"Cannot find {Files[i]}"); + } + else + { + _logger.LogInformation( + $"Writing {Files[i]} as {targetFileName} to partition {Partition}"); + + var result = await device.WriteFile(Files[i], targetFileName, Partition, cancellationToken) + .ConfigureAwait(false); + + _logger.LogDebug($"File written successfully? {result}"); + } + } + } + + private string GetTargetFileName(int i) + { + if (TargetFileNames.Any() + && TargetFileNames.Count >= i + && string.IsNullOrWhiteSpace(TargetFileNames[i]) == false) + { + return TargetFileNames[i]; + } + + return new FileInfo(Files[i]).Name; + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/MeadowSerialCommand.cs b/Meadow.CommandLine/Commands/MeadowSerialCommand.cs new file mode 100644 index 00000000..e9dd7de7 --- /dev/null +++ b/Meadow.CommandLine/Commands/MeadowSerialCommand.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands +{ + public abstract class MeadowSerialCommand : ICommand + { + private protected ILoggerFactory LoggerFactory; + private protected MeadowDeviceManager MeadowDeviceManager; + + private protected MeadowSerialCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + { + LoggerFactory = loggerFactory; + MeadowDeviceManager = meadowDeviceManager; + } + + [CommandOption('v', Description = "Log verbosity")] + public string[] Verbosity { get; init; } + + [CommandOption("port", 's', Description = "Meadow COM port", IsRequired = true)] + public string SerialPortName { get; init; } + + [CommandOption("listen", 'k', Description = "Keep port open to listen for output")] + public bool Listen {get; init;} + + //private protected CancellationToken CancellationToken { get; init; } + + public abstract ValueTask ExecuteAsync(IConsole console); + } +} diff --git a/Meadow.CommandLine/Commands/Mono/MonoDisableCommand.cs b/Meadow.CommandLine/Commands/Mono/MonoDisableCommand.cs new file mode 100644 index 00000000..dd475de2 --- /dev/null +++ b/Meadow.CommandLine/Commands/Mono/MonoDisableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Mono +{ + [Command("mono disable", Description = "Disable Mono on the Meadow")] + public class MonoDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public MonoDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.MonoDisable(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Mono/MonoEnableCommand.cs b/Meadow.CommandLine/Commands/Mono/MonoEnableCommand.cs new file mode 100644 index 00000000..43763471 --- /dev/null +++ b/Meadow.CommandLine/Commands/Mono/MonoEnableCommand.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Mono +{ + [Command("mono enable", Description = "Enable Mono on the Meadow Board")] + public class MonoEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public MonoEnableCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.MonoEnable(cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Mono/MonoFlashCommand.cs b/Meadow.CommandLine/Commands/Mono/MonoFlashCommand.cs new file mode 100644 index 00000000..c96e1f1c --- /dev/null +++ b/Meadow.CommandLine/Commands/Mono/MonoFlashCommand.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Mono +{ + [Command("mono flash", Description = "Get the Mono Run State on the Meadow Board")] + public class MonoFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public MonoFlashCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.MonoFlash(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Mono Flashed Successfully"); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Mono/MonoRunStateCommand.cs b/Meadow.CommandLine/Commands/Mono/MonoRunStateCommand.cs new file mode 100644 index 00000000..d3981c53 --- /dev/null +++ b/Meadow.CommandLine/Commands/Mono/MonoRunStateCommand.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Mono +{ + [Command("mono state", Description = "Get the Mono Run State on the Meadow Board")] + public class MonoRunStateCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public MonoRunStateCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + var runState = await device.GetMonoRunState(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Mono Run State: {(runState ? "Enabled" : "Disabled")}"); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Mono/MonoUpdateRuntimeCommand.cs b/Meadow.CommandLine/Commands/Mono/MonoUpdateRuntimeCommand.cs new file mode 100644 index 00000000..15f52e84 --- /dev/null +++ b/Meadow.CommandLine/Commands/Mono/MonoUpdateRuntimeCommand.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Mono +{ + [Command("mono update rt", Description = "Get the Mono Run State on the Meadow Board")] + public class MonoUpdateRuntimeCommand : MeadowSerialCommand + { + [CommandOption("filename",'f', Description = "The local name of the mono runtime file. Default is empty.")] + public string Filename {get; init;} + + private readonly ILogger _logger; + + public MonoUpdateRuntimeCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.UpdateMonoRuntime(Filename, cancellationToken: cancellationToken); + + await device.ResetMeadow(cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation($"Mono Flashed Successfully"); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Nsh/NshDisableCommand.cs b/Meadow.CommandLine/Commands/Nsh/NshDisableCommand.cs new file mode 100644 index 00000000..c61413c7 --- /dev/null +++ b/Meadow.CommandLine/Commands/Nsh/NshDisableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Nsh +{ + [Command("nsh disable", Description = "Disable Mono on the Meadow")] + public class NshDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public NshDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.NshDisable(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Nsh/NshEnableCommand.cs b/Meadow.CommandLine/Commands/Nsh/NshEnableCommand.cs new file mode 100644 index 00000000..fd835e10 --- /dev/null +++ b/Meadow.CommandLine/Commands/Nsh/NshEnableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Nsh +{ + [Command("nsh enable", Description = "Disable Mono on the Meadow")] + public class NshEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public NshEnableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.NshEnable(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Qspi/QspiInitCommand.cs b/Meadow.CommandLine/Commands/Qspi/QspiInitCommand.cs new file mode 100644 index 00000000..c1419799 --- /dev/null +++ b/Meadow.CommandLine/Commands/Qspi/QspiInitCommand.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Qspi +{ + [Command("qspi write", Description = "Write a QSPI value to the Meadow")] + public class QspiInitCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to initialize", IsRequired = true)] + public int Value {get; init;} + + public QspiInitCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.QspiInit(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Qspi/QspiReadCommand.cs b/Meadow.CommandLine/Commands/Qspi/QspiReadCommand.cs new file mode 100644 index 00000000..1b5d5b5a --- /dev/null +++ b/Meadow.CommandLine/Commands/Qspi/QspiReadCommand.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Qspi +{ + [Command("qspi read", Description = "Read a QSPI value from the Meadow")] + public class QspiReadCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to read", IsRequired = true)] + public int Value {get; init;} + + public QspiReadCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.QspiRead(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Qspi/QspiWriteCommand.cs b/Meadow.CommandLine/Commands/Qspi/QspiWriteCommand.cs new file mode 100644 index 00000000..d72a29b9 --- /dev/null +++ b/Meadow.CommandLine/Commands/Qspi/QspiWriteCommand.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Qspi +{ + [Command("qspi write", Description = "Write a QSPI value to the Meadow")] + public class QspiWriteCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + [CommandOption("value",'v', Description = "The QSPI Value to write", IsRequired = true)] + public int Value {get; init;} + + public QspiWriteCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.QspiWrite(Value, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Storage/EraseFlashCommand.cs b/Meadow.CommandLine/Commands/Storage/EraseFlashCommand.cs new file mode 100644 index 00000000..21d096da --- /dev/null +++ b/Meadow.CommandLine/Commands/Storage/EraseFlashCommand.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Storage +{ + [Command("flash erase", Description = "Erase the flash on the Meadow Board")] + public class EraseFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + + public EraseFlashCommand(ILoggerFactory loggerFactory, + MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation("Erasing flash."); + using var device = await MeadowDeviceManager + .GetMeadowForSerialPort( + SerialPortName, + true, + cancellationToken) + .ConfigureAwait(false); + + await device.EraseFlash(cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Commands/Storage/VerifyFlashCommand.cs b/Meadow.CommandLine/Commands/Storage/VerifyFlashCommand.cs new file mode 100644 index 00000000..81c87eb9 --- /dev/null +++ b/Meadow.CommandLine/Commands/Storage/VerifyFlashCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Storage +{ + [Command("flash verify", Description = "Erase the flash on the Meadow Board")] + public class VerifyFlashCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public VerifyFlashCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + _logger.LogInformation("Verifying flash"); + using var device = await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + await device.VerifyErasedFlash(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/TraceCommands/TraceDisableCommand.cs b/Meadow.CommandLine/Commands/TraceCommands/TraceDisableCommand.cs new file mode 100644 index 00000000..5c6eaf84 --- /dev/null +++ b/Meadow.CommandLine/Commands/TraceCommands/TraceDisableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.TraceCommands +{ + [Command("trace disable", Description = "Disable Trace Logging on the Meadow")] + public class TraceDisableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public TraceDisableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.TraceDisable(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/TraceCommands/TraceEnableCommand.cs b/Meadow.CommandLine/Commands/TraceCommands/TraceEnableCommand.cs new file mode 100644 index 00000000..5c7e987f --- /dev/null +++ b/Meadow.CommandLine/Commands/TraceCommands/TraceEnableCommand.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.NewDeviceManagement; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.TraceCommands +{ + [Command("trace enable", Description = "Enable trace logging on the Meadow")] + public class TraceEnableCommand : MeadowSerialCommand + { + private readonly ILogger _logger; + public TraceEnableCommand(ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) + : base(loggerFactory, meadowDeviceManager) + { + _logger = LoggerFactory.CreateLogger(); + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + using var device = + await MeadowDeviceManager.GetMeadowForSerialPort(SerialPortName, true, cancellationToken).ConfigureAwait(false); + + await device.TraceEnable(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Meadow.CommandLine/Commands/Utility/InstallDfuUtilCommand.cs b/Meadow.CommandLine/Commands/Utility/InstallDfuUtilCommand.cs new file mode 100644 index 00000000..57b7314e --- /dev/null +++ b/Meadow.CommandLine/Commands/Utility/InstallDfuUtilCommand.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.Versioning; +using System.Security.Principal; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI; +using Microsoft.Extensions.Logging; + +namespace Meadow.CommandLine.Commands.Utility +{ + [Command("install dfu-util", Description = "Install the DfuUtil utility")] + public class InstallDfuUtilCommand : ICommand + { + private readonly ILogger _logger; + public InstallDfuUtilCommand(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + if (OperatingSystem.IsWindows() && IsAdministrator()) + { + var downloadManager = new DownloadManager(); + downloadManager.InstallDfuUtil(Environment.Is64BitOperatingSystem); + } + else if (OperatingSystem.IsMacOS()) + { + _logger.LogInformation("To install on macOS, run: brew install dfu-util"); + } else if (OperatingSystem.IsLinux()) + { + _logger.LogInformation( + "To install on Linux, use the package manager to install the dfu-util package"); + } + return ValueTask.CompletedTask; + } + + [SupportedOSPlatform("windows")] + private static bool IsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/Meadow.CommandLine/Meadow.CommandLine.csproj b/Meadow.CommandLine/Meadow.CommandLine.csproj new file mode 100644 index 00000000..c9fee628 --- /dev/null +++ b/Meadow.CommandLine/Meadow.CommandLine.csproj @@ -0,0 +1,19 @@ + + + + Exe + net5.0 + + + + + + + + + + + + + + diff --git a/Meadow.CommandLine/Program.cs b/Meadow.CommandLine/Program.cs new file mode 100644 index 00000000..02a083b3 --- /dev/null +++ b/Meadow.CommandLine/Program.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using CliFx; +using Meadow.CLI.Core.NewDeviceManagement; +using Meadow.CommandLine.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +namespace Meadow.CommandLine +{ + public class Program + { + public static async Task Main(string[] args) + { + var services = new ServiceCollection(); + services.AddLogging( + builder => + { + var logLevel = LogLevel.Information; + var logModifier = args.FirstOrDefault(a => a.Contains("-v")) + ?.Count(x => x == 'v') ?? 0; + + logLevel -= logModifier; + if (logLevel < 0) + { + logLevel = 0; + } + + Console.WriteLine($"Using log level {logLevel}"); + builder.AddSimpleConsole(c => + { + c.ColorBehavior = LoggerColorBehavior.Enabled; + c.SingleLine = true; + c.UseUtcTimestamp = true; + }).SetMinimumLevel(logLevel); + }); + + services.AddSingleton(); + AddCommandsAsServices(services); + var serviceProvider = services.BuildServiceProvider(); + return await new CliApplicationBuilder().AddCommandsFromThisAssembly() + .UseTypeActivator(serviceProvider.GetService) + .Build() + .RunAsync(); + } + + private static void AddCommandsAsServices(IServiceCollection services) + { + var assembly = System.Reflection.Assembly.GetAssembly(typeof(Program)); + Trace.Assert(assembly != null); + var types = assembly.GetTypes(); + + var commands = types.Where( + x => x.IsAssignableTo(typeof(MeadowSerialCommand)) + || x.IsAssignableTo(typeof(ICommand))) + .Where(x => !x.IsAbstract); + + foreach (var command in commands) + { + services.AddTransient(command); + } + } + } +} \ No newline at end of file diff --git a/Meadow.CommandLine/Properties/launchSettings.json b/Meadow.CommandLine/Properties/launchSettings.json new file mode 100644 index 00000000..b7401ddb --- /dev/null +++ b/Meadow.CommandLine/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Meadow.CommandLine": { + "commandName": "Project", + "commandLineArgs": "flash os -s COM14" + } + } +} \ No newline at end of file diff --git a/MeadowCLI.sln.DotSettings b/MeadowCLI.sln.DotSettings new file mode 100644 index 00000000..d4a9fde6 --- /dev/null +++ b/MeadowCLI.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/TestApp/Program.cs b/TestApp/Program.cs index ad928550..6094a952 100644 --- a/TestApp/Program.cs +++ b/TestApp/Program.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using MeadowCLI.DeviceManagement; namespace TestApp {