diff --git a/Meadow.CLI.Core/Devices/MeadowLocalDevice.Comms.cs b/Meadow.CLI.Core/Devices/MeadowLocalDevice.Comms.cs index 34a79bd2..2bb68c7b 100644 --- a/Meadow.CLI.Core/Devices/MeadowLocalDevice.Comms.cs +++ b/Meadow.CLI.Core/Devices/MeadowLocalDevice.Comms.cs @@ -14,10 +14,10 @@ namespace Meadow.CLI.Core.Devices public partial class MeadowLocalDevice { private const int PROGESS_INCREMENTS = 5; - uint _packetCrc32; + private uint _packetCrc32; private readonly SemaphoreSlim _comPortSemaphore = new SemaphoreSlim(1, 1); - bool reUploadSkippedFiles = false; - byte reUploadCounter = 0; + private bool reUploadSkippedFiles = false; + private byte reUploadCounter = 0; public async Task SendTheEntireFile(FileCommand command, bool lastInSeries, @@ -173,7 +173,7 @@ private void WriteProgress(decimal i) if (intProgress > nextProgress) { - if (!InMeadowCLI) // In separate call as used for progress delimiter + if (!InMeadowCLI || Debugger.IsAttached) // In separate call as used for progress delimiter { Logger?.LogInformation("="); } diff --git a/Meadow.CLI.Core/Devices/MeadowLocalDevice.cs b/Meadow.CLI.Core/Devices/MeadowLocalDevice.cs index 12cd1437..0e534bea 100644 --- a/Meadow.CLI.Core/Devices/MeadowLocalDevice.cs +++ b/Meadow.CLI.Core/Devices/MeadowLocalDevice.cs @@ -1,380 +1,378 @@ -using Meadow.CLI.Core.DeviceManagement; -using Meadow.CLI.Core.Internals.MeadowCommunication; -using Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses; -using Meadow.Hcom; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Meadow.CLI.Core.Devices -{ - public abstract partial class MeadowLocalDevice : IMeadowDevice - { - private protected TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); - - public ILogger? Logger { get; } - public MeadowDataProcessor DataProcessor { get; } - public MeadowDeviceInfo? DeviceInfo { get; protected set; } - public DebuggingServer DebuggingServer { get; } - public IList FilesOnDevice { get; } = new List(); - public bool InMeadowCLI { get; set; } - - protected MeadowLocalDevice(MeadowDataProcessor dataProcessor, ILogger? logger = null) - { - Logger = logger; - DataProcessor = dataProcessor; - - var entryAssembly = Assembly.GetEntryAssembly()!; - - if (entryAssembly != null) - InMeadowCLI = entryAssembly.FullName.ToLower().Contains("meadow"); - } - - public abstract Task Write(byte[] encodedBytes, - int encodedToSend, - CancellationToken cancellationToken = default); - - public async Task GetDeviceInfo(TimeSpan timeout, CancellationToken cancellationToken = default) - { - DeviceInfo = null; - - var command = new SimpleCommandBuilder( - HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION) - .WithTimeout(timeout) - .WithResponseType(MeadowMessageType.DeviceInfo) - .WithCompletionResponseType(MeadowMessageType.Concluded) - .Build(); - - - try - { - var retryCount = 1; - - Retry: - var commandResponse = await SendCommand(command, cancellationToken); - - if (commandResponse.IsSuccess) - { - if (commandResponse.Message == String.Empty) - { // TODO: this feels like a bug lower down or in HCOM, but I can reproduce it regularly (3 Oct 2022) - if (--retryCount >= 0) - { - goto Retry; - } - } - - DeviceInfo = new MeadowDeviceInfo(commandResponse.Message!); - return DeviceInfo; - } - - 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 async Task GetDeviceName(TimeSpan timeout, CancellationToken cancellationToken = default) - { - var info = await GetDeviceInfo(timeout, cancellationToken); - - return info?.Product ?? String.Empty; - } - - public async Task GetMonoRunState(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 SendCommand(command, cancellationToken); - - 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 async Task MonoDisable(CancellationToken cancellationToken = default) - { - Logger.LogDebug("Sending Mono Disable Request"); - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_DISABLE) - .WithCompletionResponseType(MeadowMessageType.SerialReconnect) - .WithResponseType(MeadowMessageType.SerialReconnect) - .Build(); - - await SendCommand(command, cancellationToken); - } - - public async Task MonoEnable(CancellationToken cancellationToken = default) - { - Logger.LogDebug("Sending Mono Enable Request"); - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_ENABLE) - .WithCompletionResponseType(MeadowMessageType.SerialReconnect) - .WithResponseType(MeadowMessageType.SerialReconnect) - .Build(); - - await SendCommand(command, cancellationToken); - } - - public Task MonoFlash(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 SendCommand(command, cancellationToken); - } - - public async Task ResetMeadow(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU) - .WithCompletionResponseType(MeadowMessageType.SerialReconnect) - .WithResponseType(MeadowMessageType.SerialReconnect) - .Build(); - - await SendCommand(command, cancellationToken); - } - - public Task EnterDfuMode(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENTER_DFU_MODE) - .WithCompletionResponseType(MeadowMessageType.Accepted) - .WithResponseFilter(x => true) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task NshEnable(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) - .WithUserData(1) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task NshDisable(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) - .WithUserData(0) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task TraceEnable(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task Uart1Trace(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task TraceDisable(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task SetTraceLevel(uint traceLevel, CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL) - .WithUserData(traceLevel) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task SetDeveloper(ushort level, uint userData, CancellationToken cancellationToken = default) - { - var command = new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER) - .WithDeveloperLevel(level) - .WithUserData(userData) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task Uart1Apps(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_UART) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task QspiWrite(int value, CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE) - .WithUserData((uint)value) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task QspiRead(int value, CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_READ) - .WithUserData((uint)value) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public Task QspiInit(int value, CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_INIT) - .WithUserData((uint)value) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public async Task StartDebugging(int port, CancellationToken cancellationToken) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION) - .WithCompletionResponseType(MeadowMessageType.Accepted) - .WithResponseType(MeadowMessageType.Accepted) - .Build(); - - await SendCommand(command, cancellationToken); - } - - public Task RestartEsp32(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESTART_ESP32) - .WithCompletionResponseType(MeadowMessageType.Concluded) - .Build(); - - return SendCommand(command, cancellationToken); - } - - public async Task GetDeviceMacAddress(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder( - HcomMeadowRequestType.HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS).Build(); - - var commandResponse = - await SendCommand(command, cancellationToken); - - return commandResponse.Message; - } - - public async Task GetRtcTime(CancellationToken cancellationToken = default) - { - var command = - new SimpleCommandBuilder( - HcomMeadowRequestType.HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD) - .WithResponseType(MeadowMessageType.Data) - .Build(); - - var commandResponse = - await SendCommand(command, cancellationToken); - - // return will be in the format "UTC time:2022-10-22T10:40:19+0:00" - return DateTimeOffset.Parse(commandResponse.Message.Substring(9)); - } - - public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken cancellationToken) - { - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD - ) - .WithCompletionResponseType(MeadowMessageType.Accepted) - .WithResponseType(MeadowMessageType.Accepted) - .WithData(Encoding.ASCII.GetBytes(dateTime.ToString("o"))) - .Build(); - - await SendCommand(command, cancellationToken); - } - - public async Task CloudRegisterDevice(CancellationToken cancellationToken = default) - { - Logger.LogInformation("Sending Meadow Cloud registration request (~2 mins)"); - - var command = - new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE) - .WithResponseType(MeadowMessageType.DevicePublicKey) - .WithCompletionResponseType(MeadowMessageType.Concluded) - .WithTimeout(new TimeSpan(hours: 0, minutes: 5, seconds: 0)) // RSA keypair generation on device takes a while - .Build(); - - var commandResponse = - await SendCommand(command, cancellationToken); - - return commandResponse.Message; - } - - public abstract Task Initialize(CancellationToken cancellationToken); - - public abstract bool IsDeviceInitialized(); - - private protected abstract void Dispose(bool disposing); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~MeadowLocalDevice() - { - Dispose(false); - } - } -} +using Meadow.CLI.Core.DeviceManagement; +using Meadow.CLI.Core.Internals.MeadowCommunication; +using Meadow.CLI.Core.Internals.MeadowCommunication.ReceiveClasses; +using Meadow.Hcom; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Meadow.CLI.Core.Devices +{ + public abstract partial class MeadowLocalDevice : IMeadowDevice + { + private protected TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + + public ILogger? Logger { get; } + public MeadowDataProcessor DataProcessor { get; } + public MeadowDeviceInfo? DeviceInfo { get; protected set; } + public DebuggingServer DebuggingServer { get; } + public IList FilesOnDevice { get; } = new List(); + public bool InMeadowCLI { get; set; } + + protected MeadowLocalDevice(MeadowDataProcessor dataProcessor, ILogger? logger = null) + { + Logger = logger; + DataProcessor = dataProcessor; + + var entryAssembly = Assembly.GetEntryAssembly()!; + + if (entryAssembly != null) + InMeadowCLI = entryAssembly.FullName.ToLower().Contains("meadow"); + } + + public abstract Task Write(byte[] encodedBytes, + int encodedToSend, + CancellationToken cancellationToken = default); + + public async Task GetDeviceInfo(TimeSpan timeout, CancellationToken cancellationToken = default) + { + DeviceInfo = null; + + var command = new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION) + .WithTimeout(timeout) + .WithResponseType(MeadowMessageType.DeviceInfo) + .WithCompletionResponseType(MeadowMessageType.Concluded) + .Build(); + + + try + { + var retryCount = 1; + + Retry: + var commandResponse = await SendCommand(command, cancellationToken); + + if (commandResponse.IsSuccess) + { + if (commandResponse.Message == String.Empty) + { // TODO: this feels like a bug lower down or in HCOM, but I can reproduce it regularly (3 Oct 2022) + if (--retryCount >= 0) + { + goto Retry; + } + } + + DeviceInfo = new MeadowDeviceInfo(commandResponse.Message!); + return DeviceInfo; + } + + 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 async Task GetDeviceName(TimeSpan timeout, CancellationToken cancellationToken = default) + { + var info = await GetDeviceInfo(timeout, cancellationToken); + + return info?.Product ?? String.Empty; + } + + public async Task GetMonoRunState(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 SendCommand(command, cancellationToken); + + 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 async Task MonoDisable(CancellationToken cancellationToken = default) + { + Logger.LogDebug("Sending Mono Disable Request"); + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_DISABLE) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommand(command, cancellationToken); + } + + public async Task MonoEnable(CancellationToken cancellationToken = default) + { + Logger.LogDebug("Sending Mono Enable Request"); + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_ENABLE) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommand(command, cancellationToken); + } + + public Task MonoFlash(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 SendCommand(command, cancellationToken); + } + + public async Task ResetMeadow(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESET_PRIMARY_MCU) + .WithCompletionResponseType(MeadowMessageType.SerialReconnect) + .WithResponseType(MeadowMessageType.SerialReconnect) + .Build(); + + await SendCommand(command, cancellationToken); + } + + public Task EnterDfuMode(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENTER_DFU_MODE) + .WithCompletionResponseType(MeadowMessageType.Accepted) + .WithResponseFilter(x => true) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task NshEnable(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) + .WithUserData(1) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task NshDisable(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH) + .WithUserData(0) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task TraceEnable(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task Uart1Trace(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task TraceDisable(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task SetTraceLevel(uint traceLevel, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL) + .WithUserData(traceLevel) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task SetDeveloper(ushort level, uint userData, CancellationToken cancellationToken = default) + { + var command = new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_DEVELOPER) + .WithDeveloperLevel(level) + .WithUserData(userData) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task Uart1Apps(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_UART) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task QspiWrite(int value, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_WRITE) + .WithUserData((uint)value) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task QspiRead(int value, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_READ) + .WithUserData((uint)value) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public Task QspiInit(int value, CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_S25FL_QSPI_INIT) + .WithUserData((uint)value) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public async Task StartDebugging(int port, CancellationToken cancellationToken) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION) + .WithCompletionResponseType(MeadowMessageType.Accepted) + .WithResponseType(MeadowMessageType.Accepted) + .Build(); + + await SendCommand(command, cancellationToken); + } + + public Task RestartEsp32(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RESTART_ESP32) + .WithCompletionResponseType(MeadowMessageType.Concluded) + .Build(); + + return SendCommand(command, cancellationToken); + } + + public async Task GetDeviceMacAddress(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS).Build(); + + var commandResponse = + await SendCommand(command, cancellationToken); + + return commandResponse.Message; + } + + public async Task GetRtcTime(CancellationToken cancellationToken = default) + { + var command = + new SimpleCommandBuilder( + HcomMeadowRequestType.HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD) + .WithResponseType(MeadowMessageType.Data) + .Build(); + + var commandResponse = + await SendCommand(command, cancellationToken); + + // return will be in the format "UTC time:2022-10-22T10:40:19+0:00" + return DateTimeOffset.Parse(commandResponse.Message.Substring(9)); + } + + public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken cancellationToken) + { + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD + ) + .WithCompletionResponseType(MeadowMessageType.Accepted) + .WithResponseType(MeadowMessageType.Accepted) + .WithData(Encoding.ASCII.GetBytes(dateTime.ToString("o"))) + .Build(); + + await SendCommand(command, cancellationToken); + } + + public async Task CloudRegisterDevice(CancellationToken cancellationToken = default) + { + Logger.LogInformation("Sending Meadow Cloud registration request (~2 mins)"); + + var command = + new SimpleCommandBuilder(HcomMeadowRequestType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE) + .WithResponseType(MeadowMessageType.DevicePublicKey) + .WithCompletionResponseType(MeadowMessageType.Concluded) + .WithTimeout(new TimeSpan(hours: 0, minutes: 5, seconds: 0)) // RSA keypair generation on device takes a while + .Build(); + + var commandResponse = + await SendCommand(command, cancellationToken); + + return commandResponse.Message; + } + + public abstract Task Initialize(CancellationToken cancellationToken); + + public abstract bool IsDeviceInitialized(); + + private protected abstract void Dispose(bool disposing); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~MeadowLocalDevice() + { + Dispose(false); + } + } +} diff --git a/Meadow.CLI.Core/Managers/PackageManager.cs b/Meadow.CLI.Core/Managers/PackageManager.cs index 333fc204..a830cd9b 100644 --- a/Meadow.CLI.Core/Managers/PackageManager.cs +++ b/Meadow.CLI.Core/Managers/PackageManager.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using GlobExpressions; +using Meadow.CLI.Core.DeviceManagement; +using System; using System.Diagnostics; -using System.Dynamic; using System.IO; using System.IO.Compression; using System.Linq; @@ -9,8 +9,6 @@ using System.Text.Json; using System.Threading.Tasks; using System.Xml; -using GlobExpressions; -using Meadow.CLI.Core.DeviceManagement; namespace Meadow.CLI.Core { @@ -43,7 +41,7 @@ public async Task CreatePackage(string projectPath, string osVersion, st return CreateMpak(postlinkBinDir, mpakName, osVersion, globPath); } - void BuildProject(string projectPath) + private void BuildProject(string projectPath) { var proc = new Process(); proc.StartInfo.FileName = "dotnet"; @@ -73,7 +71,7 @@ void BuildProject(string projectPath) } } - string GetProjectTargetFramework(string projectPath) + private string GetProjectTargetFramework(string projectPath) { XmlDocument doc = new XmlDocument(); doc.Load(projectPath); @@ -85,7 +83,7 @@ string GetProjectTargetFramework(string projectPath) : targetFramework; } - async Task TrimDependencies(string appDllPath, string osVersion) + private async Task TrimDependencies(string appDllPath, string osVersion) { FileInfo projectAppDll = new FileInfo(appDllPath); @@ -98,7 +96,7 @@ await AssemblyManager.TrimDependencies(projectAppDll.Name, projectAppDll.Directo dependencies, null, null, false, verbose: false); } - string CreateMpak(string postlinkBinDir, string mpakName, string osVersion, string globPath) + private string CreateMpak(string postlinkBinDir, string mpakName, string osVersion, string globPath) { if (string.IsNullOrEmpty(mpakName)) { @@ -157,8 +155,8 @@ string CreateMpak(string postlinkBinDir, string mpakName, string osVersion, stri return mpakPath; } - - void CreateEntry(ZipArchive archive, string fromFile, string entryPath) + + private void CreateEntry(ZipArchive archive, string fromFile, string entryPath) { // Windows '\' Path separator character will be written to the zip which meadow os does not properly unpack // See: https://github.com/dotnet/runtime/issues/41914 diff --git a/Meadow.CLI/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json index d26c43c4..a9bc47bc 100644 --- a/Meadow.CLI/Properties/launchSettings.json +++ b/Meadow.CLI/Properties/launchSettings.json @@ -34,7 +34,7 @@ }, "FileWrite": { "commandName": "Project", - "commandLineArgs": "file write -f App.exe" + "commandLineArgs": "file write -f \"f:\\temp\\test.txt\"" }, "FlashErase": { "commandName": "Project", @@ -46,7 +46,7 @@ }, "FlashOS": { "commandName": "Project", - "commandLineArgs": "flash os" + "commandLineArgs": "flash os -d -k" }, "Help": { "commandName": "Project", @@ -106,7 +106,7 @@ }, "UsePort": { "commandName": "Project", - "commandLineArgs": "use port COM16" + "commandLineArgs": "use port COM10" }, "Version": { "commandName": "Project", diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln new file mode 100644 index 00000000..52244504 --- /dev/null +++ b/Source/v2/Meadow.CLI.v2.sln @@ -0,0 +1,78 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Hcom", "Meadow.Hcom\Meadow.Hcom.csproj", "{6C2FA084-701B-4A28-8775-BF18B84E366B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.HCom.Integration.Tests", "Meadow.HCom.Integration.Tests\Meadow.HCom.Integration.Tests.csproj", "{9EAF2357-2AB3-45BB-822B-B9B4629E651E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Cli", "Meadow.Cli\Meadow.Cli.csproj", "{1BD32521-158C-478A-AEE7-0EE52BF3571F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Contracts", "..\..\..\Meadow.Contracts\Source\Meadow.Contracts\Meadow.Contracts.csproj", "{62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_ref", "_ref", "{562945CE-DA15-4A2E-86A2-CA7E3FF22DCA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Logging", "..\..\..\Meadow.Logging\Source\Meadow.Logging\lib\Meadow.Logging.csproj", "{946B7200-8CC1-4A8D-9BCE-FCB06EDC705B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Units", "..\..\..\Meadow.Units\Source\Meadow.Units\Meadow.Units.csproj", "{5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.SoftwareManager", "Meadow.SoftwareManager\Meadow.SoftwareManager.csproj", "{B842E44B-F57F-4728-980F-776DE6E2CDAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.SoftwareManager.Unit.Tests", "Meadow.SoftwareManager.Unit.Tests\Meadow.SoftwareManager.Unit.Tests.csproj", "{B093D40B-AA2F-4BA8-9B7E-DB14DAC9AFC9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C2FA084-701B-4A28-8775-BF18B84E366B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C2FA084-701B-4A28-8775-BF18B84E366B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C2FA084-701B-4A28-8775-BF18B84E366B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C2FA084-701B-4A28-8775-BF18B84E366B}.Release|Any CPU.Build.0 = Release|Any CPU + {9EAF2357-2AB3-45BB-822B-B9B4629E651E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EAF2357-2AB3-45BB-822B-B9B4629E651E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EAF2357-2AB3-45BB-822B-B9B4629E651E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EAF2357-2AB3-45BB-822B-B9B4629E651E}.Release|Any CPU.Build.0 = Release|Any CPU + {1BD32521-158C-478A-AEE7-0EE52BF3571F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BD32521-158C-478A-AEE7-0EE52BF3571F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BD32521-158C-478A-AEE7-0EE52BF3571F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BD32521-158C-478A-AEE7-0EE52BF3571F}.Release|Any CPU.Build.0 = Release|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Release|Any CPU.Build.0 = Release|Any CPU + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47}.Release|Any CPU.Deploy.0 = Release|Any CPU + {946B7200-8CC1-4A8D-9BCE-FCB06EDC705B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {946B7200-8CC1-4A8D-9BCE-FCB06EDC705B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {946B7200-8CC1-4A8D-9BCE-FCB06EDC705B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {946B7200-8CC1-4A8D-9BCE-FCB06EDC705B}.Release|Any CPU.Build.0 = Release|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Release|Any CPU.Build.0 = Release|Any CPU + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC}.Release|Any CPU.Deploy.0 = Release|Any CPU + {B842E44B-F57F-4728-980F-776DE6E2CDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B842E44B-F57F-4728-980F-776DE6E2CDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B842E44B-F57F-4728-980F-776DE6E2CDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B842E44B-F57F-4728-980F-776DE6E2CDAA}.Release|Any CPU.Build.0 = Release|Any CPU + {B093D40B-AA2F-4BA8-9B7E-DB14DAC9AFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B093D40B-AA2F-4BA8-9B7E-DB14DAC9AFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B093D40B-AA2F-4BA8-9B7E-DB14DAC9AFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B093D40B-AA2F-4BA8-9B7E-DB14DAC9AFC9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {62D2092A-5A19-47AC-9B81-A8F5D9D7BD47} = {562945CE-DA15-4A2E-86A2-CA7E3FF22DCA} + {946B7200-8CC1-4A8D-9BCE-FCB06EDC705B} = {562945CE-DA15-4A2E-86A2-CA7E3FF22DCA} + {5DC0D8EC-85D4-4E98-9403-2D5B9700D8AC} = {562945CE-DA15-4A2E-86A2-CA7E3FF22DCA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9BA76C46-6C2E-447A-BCAD-423B3C51C52D} + EndGlobalSection +EndGlobal diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs new file mode 100644 index 00000000..f80f3f76 --- /dev/null +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -0,0 +1,90 @@ +using Meadow.Hcom; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.Cli; + +public static class AppManager +{ + private static bool MatchingDllExists(string file) + { + var root = Path.GetFileNameWithoutExtension(file); + return File.Exists($"{root}.dll"); + } + + private static bool IsPdb(string file) + { + return string.Compare(Path.GetExtension(file), ".pdb", true) == 0; + } + + private static bool IsXmlDoc(string file) + { + if (string.Compare(Path.GetExtension(file), ".xml", true) == 0) + { + return MatchingDllExists(file); + } + return false; + } + + public static async Task DeployApplication( + IMeadowConnection connection, + string localBinaryDirectory, + bool includePdbs, + bool includeXmlDocs, + ILogger logger, + CancellationToken cancellationToken) + { + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + + if (wasRuntimeEnabled) + { + logger.LogInformation("Disabling runtime..."); + + await connection.RuntimeDisable(cancellationToken); + } + + // TODO: add sub-folder support when HCOM supports it + + var localFiles = new Dictionary(); + + // get a list of files to send + logger.LogInformation("Generating the list of files to deploy..."); + foreach (var file in Directory.GetFiles(localBinaryDirectory)) + { + // TODO: add any other filtering capability here + + if (!includePdbs && IsPdb(file)) continue; + if (!includeXmlDocs && IsXmlDoc(file)) continue; + + // read the file data so we can generate a CRC + using FileStream fs = File.Open(file, FileMode.Open); + var len = (int)fs.Length; + var bytes = new byte[len]; + + await fs.ReadAsync(bytes, 0, len, cancellationToken); + + var crc = CrcTools.Crc32part(bytes, len, 0); + + localFiles.Add(file, crc); + } + + // get a list of files on-device, with CRCs + var deviceFiles = await connection.GetFileList(true, cancellationToken); + + + // erase all files on device not in list of files to send + + // send any file that has a different CRC + + + if (wasRuntimeEnabled) + { + // restore runtime state + logger.LogInformation("Enabling runtime..."); + + await connection.RuntimeEnable(cancellationToken); + } + + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs new file mode 100644 index 00000000..1296f80e --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -0,0 +1,57 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app build", Description = "Compiles a Meadow application")] +public class AppBuildCommand : BaseCommand +{ + private IPackageManager _packageManager; + + [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] + public string? Configuration { get; set; } + + [CommandParameter(0, Name = "Path to project file", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppBuildCommand(IPackageManager packageManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { + _packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + // is the path a file? + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) + { + Logger.LogError($"Invalid application path '{path}'"); + return; + } + } + + if (Configuration == null) Configuration = "Release"; + + Logger.LogInformation($"Building {Configuration} configuration of of {path}..."); + + // TODO: enable cancellation of this call + var success = _packageManager.BuildApplication(path, Configuration); + + if (!success) + { + Logger.LogError($"Build failed!"); + } + else + { + Logger.LogError($"Build success."); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs new file mode 100644 index 00000000..3ac340d5 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -0,0 +1,57 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app deploy", Description = "Deploys a built Meadow application to a target device")] +public class AppDeployCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppDeployCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + // is the path a file? + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) + { + Logger.LogError($"Invalid application path '{path}'"); + return; + } + } + else + { + // TODO: only deploy if it's App.dll + } + + // do we have the full app path, or just the project root? + + // TODO: determine the latest build + + await AppManager.DeployApplication(connection, "", true, false, Logger, cancellationToken); + + var success = false; + + if (!success) + { + Logger.LogError($"Build failed!"); + } + else + { + Logger.LogError($"Build success."); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs new file mode 100644 index 00000000..60b4d164 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -0,0 +1,63 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app trim", Description = "Runs an already-compiled Meadow application through reference trimming")] +public class AppTrimCommand : BaseCommand +{ + private IPackageManager _packageManager; + + [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] + public string? Configuration { get; set; } + + [CommandParameter(0, Name = "Path to project file", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppTrimCommand(IPackageManager packageManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { + _packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + string path = Path == null + ? AppDomain.CurrentDomain.BaseDirectory + : Path; + + // is the path a file? + FileInfo file; + + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) + { + Logger.LogError($"Invalid application path '{path}'"); + return; + } + + // it's a directory - we need to determine the latest build (they might have a Debug and Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger.LogError($"Cannot find a compiled application at '{path}'"); + return; + } + + file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + } + else + { + file = new FileInfo(path); + } + + // if no configuration was provided, find the most recently built + Logger.LogInformation($"Trimming {file.FullName} (this may take a few seconds)..."); + + await _packageManager.TrimApplication(file, false, null, cancellationToken); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs new file mode 100644 index 00000000..dad0c13b --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -0,0 +1,34 @@ +using CliFx; +using CliFx.Infrastructure; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseCommand : ICommand +{ + protected ILogger Logger { get; } + protected ISettingsManager SettingsManager { get; } + + public BaseCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + { + Logger = loggerFactory.CreateLogger(); + SettingsManager = settingsManager; + } + + protected abstract ValueTask ExecuteCommand(CancellationToken cancellationToken); + + public virtual async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + try + { + await ExecuteCommand(cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex.Message); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs new file mode 100644 index 00000000..f9201b1e --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -0,0 +1,64 @@ +using CliFx; +using CliFx.Infrastructure; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseDeviceCommand : ICommand +{ + protected ILogger Logger { get; } + protected MeadowConnectionManager ConnectionManager { get; } + + public BaseDeviceCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + { + Logger = loggerFactory.CreateLogger(); + ConnectionManager = connectionManager; + } + + protected abstract ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken); + + public virtual async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + var c = ConnectionManager.GetCurrentConnection(); + + if (c != null) + { + c.ConnectionError += (s, e) => + { + Logger.LogError(e.Message); + }; + + try + { + await c.Attach(cancellationToken); + + + if (cancellationToken.IsCancellationRequested) + { + Logger.LogInformation($"Cancelled"); + return; + } + + if (c.Device == null) + { + Logger.LogError("No device found"); + } + else + { + await ExecuteCommand(c, c.Device, cancellationToken); + Logger.LogInformation($"Done."); + } + } + catch (TimeoutException) + { + Logger.LogError($"Timeout attempting to attach to device on {c.Name}"); + } + catch (Exception ex) + { + Logger.LogError($"Failed: {ex.Message}"); + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs new file mode 100644 index 00000000..3679d8f5 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -0,0 +1,16 @@ +using Meadow.Cli; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseFileCommand : BaseCommand +{ + protected FileManager FileManager { get; } + + public BaseFileCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { + FileManager = fileManager; + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs new file mode 100644 index 00000000..3c07f021 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs @@ -0,0 +1,71 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Infrastructure; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("config", Description = "Read or modify the meadow CLI configuration")] +public class ConfigCommand : ICommand +{ + private readonly ISettingsManager _settingsManager; + private readonly ILogger? _logger; + + [CommandOption("list", IsRequired = false)] + public bool List { get; set; } + + [CommandParameter(0, Name = "Settings", IsRequired = false)] + public string[] Settings { get; set; } + + public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + { + _logger = loggerFactory?.CreateLogger(); + _settingsManager = settingsManager; + } + + public async ValueTask ExecuteAsync(IConsole console) + { + if (List) + { + _logger?.LogInformation($"Current CLI configuration"); + + // display all current config + var settings = _settingsManager.GetPublicSettings(); + if (settings.Count == 0) + { + _logger?.LogInformation($" "); + } + else + { + foreach (var kvp in _settingsManager.GetPublicSettings()) + { + _logger?.LogInformation($" {kvp.Key} = {kvp.Value}"); + } + } + } + else + { + switch (Settings.Length) + { + case 0: + // not valid + throw new CommandException($"No setting provided"); + case 1: + // erase a setting + _logger?.LogInformation($"Deleting Setting {Settings[0]}"); + _settingsManager.DeleteSetting(Settings[0]); + break; + case 2: + // set a setting + _logger?.LogInformation($"Setting {Settings[0]}={Settings[1]}"); + _settingsManager.SaveSetting(Settings[0], Settings[1]); + break; + default: + // not valid; + throw new CommandException($"Too many parameters provided"); + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs new file mode 100644 index 00000000..f2bda985 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -0,0 +1,37 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("developer", Description = "Sets a specified developer parameter on the Meadow")] +public class DeveloperCommand : BaseDeviceCommand +{ + [CommandOption("param", 'p', Description = "The parameter to set.")] + public ushort Parameter { get; set; } + + [CommandOption("value", 'v', Description = "The value to apply to the parameter. Valid values are 0 to 4,294,967,295")] + public uint Value { get; set; } + + public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + Logger.LogInformation($"Setting developer parameter {Parameter} to {Value}"); + + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + connection.ConnectionError += (s, e) => + { + Logger.LogError(e.Message); + }; + + await device.SetDeveloperParameter(Parameter, Value, cancellationToken); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs new file mode 100644 index 00000000..7980c712 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -0,0 +1,44 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("device clock", Description = "Gets or sets the device clock (in UTC time)")] +public class DeviceClockCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "Time", IsRequired = false)] + public string? Time { get; set; } + + public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + if (Time == null) + { + Logger.LogInformation($"Getting device clock..."); + var deviceTime = await device.GetRtcTime(cancellationToken); + Logger.LogInformation($"{deviceTime.Value:s}Z"); + } + else + { + if (Time == "now") + { + Logger.LogInformation($"Setting device clock..."); + await device.SetRtcTime(DateTimeOffset.UtcNow, cancellationToken); + } + else if (DateTimeOffset.TryParse(Time, out DateTimeOffset dto)) + { + Logger.LogInformation($"Setting device clock..."); + await device.SetRtcTime(dto, cancellationToken); + } + else + { + Logger.LogInformation($"Unable to parse '{Time}' to a valid time."); + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs new file mode 100644 index 00000000..a9a17f0b --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -0,0 +1,24 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("device info", Description = "Get the device info")] +public class DeviceInfoCommand : BaseDeviceCommand +{ + public DeviceInfoCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Getting device info..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + var deviceInfo = await device.GetDeviceInfo(cancellationToken); + if (deviceInfo != null) + { + Logger.LogInformation(deviceInfo.ToString()); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs new file mode 100644 index 00000000..a8cb9e64 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -0,0 +1,20 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("device reset", Description = "Resets the device")] +public class DeviceResetCommand : BaseDeviceCommand +{ + public DeviceResetCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Resetting the device..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + await device.Reset(); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs new file mode 100644 index 00000000..c71926bb --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -0,0 +1,83 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Software; +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("dfu install", Description = "Deploys a built Meadow application to a target device")] +public class DfuInstallCommand : BaseCommand +{ + public const string DefaultVersion = "0.11"; + + [CommandOption("version", 'v', IsRequired = false)] + public string? Version { get; set; } + + protected DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory, string version) + : base(settingsManager, loggerFactory) + { + Version = version; + } + + public DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + if (Version == null) + { + Version = DefaultVersion; + } + + switch (Version) + { + case "0.10": + case "0.11": + // valid + break; + default: + Logger.LogError("Only versions 0.10 and 0.11 are supported."); + return; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (IsAdministrator()) + { + await DfuUtils.InstallDfuUtil(FileManager.WildernessTempFolderPath, Version, cancellationToken); + } + else + { + Logger.LogError("To install DFU on Windows, you'll need to re-run the command from as an Administrator"); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Logger.LogWarning("To install DFU on macOS, run: brew install dfu-util"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Logger.LogWarning( + "To install DFU on Linux, use the package manager to install the dfu-util package"); + } + } + + private static bool IsAdministrator() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + else + { + return false; + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs new file mode 100644 index 00000000..5e3f1bdf --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -0,0 +1,85 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file list", Description = "Lists the files in the current device directory")] +public class FileListCommand : BaseDeviceCommand +{ + public const int FileSystemBlockSize = 4096; + + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; set; } + + public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Getting file list..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + var files = await device.GetFileList(Verbose, cancellationToken); + + if (files == null || files.Length == 0) + { + Logger.LogInformation($"No files found"); + } + else + { + if (Verbose) + { + var longestFileName = files.Select(x => x.Name.Length) + .OrderByDescending(x => x) + .FirstOrDefault(); + + var totalBytesUsed = 0L; + var totalBlocksUsed = 0L; + + foreach (var file in files) + { + totalBytesUsed += file.Size ?? 0; + totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; + + var line = $"{file.Name.PadRight(longestFileName)}"; + line = $"{line}\t{file.Crc:x8}"; + + if (file.Size > 1000000) + { + line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; + } + else if (file.Size > 1000) + { + line = $"{line}\t{file.Size / 1000,7:0} kB "; + } + else + { + line = $"{line}\t{file.Size,7} bytes"; + } + + Logger.LogInformation(line); + } + + Logger.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files\n" + + $"\t{totalBytesUsed / 1000000d:0.00}MB of file data\n" + + $"\tSpanning {totalBlocksUsed} blocks\n" + + $"\tConsuming {totalBlocksUsed * FileSystemBlockSize / 1000000d:0.00}MB on disk"); + } + else + { + foreach (var file in files) + { + Logger.LogInformation(file.Name); + } + + Logger.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} files"); + } + } + + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs new file mode 100644 index 00000000..f475987e --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -0,0 +1,36 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file read", Description = "Reads a file from the device and writes it to the local file system")] +public class FileReadCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] + public string MeadowFile { get; set; } = default!; + + [CommandParameter(1, Name = "LocalFile", IsRequired = false)] + public string? LocalFile { get; set; } + + public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + Logger.LogInformation($"Getting file '{MeadowFile}' from device..."); + + var success = await device.ReadFile(MeadowFile, LocalFile, cancellationToken); + + if (success) + { + Logger.LogInformation($"Success"); + } + else + { + Logger.LogInformation($"Failed to retrieve file"); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs new file mode 100644 index 00000000..a6360e80 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -0,0 +1,84 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file write", Description = "Writes one or more files to the device from the local file system")] +public class FileWriteCommand : BaseDeviceCommand +{ + [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(); + + public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) + { + Logger.LogError( + $"Number of files to write ({Files.Count}) does not match the number of target file names ({TargetFileNames.Count})."); + + return; + } + + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + + // Console instead of Logger due to line breaking for progress bar + Console.Write($"Writing {e.fileName}: {p:0}% \r"); + }; + + Logger.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); + + for (var i = 0; i < Files.Count; i++) + { + if (!File.Exists(Files[i])) + { + Logger.LogError($"Cannot find file '{Files[i]}'. Skippping"); + } + else + { + var targetFileName = GetTargetFileName(i); + + Logger.LogInformation( + $"Writing '{Files[i]}' as '{targetFileName}' to device"); + + try + { + await device.WriteFile(Files[i], targetFileName, cancellationToken); + } + catch (Exception ex) + { + Logger.LogError($"Error writing file: {ex.Message}"); + } + } + } + } + + 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; + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs new file mode 100644 index 00000000..37687713 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -0,0 +1,35 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware default", Description = "Sets the current default firmware package")] +public class FirmwareDefaultCommand : BaseFileCommand +{ + public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) + { + } + + [CommandParameter(0, Name = "Version number to use as default", IsRequired = true)] + public string Version { get; set; } = default!; + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + await FileManager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; + + var existing = collection.FirstOrDefault(p => p.Version == Version); + + Logger?.LogInformation($"Setting default firmware to '{Version}'..."); + + await collection.SetDefaultPackage(Version); + + Logger?.LogInformation($"Done."); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs new file mode 100644 index 00000000..511a0cc5 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -0,0 +1,33 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware delete", Description = "Delete a local firmware package")] +public class FirmwareDeleteCommand : BaseFileCommand +{ + public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) + { + } + + [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] + public string Version { get; set; } = default!; + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + await FileManager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; + + Logger?.LogInformation($"Deleting firmware '{Version}'..."); + + await collection.DeletePackage(Version); + + Logger?.LogInformation($"Done."); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs new file mode 100644 index 00000000..65fb7283 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -0,0 +1,84 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware download", Description = "Download a firmware package")] +public class FirmwareDownloadCommand : BaseFileCommand +{ + public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) + { + } + + [CommandOption("force", 'f', IsRequired = false)] + public bool Force { get; set; } + + [CommandParameter(0, Name = "Version number to download", IsRequired = false)] + public string? Version { get; set; } = default!; + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + await FileManager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; + + if (Version == null) + { + var latest = await collection.GetLatestAvailableVersion(); + + if (latest == null) + { + Logger?.LogError($"Unable to get latest version information."); + return; + } + + Logger?.LogInformation($"Latest available version is '{latest}'..."); + Version = latest; + } + else + { + Logger?.LogInformation($"Checking for firmware package '{Version}'..."); + } + + var isAvailable = await collection.IsVersionAvailableForDownload(Version); + + if (!isAvailable) + { + Logger?.LogError($"Requested package version '{Version}' is not available."); + return; + } + + Logger?.LogInformation($"Downloading firmware package '{Version}'..."); + + + try + { + collection.DownloadProgress += OnDownloadProgress; + + var result = await collection.RetrievePackage(Version, Force); + + if (!result) + { + Logger?.LogError($"Unable to download package '{Version}'."); + } + else + { + Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + } + } + catch (Exception ex) + { + Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + } + } + + private void OnDownloadProgress(object? sender, long e) + { + Console.Write($"Retrieved {e} bytes... \r"); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs new file mode 100644 index 00000000..87f08c44 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -0,0 +1,114 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.Cli; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware list", Description = "List locally available firmware")] +public class FirmwareListCommand : ICommand +{ + private readonly ISettingsManager _settingsManager; + private readonly ILogger? _logger; + + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; set; } + + private FileManager FileManager { get; } + + public FirmwareListCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + { + FileManager = fileManager; + _settingsManager = settingsManager; + _logger = loggerFactory?.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + await FileManager.Refresh(); + + if (Verbose) + { + await DisplayVerboseResults(FileManager); + } + else + { + await DisplayTerseResults(FileManager); + } + } + + private async Task DisplayVerboseResults(FileManager manager) + { + _logger?.LogInformation($" (D== Default, OSB==OS without bootloader, RT==Runtime, CP==Coprocessor){Environment.NewLine}"); + _logger?.LogInformation($" D VERSION OS OSB RT CP BCL"); + + _logger?.LogInformation($"------------------------------------------"); + + foreach (var name in manager.Firmware.CollectionNames) + { + _logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + _logger?.LogInformation( + $" * {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + else + { + _logger?.LogInformation( + $" {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + _logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } + + private async Task DisplayTerseResults(FileManager manager) + { + foreach (var name in manager.Firmware.CollectionNames) + { + _logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + _logger?.LogInformation($" * {package.Version} (default)"); + } + else + { + _logger?.LogInformation($" {package.Version}"); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + _logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs new file mode 100644 index 00000000..b6fe8a46 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -0,0 +1,208 @@ +using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public enum FirmwareType +{ + OS, + Runtime, + ESP +} + +[Command("firmware write", Description = "Download a firmware package")] +public class FirmwareWriteCommand : BaseDeviceCommand +{ + [CommandOption("version", 'v', IsRequired = false)] + public string? Version { get; set; } + + [CommandOption("use-dfu", 'd', IsRequired = false, Description = "Force using DFU for writing the OS.")] + public bool UseDfu { get; set; } + + [CommandParameter(0, Name = "Files to write", IsRequired = false)] + public FirmwareType[]? Files { get; set; } = default!; + + private FileManager FileManager { get; } + + public FirmwareWriteCommand(FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + FileManager = fileManager; + } + + public override async ValueTask ExecuteAsync(IConsole console) + { + var package = await GetSelectedPackage(); + + if (Files == null) + { + Logger.LogInformation($"Writing all firmware for version '{package.Version}'..."); + + Files = new FirmwareType[] + { + FirmwareType.OS, + FirmwareType.Runtime, + FirmwareType.ESP + }; + } + + if (!Files.Contains(FirmwareType.OS) && UseDfu) + { + Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + return; + } + + bool deviceSupportsOta = false; // TODO: get this based on device OS version + + if (package.OsWithoutBootloader == null + || !deviceSupportsOta + || UseDfu) + { + UseDfu = true; + } + + + if (UseDfu && Files.Contains(FirmwareType.OS)) + { + // no connection is required here - in fact one won't exist + // unless maybe we add a "DFUConnection"? + await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader)); + + // TODO: if the user requested flashing more than the OS, we have to wait for a connection and then proceed with that + if (Files.Any(f => f != FirmwareType.OS)) + { + var connection = ConnectionManager.GetCurrentConnection(); + if (connection == null) + { + Logger.LogError($"No connection path is defined"); + return; + } + + await connection.WaitForMeadowAttach(); + + var cancellationToken = console.RegisterCancellationHandler(); + await ExecuteCommand(connection, connection.Device, cancellationToken); + } + Logger.LogInformation($"Done."); + } + else + { + await base.ExecuteAsync(console); + } + } + + private async Task GetSelectedPackage() + { + await FileManager.Refresh(); + + var collection = FileManager.Firmware["Meadow F7"]; + FirmwarePackage package; + + if (Version != null) + { + // make sure the requested version exists + var existing = collection.FirstOrDefault(v => v.Version == Version); + + if (existing == null) + { + Logger.LogError($"Requested version '{Version}' not found."); + return null; + } + package = existing; + } + else + { + Version = collection.DefaultPackage?.Version ?? + throw new Exception("No default version set"); + + package = collection.DefaultPackage; + } + + return package; + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + // the connection passes messages back to us (info about actions happening on-device + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + var package = await GetSelectedPackage(); + + var wasRuntimeEnabled = await device.IsRuntimeEnabled(cancellationToken); + + if (wasRuntimeEnabled) + { + Logger.LogInformation("Disabling device runtime..."); + await device.RuntimeDisable(); + } + + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + Console.Write($"Writing {e.fileName}: {p:0}% \r"); + }; + + if (Files.Contains(FirmwareType.OS)) + { + if (UseDfu) + { + // this would have already happened before now (in ExecuteAsync) so ignore + } + else + { + Logger.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + + throw new NotSupportedException("OtA writes for the OS are not yet supported"); + } + } + if (Files.Contains(FirmwareType.Runtime)) + { + Logger.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + // TODO: for serial, we must wait for the flash to complete + + await device.WriteRuntime(rtpath, cancellationToken); + } + if (Files.Contains(FirmwareType.ESP)) + { + Logger.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + + var fileList = new string[] + { + package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; + + await device.WriteCoprocessorFiles(fileList, cancellationToken); + } + + Logger.LogInformation($"{Environment.NewLine}"); + + if (wasRuntimeEnabled) + { + await device.RuntimeEnable(); + } + + // TODO: if we're an F7 device, we need to reset + } + + private async Task WriteOsWithDfu(string osFile) + { + await DfuUtils.FlashFile( + osFile, + logger: Logger, + format: DfuUtils.DfuFlashFormat.ConsoleOut); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs new file mode 100644 index 00000000..6971c0d0 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -0,0 +1,50 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("listen", Description = "Listen for console output from Meadow")] +public class ListenCommand : BaseDeviceCommand +{ + [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed")] + public bool NoPrefix { get; set; } + + public ListenCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + var connection = connectionManager.GetCurrentConnection(); + + if (connection == null) + { + Logger.LogError($"No device connection configured."); + return; + } + + Logger.LogInformation($"Listening for Meadow Console output on '{connection.Name}'. Press Ctrl+C to exit..."); + + connection.DeviceMessageReceived += OnDeviceMessageReceived; + } + + private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) + { + if (NoPrefix) + { + Logger.LogInformation($"{e.message.TrimEnd('\n', '\r')}"); + } + else + { + Logger.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); + } + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger.LogInformation($"Cancelled."); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/PortListCommand.cs new file mode 100644 index 00000000..fe887929 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/PortListCommand.cs @@ -0,0 +1,22 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("port list", Description = "List available local serial ports")] +public class PortListCommand : BaseDeviceCommand +{ + public PortListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + foreach (var port in await MeadowConnectionManager.GetSerialPorts()) + { + Logger.LogInformation("Found Meadow: {port}", port); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs new file mode 100644 index 00000000..2c36869f --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -0,0 +1,24 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("runtime disable", Description = "Sets the runtime to NOT run on the Meadow board then resets it")] +public class RuntimeDisableCommand : BaseDeviceCommand +{ + public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Disabling runtime..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + await device.RuntimeDisable(cancellationToken); + + var state = await device.IsRuntimeEnabled(cancellationToken); + + Logger.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs new file mode 100644 index 00000000..7cfb8a27 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -0,0 +1,24 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("runtime enable", Description = "Sets the runtime to run on the Meadow board then resets it")] +public class RuntimeEnableCommand : BaseDeviceCommand +{ + public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Enabling runtime..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + await device.RuntimeEnable(cancellationToken); + + var state = await device.IsRuntimeEnabled(cancellationToken); + + Logger.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs new file mode 100644 index 00000000..de465c7a --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -0,0 +1,22 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("runtime state", Description = "Gets the device's current runtime state")] +public class RuntimeStateCommand : BaseDeviceCommand +{ + public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogInformation($"Querying runtime state..."); + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + var state = await device.IsRuntimeEnabled(cancellationToken); + + Logger.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs new file mode 100644 index 00000000..35a8dead --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -0,0 +1,27 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("trace disable", Description = "Disable trace logging on the Meadow")] +public class TraceDisableCommand : BaseDeviceCommand +{ + public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + Logger.LogInformation("Disabling tracing..."); + + await device.TraceDisable(cancellationToken); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs new file mode 100644 index 00000000..a0c50258 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -0,0 +1,36 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("trace enable", Description = "Enable trace logging on the Meadow")] +public class TraceEnableCommand : BaseDeviceCommand +{ + [CommandOption("level", 'l', Description = "The desired trace level", IsRequired = false)] + public int? Level { get; init; } + + public TraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + if (Level != null) + { + Logger.LogInformation($"Setting trace level to {Level}..."); + await device.SetTraceLevel(Level.Value, cancellationToken); + } + + Logger.LogInformation("Enabling tracing..."); + + await device.TraceEnable(cancellationToken); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs new file mode 100644 index 00000000..3c7300a5 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -0,0 +1,40 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("trace level", Description = "Sets the trace logging level on the Meadow")] +public class TraceLevelCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "Level", IsRequired = true)] + public int Level { get; set; } + + public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + if (Level <= 0) + { + Logger.LogInformation("Disabling tracing..."); + + await device.SetTraceLevel(Level, cancellationToken); + } + else + { + Logger.LogInformation($"Setting trace level to {Level}..."); + await device.SetTraceLevel(Level, cancellationToken); + + Logger.LogInformation("Enabling tracing..."); + await device.TraceEnable(cancellationToken); + } + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs new file mode 100644 index 00000000..f30fa3c6 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs @@ -0,0 +1,26 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("uart trace disable", Description = "Disables trace log output to UART")] +public class UartTraceDisableCommand : BaseDeviceCommand +{ + public UartTraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + Logger.LogInformation("Setting UART to application use..."); + + await device.UartTraceDisable(cancellationToken); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs new file mode 100644 index 00000000..44bd8504 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs @@ -0,0 +1,26 @@ +using CliFx.Attributes; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("uart trace enable", Description = "Enables trace log output to UART")] +public class UartTraceEnableCommand : BaseDeviceCommand +{ + public UartTraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + connection.DeviceMessageReceived += (s, e) => + { + Logger.LogInformation(e.message); + }; + + Logger.LogInformation("Setting UART to output trace messages..."); + + await device.UartTraceEnable(cancellationToken); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs new file mode 100644 index 00000000..26a2ecff --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs @@ -0,0 +1,16 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("install dfu", Description = "** deprecated **")] +public class InstallDfuCommand : DfuInstallCommand +{ + public InstallDfuCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory, "0.11") + { + Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); + } +} + diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs new file mode 100644 index 00000000..e1129b70 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("mono disable", Description = "** deprecated **")] +public class MonoDisableCommand : RuntimeDisableCommand +{ + public MonoDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogWarning($"Deprecated command. Use `runtime disable` instead"); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs new file mode 100644 index 00000000..4fa4860a --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("mono enable", Description = "** deprecated **")] +public class MonoEnableCommand : RuntimeEnableCommand +{ + public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/DFU/DfuContext.cs b/Source/v2/Meadow.Cli/DFU/DfuContext.cs new file mode 100644 index 00000000..95163a63 --- /dev/null +++ b/Source/v2/Meadow.Cli/DFU/DfuContext.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using DfuSharp; + +namespace MeadowCLI +{ + public class DfuContext + { + private List validVendorIDs = new List + { + 0x22B1, // secret labs + 0x1B9F, // ghi + 0x05A, // who knows + 0x0483 // bootloader + }; + + // --------------------------- INSTANCE + public static DfuContext Current; + + public static void Init() + { + Current = new DfuContext(); + Current._context = new Context(); + } + + public static void Dispose() + { + Current._context.Dispose(); + } + // --------------------------- INSTANCE + + private Context _context; + + public List GetDevices() + { + return _context.GetDfuDevices(validVendorIDs); + } + + public bool HasCapability(Capabilities caps) + { + return _context.HasCapability(caps); + } + + public void BeginListeningForHotplugEvents() + { + _context.BeginListeningForHotplugEvents(); + } + + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs b/Source/v2/Meadow.Cli/DFU/DfuSharp.cs new file mode 100644 index 00000000..7f42d446 --- /dev/null +++ b/Source/v2/Meadow.Cli/DFU/DfuSharp.cs @@ -0,0 +1,827 @@ +using System; +using System.IO; +using System.Threading; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace DfuSharp +{ + enum Consts + { + USB_DT_DFU = 0x21 + } + + public enum LogLevel + { + None = 0, + Error, + Warning, + Info, + Debug + } + + public delegate void HotplugCallback(IntPtr ctx, IntPtr device, HotplugEventType eventType, IntPtr userData); + + + class NativeMethods + { + + const string LIBUSB_LIBRARY = "libusb-1.0.dll"; + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_init(ref IntPtr ctx); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern void libusb_exit(IntPtr ctx); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern void libusb_set_debug(IntPtr ctx, LogLevel level); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_get_device_list(IntPtr ctx, ref IntPtr list); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_free_device_list(IntPtr list, int free_devices); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_get_device_descriptor(IntPtr dev, ref DeviceDescriptor desc); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_get_config_descriptor(IntPtr dev, ushort config_index, out IntPtr desc); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_open(IntPtr dev, ref IntPtr handle); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_close(IntPtr handle); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_claim_interface(IntPtr dev, int interface_number); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_set_interface_alt_setting(IntPtr dev, int interface_number, int alternate_setting); + + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_control_transfer(IntPtr dev, byte bmRequestType, byte bRequest, ushort wValue, ushort wIndex, IntPtr data, ushort wLength, uint timeout); + + /// + /// Whether or not the USB supports a particular feature. + /// + /// nonzero if the running library has the capability, 0 otherwise + /// Capability. + [DllImport(LIBUSB_LIBRARY)] + internal static extern int libusb_has_capability(Capabilities capability); + + + [DllImport(LIBUSB_LIBRARY)] + internal static extern ErrorCodes libusb_hotplug_register_callback(IntPtr ctx, HotplugEventType eventType, HotplugFlags flags, + int vendorID, int productID, int deviceClass, + HotplugCallback callback, IntPtr userData, + out IntPtr callbackHandle); + [DllImport(LIBUSB_LIBRARY)] + internal static extern void libusb_hotplug_deregister_callback(IntPtr ctx, IntPtr callbackHandle); + + } + + [Flags] + public enum HotplugEventType : uint + { + /** A device has been plugged in and is ready to use */ + //LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED + DeviceArrived = 0x01, + + /** A device has left and is no longer available. + * It is the user's responsibility to call libusb_close on any handle associated with a disconnected device. + * It is safe to call libusb_get_device_descriptor on a device that has left */ + //LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT + DeviceLeft = 0x02 + } + + [Flags] + public enum HotplugFlags : uint + { + /** Default value when not using any flags. */ + //LIBUSB_HOTPLUG_NO_FLAGS = 0, + DefaultNoFlags = 0, + + /** Arm the callback and fire it for all matching currently attached devices. */ + //LIBUSB_HOTPLUG_ENUMERATE + EnumerateNow = 1 << 0, + } + + [Flags] + public enum Capabilities : uint + { + /** The libusb_has_capability() API is available. */ + //LIBUSB_CAP_HAS_CAPABILITY + HasCapabilityAPI = 0x0000, + /** Hotplug support is available on this platform. */ + //LIBUSB_CAP_HAS_HOTPLUG + SupportsHotplug = 0x0001, + /** The library can access HID devices without requiring user intervention. + * Note that before being able to actually access an HID device, you may + * still have to call additional libusb functions such as + * \ref libusb_detach_kernel_driver(). */ + //LIBUSB_CAP_HAS_HID_ACCESS + SupportsHidDevices = 0x0100, + /** The library supports detaching of the default USB driver, using + * \ref libusb_detach_kernel_driver(), if one is set by the OS kernel */ + //LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER + SupportsKernalDriverDetaching = 0x0101 + } + + public enum ErrorCodes : int + { + /** Success (no error) */ + Success = 0, + + /** Input/output error */ + IOError = -1, + + /** Invalid parameter */ + InvalidParamter = -2, + + /** Access denied (insufficient permissions) */ + AccessDenied = -3, + + /** No such device (it may have been disconnected) */ + NoSuchDevice = -4, + + /** Entity not found */ + EntityNotFound = -5, + + /** Resource busy */ + ResourceBusy = -6, + + /** Operation timed out */ + OperationTimedout = -7, + + /** Overflow */ + Overflow = -8, + + /** Pipe error */ + PipeError = -9, + + /** System call interrupted (perhaps due to signal) */ + SystemCallInterrupted = -10, + + /** Insufficient memory */ + InsufficientMemory = -11, + + /** Operation not supported or unimplemented on this platform */ + OperationNotSupported = -12, + + /* NB: Remember to update LIBUSB_ERROR_COUNT below as well as the + message strings in strerror.c when adding new error codes here. */ + + /** Other error */ + OtherError = -99, + }; + + struct DeviceDescriptor + { + public byte bLength; + public byte bDescriptorType; + public ushort bcdUSB; + public byte bDeviceClass; + public byte bDeviceSubClass; + public byte bDeviceProtocol; + public byte bMaxPacketSize0; + public ushort idVendor; + public ushort idProduct; + public ushort bcdDevice; + public byte iManufacturer; + public byte iProduct; + public byte iSerialNumber; + public byte bNumConfigurations; + } + + struct ConfigDescriptor + { + public byte bLength; + public byte bDescriptorType; + public ushort wTotalLength; + public byte bNumInterfaces; + public byte bConfigurationValue; + public byte iConfiguration; + public byte bmAttributes; + public byte MaxPower; + public IntPtr interfaces; + public IntPtr extra; + public int extra_length; + } + + struct @Interface + { + public IntPtr altsetting; + public int num_altsetting; + + public InterfaceDescriptor[] Altsetting + { + get + { + var descriptors = new InterfaceDescriptor[num_altsetting]; + for (int i = 0; i < num_altsetting; i++) + { + descriptors[i] = Marshal.PtrToStructure(altsetting + i * Marshal.SizeOf()); + } + + return descriptors; + } + } + } + + public struct InterfaceDescriptor + { + public byte bLength; + public byte bDescriptorType; + public byte bInterfaceNumber; + public byte bAlternateSetting; + public byte bNumEndpoints; + public byte bInterfaceClass; + public byte bInterfaceSubClass; + public byte bInterfaceProtocol; + public byte iInterface; + public IntPtr endpoint; + public IntPtr extra; + public int extra_length; + } + + public struct DfuFunctionDescriptor + { + public byte bLength; + public byte bDescriptorType; + public byte bmAttributes; + public ushort wDetachTimeOut; + public ushort wTransferSize; + public ushort bcdDFUVersion; + } + + public delegate void UploadingEventHandler(object sender, UploadingEventArgs e); + + public class UploadingEventArgs : EventArgs + { + public int BytesUploaded { get; private set; } + + public UploadingEventArgs(int bytesUploaded) + { + this.BytesUploaded = bytesUploaded; + } + } + + public class DfuDevice : IDisposable + { + // FIXME: Figure out why dfu_function_descriptor.wTransferSize isn't right and why STM isn't reporting flash_size right + const int flash_size = 0x200000; + const int transfer_size = 0x800; + const int address = 0x08000000; + + IntPtr handle; + InterfaceDescriptor interface_descriptor; + DfuFunctionDescriptor dfu_descriptor; + + public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFunctionDescriptor dfu_descriptor) + { + this.interface_descriptor = interface_descriptor; + this.dfu_descriptor = dfu_descriptor; + if (NativeMethods.libusb_open(device, ref handle) < 0) + throw new Exception("Error opening device"); + } + + public event UploadingEventHandler Uploading; + + protected virtual void OnUploading(UploadingEventArgs e) + { + if (Uploading != null) + Uploading(this, e); + } + public void ClaimInterface() + { + NativeMethods.libusb_claim_interface(handle, interface_descriptor.bInterfaceNumber); + } + + public void SetInterfaceAltSetting(int alt_setting) + { + NativeMethods.libusb_set_interface_alt_setting(handle, interface_descriptor.bInterfaceNumber, alt_setting); + } + + public void Clear() + { + var state = (byte)0xff; + + while (state != 0 && state != 2) + { + state = GetStatus(handle, interface_descriptor.bInterfaceNumber); + + switch (state) + { + case 5: + case 9: + Abort(handle, interface_descriptor.bInterfaceNumber); + break; + case 10: + ClearStatus(handle, interface_descriptor.bInterfaceNumber); + break; + default: + break; + } + } + } + + public void Upload(FileStream file, int? baseAddress = null) + { + var buffer = new byte[transfer_size]; + + using (var reader = new BinaryReader(file)) + { + for (var pos = 0; pos < flash_size; pos += transfer_size) + { + int write_address = (baseAddress ?? address) + pos; + var count = reader.Read(buffer, 0, transfer_size); + + if (count == 0) + return; + + Upload(buffer, write_address); + } + } + } + + public void Upload(byte[] data, int? baseAddress = null, int altSetting = 0) + { + var mem = Marshal.AllocHGlobal(transfer_size); + + try + { + //Clear(); + //ClaimInterface(); + //if (altSetting != 0) SetInterfaceAltSetting(altSetting); + + for (var pos = 0; pos < flash_size; pos += transfer_size) + { + int write_address = (baseAddress ?? address) + pos; + var count = Math.Min(data.Length - pos, transfer_size); + + if (count <= 0) + return; + + Clear(); + ClaimInterface(); + if (altSetting != 0) SetInterfaceAltSetting(altSetting); + SetAddress(write_address); + Clear(); + + Marshal.Copy(data, pos, mem, count); + + var ret = NativeMethods.libusb_control_transfer( + handle, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 1 /*DFU_DNLOAD*/, + 2, + interface_descriptor.bInterfaceNumber, + mem, + (ushort)count, + 5000); + + if (ret < 0) + throw new Exception(string.Format("Error with WRITE_SECTOR: {0}", ret)); + var status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + + while (status == 4) + { + Thread.Sleep(100); + status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + } + OnUploading(new UploadingEventArgs(count)); + } + } + finally + { + Marshal.FreeHGlobal(mem); + } + } + + public void Download(FileStream file) + { + var buffer = new byte[transfer_size]; + var mem = Marshal.AllocHGlobal(transfer_size); + + try + { + int count = 0; + ushort transaction = 2; + using (var writer = new BinaryWriter(file)) + { + while (count < flash_size) + { + Clear(); + ClaimInterface(); + + int ret = NativeMethods.libusb_control_transfer( + handle, + 0x80 /*LIBUSB_ENDPOINT_IN*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 2 /*DFU_UPLOAD*/, + transaction++, + interface_descriptor.bInterfaceNumber, + mem, + transfer_size, + 5000); + if (ret < 0) + throw new Exception(string.Format("Error with DFU_UPLOAD: {0}", ret)); + + count += ret; + Marshal.Copy(mem, buffer, 0, ret); + writer.Write(buffer, 0, ret); + } + } + } + finally + { + Marshal.FreeHGlobal(mem); + } + } + + public void Download(byte[] block, int address, int altSetting = 0) + { + int size = block.Length; + + var mem = Marshal.AllocHGlobal(size); + + try + { + ushort transaction = 2; + + Clear(); + ClaimInterface(); + if (altSetting != 0) SetInterfaceAltSetting(altSetting); + SetAddress(address); + Clear(); + + int ret = NativeMethods.libusb_control_transfer( + handle, + 0x80 /*LIBUSB_ENDPOINT_IN*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 2 /*DFU_UPLOAD*/, + transaction++, + interface_descriptor.bInterfaceNumber, + mem, + (ushort)size, + 5000); + if (ret < 0) + throw new Exception(string.Format("Error with DFU_UPLOAD: {0}", ret)); + + Marshal.Copy(mem, block, 0, ret); + } + finally + { + Marshal.FreeHGlobal(mem); + Clear(); + } + } + + public void EraseSector(int address) + { + var mem = Marshal.AllocHGlobal(5); + + try + { + Marshal.WriteByte(mem, 0, 0x41); + Marshal.WriteByte(mem, 1, (byte)((address >> 0) & 0xff)); + Marshal.WriteByte(mem, 2, (byte)((address >> 8) & 0xff)); + Marshal.WriteByte(mem, 3, (byte)((address >> 16) & 0xff)); + Marshal.WriteByte(mem, 4, (byte)((address >> 24) & 0xff)); + + + var ret = NativeMethods.libusb_control_transfer( + handle, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 1 /*DFU_DNLOAD*/, + 0, + interface_descriptor.bInterfaceNumber, + mem, + 5, + 5000); + + if (ret < 0) + throw new Exception(string.Format("Error with ERASE_SECTOR: {0}", ret)); + + var status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + + while (status == 4) + { + Thread.Sleep(100); + status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + } + } + finally + { + Marshal.FreeHGlobal(mem); + } + } + + public void Reset() + { + var mem = Marshal.AllocHGlobal(0); + + try + { + var ret = NativeMethods.libusb_control_transfer( + handle, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 1 /*DFU_DNLOAD*/, + 0, + interface_descriptor.bInterfaceNumber, + mem, + 0, + 5000); + + if (ret < 0) + throw new Exception(string.Format("Error with RESET: {0}", ret)); + + var status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + + while (status == 4) + { + Thread.Sleep(100); + status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + } + } + finally + { + Marshal.FreeHGlobal(mem); + } + } + + public void SetAddress(int address) + { + var mem = Marshal.AllocHGlobal(5); + + try + { + Marshal.WriteByte(mem, 0, 0x21); + Marshal.WriteByte(mem, 1, (byte)((address >> 0) & 0xff)); + Marshal.WriteByte(mem, 2, (byte)((address >> 8) & 0xff)); + Marshal.WriteByte(mem, 3, (byte)((address >> 16) & 0xff)); + Marshal.WriteByte(mem, 4, (byte)((address >> 24) & 0xff)); + + + var ret = NativeMethods.libusb_control_transfer( + handle, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 1 /*DFU_DNLOAD*/, + 0, + interface_descriptor.bInterfaceNumber, + mem, + 5, + 5000); + + if (ret < 0) + throw new Exception(string.Format("Error with ERASE_SECTOR: {0}", ret)); + + var status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + + while (status == 4) + { + Thread.Sleep(100); + status = GetStatus(handle, interface_descriptor.bInterfaceNumber); + } + } + finally + { + Marshal.FreeHGlobal(mem); + } + } + + static byte GetStatus(IntPtr dev, ushort interface_number) + { + var buffer = Marshal.AllocHGlobal(6); + + try + { + int ret = NativeMethods.libusb_control_transfer( + dev, + 0x80 /*LIBUSB_ENDPOINT_IN*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 3 /*DFU_GETSTATUS*/, + 0, + interface_number, + buffer, + 6, + 5000); + + if (ret == 6) + return Marshal.ReadByte(buffer, 4); + + return 0xff; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + static void Abort(IntPtr dev, ushort interface_number) + { + int ret = NativeMethods.libusb_control_transfer( + dev, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 6 /*DFU_ABORT*/, + 0, + interface_number, + IntPtr.Zero, + 0, + 5000); + } + static void ClearStatus(IntPtr dev, ushort interface_number) + { + int ret = NativeMethods.libusb_control_transfer( + dev, + 0x00 /*LIBUSB_ENDPOINT_OUT*/ | (0x1 << 5) /*LIBUSB_REQUEST_TYPE_CLASS*/ | 0x01 /*LIBUSB_RECIPIENT_INTERFACE*/, + 4 /*DFU_GETSTATUS*/, + 0, + interface_number, + IntPtr.Zero, + 0, + 5000); + } + public void Dispose() + { + NativeMethods.libusb_close(handle); + } + } + + public class Context : IDisposable + { + public event EventHandler DeviceConnected = delegate { }; + + // doing this here so its lifecycle is tied to the class + protected HotplugCallback _hotplugCallbackHandler; + + IntPtr _callbackHandle = IntPtr.Zero; + + + IntPtr handle; + public Context(LogLevel debug_level = LogLevel.None) + { + var ret = NativeMethods.libusb_init(ref handle); + + NativeMethods.libusb_set_debug(handle, debug_level); + if (ret != 0) + throw new Exception(string.Format("Error: {0} while trying to initialize libusb", ret)); + + // instantiate our callback handler + //this._hotplugCallbackHandler = new HotplugCallback(HandleHotplugCallback); + } + + public void Dispose() + { + //this.StopListeningForHotplugEvents(); // not needed, they're automatically deregistered in libusb_exit. + NativeMethods.libusb_exit(handle); + } + + public List GetDfuDevices(List idVendors) + { + var list = IntPtr.Zero; + var dfu_devices = new List(); + var ret = NativeMethods.libusb_get_device_list(handle, ref list); + + if (ret < 0) + throw new Exception(string.Format("Error: {0} while trying to get the device list", ret)); + + var devices = new IntPtr[ret]; + Marshal.Copy(list, devices, 0, ret); + + // This is awful nested looping -- we should fix it. + for (int i = 0; i < ret; i++) + { + var device_descriptor = new DeviceDescriptor(); + var ptr = IntPtr.Zero; + + if (NativeMethods.libusb_get_device_descriptor(devices[i], ref device_descriptor) != 0) + continue; + + //if (!idVendors.Contains(device_descriptor.idVendor)) + // continue; + + for (int j = 0; j < device_descriptor.bNumConfigurations; j++) + { + var ret2 = NativeMethods.libusb_get_config_descriptor(devices[i], (ushort)j, out ptr); + + if (ret2 < 0) + continue; + //throw new Exception(string.Format("Error: {0} while trying to get the config descriptor", ret2)); + + var config_descriptor = Marshal.PtrToStructure(ptr); + + for (int k = 0; k < config_descriptor.bNumInterfaces; k++) + { + var p = config_descriptor.interfaces + j * Marshal.SizeOf<@Interface>(); + + if (p == IntPtr.Zero) + continue; + + var @interface = Marshal.PtrToStructure<@Interface>(p); + for (int l = 0; l < @interface.num_altsetting; l++) + { + var interface_descriptor = @interface.Altsetting[l]; + + // Ensure this is a DFU descriptor + if (interface_descriptor.bInterfaceClass != 0xfe || interface_descriptor.bInterfaceSubClass != 0x1) + continue; + + var dfu_descriptor = FindDescriptor(interface_descriptor.extra, interface_descriptor.extra_length, (byte)Consts.USB_DT_DFU); + if (dfu_descriptor != null) + dfu_devices.Add(new DfuDevice(devices[i], interface_descriptor, dfu_descriptor.Value)); + } + } + } + } + + NativeMethods.libusb_free_device_list(list, 1); + return dfu_devices; + } + + static DfuFunctionDescriptor? FindDescriptor(IntPtr desc_list, int list_len, byte desc_type) + { + int p = 0; + + while (p + 1 < list_len) + { + int len, type; + + len = Marshal.ReadByte(desc_list, p); + type = Marshal.ReadByte(desc_list, p + 1); + + if (type == desc_type) + { + return Marshal.PtrToStructure(desc_list + p); + } + p += len; + } + + return null; + } + + public bool HasCapability(Capabilities caps) + { + return NativeMethods.libusb_has_capability(caps) == 0 ? false : true; + } + + public void BeginListeningForHotplugEvents() + { + if (_callbackHandle != IntPtr.Zero) + { + Debug.WriteLine("Already listening for events."); + return; + } + + if (!HasCapability(Capabilities.HasCapabilityAPI)) + { + Debug.WriteLine("Capability API not supported."); + return; + } + + if (!HasCapability(Capabilities.SupportsHotplug)) + { + Debug.WriteLine("Hotplug notifications not supported."); + return; + } + + int vendorID = -1; // wildcard match (all) + int productID = -1; + int deviceClass = -1; + IntPtr userData = IntPtr.Zero; + + ErrorCodes success = NativeMethods.libusb_hotplug_register_callback(this.handle, HotplugEventType.DeviceArrived | HotplugEventType.DeviceLeft, HotplugFlags.DefaultNoFlags, + vendorID, productID, deviceClass, this._hotplugCallbackHandler, userData, out _callbackHandle); + + if (success == ErrorCodes.Success) + { + Debug.WriteLine("Callback registration successful"); + } + else + { + throw new Exception("callback registration failed, error: " + success.ToString()); + } + + } + + public void StopListeningForHotplugEvents() + { + if (_callbackHandle == IntPtr.Zero) + { + Debug.WriteLine("Not listening already."); + return; + } + + NativeMethods.libusb_hotplug_deregister_callback(this.handle, this._callbackHandle); + + } + + public void HandleHotplugCallback(IntPtr ctx, IntPtr device, HotplugEventType eventType, IntPtr userData) + { + Debug.WriteLine("Hotplug Callback called, event type: " + eventType.ToString()); + // raise the event + this.DeviceConnected(this, new EventArgs()); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/DFU/DfuUtils.cs b/Source/v2/Meadow.Cli/DFU/DfuUtils.cs new file mode 100644 index 00000000..1a020ab7 --- /dev/null +++ b/Source/v2/Meadow.Cli/DFU/DfuUtils.cs @@ -0,0 +1,380 @@ +using LibUsbDotNet.LibUsb; +using Meadow.Hcom; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.ComponentModel; +using System.Diagnostics; +using System.IO.Compression; +using System.Runtime.InteropServices; + +namespace Meadow.CLI.Core.Internals.Dfu; + +public static class DfuUtils +{ + private static int _osAddress = 0x08000000; + private static string _usbStmName = "STM32 BOOTLOADER"; + private static int _usbBootLoaderVenderID = 1155; // Equivalent to _usbStmName but for the LibUsbDotNet 3.x + + public static string LastSerialNumber { get; private set; } = ""; + + public static bool CheckForValidDevice() + { + try + { + GetDeviceInBootloaderMode(); + return true; + } + catch (Exception) + { + return false; + } + } + + public static IUsbDevice GetDeviceInBootloaderMode() + { + var allDevices = GetDevicesInBootloaderMode(); + if (allDevices.Count() > 1) + { + throw new Exception("More than one DFU device found, please connect only one and try again."); + } + + var device = allDevices.SingleOrDefault(); + if (device == null) + { + throw new Exception("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 IEnumerable GetDevicesInBootloaderMode() + { + using (UsbContext context = new UsbContext()) + { + var allDevices = context.List(); + var ourDevices = allDevices.Where(d => d.Info.VendorId == _usbBootLoaderVenderID); + if (ourDevices.Count() < 1) + { + throw new Exception("No Devices 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 ourDevices; + } + } + + public static string GetDeviceSerial(IUsbDevice device) + { + var serialNumber = string.Empty; + + if (device != null) + { + device.Open(); + if (device.IsOpen) + { + serialNumber = device.Info?.SerialNumber; + device.Close(); + } + } + + return serialNumber; + } + + public enum DfuFlashFormat + { + /// + /// Percentage only + /// + Percent, + /// + /// Full console output, no formatting + /// + Full, + /// + /// Console.WriteLine for CLI - ToDo - remove + /// + ConsoleOut, + } + + private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) + { + if (format == DfuFlashFormat.Full) + { + logger?.LogInformation(logLine); + } + else if (format == DfuFlashFormat.Percent) + { + if (logLine.Contains("%")) + { + var operation = logLine.Substring(0, + logLine.IndexOf("\t", StringComparison.Ordinal)).Trim(); + var progressBarEnd = logLine.IndexOf("]", StringComparison.Ordinal) + 1; + var progress = logLine.Substring(progressBarEnd, logLine.IndexOf("%", StringComparison.Ordinal) - progressBarEnd + 1).TrimStart(); + if (progress != "100%") + { + logger?.LogInformation(progress); + } + } + else + { + logger?.LogInformation(logLine); + } + } + else //Console out + { + Console.Write(logLine); + + Console.Write(logLine.Contains("%") ? "\r" : "\r\n"); + } + } + + public static async Task FlashFile(string fileName, IUsbDevice? device = null, ILogger? logger = null, DfuFlashFormat format = DfuFlashFormat.Percent) + { + logger ??= NullLogger.Instance; + device ??= GetDeviceInBootloaderMode(); + + if (!File.Exists(fileName)) + { + logger.LogError($"Unable to flash {fileName} - file or folder does not exist"); + return false; + } + + if (!File.Exists(fileName)) + { + logger.LogError($"Unable to find file '{fileName}'. Please specify valid --File or download the latest with: meadow download os"); + return false; + } + else + { + logger.LogInformation($"Flashing OS with {fileName}"); + } + + LastSerialNumber = GetDeviceSerial(device); + + var dfuUtilVersion = GetDfuUtilVersion(); + logger.LogDebug("Detected OS: {os}", RuntimeInformation.OSDescription); + + if (string.IsNullOrEmpty(dfuUtilVersion)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + logger.LogError("dfu-util not found - to install, run: `meadow install dfu-util` (may require administrator mode)"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + logger.LogError("dfu-util not found - to install run: `brew install dfu-util`"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + logger.LogError("dfu-util not found - install using package manager, for example: `apt install dfu-util`"); + } + return false; + } + else if (dfuUtilVersion != "0.10") + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + logger.LogError("dfu-util update required. To install, run in administrator mode: meadow install dfu-util"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + logger.LogError("dfu-util update required. To install, run: brew upgrade dfu-util"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (dfuUtilVersion != "0.9") + return false; + } + else + { + return false; + } + } + + try + { + var args = $"-a 0 -S {LastSerialNumber} -D \"{fileName}\" -s {_osAddress}:leave"; + + await RunDfuUtil(args, logger, format); + } + catch (Exception ex) + { + logger.LogError($"There was a problem executing dfu-util: {ex.Message}"); + return false; + } + + return true; + } + + private static async Task RunDfuUtil(string args, ILogger? logger, DfuFlashFormat format = DfuFlashFormat.Percent) + { + var startInfo = new ProcessStartInfo("dfu-util", args) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = false, + CreateNoWindow = true + }; + using var process = Process.Start(startInfo); + + if (process == null) + { + throw new Exception("Failed to start dfu-util"); + } + + var informationLogger = logger != null + ? Task.Factory.StartNew( + () => + { + var lastProgress = string.Empty; + + while (process.HasExited == false) + { + var logLine = process.StandardOutput.ReadLine(); + // Ignore empty output + if (logLine == null) + continue; + + FormatDfuOutput(logLine, logger, format); + } + }) : Task.CompletedTask; + + var errorLogger = logger != null + ? Task.Factory.StartNew( + () => + { + while (process.HasExited == false) + { + var logLine = process.StandardError.ReadLine(); + logger.LogError(logLine); + } + }) : Task.CompletedTask; + await informationLogger; + await errorLogger; + process.WaitForExit(); + } + + private static string GetDfuUtilVersion() + { + 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) + { + switch (ex.NativeErrorCode) + { + case 0x0002: // ERROR_FILE_NOT_FOUND + case 0x0003: // ERROR_PATH_NOT_FOUND + case 0x000F: // ERROR_INVALID_DRIVE + case 0x0014: // ERROR_BAD_UNIT + case 0x001B: // ERROR_SECTOR_NOT_FOUND + case 0x0033: // ERROR_REM_NOT_LIST + case 0x013D: // ERROR_MR_MID_NOT_FOUND + return string.Empty; + + default: + throw; + } + } + } + + public static async Task InstallDfuUtil( + string tempFolder, + string dfuUtilVersion = "0.11", + CancellationToken cancellationToken = default) + { + try + { + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, true); + } + + using var client = new HttpClient(); + + Directory.CreateDirectory(tempFolder); + + var downloadUrl = $"https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-{dfuUtilVersion}-binaries.zip"; + + var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); + var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + if (response.IsSuccessStatusCode == false) + { + throw new Exception("Failed to download dfu-util"); + } + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var downloadFileStream = new DownloadFileStream(stream)) + using (var fs = File.OpenWrite(Path.Combine(tempFolder, downloadFileName))) + { + await downloadFileStream.CopyToAsync(fs); + } + + ZipFile.ExtractToDirectory( + Path.Combine(tempFolder, downloadFileName), + tempFolder); + + var is64Bit = Environment.Is64BitOperatingSystem; + + var dfuUtilExe = new FileInfo( + Path.Combine(tempFolder, is64Bit ? "win64" : "win32", "dfu-util.exe")); + + var libUsbDll = new FileInfo( + Path.Combine( + tempFolder, + is64Bit ? "win64" : "win32", + "libusb-1.0.dll")); + + var targetDir = is64Bit + ? Environment.GetFolderPath(Environment.SpecialFolder.System) + : Environment.GetFolderPath( + Environment.SpecialFolder.SystemX86); + + File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); + File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); + + // clean up from previous version + var dfuPath = Path.Combine(@"C:\Windows\System", dfuUtilExe.Name); + var libUsbPath = Path.Combine(@"C:\Windows\System", libUsbDll.Name); + if (File.Exists(dfuPath)) + { + File.Delete(dfuPath); + } + + if (File.Exists(libUsbPath)) + { + File.Delete(libUsbPath); + } + } + finally + { + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, true); + } + } + } + +} diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs new file mode 100644 index 00000000..55909a18 --- /dev/null +++ b/Source/v2/Meadow.Cli/IPackageManager.cs @@ -0,0 +1,11 @@ +namespace Meadow.Cli; + +public interface IPackageManager +{ + bool BuildApplication(string projectFilePath, string configuration = "Release"); + Task TrimApplication( + FileInfo applicationFilePath, + bool includePdbs = false, + IList? noLink = null, + CancellationToken cancellationToken = default); +} diff --git a/Source/v2/Meadow.Cli/ISettingsManager.cs b/Source/v2/Meadow.Cli/ISettingsManager.cs new file mode 100644 index 00000000..b25b8830 --- /dev/null +++ b/Source/v2/Meadow.Cli/ISettingsManager.cs @@ -0,0 +1,10 @@ +namespace Meadow.Cli; + +public interface ISettingsManager +{ + void DeleteSetting(string setting); + string? GetAppSetting(string name); + Dictionary GetPublicSettings(); + string? GetSetting(string setting); + void SaveSetting(string setting, string value); +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj new file mode 100644 index 00000000..172cfa33 --- /dev/null +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -0,0 +1,69 @@ + + + + Exe + net6.0 + enable + true + Wilderness Labs, Inc + meadow + WildernessLabs.Meadow.CLI + Wilderness Labs, Inc + Wilderness Labs, Inc + true + 2.0.0.0 + AnyCPU + http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ + icon.png + https://github.com/WildernessLabs/Meadow.CLI + Meadow, Meadow.Foundation, Meadow.CLI + Command-line interface for Meadow + false + false + false + false + meadow + latest + Copyright 2020-2022 Wilderness Labs + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs new file mode 100644 index 00000000..361c4b09 --- /dev/null +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -0,0 +1,250 @@ +using Meadow.Cli; +using Meadow.Hcom; +using System.Diagnostics; +using System.IO.Ports; +using System.Management; +using System.Net; +using System.Runtime.InteropServices; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public class MeadowConnectionManager +{ + public const string WILDERNESS_LABS_USB_VID = "2E6A"; + private static object _lockObject = new(); + + private ISettingsManager _settingsManager; + private IMeadowConnection? _currentConnection; + + public MeadowConnectionManager(ISettingsManager settingsManager) + { + _settingsManager = settingsManager; + } + + public IMeadowConnection? GetCurrentConnection() + { + var route = _settingsManager.GetSetting(SettingsManager.PublicSettings.Route); + + if (route == null) + { + throw new Exception("No 'route' configuration set"); + } + + // TODO: support connection changing (CLI does this rarely as it creates a new connection with each command) + if (_currentConnection != null) return _currentConnection; + + // try to determine what the route is + string? uri = null; + if (route.StartsWith("http")) + { + uri = route; + } + else if (IPAddress.TryParse(route, out var ipAddress)) + { + uri = $"http://{route}:5000"; + } + else if (IPEndPoint.TryParse(route, out var endpoint)) + { + uri = $"http://{route}"; + } + + if (uri != null) + { + _currentConnection = new TcpConnection(uri); + } + else + { + _currentConnection = new SerialConnection(route); + } + + return _currentConnection; + } + + public static async Task> GetSerialPorts() + { + try + { + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return await GetMeadowSerialPortsForLinux(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return await GetMeadowSerialPortsForOsx(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + lock (_lockObject) + { + return GetMeadowSerialPortsForWindows(); + } + } + else + { + throw new Exception("Unknown operating system."); + } + } + catch (Exception ex) + { + throw new Exception($"Error Finding Meadow Devices on available Serial Ports: {ex.Message}"); + } + } + + public static async Task> GetMeadowSerialPortsForOsx() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) == false) + throw new PlatformNotSupportedException("This method is only supported on macOS"); + + return await Task.Run(() => + { + var ports = new List(); + + var psi = new ProcessStartInfo + { + FileName = "/usr/sbin/ioreg", + UseShellExecute = false, + RedirectStandardOutput = true, + Arguments = "-r -c IOUSBHostDevice -l" + }; + + string output = string.Empty; + + using (var p = Process.Start(psi)) + { + if (p != null) + { + output = p.StandardOutput.ReadToEnd(); + p.WaitForExit(); + } + } + + //split into lines + var lines = output.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + + var foundMeadow = false; + foreach (var line in lines) + { + if (line.Contains("Meadow F7 Micro")) + { + foundMeadow = true; + } + else if (line.IndexOf("+-o", StringComparison.Ordinal) == 0) + { + foundMeadow = false; + } + + //now find the IODialinDevice entry which contains the serial port name + if (foundMeadow && line.Contains("IODialinDevice")) + { + int startIndex = line.IndexOf("/"); + int endIndex = line.IndexOf("\"", startIndex + 1); + var port = line.Substring(startIndex, endIndex - startIndex); + + ports.Add(port); + foundMeadow = false; + } + } + + return ports; + }); + } + + public static async Task> GetMeadowSerialPortsForLinux() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) == false) + throw new PlatformNotSupportedException("This method is only supported on Linux"); + + return await Task.Run(() => + { + const string devicePath = "/dev/serial/by-id"; + var psi = new ProcessStartInfo() + { + FileName = "ls", + Arguments = $"-l {devicePath}", + UseShellExecute = false, + RedirectStandardOutput = true + }; + + using var proc = Process.Start(psi); + _ = proc?.WaitForExit(1000); + var output = proc?.StandardOutput.ReadToEnd(); + + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Where(x => x.Contains("Wilderness_Labs")) + .Select( + line => + { + var parts = line.Split(new[] { "-> " }, StringSplitOptions.RemoveEmptyEntries); + var target = parts[1]; + var port = Path.GetFullPath(Path.Combine(devicePath, target)); + return port; + }).ToArray(); + }); + } + + public static IList GetMeadowSerialPortsForWindows() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) + throw new PlatformNotSupportedException("This method is only supported on Windows"); + + try + { + const string WildernessLabsPnpDeviceIDPrefix = @"USB\VID_" + WILDERNESS_LABS_USB_VID; + + // Win32_PnPEntity lives in root\CIMV2 + const string wmiScope = "root\\CIMV2"; + + // escape special characters in the device id prefix + string escapedPrefix = WildernessLabsPnpDeviceIDPrefix.Replace("\\", "\\\\").Replace("_", "[_]"); + + // our query for all ports that have a PnP device id starting with Wilderness Labs' USB VID. + string query = @$"SELECT Name, Caption, PNPDeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND PNPDeviceID like '{escapedPrefix}%'"; + + List results = new(); + + // build the searcher for the query + using ManagementObjectSearcher searcher = new(wmiScope, query); + + // get the query results + foreach (ManagementObject moResult in searcher.Get()) + { + // Try Caption and if not Name, they both seems to contain the COM port + string portLongName = moResult["Caption"].ToString(); + if (string.IsNullOrEmpty(portLongName)) + portLongName = moResult["Name"].ToString(); + string pnpDeviceId = moResult["PNPDeviceID"].ToString(); + + // we could collect and return a fair bit of other info from the query: + + //string description = moResult["Description"].ToString(); + //string service = moResult["Service"].ToString(); + //string manufacturer = moResult["Manufacturer"].ToString(); + + var comIndex = portLongName.IndexOf("(COM") + 1; + var copyLength = portLongName.IndexOf(")") - comIndex; + var port = portLongName.Substring(comIndex, copyLength); + + // the meadow serial is in the device id, after + // the characters: USB\VID_XXXX&PID_XXXX\ + // so we'll just split is on \ and grab the 3rd element as the format is standard, but the length may vary. + var splits = pnpDeviceId.Split('\\'); + var serialNumber = splits[2]; + + results.Add($"{port}"); // removed serial number for consistency and will break fallback ({serialNumber})"); + } + + return results.ToArray(); + } + catch (Exception) + { + // Since WMI Failed fall back to using SerialPort + var ports = SerialPort.GetPortNames(); + + //hack to skip COM1 + ports = ports.Where((source, index) => source != "COM1").Distinct().ToArray(); + + return ports; + } + } +} diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs new file mode 100644 index 00000000..392ce786 --- /dev/null +++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs @@ -0,0 +1,211 @@ +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace Meadow.Cli; + +public partial class PackageManager +{ + private readonly List dependencyMap = new(); + + private string? _meadowAssembliesPath; + + private string? MeadowAssembliesPath + { + get + { + if (_meadowAssembliesPath == null) + { + // for now we only support F7 + // TODO: add switch and support for other platforms + var store = _fileManager.Firmware["Meadow F7"]; + store.Refresh(); + _meadowAssembliesPath = store.DefaultPackage.GetFullyQualifiedPath(store.DefaultPackage.BclFolder); + } + + return _meadowAssembliesPath; + } + } + + private const string IL_LINKER_DIR = "lib"; + + public async Task?> TrimDependencies(FileInfo file, List dependencies, IList? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) + { + var prelink_dir = Path.Combine(file.DirectoryName, "prelink_bin"); + var prelink_app = Path.Combine(prelink_dir, file.Name); + var prelink_os = Path.Combine(prelink_dir, "Meadow.dll"); + + if (Directory.Exists(prelink_dir)) + { + Directory.Delete(prelink_dir, recursive: true); + } + + Directory.CreateDirectory(prelink_dir); + File.Copy(file.FullName, prelink_app, overwrite: true); + + foreach (var dependency in dependencies) + { + File.Copy(dependency, + Path.Combine(prelink_dir, Path.GetFileName(Path.GetFileName(dependency))), + overwrite: true); + + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(dependency, "pdb"); + if (File.Exists(pdbFile)) + { + File.Copy(pdbFile, + Path.Combine(prelink_dir, Path.GetFileName(pdbFile)), + overwrite: true); + } + } + } + + var postlink_dir = Path.Combine(file.DirectoryName, "postlink_bin"); + if (Directory.Exists(postlink_dir)) + { + Directory.Delete(postlink_dir, recursive: true); + } + Directory.CreateDirectory(postlink_dir); + + var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var illinker_path = Path.Combine(base_path, IL_LINKER_DIR, "illink.dll"); + var descriptor_path = Path.Combine(base_path, IL_LINKER_DIR, "meadow_link.xml"); + + if (!File.Exists(illinker_path)) + { + throw new FileNotFoundException("Cannot run trimming operation. illink.dll not found."); + } + + if (linkerOptions != null) + { + var fi = new FileInfo(linkerOptions); + + if (fi.Exists) + { + logger?.LogInformation($"Using linker options from '{linkerOptions}'"); + } + else + { + logger?.LogWarning($"Linker options file '{linkerOptions}' not found"); + } + } + + // add in any run-time no-link arguments + var no_link_args = string.Empty; + if (noLink != null) + { + no_link_args = string.Join(" ", noLink.Select(o => $"-p copy \"{o}\"")); + } + + var monolinker_args = $"\"{illinker_path}\" -x \"{descriptor_path}\" {no_link_args} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlink_dir}\" -r \"{prelink_app}\" -a \"{prelink_os}\" -d \"{prelink_dir}\""; + + Debug.WriteLine("Trimming assemblies to reduce size (may take several seconds)..."); + + using (var process = new Process()) + { + process.StartInfo.FileName = "dotnet"; + process.StartInfo.Arguments = monolinker_args; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + + // To avoid deadlocks, read the output stream first and then wait + string stdOutReaderResult; + using (StreamReader stdOutReader = process.StandardOutput) + { + stdOutReaderResult = await stdOutReader.ReadToEndAsync(); + if (verbose) + { + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + } + + } + + string stdErrorReaderResult; + using (StreamReader stdErrorReader = process.StandardError) + { + stdErrorReaderResult = await stdErrorReader.ReadToEndAsync(); + if (!string.IsNullOrEmpty(stdErrorReaderResult)) + { + Console.WriteLine("StandardError Contains: " + stdErrorReaderResult); + } + } + + process.WaitForExit(60000); + if (process.ExitCode != 0) + { + Debug.WriteLine($"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + throw new Exception("Trimming failed"); + } + } + + + return Directory.EnumerateFiles(postlink_dir); + } + + private List GetDependencies(FileInfo file) + { + dependencyMap.Clear(); + + var refs = GetAssemblyNameReferences(file.Name, file.DirectoryName); + + var dependencies = GetDependencies(refs, dependencyMap, file.DirectoryName); + + return dependencies; + } + + private (Collection?, string?) GetAssemblyNameReferences(string fileName, string path) + { + static string? ResolvePath(string fileName, string path) + { + string attempted_path = Path.Combine(path, fileName); + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") + { + attempted_path += ".dll"; + } + return File.Exists(attempted_path) ? attempted_path : null; + } + + //ToDo - is it ever correct to fall back to the root path without a version? + string? resolved_path = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); + + if (resolved_path is null) + { + return (null, null); + } + + Collection references; + + using (var definition = Mono.Cecil.AssemblyDefinition.ReadAssembly(resolved_path)) + { + references = definition.MainModule.AssemblyReferences; + } + return (references, resolved_path); + } + + private List GetDependencies((Collection?, string?) references, List dependencyMap, string folderPath) + { + if (dependencyMap.Contains(references.Item2)) + return dependencyMap; + + dependencyMap.Add(references.Item2); + + foreach (var ar in references.Item1) + { + var namedRefs = GetAssemblyNameReferences(ar.Name, folderPath); + + if (namedRefs.Item1 == null) + continue; + + GetDependencies(namedRefs, dependencyMap, folderPath); + } + + return dependencyMap; + } +} diff --git a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs new file mode 100644 index 00000000..61c3eb82 --- /dev/null +++ b/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs @@ -0,0 +1,15 @@ +namespace Meadow.Cli; + +public partial class PackageManager +{ + private record BuildOptions + { + public DeployOptions Deploy { get; set; } + + public record DeployOptions + { + public List NoLink { get; set; } + public bool? IncludePDBs { get; set; } + } + } +} diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs new file mode 100644 index 00000000..480b959c --- /dev/null +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -0,0 +1,142 @@ +using Meadow.Software; +using System.Diagnostics; +using YamlDotNet.Serialization; + +namespace Meadow.Cli; + +public partial class PackageManager : IPackageManager +{ + public const string BuildOptionsFileName = "app.build.yaml"; + + private FileManager _fileManager; + + public PackageManager(FileManager fileManager) + { + _fileManager = fileManager; + } + + public bool BuildApplication(string projectFilePath, string configuration = "Release") + { + var proc = new Process(); + proc.StartInfo.FileName = "dotnet"; + proc.StartInfo.Arguments = $"build {projectFilePath} -c {configuration}"; + + proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.ErrorDialog = false; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.UseShellExecute = false; + + var success = true; + + proc.ErrorDataReceived += (sendingProcess, errorLine) => + { + // this gets called (with empty data) even on a successful build + Debug.WriteLine(errorLine.Data); + }; + proc.OutputDataReceived += (sendingProcess, dataLine) => + { + // look for "Build FAILED" + if (dataLine.Data != null) + { + Debug.WriteLine(dataLine.Data); + if (dataLine.Data.Contains("Build FAILED", StringComparison.InvariantCultureIgnoreCase)) + { + Debug.WriteLine("Build failed"); + success = false; + } + } + // TODO: look for "X Warning(s)" and "X Error(s)"? + // TODO: do we want to enable forwarding these messages for "verbose" output? + }; + + proc.Start(); + proc.BeginErrorReadLine(); + proc.BeginOutputReadLine(); + + proc.WaitForExit(); + var exitCode = proc.ExitCode; + proc.Close(); + + return success; + } + + public async Task TrimApplication( + FileInfo applicationFilePath, + bool includePdbs = false, + IList? noLink = null, + CancellationToken cancellationToken = default) + { + if (!applicationFilePath.Exists) + { + throw new FileNotFoundException($"{applicationFilePath} not found"); + } + + // does a meadow.build.yml file exist? + var buildOptionsFile = Path.Combine( + applicationFilePath.DirectoryName ?? string.Empty, + BuildOptionsFileName); + + if (File.Exists(buildOptionsFile)) + { + var yaml = File.ReadAllText(buildOptionsFile); + var deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + var opts = deserializer.Deserialize(yaml); + + if (opts.Deploy.NoLink != null && opts.Deploy.NoLink.Count > 0) + { + noLink = opts.Deploy.NoLink; + } + if (opts.Deploy.IncludePDBs != null) + { + includePdbs = opts.Deploy.IncludePDBs.Value; + } + } + + var dependencies = GetDependencies(applicationFilePath) + .Where(x => x.Contains("App.") == false) + .ToList(); + + await TrimDependencies( + applicationFilePath, + dependencies, + noLink, + null, // ILogger + includePdbs, + verbose: false); + } + + public async Task DeployApplication() + { + } + + public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, string appName = "App.dll") + { + if (!Directory.Exists(rootFolder)) throw new FileNotFoundException(); + + // look for a 'bin' folder + var path = Path.Combine(rootFolder, "bin"); + if (!Directory.Exists(path)) throw new FileNotFoundException("No 'bin' folder found. have you compiled?"); + + var files = new List(); + FindApp(path, files); + + void FindApp(string directory, List fileList) + { + foreach (var dir in Directory.GetDirectories(directory)) + { + var file = Directory.GetFiles(dir).FirstOrDefault(f => string.Compare(Path.GetFileName(f), appName, true) == 0); + if (file != null) + { + fileList.Add(new FileInfo(file)); + } + + FindApp(dir, fileList); + } + } + + return files.ToArray(); + } +} diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs new file mode 100644 index 00000000..aa956ca3 --- /dev/null +++ b/Source/v2/Meadow.Cli/Program.cs @@ -0,0 +1,101 @@ +using CliFx; +using Meadow.Cli; +using Meadow.CLI.Commands.DeviceManagement; +using Meadow.Software; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; +using System.Diagnostics; + +public class Program +{ + public static async Task Main(string[] args) + { + var logLevel = LogEventLevel.Information; + var logModifier = args.FirstOrDefault(a => a.Contains("-m")) + ?.Count(x => x == 'm') ?? 0; + + logLevel -= logModifier; + if (logLevel < 0) + { + logLevel = 0; + } + + 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(); + + // Log that we're using a log level other than default of Information + if (logLevel != LogEventLevel.Information) + { + Console.WriteLine($"Using log level {logLevel}"); + } + + var services = new ServiceCollection(); + + services.AddLogging( + builder => + { + builder.AddSerilog(Log.Logger, dispose: true); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + /* + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + */ + + AddCommandsAsServices(services); + + var serviceProvider = services.BuildServiceProvider(); + + try + { + await new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .UseTypeActivator(serviceProvider.GetService) + .SetExecutableName("meadow") + .Build() + .RunAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Operation failed: {ex.Message}"); +#if DEBUG + throw; //debug spew for debug builds +#endif + } + + Environment.Exit(0); + return 0; + } + + private static void AddCommandsAsServices(IServiceCollection services) + { + var assembly = System.Reflection.Assembly.GetEntryAssembly(); //.GetAssembly(typeof(Program)); + Trace.Assert(assembly != null); + var types = assembly.GetTypes(); + + var commands = types.Where( + x => x.IsAssignableTo(typeof(ICommand))) + .Where(x => !x.IsAbstract); + + foreach (var command in commands) + { + services.AddTransient(command); + } + } +} diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json new file mode 100644 index 00000000..25caa7bc --- /dev/null +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -0,0 +1,175 @@ +{ + "profiles": { + "Meadow.Cli": { + "commandName": "Project" + }, + "Help": { + "commandName": "Project", + "commandLineArgs": "--help" + }, + "Version": { + "commandName": "Project", + "commandLineArgs": "--version" + }, + "Port List": { + "commandName": "Project", + "commandLineArgs": "port list" + }, + "Listen": { + "commandName": "Project", + "commandLineArgs": "listen" + }, + "Listen (no prefix)": { + "commandName": "Project", + "commandLineArgs": "listen -n" + }, + "Device Info": { + "commandName": "Project", + "commandLineArgs": "device info" + }, + "Device Reset": { + "commandName": "Project", + "commandLineArgs": "device reset" + }, + "Device Clock Read": { + "commandName": "Project", + "commandLineArgs": "device clock" + }, + "Device Clock Set": { + "commandName": "Project", + "commandLineArgs": "device clock now" + }, + "Config: Set Route Serial": { + "commandName": "Project", + "commandLineArgs": "config route COM4" + }, + "Config: Set Route TCP": { + "commandName": "Project", + "commandLineArgs": "config route 172.26.8.20" + }, + "Config: List": { + "commandName": "Project", + "commandLineArgs": "config --list" + }, + "Config: Help": { + "commandName": "Project", + "commandLineArgs": "config --help" + }, + "Runtime Enable": { + "commandName": "Project", + "commandLineArgs": "runtime enable" + }, + "Runtime Disable": { + "commandName": "Project", + "commandLineArgs": "runtime disable" + }, + "Runtime State": { + "commandName": "Project", + "commandLineArgs": "runtime state" + }, + "File List": { + "commandName": "Project", + "commandLineArgs": "file list" + }, + "File List verbose": { + "commandName": "Project", + "commandLineArgs": "file list --verbose" + }, + "File Read": { + "commandName": "Project", + "commandLineArgs": "file read test.txt \"f:\\temp\\test2.txt\"" + }, + "File Write": { + "commandName": "Project", + "commandLineArgs": "file write -f \"f:\\temp\\test.txt\"" + }, + "Firmware List": { + "commandName": "Project", + "commandLineArgs": "firmware list" + }, + "Firmware List verbose": { + "commandName": "Project", + "commandLineArgs": "firmware list --verbose" + }, + "Firmware Download latest": { + "commandName": "Project", + "commandLineArgs": "firmware download" + }, + "Firmware Download version": { + "commandName": "Project", + "commandLineArgs": "firmware download 1.0.2.0 --force" + }, + "Firmware Default": { + "commandName": "Project", + "commandLineArgs": "firmware default 1.2.0.1" + }, + "Firmware Delete": { + "commandName": "Project", + "commandLineArgs": "firmware delete 1.2.0.1" + }, + "Firmware Write all": { + "commandName": "Project", + "commandLineArgs": "firmware write runtime esp" + }, + "Firmware Write version": { + "commandName": "Project", + "commandLineArgs": "firmware write -v 1.2.0.1" + }, + "Firmware Write runtime": { + "commandName": "Project", + "commandLineArgs": "firmware write runtime" + }, + "Firmware Write esp": { + "commandName": "Project", + "commandLineArgs": "firmware write esp" + }, + "Firmware Write os DFU": { + "commandName": "Project", + "commandLineArgs": "firmware write os -v 1.2.0.1" + }, + "Trace enable": { + "commandName": "Project", + "commandLineArgs": "trace enable" + }, + "Trace disable": { + "commandName": "Project", + "commandLineArgs": "trace disable" + }, + "Trace level": { + "commandName": "Project", + "commandLineArgs": "trace level 2" + }, + "Developer": { + "commandName": "Project", + "commandLineArgs": "developer -p 2 -v 20" + }, + "Uart trace enable": { + "commandName": "Project", + "commandLineArgs": "uart trace enable" + }, + "Uart trace disable": { + "commandName": "Project", + "commandLineArgs": "uart trace disable" + }, + "App Build": { + "commandName": "Project", + "commandLineArgs": "app build F:\\temp\\MeadowApplication1" + }, + "App Build Debug": { + "commandName": "Project", + "commandLineArgs": "app build F:\\temp\\MeadowApplication1 -c Debug" + }, + "App Trim": { + "commandName": "Project", + "commandLineArgs": "app trim F:\\temp\\MeadowApplication1" + }, + "Dfu Install": { + "commandName": "Project", + "commandLineArgs": "dfu install" + }, + "Dfu Install 0.10": { + "commandName": "Project", + "commandLineArgs": "dfu install -v 0.10" + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/SettingsManager.cs b/Source/v2/Meadow.Cli/SettingsManager.cs new file mode 100644 index 00000000..a3e6fb18 --- /dev/null +++ b/Source/v2/Meadow.Cli/SettingsManager.cs @@ -0,0 +1,123 @@ +using System.Configuration; +using System.Text.Json; + +namespace Meadow.Cli; + +public class SettingsManager : ISettingsManager +{ + private class Settings + { + public Dictionary Public { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + public Dictionary Private { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + public static class PublicSettings + { + public const string Route = "route"; + } + + private readonly string Path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "cli.settings"); + private const string PrivatePrefix = "private."; + + public Dictionary GetPublicSettings() + { + var settings = GetSettings(); + return settings.Public; + } + + public string? GetSetting(string setting) + { + var settings = GetSettings(); + if (settings.Public.TryGetValue(setting.ToString(), out var ret)) + { + return ret; + } + else if (settings.Private.TryGetValue(setting.ToString(), out var pret)) + { + return pret; + } + return null; + } + + public void DeleteSetting(string setting) + { + var settings = GetSettings(); + Dictionary target; + + if (setting.StartsWith(PrivatePrefix)) + { + setting = setting.Substring(PrivatePrefix.Length); + target = settings.Private; + } + else + { + target = settings.Public; + } + + if (target.ContainsKey(setting.ToString())) + { + target.Remove(setting); + + var json = JsonSerializer.Serialize(settings); + File.WriteAllText(Path, json); + } + } + + public void SaveSetting(string setting, string value) + { + var settings = GetSettings(); + Dictionary target; + + if (setting.StartsWith(PrivatePrefix)) + { + setting = setting.Substring(PrivatePrefix.Length); + target = settings.Private; + } + else + { + target = settings.Public; + } + + if (target.ContainsKey(setting)) + { + target[setting] = value; + } + else + { + target.Add(setting, value); + } + + var json = JsonSerializer.Serialize(settings); + File.WriteAllText(Path, json); + } + + public string? GetAppSetting(string name) + { + if (ConfigurationManager.AppSettings.AllKeys.Contains(name)) + { + return ConfigurationManager.AppSettings[name]; + } + else + { + throw new ArgumentException($"{name} setting not found."); + } + } + + private Settings GetSettings() + { + var fi = new FileInfo(Path); + + if (!Directory.Exists(fi.Directory.FullName)) + { + Directory.CreateDirectory(fi.Directory.FullName); + } + + if (File.Exists(Path)) + { + var json = File.ReadAllText(Path); + return JsonSerializer.Deserialize(json) ?? new Settings(); + } + + return new Settings(); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll b/Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll new file mode 100644 index 00000000..d6f5ee58 Binary files /dev/null and b/Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll differ diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll b/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll new file mode 100644 index 00000000..682aef81 Binary files /dev/null and b/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll differ diff --git a/Source/v2/Meadow.Cli/lib/illink.deps.json b/Source/v2/Meadow.Cli/lib/illink.deps.json new file mode 100644 index 00000000..d7b91143 --- /dev/null +++ b/Source/v2/Meadow.Cli/lib/illink.deps.json @@ -0,0 +1,112 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v3.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v3.0": { + "illink/5.0.0-dev": { + "dependencies": { + "Microsoft.Net.Compilers.Toolset": "3.8.0-4.20503.2", + "Microsoft.SourceLink.AzureRepos.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.GitHub": "1.1.0-beta-20206-02", + "Mono.Cecil": "0.11.2", + "Mono.Cecil.Pdb": "0.11.2", + "XliffTasks": "1.0.0-beta.20502.2" + }, + "runtime": { + "illink.dll": {} + } + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": {}, + "Microsoft.Net.Compilers.Toolset/3.8.0-4.20503.2": {}, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": {}, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "XliffTasks/1.0.0-beta.20502.2": {}, + "Mono.Cecil/0.11.2": { + "runtime": { + "Mono.Cecil.dll": {} + } + }, + "Mono.Cecil.Pdb/0.11.2": { + "dependencies": { + "Mono.Cecil": "0.11.2" + }, + "runtime": { + "Mono.Cecil.Pdb.dll": {} + } + } + } + }, + "libraries": { + "illink/5.0.0-dev": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hZ9leS9Yd9MHpqvviMftSJFDcLYu2h1DrapW1TDm1s1fgOy71c8HvArNMd3fseVkXmp3VTfGnkgcw0FR+TI6xw==", + "path": "microsoft.build.tasks.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.build.tasks.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.Net.Compilers.Toolset/3.8.0-4.20503.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-jfscID/5IHHPVVEbFCAJEUEWCeWNZCLwyBcUFG3/u44oiRd/aseDOYRzl3OnIIvcwzi0U2lSAs6Lt2+rdRIDMg==", + "path": "microsoft.net.compilers.toolset/3.8.0-4.20503.2", + "hashPath": "microsoft.net.compilers.toolset.3.8.0-4.20503.2.nupkg.sha512" + }, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-vVYhSds9TfraTQkGHHMDMVWnr3kCkTZ7vmqUmrXQBDJFXiWTuMoP5RRa9s1M/KmgB4szi5TOb7sOaHWKDT9qDA==", + "path": "microsoft.sourcelink.azurerepos.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.azurerepos.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aek0RTQ+4Bf11WvqaXajwYoaBWkX2edBjAr5XJOvhAsHX6/9vPOb7IpHAiE/NyCse7IcpGWslJZHNkv4UBEFqw==", + "path": "microsoft.sourcelink.common/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.common.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-7A7P0EwL+lypaI/CEvG4IcpAlQeAt04uPPw1SO6Q9Jwz2nE9309pQXJ4TfP/RLL8IOObACidN66+gVR+bJDZHw==", + "path": "microsoft.sourcelink.github/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.github.1.1.0-beta-20206-02.nupkg.sha512" + }, + "XliffTasks/1.0.0-beta.20502.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fnLroyas9Lfo7+YWFHjfAALbTODgNDY4z8GB4uT9OKiqWwYje/bcW5QJuRCWCkGtC1uuAx9oxNYH/MZ9G9/fmw==", + "path": "xlifftasks/1.0.0-beta.20502.2", + "hashPath": "xlifftasks.1.0.0-beta.20502.2.nupkg.sha512" + }, + "Mono.Cecil/0.11.2": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Mono.Cecil.Pdb/0.11.2": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/lib/illink.dll b/Source/v2/Meadow.Cli/lib/illink.dll new file mode 100644 index 00000000..f392213b Binary files /dev/null and b/Source/v2/Meadow.Cli/lib/illink.dll differ diff --git a/Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json b/Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json new file mode 100644 index 00000000..617ab505 --- /dev/null +++ b/Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json @@ -0,0 +1,10 @@ +{ + "runtimeOptions": { + "tfm": "netcoreapp3.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "3.0.0" + }, + "rollForwardOnNoCandidateFx": 2 + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/lib/meadow_link.xml b/Source/v2/Meadow.Cli/lib/meadow_link.xml new file mode 100644 index 00000000..0fe29752 --- /dev/null +++ b/Source/v2/Meadow.Cli/lib/meadow_link.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Source/v2/Meadow.HCom.Integration.Tests/CliTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/CliTests.cs new file mode 100644 index 00000000..e6de2eb6 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/CliTests.cs @@ -0,0 +1,39 @@ +using CliFx.Infrastructure; +using Meadow.CLI.Commands.DeviceManagement; +using Microsoft.Extensions.Logging; +using Serilog; + +namespace Meadow.HCom.Integration.Tests +{ + public class CliTests + { + [Fact] + public async Task ConfigTest() + { + var factory = new LoggerFactory().AddSerilog(Log.Logger); + + using var console = new FakeInMemoryConsole(); + + var listCommand = new ConfigCommand(new InMemorySettingsManager(), factory) + { + List = true + }; + + var setCommand = new ConfigCommand(new InMemorySettingsManager(), factory) + { + Settings = new string[] { "route", "COM8" } + }; + + await setCommand.ExecuteAsync(console); + + var stdOut = console.ReadOutputString(); + + // Act + await listCommand.ExecuteAsync(console); + + // Assert + stdOut = console.ReadOutputString(); + // Assert.That(stdOut, Is.EqualTo("foo bar")); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs new file mode 100644 index 00000000..f5f025d4 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs @@ -0,0 +1,215 @@ +using Meadow.Hcom; +using System.Diagnostics; + +namespace Meadow.HCom.Integration.Tests; + +public class ConnectionManagerTests +{ + public string ValidPortName { get; } = "COM3"; + + private SerialConnection GetConnection(string port) + { + // windows sucks and doesn't release the port, even after Dispose, + // so this is a workaround + return ConnectionManager + .GetConnection(port); + } + + [Fact] + public void TestInvalidPortName() + { + Assert.Throws(() => + { + GetConnection("InvalidPortName"); + }); + } + + [Fact] + public async Task TestDeviceReset() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + + await device.Reset(); + + // just getting here with no exception means success + } + + [Fact] + public async Task TestReadFileBadLocalPath() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + + var enabled = await device.IsRuntimeEnabled(); + + if (enabled) + { + await device.RuntimeDisable(); + } + + var dest = "c:\\invalid_local_path\\app.config.yaml"; + + await Assert.ThrowsAsync(async () => + { + await device.ReadFile("app.config.yaml", dest); + }); + } + + [Fact] + public async Task TestReadFilePositive() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + + var enabled = await device.IsRuntimeEnabled(); + + if (enabled) + { + await device.RuntimeDisable(); + } + + var dest = "f:\\temp\\app.config.yaml"; // <-- this needs to be valid on the test machine + if (File.Exists(dest)) File.Delete(dest); + Assert.False(File.Exists(dest)); + + var result = await device.ReadFile("app.config.yaml", dest); + Assert.True(result); + Assert.True(File.Exists(dest)); + } + + [Fact] + public async Task TestGetDeviceInfo() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + + var info = await device.GetDeviceInfo(); + Assert.NotNull(info); + Assert.True(info.Properties.Any()); + } + + [Fact] + public async Task TestGetFileListWithoutCrcs() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + var files = await device.GetFileList(false); + Assert.NotNull(files); + Assert.True(files.Any()); + Assert.True(files.All(f => f.Name != null)); + Assert.True(files.All(f => f.Crc == null)); + Assert.True(files.All(f => f.Size == null)); + } + + [Fact] + public async Task TestGetFileListWithCrcs() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + var files = await device.GetFileList(true); + Assert.NotNull(files); + Assert.True(files.Any()); + Assert.True(files.All(f => f.Name != null)); + Assert.True(files.All(f => f.Crc != null)); + Assert.True(files.All(f => f.Size != null)); + } + + [Fact] + public async Task TestRuntimeEnableAndDisable() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + // get the current runtime state + var start = await device.IsRuntimeEnabled(); + + if (start) + { + Debug.WriteLine("*** Runtime started enabled."); + Debug.WriteLine("*** Disabling..."); + await device.RuntimeDisable(); + Debug.WriteLine("*** Enabling..."); + await device.RuntimeEnable(); + + Assert.True(await device.IsRuntimeEnabled()); + } + else + { + Debug.WriteLine("*** Runtime started disabled."); + Debug.WriteLine("*** Enabling..."); + await device.RuntimeEnable(); + Debug.WriteLine("*** Disabling..."); + await device.RuntimeDisable(); + + Assert.False(await device.IsRuntimeEnabled()); + } + } + + [Fact] + public async Task TestRtcFunctions() + { + var c = GetConnection(ValidPortName); + var device = await c.Attach(); + + if (device == null) + { + Assert.Fail("no device"); + return; + } + // get the current runtime state + var time = await device.GetRtcTime(); + + var newTime = time.Value.AddMinutes(17); + await device.SetRtcTime(newTime); + + time = await device.GetRtcTime(); + + // should be withing a few seconds of newTime + var delta = Math.Abs(time.Value.Ticks - newTime.Ticks); + var deltaOffset = new TimeSpan(delta); + Assert.True(deltaOffset.TotalSeconds < 5); + } +} + diff --git a/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs b/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs new file mode 100644 index 00000000..0b64d0fb --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs @@ -0,0 +1,58 @@ +using Meadow.Cli; + +namespace Meadow.HCom.Integration.Tests +{ + public class InMemorySettingsManager : ISettingsManager + { + private Dictionary _publicSettings = new(); + + public void DeleteSetting(string setting) + { + lock (_publicSettings) + { + if (_publicSettings.ContainsKey(setting)) + { + _publicSettings.Remove(setting); + } + } + } + + public string? GetAppSetting(string name) + { + throw new NotImplementedException(); + } + + public Dictionary GetPublicSettings() + { + return _publicSettings; + } + + public string? GetSetting(string setting) + { + lock (_publicSettings) + { + if (_publicSettings.TryGetValue(setting, out var value)) + { + return value; + } + } + + return null; + } + + public void SaveSetting(string setting, string value) + { + lock (_publicSettings) + { + if (_publicSettings.ContainsKey(setting)) + { + _publicSettings[setting] = value; + } + else + { + _publicSettings.Add(setting, value); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj new file mode 100644 index 00000000..6a20759c --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs new file mode 100644 index 00000000..d60dc865 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -0,0 +1,63 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class SerialCommandTests + { + public string ValidPortName { get; } = "COM10"; + + [Fact] + public async void TestDeviceReset() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + await connection.ResetDevice(); + + // TODO: find a way to verify device reset + } + } + + [Fact] + public async void TestGetDeviceInfo() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var info = await connection.GetDeviceInfo(); + + Assert.NotNull(info); + } + } + + [Fact] + public async void TestGetFileListNoCrc() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var files = await connection.GetFileList(false); + + Assert.NotNull(files); + Assert.True(files.Length > 0); + } + } + + [Fact] + public async void TestGetFileListWithCrc() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var files = await connection.GetFileList(true); + + Assert.NotNull(files); + Assert.True(files.Length > 0); + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs new file mode 100644 index 00000000..92467a24 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -0,0 +1,65 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class SerialConnectionTests + { + public string ValidPortName { get; } = "COM10"; + + [Fact] + public void TestInvalidPortName() + { + Assert.Throws(() => + { + var connection = new SerialConnection("COMxx"); + }); + } + + [Fact] + public async void TestListen() + { + using (var connection = new SerialConnection(ValidPortName)) + { + /* + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var listener = new TestListener(); + connection.AddListener(listener); + + // dev note: something has to happen to generate messages - right now a manual reset is the action + // in the future, we'll implement a Reset() command + + var timeoutSecs = 10; + + while (timeoutSecs-- > 0) + { + if (listener.Messages.Count > 0) + { + break; + } + + await Task.Delay(1000); + } + + Assert.True(listener.Messages.Count > 0); + */ + } + } + + [Fact] + public async void TestAttachPositive() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + var connected = await connection.Attach(null, 2); + Assert.Equal(ConnectionState.Connected, connection.State); + + while (true) + { + await Task.Delay(1000); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs new file mode 100644 index 00000000..f98ea581 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs @@ -0,0 +1,23 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class TcpCommandTests + { + public string ValidPortName { get; } = "http://172.26.8.20:5000"; + + [Fact] + public async void TestGetDeviceInfo() + { + using (var connection = new TcpConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var info = await connection.GetDeviceInfo(); + + Assert.NotNull(info); + } + } + + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/TcpConnectionTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/TcpConnectionTests.cs new file mode 100644 index 00000000..42b3adc4 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/TcpConnectionTests.cs @@ -0,0 +1,25 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class TcpConnectionTests + { + public string ValidPortName { get; } = "http://172.26.8.20:5000"; + + [Fact] + public async void TestAttachPositive() + { + using (var connection = new TcpConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + var connected = await connection.Attach(null, 20); + Assert.Equal(ConnectionState.Connected, connection.State); + + while (true) + { + await Task.Delay(1000); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom.Integration.Tests/Usings.cs b/Source/v2/Meadow.HCom.Integration.Tests/Usings.cs new file mode 100644 index 00000000..8bd22938 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/Usings.cs @@ -0,0 +1,3 @@ +global using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/Source/v2/Meadow.Hcom/AssemblyAttributes.cs b/Source/v2/Meadow.Hcom/AssemblyAttributes.cs new file mode 100644 index 00000000..393a02e2 --- /dev/null +++ b/Source/v2/Meadow.Hcom/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Meadow.HCom.Integration.Tests")] \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/CobsTools.cs b/Source/v2/Meadow.Hcom/CobsTools.cs new file mode 100644 index 00000000..8736353a --- /dev/null +++ b/Source/v2/Meadow.Hcom/CobsTools.cs @@ -0,0 +1,101 @@ +namespace Meadow.Hcom +{ + internal static class CobsTools + { + //============================================================== + // Consistent Overhead Byte Stuffing (COBS) is a scheme to take binary data + // replace an arbituary byte value, usually 0x00, with an encoding that replaces + // this value, in a way, that allows the orginal data can be recovered while + // creating frames around the data. + // + // The following C# code was ported from a 'C' example licensed under MIT License + // https://github.com/bakercp/PacketSerial/blob/master/src/Encoding/COBS.h. + // After porting, I found errors. I referred to the original authors paper at + // http://conferences.sigcomm.org/sigcomm/1997/papers/p062.pdf for additional insights. + // Modifications were needed and adding a starting offset to support large buffers was + // added to allow sub-segments to be encoded. + // + public static int CobsEncoding(byte[] source, int startingSkip, int length, + ref byte[] encoded, int encodeSkip) + { + int sourceOffset = startingSkip; // Offset into source buffer + // Add 1 because first byte filled with first replaceValue value + int encodedOffset = 1; // Offset into destination buffer + int replaceOffset = 0; // Offset where replaceValue is being tracked + byte replaceValue = 1; // Value of the offset to the next delimiter + try + { + while (sourceOffset < length + startingSkip) + { + // Is source value the one we must replace? + if (source[sourceOffset] == 0x00) + { + encoded[encodeSkip + replaceOffset] = replaceValue; // Replace original value with offset to replaceValue + replaceOffset = encodedOffset++; // Update replace offset and bump encoded offset + replaceValue = 1; // Reset replaceValue + } + else + { + if (encodeSkip + encodedOffset == encoded.Length) + { + Console.WriteLine($"encodeSkip + encodedOffset == encoded.Length"); + return -1; + } + encoded[encodeSkip + encodedOffset++] = source[sourceOffset]; // Just copy original value + replaceValue++; // Keep zero offset tracker + + // replaceValue has been tracking the delimiter offset. If it's 0xff then + // special action is needed, because we reached the end of a 254 byte block + // of data. And now encoding starts like at the first. + if (replaceValue == 0xff) // Signals maximum possible offset + { + encoded[encodeSkip + replaceOffset] = replaceValue; + replaceOffset = encodedOffset++; + replaceValue = 1; + } + } + sourceOffset++; // Point to next source value + } + } + catch (Exception except) + { + Console.WriteLine($"An exception was caught: {except}"); + Thread.Sleep(10 * 60 * 1000); // Sleep for 10 minutes + throw; + } + + // Last character + encoded[encodeSkip + replaceOffset] = replaceValue; + return encodedOffset; // Number of bytes written to result buffer + } + + //--------------------------------------------------------------------------- + public static int CobsDecoding(byte[] encoded, int length, ref byte[] decoded) + { + int encodedOffset = 0; // Offset into original (encoded) buffer + int decodedOffset = 0; // Offset into destination (decoded) buffer + byte replaceValue = 0; // Value that will be inserted to indicate replaced value + + while (encodedOffset < length) + { + replaceValue = encoded[encodedOffset]; // Grab next byte + + if (((encodedOffset + replaceValue) > length) && (replaceValue != 1)) + return 0; + + encodedOffset++; // Point to next source + + // Copy all unchanged bytes + // C# would Array.Copy be noticably better? + for (int i = 1; i < replaceValue; i++) + decoded[decodedOffset++] = encoded[encodedOffset++]; + + // Sometimes don't need a trailing delimiter added + if (replaceValue < 0xff && encodedOffset != length) + decoded[decodedOffset++] = 0x00; + } + + return decodedOffset; + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/ConnectionManager.cs b/Source/v2/Meadow.Hcom/ConnectionManager.cs new file mode 100644 index 00000000..ac78d2a9 --- /dev/null +++ b/Source/v2/Meadow.Hcom/ConnectionManager.cs @@ -0,0 +1,29 @@ +namespace Meadow.Hcom +{ + public class ConnectionManager + { + private static List _connections = new List(); + + public static TConnection GetConnection(string connectionName) + where TConnection : class, IMeadowConnection + { + // see if it already is known + var existing = _connections.FirstOrDefault(c => c.Name == connectionName) as TConnection; + if (existing != null) return existing; + + // otherwise create + switch (typeof(TConnection)) + { + case Type t when t == typeof(SerialConnection): + var c = new SerialConnection(connectionName); + _connections.Add(c); +#pragma warning disable 8603 + return c as TConnection; +#pragma warning restore + default: + throw new NotSupportedException(); + }; + + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/ConnectionState.cs b/Source/v2/Meadow.Hcom/ConnectionState.cs new file mode 100644 index 00000000..b0c3e15b --- /dev/null +++ b/Source/v2/Meadow.Hcom/ConnectionState.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom +{ + public enum ConnectionState + { + Disconnected, + Connected, + MeadowAttached + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs new file mode 100644 index 00000000..ec7984e8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -0,0 +1,81 @@ +namespace Meadow.Hcom; + +public abstract class ConnectionBase : IMeadowConnection, IDisposable +{ + private bool _isDisposed; + + public ConnectionState State { get; protected set; } + public IMeadowDevice? Device { get; protected set; } + + public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; + public event EventHandler ConnectionError = default!; + public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress = default!; + public event EventHandler ConnectionMessage = default!; + + public abstract string Name { get; } + + public abstract Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); + public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + public abstract Task GetDeviceInfo(CancellationToken? cancellationToken = null); + public abstract Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + public abstract Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); + public abstract Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); + public abstract Task ResetDevice(CancellationToken? cancellationToken = null); + public abstract Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); + public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null); + public abstract Task RuntimeEnable(CancellationToken? cancellationToken = null); + public abstract Task GetRtcTime(CancellationToken? cancellationToken = null); + public abstract Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + public abstract Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null); + public abstract Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null); + public abstract Task TraceEnable(CancellationToken? cancellationToken = null); + public abstract Task TraceDisable(CancellationToken? cancellationToken = null); + public abstract Task SetTraceLevel(int level, CancellationToken? cancellationToken = null); + public abstract Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null); + public abstract Task UartTraceEnable(CancellationToken? cancellationToken = null); + public abstract Task UartTraceDisable(CancellationToken? cancellationToken = null); + + public ConnectionBase() + { + } + + protected void RaiseConnectionMessage(string message) + { + ConnectionMessage?.Invoke(this, message); + } + + protected void RaiseFileWriteProgress(string fileName, long progress, long total) + { + FileWriteProgress?.Invoke(this, (fileName, progress, total)); + } + + protected void RaiseDeviceMessageReceived(string message, string? source) + { + DeviceMessageReceived?.Invoke(this, (message, source)); + } + + protected void RaiseConnectionError(Exception error) + { + ConnectionError?.Invoke(this, error); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // Close(); + // _port.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs new file mode 100644 index 00000000..b8162443 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -0,0 +1,274 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace Meadow.Hcom +{ + public partial class SerialConnection + { + private bool _reconnectInProgress = false; + + public event EventHandler FileException = delegate { }; + + public override async Task WaitForMeadowAttach(CancellationToken? cancellationToken) + { + var timeout = 20; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) throw new TaskCanceledException(); + if (timeout <= 0) throw new TimeoutException(); + + if (State == ConnectionState.MeadowAttached) return; + + await Task.Delay(500); + } + + throw new TimeoutException(); + } + + private async Task ListenerProc() + { + var readBuffer = new byte[ReadBufferSizeBytes]; + var decodedBuffer = new byte[8192]; + var messageBytes = new CircularBuffer(8192 * 2); + var delimiter = new byte[] { 0x00 }; + + while (!_isDisposed) + { + if (_port.IsOpen) + { + try + { + Debug.WriteLine($"listening..."); + + var receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); + + Debug.WriteLine($"Received {receivedLength} bytes"); + + if (receivedLength > 0) + { + messageBytes.Append(readBuffer, 0, receivedLength); + + while (messageBytes.Count > 0) + { + var index = messageBytes.FirstIndexOf(delimiter); + + if (index < 0) + { + Debug.WriteLine($"No delimiter"); + break; + } + var packetBytes = messageBytes.Remove(index + 1); + + if (packetBytes.Length == 1) + { + // 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. + + // we discard this single 0x00 byte + } + else + { + Debug.WriteLine($"Received a {packetBytes.Length} byte packet"); + + var decodedSize = CobsTools.CobsDecoding(packetBytes, packetBytes.Length - delimiter.Length, ref decodedBuffer); + + // now parse this per the HCOM protocol definition + var response = SerialResponse.Parse(decodedBuffer, decodedSize); + + Debug.WriteLine($"{response.RequestType}"); + _state = ConnectionState.MeadowAttached; + + if (response != null) + { + _messageCount++; + } + + if (response is TextInformationResponse tir) + { + // send the message to any listeners + Debug.WriteLine($"INFO> {tir.Text}"); + + InfoMessages.Add(tir.Text); + base.RaiseDeviceMessageReceived(tir.Text, "info"); + } + else if (response is TextStdOutResponse tso) + { + // send the message to any listeners + Debug.WriteLine($"STDOUT> {tso.Text}"); + + StdOut.Add(tso.Text); + base.RaiseDeviceMessageReceived(tso.Text, "stdout"); + } + else if (response is TextStdErrResponse tse) + { + // send the message to any listeners + Debug.WriteLine($"STDERR> {tse.Text}"); + + StdErr.Add(tse.Text); + base.RaiseDeviceMessageReceived(tse.Text, "stderr"); + } + else if (response is TextListHeaderResponse tlh) + { + // start of a list + _textListComplete = false; + _textList.Clear(); + } + else if (response is TextListMemberResponse tlm) + { + _textList.Add(tlm.Text); + } + else if (response is TextCrcMemberResponse tcm) + { + _textList.Add(tcm.Text); + } + else if (response is TextConcludedResponse tcr) + { + _lastRequestConcluded = (RequestType)tcr.RequestType; + + if (_reconnectInProgress) + { + _state = ConnectionState.MeadowAttached; + _reconnectInProgress = false; + } + else if (_textListComplete != null) + { + _textListComplete = true; + } + } + else if (response is TextRequestResponse trr) + { + // this is a response to a text request - the exact request is cached + Debug.WriteLine($"RESPONSE> {trr.Text}"); + } + else if (response is DeviceInfoSerialResponse dir) + { + _deviceInfo = new DeviceInfo(dir.Fields); + } + else if (response is ReconnectRequiredResponse rrr) + { + // the device is going to restart - we need to wait for a HCOM_HOST_REQUEST_TEXT_CONCLUDED to know it's back + _state = ConnectionState.Disconnected; + _reconnectInProgress = true; + } + else if (response is FileReadInitOkResponse fri) + { + // Once HCOM_MDOW_REQUEST_UPLOAD_FILE_INIT is sent the F7 will respond + // with either HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY or + // HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL. + // + // If we get HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY we must open a file on + // this machine and respond with HCOM_MDOW_REQUEST_UPLOAD_READY_SEND_DATA. + // + // The F7 will begin to send HCOM_HOST_REQUEST_UPLOADING_FILE_DATA which + // contains the file data, which we must write to the open file. + // + // When the F7 has finished sending the data it will send a + // HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED message. When it is received + // we then close the open file and the process is completed. + var folder = Path.GetDirectoryName(_readFileInfo.LocalFileName); + if (!Directory.Exists(folder)) throw new DirectoryNotFoundException(folder); + + _readFileInfo.FileStream = File.Create(_readFileInfo.LocalFileName); + + var uploadRequest = RequestBuilder.Build(); + EncodeAndSendPacket(uploadRequest.Serialize()); + // (this as IMeadowConnection).EnqueueRequest(uploadRequest); + } + else if (response is UploadDataPacketResponse udp) + { + if (_readFileInfo == null) + { + throw new Exception("Data received for unknown file"); + } + + _readFileInfo.FileStream.Write(udp.FileData); + } + else if (response is UploadCompletedResponse ucr) + { + if (_readFileInfo == null) + { + throw new Exception("File Complete received for unknown file"); + } + + var fn = _readFileInfo.LocalFileName; + + _readFileInfo.FileStream.Flush(); + _readFileInfo.FileStream.Dispose(); + _readFileInfo = null; + + FileReadCompleted?.Invoke(this, fn); + } + else if (response is FileReadInitFailedResponse frf) + { + _readFileInfo = null; + throw new Exception(_lastError ?? "unknown error"); + } + else if (response is RequestErrorTextResponse ret) + { + RaiseDeviceMessageReceived(ret.Text, "hcom"); + _lastError = ret.Text; + } + else if (response is FileWriteInitFailedSerialResponse fwf) + { + _readFileInfo = null; + FileException?.Invoke(this, new Exception(_lastError ?? "unknown error")); + } + else if (response is FileWriteInitOkSerialResponse) + { + FileWriteAccepted?.Invoke(this, EventArgs.Empty); + } + else + { + Debug.WriteLine($"{response.GetType().Name} for:{response.RequestType}"); + // try to match responses with the requests + } + } + } + } + } + catch (DirectoryNotFoundException dnf) + { + FileException?.Invoke(this, dnf); + } + catch (IOException ioe) + { + + FileException?.Invoke(this, ioe); + // attempt to read timed out (i.e. there's just no data) + // NOP + } + catch (TimeoutException) + { + Debug.WriteLine($"listen timeout"); + } + catch (ThreadAbortException) + { + //ignoring for now until we wire cancellation ... + //this blocks the thread abort exception when the console app closes + Debug.WriteLine($"listen abort"); + } + catch (InvalidOperationException) + { + // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info + Debug.WriteLine($"listen on closed port"); + } + catch (Exception ex) + { + Debug.WriteLine($"listen error {ex.Message}"); + _logger?.LogTrace(ex, "An error occurred while listening to the serial port."); + await Task.Delay(1000); + } + } + else + { + await Task.Delay(500); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs new file mode 100644 index 00000000..61eff517 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -0,0 +1,1072 @@ +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Diagnostics; +using System.IO.Ports; + +namespace Meadow.Hcom; + +public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); + +public partial class SerialConnection : ConnectionBase, IDisposable +{ + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + + private event EventHandler FileReadCompleted = delegate { }; + private event EventHandler FileWriteAccepted; + + public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + + private SerialPort _port; + private ILogger? _logger; + private bool _isDisposed; + private ConnectionState _state; + private List _listeners = new List(); + private Queue _pendingCommands = new Queue(); + private bool _maintainConnection; + private Thread? _connectionManager = null; + private List _textList = new List(); + private int _messageCount = 0; + private ReadFileInfo? _readFileInfo = null; + private string? _lastError = null; + + public override string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) + { + if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) + { + throw new ArgumentException($"Serial Port '{port}' not found."); + } + + Name = port; + State = ConnectionState.Disconnected; + _logger = logger; + _port = new SerialPort(port); + _port.ReadTimeout = _port.WriteTimeout = 5000; + + new Task( + () => _ = ListenerProc(), + TaskCreationOptions.LongRunning) + .Start(); + + new Thread(CommandManager) + { + IsBackground = true, + Name = "HCOM Sender" + } + .Start(); + } + + private bool MaintainConnection + { + get => _maintainConnection; + set + { + if (value == MaintainConnection) return; + + _maintainConnection = value; + + if (value) + { + if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) + { + _connectionManager = new Thread(ConnectionManagerProc) + { + IsBackground = true, + Name = "HCOM Connection Manager" + }; + _connectionManager.Start(); + + } + } + } + } + + private void ConnectionManagerProc() + { + while (_maintainConnection) + { + if (!_port.IsOpen) + { + try + { + Debug.WriteLine("Opening COM port..."); + _port.Open(); + Debug.WriteLine("Opened COM port"); + } + catch (Exception ex) + { + Debug.WriteLine($"{ex.Message}"); + Thread.Sleep(1000); + } + } + else + { + Thread.Sleep(1000); + } + } + } + + public void AddListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Add(listener); + } + + Open(); + + MaintainConnection = true; + } + + public void RemoveListener(IConnectionListener listener) + { + lock (_listeners) + { + _listeners.Remove(listener); + } + + // TODO: stop maintaining connection? + } + + public ConnectionState State + { + get => _state; + private set + { + if (value == State) return; + + var old = _state; + _state = value; + ConnectionStateChanged?.Invoke(this, old, State); + } + } + + private void Open() + { + if (!_port.IsOpen) + { + try + { + _port.Open(); + } + catch (FileNotFoundException) + { + throw new Exception($"Serial port '{_port.PortName}' not found"); + } + } + State = ConnectionState.Connected; + } + + private void Close() + { + if (_port.IsOpen) + { + _port.Close(); + } + + State = ConnectionState.Disconnected; + } + + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + try + { + // ensure the port is open + Open(); + + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" + var command = RequestBuilder.Build(); + + // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + + _port.DiscardInBuffer(); + + // wait for a response + var timeout = timeoutSeconds * 2; + var dataReceived = false; + + // local function so we can unsubscribe + var count = _messageCount; + + _pendingCommands.Enqueue(command); + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); + + if (count != _messageCount) + { + dataReceived = true; + break; + } + + await Task.Delay(500); + } + + // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no + + // create the device instance + if (dataReceived) + { + Device = new MeadowDevice(this); + } + + return Device; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to connect"); + throw; + } + } + + private void CommandManager() + { + while (!_isDisposed) + { + while (_pendingCommands.Count > 0) + { + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + + var command = _pendingCommands.Dequeue() as Request; + + // if this is a file write, we need to packetize for progress + + var payload = command.Serialize(); + EncodeAndSendPacket(payload); + + // TODO: re-queue on fail? + } + + Thread.Sleep(1000); + } + } + + private class ReadFileInfo + { + private string? _localFileName; + + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get + { + if (_localFileName != null) return _localFileName; + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); + } + set => _localFileName = value; + } + public FileStream FileStream { get; set; } = default!; + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) + { + await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); + } + + private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = null) + { + Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); + + while (!_port.IsOpen) + { + _state = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } + + _state = ConnectionState.Connected; + + try + { + int encodedToSend; + byte[] encodedBytes; + + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + + // For testing calculate the crc including the sequence number + //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + try + { + // The encoded size using COBS is just a bit more than the original size adding 1 byte + // every 254 bytes plus 1 and need room for beginning and ending delimiters. + var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; + encodedBytes = new byte[l + 2]; + + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + + // DEBUG TESTING + if (encodedToSend == -1) + { + _logger?.LogError($"Error - encodedToSend == -1"); + return; + } + + if (_port == null) + { + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); + } + } + catch (Exception except) + { + string msg = string.Format("Send setup Exception: {0}", except); + _logger?.LogError(msg); + throw; + } + + // Add delimiters to packet boundaries + try + { + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + } + catch (Exception encodedBytesEx) + { + // This should drop the connection and retry + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; + } + + try + { + // Send the data to Meadow + // Debug.Write($"Sending {encodedToSend} bytes..."); + await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken ?? CancellationToken.None); + // Debug.WriteLine($"sent"); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); + throw; + } + } + catch (Exception except) + { + // DID YOU RESTART MEADOW? + // This should drop the connection and retry + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + throw; + } + } + + + private class SerialMessage + { + private readonly IList> _segments; + + public SerialMessage(Memory segment) + { + _segments = new List>(); + _segments.Add(segment); + } + + public void AddSegment(Memory segment) + { + _segments.Add(segment); + } + + public byte[] ToArray() + { + using var ms = new MemoryStream(); + foreach (var segment in _segments) + { + // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. + var tmp = ArrayPool.Shared.Rent(segment.Length); + segment.CopyTo(tmp); + ms.Write(tmp, 0, segment.Length); + ArrayPool.Shared.Return(tmp); + } + return ms.ToArray(); + } + } + + private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) + { + var decodedBuffer = ArrayPool.Shared.Rent(8192); + var packetLength = packetBuffer.Length; + // It's possible that we may find a series of 0x00 values in the buffer. + // This is because when the sender is blocked (because this code isn't + // running) it will attempt to send a single 0x00 before the full message. + // This allows it to test for a connection. When the connection is + // unblocked this 0x00 is sent and gets put into the buffer along with + // any others that were queued along the usb serial pipe line. + if (packetLength == 1) + { + //_logger.LogTrace("Throwing out 0x00 from buffer"); + return false; + } + + var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); + + /* + // If a message is too short it is ignored + if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + { + return false; + } + + Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + + // Process the received packet + ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), + cancellationToken); + + */ + ArrayPool.Shared.Return(decodedBuffer); + return true; + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Close(); + _port.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // ---------------------------------------------- + // ---------------------------------------------- + // ---------------------------------------------- + + private Exception? _lastException; + private bool? _textListComplete; + private DeviceInfo? _deviceInfo; + private RequestType? _lastRequestConcluded = null; + private List StdOut { get; } = new List(); + private List StdErr { get; } = new List(); + private List InfoMessages { get; } = new List(); + + private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; + private const string RuntimeSucessfullyDisabledToken = "Mono is disabled"; + private const string RuntimeStateToken = "Mono is"; + private const string RuntimeIsEnabledToken = "Mono is enabled"; + private const string RtcRetrievalToken = "UTC time:"; + + public int CommandTimeoutSeconds { get; set; } = 30; + + private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) + { + var timeout = CommandTimeoutSeconds * 2; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (_lastException != null) return false; + + if (timeout <= 0) throw new TimeoutException(); + + if (checkAction()) + { + break; + } + + await Task.Delay(500); + } + + return true; + } + + private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + if (requestType == null || requestType == _lastRequestConcluded) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.Time = dateTime; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } + + public override async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + DateTimeOffset? now = null; + + var success = await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); + if (m != null) + { + var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); + now = DateTimeOffset.Parse(timeString); + return true; + } + } + + return false; + }, cancellationToken); + + return now; + } + + public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + // wait for an information response + var timeout = CommandTimeoutSeconds * 2; + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (timeout <= 0) throw new TimeoutException(); + + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeStateToken)); + if (m != null) + { + return m == RuntimeIsEnabledToken; + } + } + + await Task.Delay(500); + } + return false; + } + + public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + var success = await WaitForResponseText(RuntimeSucessfullyEnabledToken); + + if (!success) throw new Exception("Unable to enable runtime"); + } + + public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + var success = await WaitForResponseText(RuntimeSucessfullyDisabledToken); + + if (!success) throw new Exception("Unable to disable runtime"); + } + + public override async Task TraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.UserData = (uint)level; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.ExtraData = parameter; + command.UserData = value; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task ResetDevice(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + await WaitForMeadowAttach(cancellationToken); + } + + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _deviceInfo = null; + + _lastException = null; + EnqueueRequest(command); + + if (!await WaitForResult( + () => _deviceInfo != null, + cancellationToken)) + { + return null; + } + + return _deviceInfo; + } + + public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.IncludeCrcs = includeCrcs; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => _textListComplete ?? false, + cancellationToken)) + { + _textListComplete = null; + return null; + } + + var list = new List(); + + foreach (var candidate in _textList) + { + var fi = MeadowFileInfo.Parse(candidate); + if (fi != null) + { + list.Add(fi); + } + } + + _textListComplete = null; + return list.ToArray(); + } + + public override async Task WriteFile( + string localFileName, + string? meadowFileName = null, + CancellationToken? cancellationToken = null) + { + return await WriteFile(localFileName, meadowFileName, + RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, + 0, + cancellationToken); + } + + public override async Task WriteRuntime( + string localFileName, + CancellationToken? cancellationToken = null) + { + var commandTimeout = CommandTimeoutSeconds; + + + CommandTimeoutSeconds = 120; + _lastRequestConcluded = null; + + try + { + InfoMessages.Clear(); + + var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, + 0, + cancellationToken); + + RaiseConnectionMessage("\nErasing runtime flash blocks..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Moving runtime to flash..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Verifying..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + return true; + } + + return false; + }, + cancellationToken); + + return status; + } + finally + { + CommandTimeoutSeconds = commandTimeout; + } + } + + public override async Task WriteCoprocessorFile( + string localFileName, + int destinationAddress, + CancellationToken? cancellationToken = null) + { + // make the timeouts much bigger, as the ESP flash takes a lot of time + var readTimeout = _port.ReadTimeout; + var commandTimeout = CommandTimeoutSeconds; + _lastRequestConcluded = null; + + _port.ReadTimeout = 60000; + CommandTimeoutSeconds = 180; + InfoMessages.Clear(); + var infoCount = 0; + + try + { + // push the file to the device + await WriteFile(localFileName, null, + RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, + destinationAddress, + cancellationToken); + + RaiseConnectionMessage("\nTransferring file to coprocessor..."); + + // now wait for the STM32 to finish writing to the ESP32 + return await WaitForResult(() => + { + if (InfoMessages.Count != infoCount) + { + infoCount = InfoMessages.Count; + RaiseConnectionMessage(InfoMessages.Last()); + } + + if (_lastRequestConcluded != null) + { + return true; + } + + return false; + }, + cancellationToken); + } + finally + { + _port.ReadTimeout = readTimeout; + CommandTimeoutSeconds = commandTimeout; + } + } + + private async Task WriteFile( + string localFileName, + string? meadowFileName, + RequestType initialRequestType, + RequestType endRequestType, + int writeAddress = 0, + CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.SetParameters( + localFileName, + meadowFileName ?? Path.GetFileName(localFileName), + writeAddress, + initialRequestType); + + var accepted = false; + Exception? ex = null; + + void OnFileWriteAccepted(object? sender, EventArgs a) + { + accepted = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + + FileWriteAccepted += OnFileWriteAccepted; + FileException += OnFileError; + + EnqueueRequest(command); + + // this will wait for a "file write accepted" from the target + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return accepted; + }, + cancellationToken)) + { + return false; + } + + // now send the file data + + using FileStream fs = File.OpenRead(localFileName); + + // The maximum data bytes is max packet size - 2 bytes for the sequence number + byte[] packet = new byte[Protocol.HCOM_PROTOCOL_PACKET_MAX_SIZE - 2]; + int bytesRead; + ushort sequenceNumber = 0; + + var progress = 0; + var expected = fs.Length; + + var fileName = Path.GetFileName(localFileName); + + base.RaiseFileWriteProgress(fileName, progress, expected); + + while (true) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) + { + return false; + } + + sequenceNumber++; + + // sequence number at the start of the packet + Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); + // followed by the file data + bytesRead = fs.Read(packet, 2, packet.Length - 2); + if (bytesRead <= 0) break; + await EncodeAndSendPacket(packet, bytesRead + 2, cancellationToken); + progress += bytesRead; + base.RaiseFileWriteProgress(fileName, progress, expected); + } + + base.RaiseFileWriteProgress(fileName, expected, expected); + + // finish with an "end" message - not enqued because this is all a serial operation + var request = RequestBuilder.Build(); + request.SetRequestType(endRequestType); + var p = request.Serialize(); + await EncodeAndSendPacket(p, cancellationToken); + + return true; + + } + + public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + command.LocalFileName = localFileName; + + var completed = false; + Exception? ex = null; + + void OnFileReadCompleted(object? sender, string filename) + { + completed = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + + try + { + FileReadCompleted += OnFileReadCompleted; + FileException += OnFileError; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return completed; + }, + cancellationToken)) + { + return false; + } + + return true; + } + finally + { + FileReadCompleted -= OnFileReadCompleted; + FileException -= OnFileError; + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs new file mode 100644 index 00000000..49eeffd3 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -0,0 +1,154 @@ +using System.Text.Json; + +namespace Meadow.Hcom; + +public class TcpConnection : ConnectionBase +{ + private HttpClient _client; + private string _baseUri; + + public override string Name => _baseUri; + + public TcpConnection(string uri) + { + _baseUri = uri; + _client = new HttpClient(); + } + + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + /* + var request = RequestBuilder.Build(); + + base.EnqueueRequest(request); + + // get the info and "attach" + var timeout = timeoutSeconds * 2; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); + + // do we have a device info? + + if (State == ConnectionState.MeadowAttached) + { + break; + } + + await Task.Delay(500); + } + */ + + // TODO: is there a way to "attach"? ping result? device info? + return Device = new MeadowDevice(this); + + // TODO: web socket for listen? + } + + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var response = await _client.GetAsync($"{_baseUri}/api/info"); + + if (response.IsSuccessStatusCode) + { + var r = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); + return new DeviceInfo(r.ToDictionary()); + } + else + { + throw new Exception($"API responded with {response.StatusCode}"); + } + } + + public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ResetDevice(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetRtcTime(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task TraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task TraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/DeviceInfo.cs b/Source/v2/Meadow.Hcom/DeviceInfo.cs new file mode 100644 index 00000000..b25809c5 --- /dev/null +++ b/Source/v2/Meadow.Hcom/DeviceInfo.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace Meadow.Hcom +{ + public class DeviceInfo + { + public Dictionary Properties { get; } + + internal DeviceInfo(Dictionary properties) + { + Properties = properties; + } + + public string this[string propname] => Properties[propname]; + public string OsVersion => this["OSVersion"]; + public string CoprocessorOsVersion => this["CoprocessorVersion"]; + public string RuntimeVersion => this["MonoVersion"]; + public string Model => this["Model"]; + public string HardwareVersion => this["Hardware"]; + public string DeviceName => this["DeviceName"]; + public string ProcessorType => this["ProcessorType"]; + public string UniqueID => this["ProcessorId"]; + public string SerialNumber => this["SerialNo"]; + public string CoprocessorType => this["CoprocessorType"]; + public string MacAddress => this["WiFiMAC"]; + + public override string ToString() + { + var info = new StringBuilder(); + + foreach (var prop in Properties) + { + info.AppendLine($"{prop.Key}: {prop.Value}"); + } + + return info.ToString(); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Extensions.cs b/Source/v2/Meadow.Hcom/Extensions.cs new file mode 100644 index 00000000..a1c27863 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Extensions.cs @@ -0,0 +1,16 @@ +namespace Meadow.Hcom; + +public static class Extensions +{ + public static Version ToVersion(this string s) + { + if (Version.TryParse(s, out var result)) + { + return result; + } + else + { + return new Version(); + } + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs new file mode 100644 index 00000000..4c37a41a --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; + +namespace Meadow.Hcom; + +public class DownloadFileStream : Stream, IDisposable +{ + private readonly ILogger? _logger; + private readonly Stream _stream; + + private long _position; + private DateTimeOffset _lastLog; + private long _lastPosition; + + public DownloadFileStream(Stream stream, ILogger? logger = null) + { + _stream = stream; + _logger = logger; + _lastLog = DateTimeOffset.Now; + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => _stream.Length; + public override long Position { get => _position; set => throw new NotImplementedException(); } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var b = _stream.Read(buffer, offset, count); + _position += b; + var now = DateTimeOffset.Now; + if (_lastLog.AddSeconds(5) < now) + { + LogPosition(); + _lastLog = now; + } + return b; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + LogPosition(); + base.Dispose(disposing); + } + + private void LogPosition() + { + if (_position == _lastPosition) + { + return; + } + + if (_position < 1024) + { + _logger?.LogInformation("Downloaded {position} bytes", _position); + _lastPosition = _position; + } + else if (_position < (1024 * 1024)) + { + _logger?.LogInformation("Downloaded {position} KiB", Math.Round(_position / 1024M, 2, MidpointRounding.ToEven)); + _lastPosition = _position; + } + else + { + _logger?.LogInformation("Downloaded {position} MiB", Math.Round(_position / 1024M / 1024M, 2, MidpointRounding.ToEven)); + _lastPosition = _position; + } + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs new file mode 100644 index 00000000..0ce95cd6 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -0,0 +1,351 @@ +using Microsoft.Extensions.Logging; +using System.IO.Compression; +using System.Reflection; +using System.Text.Json; + +namespace Meadow.Hcom; + +public class DownloadManager +{ + public static readonly string FirmwareDownloadsFilePathRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WildernessLabs", + "Firmware"); + + public static string FirmwareLatestVersion + { + get + { + string latest_txt = Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"); + if (File.Exists(latest_txt)) + return File.ReadAllText(latest_txt); + else + throw new FileNotFoundException("OS download was not found."); + } + } + + public static string FirmwareDownloadsFilePath => FirmwarePathForVersion(FirmwareLatestVersion); + + public static string FirmwarePathForVersion(string firmwareVersion) + { + return Path.Combine(FirmwareDownloadsFilePathRoot, firmwareVersion); + } + + public static readonly string WildernessLabsTemp = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WildernessLabs", + "temp"); + + 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"; + internal static readonly string VersionCheckUrlRoot = + "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; + + public static readonly string UpdateCommand = "dotnet tool update WildernessLabs.Meadow.CLI --global"; + + private static readonly HttpClient Client = new() + { + Timeout = TimeSpan.FromMinutes(5) + }; + + private readonly ILogger _logger; + + public DownloadManager(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public DownloadManager(ILogger logger) + { + _logger = logger; + } + + internal async Task DownloadMeadowOSVersionFile(string? version) + { + string versionCheckUrl; + if (version is null || string.IsNullOrWhiteSpace(version)) + { + _logger.LogInformation("Downloading latest version file" + Environment.NewLine); + versionCheckUrl = VersionCheckUrlRoot + "latest.json"; + } + else + { + _logger.LogInformation("Downloading version file for Meadow OS " + version + Environment.NewLine); + versionCheckUrl = VersionCheckUrlRoot + version + ".json"; + } + + string versionCheckFile; + + try + { + versionCheckFile = await DownloadFile(new Uri(versionCheckUrl)); + } + catch + { + return null; + } + + return versionCheckFile; + } + + //ToDo rename this method - DownloadOSAsync? + public async Task DownloadOsBinaries(string? version = null, bool force = false) + { + var versionCheckFilePath = await DownloadMeadowOSVersionFile(version); + + if (versionCheckFilePath == null) + { + _logger.LogError($"Meadow OS {version} cannot be downloaded or is not available"); + return; + } + + var payload = File.ReadAllText(versionCheckFilePath); + var release = JsonSerializer.Deserialize(payload); + + if (release == null) + { + _logger.LogError($"Unable to read release details for Meadow OS {version}. Payload: {payload}"); + return; + } + + if (!Directory.Exists(FirmwareDownloadsFilePathRoot)) + { + Directory.CreateDirectory(FirmwareDownloadsFilePathRoot); + //we'll write latest.txt regardless of version if it doesn't exist + File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); + } + else if (version == null) + { //otherwise only update if we're pulling the latest release OS + File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); + } + + if (release.Version.ToVersion() < "0.6.0.0".ToVersion()) + { + _logger.LogInformation( + $"Downloading OS version {release.Version} is no longer supported. The minimum OS version is 0.6.0.0." + Environment.NewLine); + return; + } + + var local_path = Path.Combine(FirmwareDownloadsFilePathRoot, release.Version); + + if (Directory.Exists(local_path)) + { + if (force) + { + CleanPath(local_path); + } + else + { + _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded." + Environment.NewLine); + return; + } + } + + Directory.CreateDirectory(local_path); + + try + { + _logger.LogInformation($"Downloading Meadow OS" + Environment.NewLine); + await DownloadAndExtractFile(new Uri(release.DownloadURL), local_path); + } + catch + { + _logger.LogError($"Unable to download Meadow OS {version}"); + return; + } + + try + { + _logger.LogInformation("Downloading coprocessor firmware" + Environment.NewLine); + await DownloadAndExtractFile(new Uri(release.NetworkDownloadURL), local_path); + } + catch + { + _logger.LogError($"Unable to download coprocessor firmware {version}"); + return; + } + + _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {local_path}" + Environment.NewLine); + } + + public async Task InstallDfuUtil(bool is64Bit = true, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Installing dfu-util..."); + + if (Directory.Exists(WildernessLabsTemp)) + { + Directory.Delete(WildernessLabsTemp, true); + } + + Directory.CreateDirectory(WildernessLabsTemp); + + const string downloadUrl = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/public/dfu-util-0.10-binaries.zip"; + + var downloadFileName = downloadUrl.Substring(downloadUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); + var response = await Client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + if (response.IsSuccessStatusCode == false) + { + throw new Exception("Failed to download dfu-util"); + } + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var downloadFileStream = new DownloadFileStream(stream, _logger)) + using (var fs = File.OpenWrite(Path.Combine(WildernessLabsTemp, downloadFileName))) + { + await downloadFileStream.CopyToAsync(fs); + } + + ZipFile.ExtractToDirectory( + Path.Combine(WildernessLabsTemp, downloadFileName), + WildernessLabsTemp); + + var dfuUtilExe = new FileInfo( + Path.Combine(WildernessLabsTemp, is64Bit ? "win64" : "win32", "dfu-util.exe")); + + var libUsbDll = new FileInfo( + Path.Combine( + WildernessLabsTemp, + is64Bit ? "win64" : "win32", + "libusb-1.0.dll")); + + var targetDir = is64Bit + ? Environment.GetFolderPath(Environment.SpecialFolder.System) + : Environment.GetFolderPath( + Environment.SpecialFolder.SystemX86); + + File.Copy(dfuUtilExe.FullName, Path.Combine(targetDir, dfuUtilExe.Name), true); + File.Copy(libUsbDll.FullName, Path.Combine(targetDir, libUsbDll.Name), true); + + // clean up from previous version + var dfuPath = Path.Combine(@"C:\Windows\System", dfuUtilExe.Name); + var libUsbPath = Path.Combine(@"C:\Windows\System", libUsbDll.Name); + if (File.Exists(dfuPath)) + { + File.Delete(dfuPath); + } + + if (File.Exists(libUsbPath)) + { + File.Delete(libUsbPath); + } + + _logger.LogInformation("dfu-util 0.10 installed"); + } + catch (Exception ex) + { + _logger.LogError( + ex, + ex.Message.Contains("Access to the path") + ? $"Run terminal as administrator and try again." + : "Unexpected error"); + } + finally + { + if (Directory.Exists(WildernessLabsTemp)) + { + Directory.Delete(WildernessLabsTemp, true); + } + } + } + + public async Task<(bool updateExists, string latestVersion, string currentVersion)> CheckForUpdates() + { + try + { + var packageId = "WildernessLabs.Meadow.CLI"; + var appVersion = Assembly.GetEntryAssembly()! + .GetCustomAttribute() + .Version; + + var json = await Client.GetStringAsync( + $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLower()}/index.json"); + + var result = JsonSerializer.Deserialize(json); + + if (!string.IsNullOrEmpty(result?.Versions.LastOrDefault())) + { + var latest = result!.Versions!.Last(); + return (latest.ToVersion() > appVersion.ToVersion(), latest, appVersion); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error checking for updates to Meadow.CLI"); + } + + return (false, string.Empty, string.Empty); + } + + private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri); + using var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + response.EnsureSuccessStatusCode(); + + var downloadFileName = Path.GetTempFileName(); + _logger.LogDebug("Copying downloaded file to temp file {filename}", downloadFileName); + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var downloadFileStream = new DownloadFileStream(stream, _logger)) + using (var firmwareFile = File.OpenWrite(downloadFileName)) + { + await downloadFileStream.CopyToAsync(firmwareFile); + } + return downloadFileName; + } + + private async Task DownloadAndExtractFile(Uri uri, string target_path, CancellationToken cancellationToken = default) + { + var downloadFileName = await DownloadFile(uri, cancellationToken); + + _logger.LogDebug("Extracting firmware to {path}", target_path); + ZipFile.ExtractToDirectory( + downloadFileName, + target_path); + try + { + File.Delete(downloadFileName); + } + catch (Exception ex) + { + _logger.LogWarning("Unable to delete temporary file"); + _logger.LogDebug(ex, "Unable to delete temporary file"); + } + } + + private void CleanPath(string path) + { + var di = new DirectoryInfo(path); + foreach (FileInfo file in di.GetFiles()) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to delete file {file} in firmware path", file.FullName); + _logger.LogDebug(ex, "Failed to delete file"); + } + } + foreach (DirectoryInfo dir in di.GetDirectories()) + { + try + { + dir.Delete(true); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to delete directory {directory} in firmware path", dir.FullName); + _logger.LogDebug(ex, "Failed to delete directory"); + } + } + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs new file mode 100644 index 00000000..0d8f448f --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Meadow.Hcom; + +public class BuildDateConverter : JsonConverter +{ + // build date is in the format "2022-09-01 09:47:26" + private const string FormatString = "yyyy-MM-dd HH:mm:ss"; + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert == typeof(DateTime)); + + if (!reader.TryGetDateTime(out DateTime value)) + { + value = DateTime.ParseExact(reader.GetString(), FormatString, CultureInfo.InvariantCulture); + } + + return value; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(FormatString)); + } +} + +public class FirmwareInfo +{ + public string Version { get; set; } = string.Empty; + [JsonPropertyName("build-date")] + public DateTime BuildDate { get; set; } + [JsonPropertyName("build-hash")] + public string BuildHash { get; set; } = string.Empty; + public bool IsLatest { get; set; } + + public override bool Equals(object obj) + { + var other = obj as FirmwareInfo; + if (other == null) return false; + return BuildHash.Equals(other.BuildHash); + } + + public override int GetHashCode() + { + return BuildHash.GetHashCode(); + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs new file mode 100644 index 00000000..a7c3fa85 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs @@ -0,0 +1,148 @@ +using Microsoft.Extensions.Logging; +using System.Net; +using System.Text.Json; + +namespace Meadow.Hcom; + +public static partial class JsonSerializerExtensions +{ + public static T? DeserializeAnonymousType(string json, T anonymousTypeObject, JsonSerializerOptions? options = default) + => JsonSerializer.Deserialize(json, options); + + public static ValueTask DeserializeAnonymousTypeAsync(Stream stream, TValue anonymousTypeObject, JsonSerializerOptions? options = default, CancellationToken cancellationToken = default) + => JsonSerializer.DeserializeAsync(stream, options, cancellationToken); // Method to deserialize from a stream added for completeness +} + +public static class FirmwareManager +{ + public static async Task GetRemoteFirmwareInfo(string versionNumber, ILogger logger) + { + var manager = new DownloadManager(logger); + + return await manager.DownloadMeadowOSVersionFile(versionNumber); + } + + public static async Task GetRemoteFirmware(string versionNumber, ILogger logger) + { + var manager = new DownloadManager(logger); + + await manager.DownloadOsBinaries(versionNumber, true); + } + + public static async Task GetCloudLatestFirmwareVersion() + { + var request = (HttpWebRequest)WebRequest.Create($"{DownloadManager.VersionCheckUrlRoot}latest.json"); + using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync()) + using (Stream stream = response.GetResponseStream()) + using (StreamReader reader = new StreamReader(stream)) + { + var json = await reader.ReadToEndAsync(); + + if (json == null) return string.Empty; + + return JsonSerializerExtensions.DeserializeAnonymousType(json, new { version = string.Empty }).version; + } + } + + public static string GetLocalLatestFirmwareVersion() + { + var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFilePathRoot); + var latest = string.Empty; + var latestFile = di.GetFiles("latest.txt").FirstOrDefault(); + if (latestFile != null) + { + latest = File.ReadAllText(latestFile.FullName).Trim(); + } + return latest; + } + + public static FirmwareInfo[] GetAllLocalFirmwareBuilds() + { + var list = new List(); + + var di = new DirectoryInfo(DownloadManager.FirmwareDownloadsFilePathRoot); + + var latest = GetLocalLatestFirmwareVersion(); + + var options = new JsonSerializerOptions(); + options.Converters.Add(new BuildDateConverter()); + + FirmwareInfo? ParseInfo(string version, string json) + { + var fi = JsonSerializer.Deserialize(json, options); + if (fi == null) return null; + fi.Version = version; + fi.IsLatest = version == latest; + return fi; + } + + foreach (var dir in di.EnumerateDirectories()) + { + var info = dir.GetFiles("build-info.json").FirstOrDefault(); + if (info == null) continue; + var json = File.ReadAllText(info.FullName); + try + { + var fi = ParseInfo(dir.Name, json); + if (fi != null) + { + list.Add(fi); + } + } + catch (JsonException ex) + { + // work around for Issue #229 (bad json) + var index = json.IndexOf(']'); + if (index != -1 && json[index + 1] == ',') + { + var fix = $"{json.Substring(0, index + 1)}{json.Substring(index + 2)}"; + try + { + var fi = ParseInfo(dir.Name, fix); + if (fi != null) + { + list.Add(fi); + } + } + catch + { + continue; + } + } + + continue; + } + } + return list.ToArray(); + } + + public static FirmwareUpdater GetFirmwareUpdater(IMeadowConnection connection) + { + return new FirmwareUpdater(connection); + } + + + + public static async Task PushApplicationToDevice(IMeadowConnection connection, DirectoryInfo appFolder, ILogger? logger = null) + { + try + { + if (connection == null) throw new ArgumentNullException("connection"); + if (connection.Device == null) throw new ArgumentNullException("connection.Device"); + + + var info = await connection.Device.GetDeviceInfo(); + + await connection.Device.RuntimeDisable(); + // the device will disconnect and reconnect here + + // await connection.Device.DeployApp(Path.Combine(appFolder.FullName, "App.dll"), osVersion); + + await connection.Device.RuntimeEnable(); + } + catch (Exception ex) + { + logger?.LogError(ex, "Error flashing OS to Meadow"); + } + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs new file mode 100644 index 00000000..03c70cd9 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -0,0 +1,331 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace Meadow.Hcom; + +public class FirmwareUpdater +{ + private ILogger? _logger; + private Task? _updateTask; + private IMeadowConnection _connection; + private UpdateState _state; + + private string RequestedVersion { get; set; } + + public enum UpdateState + { + NotStarted, + EnteringDFUMode, + InDFUMode, + UpdatingOS, + DFUCompleted, + DisablingMonoForRuntime, + UpdatingRuntime, + DisablingMonoForCoprocessor, + UpdatingCoprocessor, + AllWritesComplete, + VerifySuccess, + UpdateSuccess, + Error + } + + public UpdateState PreviousState { get; private set; } + + internal FirmwareUpdater(IMeadowConnection connection) + { + _connection = connection; + // _logger = connection.Logger; + } + + public UpdateState CurrentState + { + get => _state; + private set + { + if (value == _state) return; + PreviousState = CurrentState; + _state = value; + _logger.LogDebug($"Firmware Updater: {PreviousState}->{CurrentState}"); + } + } + + private async void StateMachine() + { + var tries = 0; + + DeviceInfo? info = null; + + while (true) + { + switch (CurrentState) + { + case UpdateState.NotStarted: + try + { + // make sure we have a current device info + info = await _connection.Device.GetDeviceInfo(); + + if (info.OsVersion == RequestedVersion) + { + // no need to update, it's already there + CurrentState = UpdateState.DFUCompleted; + break; + } + + // enter DFU mode + // await _connection.Device.EnterDfuMode(); + CurrentState = UpdateState.EnteringDFUMode; + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + + break; + case UpdateState.EnteringDFUMode: + // look for DFU device + try + { + //var dfu = DfuUtils.GetDeviceInBootloaderMode(); + CurrentState = UpdateState.InDFUMode; + } + catch (Exception ex) + { + ++tries; + if (tries > 5) + { + _logger.LogError($"Failed to enter DFU mode: {ex.Message}"); + CurrentState = UpdateState.Error; + + // exit state machine + return; + } + await Task.Delay(1000); + } + break; + case UpdateState.InDFUMode: + try + { + //var success = await DfuUtils.FlashVersion(RequestedVersion, _logger); + var success = false; + if (success) + { + CurrentState = UpdateState.DFUCompleted; + } + else + { + CurrentState = UpdateState.Error; + + // exit state machine + return; + } + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + break; + case UpdateState.DFUCompleted: + // if we started in DFU mode, we'll have no connection. We'll have to just assume the first one to appear is what we're after + try + { + // wait for device to reconnect + await _connection.WaitForMeadowAttach(); + await Task.Delay(2000); // wait 2 seconds to allow full boot + + if (info == null) + { + info = await _connection.Device.GetDeviceInfo(); + } + + CurrentState = UpdateState.DisablingMonoForRuntime; + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + break; + case UpdateState.DisablingMonoForRuntime: + try + { + await _connection.Device.RuntimeDisable(); + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + CurrentState = UpdateState.UpdatingRuntime; + break; + case UpdateState.UpdatingRuntime: + if (info.RuntimeVersion == RequestedVersion) + { + // no need to update, it's already there + } + else + { + try + { + await _connection.WaitForMeadowAttach(); + await Task.Delay(2000); // wait 2 seconds to allow full boot + + if (info == null) + { + info = await _connection.Device.GetDeviceInfo(); + } + + // await _connection.Device.FlashRuntime(RequestedVersion); + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + } + CurrentState = UpdateState.DisablingMonoForCoprocessor; + break; + case UpdateState.DisablingMonoForCoprocessor: + try + { + await _connection.Device.RuntimeDisable(); + + CurrentState = UpdateState.UpdatingCoprocessor; + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + CurrentState = UpdateState.UpdatingCoprocessor; + break; + case UpdateState.UpdatingCoprocessor: + if (info.CoprocessorOsVersion == RequestedVersion) + { + // no need to update, it's already there + } + else + { + try + { + Debug.WriteLine(">> waiting for connection"); + await _connection.WaitForMeadowAttach(); + Debug.WriteLine(">> delay"); + await Task.Delay(3000); // wait to allow full boot - no idea why this takes longer + + if (info == null) + { + Debug.WriteLine(">> query device info"); + info = await _connection.Device.GetDeviceInfo(); + } + + Debug.WriteLine(">> flashing ESP"); + //await _connection.Device.FlashCoprocessor(RequestedVersion); + // await _connection.Device.FlashCoprocessor(DownloadManager.FirmwareDownloadsFilePath, RequestedVersion); + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + } + CurrentState = UpdateState.AllWritesComplete; + break; + case UpdateState.AllWritesComplete: + try + { + await _connection.Device.Reset(); + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + break; + CurrentState = UpdateState.VerifySuccess; + case UpdateState.VerifySuccess: + try + { + await _connection.WaitForMeadowAttach(); + await Task.Delay(2000); // wait 2 seconds to allow full boot + info = await _connection.Device.GetDeviceInfo(); + if (info.OsVersion != RequestedVersion) + { + // this is a failure + _logger?.LogWarning($"OS version {info.OsVersion} does not match requested version {RequestedVersion}"); + } + if (info.RuntimeVersion != RequestedVersion) + { + // this is a failure + _logger?.LogWarning($"Runtime version {info.RuntimeVersion} does not match requested version {RequestedVersion}"); + } + if (info.CoprocessorOsVersion != RequestedVersion) + { + // not necessarily an error + _logger?.LogWarning($"Coprocessor version {info.CoprocessorOsVersion} does not match requested version {RequestedVersion}"); + } + } + catch (Exception ex) + { + _logger?.LogError(ex.Message); + CurrentState = UpdateState.Error; + return; + } + CurrentState = UpdateState.UpdateSuccess; + break; + case UpdateState.UpdateSuccess: + _logger?.LogInformation("Update complete"); + return; + default: + break; + } + + await Task.Delay(1000); + } + } + + public Task Update(IMeadowConnection? connection, string? version = null) + { + string updateVersion; + if (version == null) + { + // use "latest" + updateVersion = FirmwareManager.GetLocalLatestFirmwareVersion(); + } + else + { + // verify the version requested is valid + var build = FirmwareManager.GetAllLocalFirmwareBuilds().FirstOrDefault(b => b.Version == version); + if (build == null) + { + throw new Exception($"Unknown build: '{version}'"); + } + updateVersion = build.Version; + } + + RequestedVersion = updateVersion; + + if (connection == null) + { + // assume DFU mode startup + CurrentState = UpdateState.EnteringDFUMode; + } + else + { + _connection = connection; + CurrentState = UpdateState.NotStarted; + } + + return Task.Run(StateMachine); + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs b/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs new file mode 100644 index 00000000..ec08c6f1 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; +using System.IO.Compression; + +namespace Meadow.Hcom; + +public class PackageManager +{ + public PackageManager(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + private readonly ILogger _logger; + + public string CreatePackage(string applicationPath, string osVersion) + { + var zipFile = Path.Combine(Environment.CurrentDirectory, $"{DateTime.UtcNow.ToString("yyyyMMdd")}{DateTime.UtcNow.Millisecond.ToString()}.mpak"); + + if (!Directory.Exists(applicationPath)) + { + throw new ArgumentException($"Invalid applicationPath: {applicationPath}"); + } + + var osFilePath = Path.Combine(DownloadManager.FirmwareDownloadsFilePathRoot, osVersion); + if (!Directory.Exists(osFilePath)) + { + throw new ArgumentException($"osVersion {osVersion} not found. Please download."); + } + + var osFiles = Directory.GetFiles(osFilePath); + var files = Directory.GetFiles(applicationPath); + + using (var archive = ZipFile.Open(zipFile, ZipArchiveMode.Create)) + { + foreach (var fPath in files) + { + archive.CreateEntryFromFile(fPath, Path.Combine("app", Path.GetFileName(fPath))); + } + + foreach (var fPath in osFiles) + { + archive.CreateEntryFromFile(fPath, Path.Combine("os", Path.GetFileName(fPath))); + } + } + + return zipFile; + } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs new file mode 100644 index 00000000..e5c460a9 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Meadow.Hcom; + +public class PackageVersions +{ + [JsonPropertyName("versions")] + public string[] Versions { get; set; } +} diff --git a/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs b/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs new file mode 100644 index 00000000..823f1e01 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Meadow.Hcom; + +public class ReleaseMetadata +{ + [JsonPropertyName("version")] + public string Version { get; set; } + [JsonPropertyName("minCLIVersion")] + public string MinCLIVersion { get; set; } + [JsonPropertyName("downloadUrl")] + public string DownloadURL { get; set; } + [JsonPropertyName("networkDownloadUrl")] + public string NetworkDownloadURL { get; set; } + +} diff --git a/Source/v2/Meadow.Hcom/Http Responses/DeviceInfoHttpResponse.cs b/Source/v2/Meadow.Hcom/Http Responses/DeviceInfoHttpResponse.cs new file mode 100644 index 00000000..ccdfe3bd --- /dev/null +++ b/Source/v2/Meadow.Hcom/Http Responses/DeviceInfoHttpResponse.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; + +namespace Meadow.Hcom; + +internal class DeviceInfoHttpResponse +{ + /* + { + "service": "Wilderness Labs Meadow.Daemon", + "up_time": 1691423994, + "version": "1.0", + "status": "Running", + "device_info": { + "serial_number": "d2096851d77a47ad74ff22a862aca5f2", + "device_name": "DESKTOP-PGERLRJ", + "platform": "MeadowForLinux", + "os_version": "#1 SMP Fri Jan 27 02:56:13 UTC 2023", + "os_release": "5.15.90.1-microsoft-standard-WSL2", + "os_name": "Linux", + "machine": "x86_64" + } + } + */ + [JsonPropertyName("service")] + public string ServiceName { get; set; } = default!; + [JsonPropertyName("version")] + public string ServiceVersion { get; set; } = default!; + [JsonPropertyName("status")] + public string ServiceStatus { get; set; } = default!; + [JsonPropertyName("device_info")] + public DeviceFields DeviceInfo { get; set; } = default!; + + internal class DeviceFields + { + [JsonPropertyName("serial_number")] + public string SerialNumber { get; set; } = default!; + [JsonPropertyName("device_name")] + public string DeviceName { get; set; } = default!; + [JsonPropertyName("platform")] + public string Platform { get; set; } = default!; + [JsonPropertyName("os_version")] + public string OsVersion { get; set; } = default!; + [JsonPropertyName("os_release")] + public string OsRelease { get; set; } = default!; + [JsonPropertyName("os_name")] + public string OsName { get; set; } = default!; + [JsonPropertyName("machine")] + public string Machine { get; set; } = default!; + } + + public Dictionary ToDictionary() + { + var d = new Dictionary + { + { "SerialNumber", DeviceInfo.SerialNumber }, + { "DeviceName", DeviceInfo.DeviceName}, + { "OsVersion", DeviceInfo.OsVersion}, + { "OsName", DeviceInfo.OsName}, + }; + return d; + } +} diff --git a/Source/v2/Meadow.Hcom/IConnectionListener.cs b/Source/v2/Meadow.Hcom/IConnectionListener.cs new file mode 100644 index 00000000..09e5e94c --- /dev/null +++ b/Source/v2/Meadow.Hcom/IConnectionListener.cs @@ -0,0 +1,14 @@ +namespace Meadow.Hcom +{ + public interface IConnectionListener + { + void OnInformationMessageReceived(string message); + void OnStdOutReceived(string message); + void OnStdErrReceived(string message); + void OnDeviceInformationMessageReceived(Dictionary deviceInfo); + void OnTextListReceived(string[] list); + void OnErrorTextReceived(string message); + void OnFileError(); + void OnTextMessageConcluded(int requestType); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs new file mode 100644 index 00000000..959cce8c --- /dev/null +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -0,0 +1,39 @@ +namespace Meadow.Hcom +{ + public interface IMeadowConnection + { + event EventHandler<(string message, string? source)> DeviceMessageReceived; + event EventHandler ConnectionError; + event EventHandler ConnectionMessage; + event EventHandler<(string fileName, long completed, long total)> FileWriteProgress; + + string Name { get; } + IMeadowDevice? Device { get; } + Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); + ConnectionState State { get; } + + Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); + Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); + Task GetDeviceInfo(CancellationToken? cancellationToken = null); + Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task ResetDevice(CancellationToken? cancellationToken = null); + Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); + Task RuntimeDisable(CancellationToken? cancellationToken = null); + Task RuntimeEnable(CancellationToken? cancellationToken = null); + Task GetRtcTime(CancellationToken? cancellationToken = null); + Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + + Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null); + Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null); + + Task TraceEnable(CancellationToken? cancellationToken = null); + Task TraceDisable(CancellationToken? cancellationToken = null); + Task SetTraceLevel(int level, CancellationToken? cancellationToken = null); + + Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null); + + Task UartTraceEnable(CancellationToken? cancellationToken = null); + Task UartTraceDisable(CancellationToken? cancellationToken = null); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs new file mode 100644 index 00000000..fce2fec5 --- /dev/null +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -0,0 +1,24 @@ +namespace Meadow.Hcom +{ + public interface IMeadowDevice + { + Task Reset(CancellationToken? cancellationToken = null); + Task RuntimeDisable(CancellationToken? cancellationToken = null); + Task RuntimeEnable(CancellationToken? cancellationToken = null); + Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); + Task GetDeviceInfo(CancellationToken? cancellationToken = null); + Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); + Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); + Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null); + Task GetRtcTime(CancellationToken? cancellationToken = null); + Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + Task WriteCoprocessorFiles(string[] localFileNames, CancellationToken? cancellationToken = null); + Task TraceEnable(CancellationToken? cancellationToken = null); + Task TraceDisable(CancellationToken? cancellationToken = null); + Task SetTraceLevel(int level, CancellationToken? cancellationToken = null); + Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null); + Task UartTraceEnable(CancellationToken? cancellationToken = null); + Task UartTraceDisable(CancellationToken? cancellationToken = null); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Meadow.Hcom.csproj b/Source/v2/Meadow.Hcom/Meadow.Hcom.csproj new file mode 100644 index 00000000..404035bc --- /dev/null +++ b/Source/v2/Meadow.Hcom/Meadow.Hcom.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.1 + enable + enable + 10 + + + + + + + + + + + + + + + + + diff --git a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln new file mode 100644 index 00000000..93b78ab2 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Hcom", "Meadow.Hcom.csproj", "{0CB726B4-B03B-45DB-BEF2-BB811BDAE8BA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.HCom.Integration.Tests", "..\Meadow.HCom.Integration.Tests\Meadow.HCom.Integration.Tests.csproj", "{F8830C1D-8343-4700-A849-B22537411E98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cli", "..\Meadow.Cli\Meadow.Cli.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0CB726B4-B03B-45DB-BEF2-BB811BDAE8BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CB726B4-B03B-45DB-BEF2-BB811BDAE8BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CB726B4-B03B-45DB-BEF2-BB811BDAE8BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CB726B4-B03B-45DB-BEF2-BB811BDAE8BA}.Release|Any CPU.Build.0 = Release|Any CPU + {F8830C1D-8343-4700-A849-B22537411E98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8830C1D-8343-4700-A849-B22537411E98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8830C1D-8343-4700-A849-B22537411E98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8830C1D-8343-4700-A849-B22537411E98}.Release|Any CPU.Build.0 = Release|Any CPU + {5E2ACCA3-232B-4B79-BCB9-A7184E42816B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E2ACCA3-232B-4B79-BCB9-A7184E42816B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E2ACCA3-232B-4B79-BCB9-A7184E42816B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E2ACCA3-232B-4B79-BCB9-A7184E42816B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67AF1359-CDC4-47BC-8A5B-0BBC28481C6C} + EndGlobalSection +EndGlobal diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs new file mode 100644 index 00000000..9e9bacaf --- /dev/null +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -0,0 +1,133 @@ +namespace Meadow.Hcom +{ + public partial class MeadowDevice : IMeadowDevice + { + private IMeadowConnection _connection; + + internal MeadowDevice(IMeadowConnection connection) + { + _connection = connection; + } + + public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + return await _connection.IsRuntimeEnabled(cancellationToken); + } + + public async Task Reset(CancellationToken? cancellationToken = null) + { + await _connection.ResetDevice(cancellationToken); + } + + public async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + await _connection.RuntimeDisable(cancellationToken); + } + + public async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + await _connection.RuntimeEnable(cancellationToken); + } + + public async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + return await _connection.GetDeviceInfo(cancellationToken); + } + + public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + return await _connection.GetFileList(includeCrcs, cancellationToken); + } + + public async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + return await _connection.ReadFile(meadowFileName, localFileName, cancellationToken); + } + + public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + return await _connection.WriteFile(localFileName, meadowFileName, cancellationToken); + } + + public async Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + return await _connection.WriteRuntime(localFileName, cancellationToken); + } + + public async Task WriteCoprocessorFiles(string[] localFileNames, CancellationToken? cancellationToken = null) + { + foreach (var file in localFileNames) + { + var result = await _connection.WriteCoprocessorFile( + file, + GetFileTargetAddress(file), + cancellationToken); + + if (!result) + { + return false; + } + } + + return true; + } + + public async Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + await _connection.UartTraceEnable(cancellationToken); + } + + public async Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + await _connection.UartTraceDisable(cancellationToken); + } + + private int GetFileTargetAddress(string fileName) + { + // TODO: determine device type so we can map the file names to target locations + // for now we only support the F7 so these are static and well-known + + var fn = Path.GetFileName(fileName).ToLower(); + switch (fn) + { + case "meadowcomms.bin": + return 0x10000; + case "bootloader.bin": + return 0x1000; + case "partition-table.bin": + return 0x8000; + default: throw new NotSupportedException($"Unsupported coprocessor file: '{fn}'"); + } + } + + public async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + return await _connection.GetRtcTime(cancellationToken); + } + + public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + await _connection.SetRtcTime(dateTime, cancellationToken); + } + + public async Task TraceEnable(CancellationToken? cancellationToken = null) + { + await _connection.TraceEnable(cancellationToken); + } + + public async Task TraceDisable(CancellationToken? cancellationToken = null) + { + await _connection.TraceDisable(cancellationToken); + } + + public async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + await _connection.SetTraceLevel(level, cancellationToken); + } + + public async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + await _connection.SetDeveloperParameter(parameter, value, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs new file mode 100644 index 00000000..46590ad9 --- /dev/null +++ b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs @@ -0,0 +1,33 @@ +public class MeadowFileInfo +{ + public string Name { get; private set; } = default!; + public long? Size { get; private set; } + public string? Crc { get; private set; } + + public static MeadowFileInfo? Parse(string info) + { + MeadowFileInfo? mfi = null; + + // parse the input to a file info + if (info.StartsWith("/")) + { + mfi = new MeadowFileInfo(); + + // "/meadow0/App.deps.json [0xa0f6d6a2] 28 KB (26575 bytes)" + var indexOfSquareBracket = info.IndexOf('['); + if (indexOfSquareBracket <= 0) + { + mfi.Name = info; + } + else + { + mfi.Name = info.Substring(0, indexOfSquareBracket - 1).Trim(); + mfi.Crc = info.Substring(indexOfSquareBracket + 1, 10); + var indexOfParen = info.IndexOf("("); + var end = info.IndexOf(' ', indexOfParen); + mfi.Size = int.Parse(info.Substring(indexOfParen + 1, end - indexOfParen)); + } + } + return mfi; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Protocol.cs b/Source/v2/Meadow.Hcom/Protocol.cs new file mode 100644 index 00000000..88e6ae07 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Protocol.cs @@ -0,0 +1,38 @@ +namespace Meadow.Hcom +{ + internal class Protocol + { + // There is no length field. Since the packet boundaries are delimited and the + // header is fixed length. Therefore, any additional data length is easily + // determined. + public const UInt16 HCOM_PROTOCOL_HCOM_VERSION_NUMBER = 0x0007; + + // COBS needs a specific delimiter. Zero seems to be traditional. + public const UInt16 HCOM_PROTOCOL_COBS_ENCODING_DELIMITER_VALUE = 0x00; + + // What sequence number is used to identify a non-data message? + public const UInt16 HCOM_PROTOCOL_NON_DATA_SEQUENCE_NUMBER = 0; + + // Note: while the MD5 hash is 128-bits (16-bytes), it is 32 character + // hex string from ESP32 + public const UInt16 HCOM_PROTOCOL_COMMAND_MD5_HASH_LENGTH = 32; + + // Define the absolute maximum packet sizes for sent and receive. + // Note: The length on the wire will be longer because it's encoded. + public const int HCOM_PROTOCOL_PACKET_MAX_SIZE = 8192; + public const int HCOM_PROTOCOL_ENCODED_MAX_SIZE = HCOM_PROTOCOL_PACKET_MAX_SIZE; + + // The maximum payload is max packet - header (12 bytes) + public const int HCOM_PROTOCOL_DATA_MAX_SIZE = HCOM_PROTOCOL_PACKET_MAX_SIZE - 12; + + //static public int HcomProtoHdrMessageSize() + //{ + // return Marshal.SizeOf(typeof(HcomProtoHdrMessage)); + //} + + //static public int HcomProtoFSInfoMsgSize() + //{ + // return Marshal.SizeOf(typeof(HcomProtoFSInfoMsg)); + //} + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/ProtocolType.cs b/Source/v2/Meadow.Hcom/ProtocolType.cs new file mode 100644 index 00000000..7addac69 --- /dev/null +++ b/Source/v2/Meadow.Hcom/ProtocolType.cs @@ -0,0 +1,31 @@ +namespace Meadow.Hcom +{ + //-------------------------------------------------------------------------- + // HCOM Protocol message type definitions + //-------------------------------------------------------------------------- + public enum ProtocolType : UInt16 + { + // When the time comes the following Major types should reflect the + // name of the above structure is used to send it. + HCOM_PROTOCOL_HEADER_UNDEFINED_TYPE = 0x0000, + + // The header of all mesasges include a 4-byte field called user data. The + // User data field's meaning is determined by the message type + + // Header only request types, + HCOM_PROTOCOL_HEADER_ONLY_TYPE = 0x0100, + + // File related types includes 4-byte user data (used for the destination + // partition id), 4-byte file size, 4-byte checksum, 4-byte destination address + // and variable length destination file name. Note: The 4-byte destination address + // is currently only used for the STM32F7 to ESP32 downloads. + HCOM_PROTOCOL_HEADER_FILE_START_TYPE = 0x0200, + + // Simple text is a header followed by text without a terminating NULL. + HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE = 0x0300, + + // Simple binary is a 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_SIMPLE_BINARY_TYPE = 0x0400, + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/DeveloperRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/DeveloperRequest.cs new file mode 100644 index 00000000..a929cb67 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/DeveloperRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class DeveloperRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_DEVELOPER; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs new file mode 100644 index 00000000..50aedf47 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs @@ -0,0 +1,13 @@ +namespace Meadow.Hcom; + +internal class EndFileWriteRequest : Request +{ + private RequestType _requestType = RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER; + + public override RequestType RequestType => _requestType; + + public void SetRequestType(RequestType requestType) + { + _requestType = requestType; + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/FileReadDataRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/FileReadDataRequest.cs new file mode 100644 index 00000000..8ffd95b0 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/FileReadDataRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class FileReadDataRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/GetDeviceInfoRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/GetDeviceInfoRequest.cs new file mode 100644 index 00000000..7c01c5d7 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/GetDeviceInfoRequest.cs @@ -0,0 +1,16 @@ +namespace Meadow.Hcom +{ + internal class GetDeviceInfoRequest : Request + { + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION; + + public GetDeviceInfoRequest() + { + } + // Serialized example: + // message + // 01-00-07-00-12-01-00-00-00-00-00-00" + // encoded + // 00-02-2A-02-06-03-12-01-01-01-01-01-01-01-00 + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/GetFileListRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/GetFileListRequest.cs new file mode 100644 index 00000000..117f0468 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/GetFileListRequest.cs @@ -0,0 +1,15 @@ +namespace Meadow.Hcom +{ + internal class GetFileListRequest : Request + { + public override RequestType RequestType => IncludeCrcs + ? RequestType.HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC + : RequestType.HCOM_MDOW_REQUEST_LIST_PARTITION_FILES; + + public bool IncludeCrcs { get; set; } + + public GetFileListRequest() + { + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/GetRtcTimeRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/GetRtcTimeRequest.cs new file mode 100644 index 00000000..18ea7e3c --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/GetRtcTimeRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class GetRtcTimeRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/GetRuntimeStateRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/GetRuntimeStateRequest.cs new file mode 100644 index 00000000..4184a1b6 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/GetRuntimeStateRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class GetRuntimeStateRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_MONO_RUN_STATE; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs new file mode 100644 index 00000000..44f1c9d1 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs @@ -0,0 +1,22 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class InitFileReadRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_UPLOAD_FILE_INIT; + + public string? LocalFileName { get; set; } = default!; + public string MeadowFileName + { + get + { + if (Payload == null) return string.Empty; + return Encoding.ASCII.GetString(Payload); + } + set + { + Payload = Encoding.ASCII.GetBytes(value); + } + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs new file mode 100644 index 00000000..2035f349 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs @@ -0,0 +1,109 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal static class NuttxCrc +{ + //============================================================== + // The following crc32 table and calculation code was copied from + // '...\nuttx\libs\libc\misc\lib_crc32.c'. Minor changes have been made. + // The file’s title block contains the following text: + //* The logic in this file was developed by Gary S.Brown: + //* COPYRIGHT (C) 1986 Gary S.Brown.You may use this program, or code or tables + //* extracted from it, as desired without restriction. + private static readonly uint[] crc32_tab = + { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + + //--------------------------------------------------------------------- + // Calculate the checksum of a buffer starting at an offset + public static uint Crc32part(byte[] buffer, uint length, uint offset, uint crc32val) + { + for (uint i = offset; i < length + offset; i++) + { + crc32val = crc32_tab[(crc32val & 0xff) ^ buffer[i]] ^ (crc32val >> 8); + } + return crc32val; + } + + //--------------------------------------------------------------------- + // Calculate the checksum of a buffer starting at the beginning + public static uint Crc32part(byte[] buffer, uint length, uint crc32val) + { + return Crc32part(buffer, length, 0, crc32val); + } +} + +internal class InitFileWriteRequest : Request +{ + private RequestType _requestType; + + public override RequestType RequestType => _requestType; + + public uint FileSize { get; set; } + public uint CheckSum { get; set; } + public uint FlashAddress { get; set; } + public byte[] Esp32MD5 { get; set; } = new byte[32]; + + public string LocalFileName { get; private set; } = default!; + public string MeadowFileName { get; private set; } + + public void SetParameters( + string localFile, + string meadowFileName, + int espAddress = 0, + RequestType requestType = RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER) + { + // file write has additional header payload that's sent with the request, build it here + _requestType = requestType; + + var source = new FileInfo(localFile); + + if (!source.Exists) throw new FileNotFoundException(); + + LocalFileName = localFile; + MeadowFileName = meadowFileName; + + var nameBytes = Encoding.ASCII.GetBytes(meadowFileName); + var espHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // Must be 32 bytes + + Payload = new byte[4 + 4 + 4 + 32 + nameBytes.Length]; + Array.Copy(BitConverter.GetBytes((uint)source.Length), 0, Payload, 0, 4); // file size + Array.Copy(BitConverter.GetBytes((uint)source.Length), 0, Payload, 4, 4); // file crc + Array.Copy(BitConverter.GetBytes(espAddress), 0, Payload, 8, 4); // ESP flash address offset + Array.Copy(espHash, 0, Payload, 12, espHash.Length); // TODO: ESP hash (dev note: this appears to never be used or needed?) + Array.Copy(nameBytes, 0, Payload, 44, nameBytes.Length); // file name + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/Request.cs b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs new file mode 100644 index 00000000..229ea4fa --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs @@ -0,0 +1,84 @@ +namespace Meadow.Hcom; + +public interface IRequest +{ +} + +public abstract class Request : IRequest +{ + public abstract RequestType RequestType { get; } + + public ushort SequenceNumber { get; set; } + public ushort ProtocolVersion { get; set; } + public ushort ExtraData { get; set; } + public uint UserData { get; set; } + + public Request() + { + } + + public byte[]? Payload { get; protected set; } + + public virtual byte[] Serialize() + { + var messageBytes = new byte[2 + 2 + 2 + 2 + 4 + (Payload?.Length ?? 0)]; + + int offset = 0; + + // Two byte seq numb + Array.Copy( + BitConverter.GetBytes(SequenceNumber), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // Protocol version + Array.Copy( + BitConverter.GetBytes(ProtocolVersion), + 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(ExtraData), + 0, + messageBytes, + offset, + sizeof(ushort)); + + offset += sizeof(ushort); + + // User Data + Array.Copy(BitConverter.GetBytes(UserData), 0, messageBytes, offset, sizeof(uint)); + offset += sizeof(uint); + + if (Payload != null) + { + Array.Copy( + Payload, + 0, + messageBytes, + offset, + Payload.Length); + } + + return messageBytes; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/RequestBuilder.cs b/Source/v2/Meadow.Hcom/Serial Requests/RequestBuilder.cs new file mode 100644 index 00000000..0fac22e8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/RequestBuilder.cs @@ -0,0 +1,19 @@ +namespace Meadow.Hcom +{ + public static class RequestBuilder + { + private static uint _sequenceNumber; + + public static T Build(uint userData = 0, ushort extraData = 0, ushort protocol = Protocol.HCOM_PROTOCOL_HCOM_VERSION_NUMBER) + where T : Request, new() + { + return new T + { + SequenceNumber = 0, + ProtocolVersion = protocol, + UserData = userData, + ExtraData = extraData + }; + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs b/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs new file mode 100644 index 00000000..ad98c3cc --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs @@ -0,0 +1,85 @@ +namespace Meadow.Hcom; + +// Messages sent from host to Meadow +public enum RequestType : ushort +{ + HCOM_MDOW_REQUEST_UNDEFINED_REQUEST = 0x00 | ProtocolType.HCOM_PROTOCOL_HEADER_UNDEFINED_TYPE, + + // No longer supported + // HCOM_MDOW_REQUEST_CREATE_ENTIRE_FLASH_FS = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL = 0x02 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_FORMAT_FLASH_FILE_SYS = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_END_FILE_TRANSFER = 0x04 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_RESTART_PRIMARY_MCU = 0x05 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_VERIFY_ERASED_FLASH = 0x06 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + // No longer supported + // HCOM_MDOW_REQUEST_PARTITION_FLASH_FS = 0x07 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + // No longer supported + // HCOM_MDOW_REQUEST_MOUNT_FLASH_FS = 0x08 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + // No longer supported + // HCOM_MDOW_REQUEST_INITIALIZE_FLASH_FS = 0x09 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_BULK_FLASH_ERASE = 0x0a | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_ENTER_DFU_MODE = 0x0b | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_ENABLE_DISABLE_NSH = 0x0c | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_LIST_PARTITION_FILES = 0x0d | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_LIST_PART_FILES_AND_CRC = 0x0e | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_DISABLE = 0x0f | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_ENABLE = 0x10 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_RUN_STATE = 0x11 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_GET_DEVICE_INFORMATION = 0x12 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_PART_RENEW_FILE_SYS = 0x13 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST = 0x14 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST = 0x15 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER = 0x16 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_READ_ESP_MAC_ADDRESS = 0x17 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_RESTART_ESP32 = 0x18 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_FLASH = 0x19 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART = 0x1a | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_NO_TRACE_TO_UART = 0x1b | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + // >>> Breaking protocol change. + // ToDo: This message is miscategorized should be ProtocolType.HCOM_PROTOCOL_HEADER_FILE_START_TYPE + // like HCOM_MDOW_REQUEST_START_FILE_TRANSFER. + HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME = 0x1c | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END = 0x1d | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_MONO_START_DBG_SESSION = 0x1e | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_GET_DEVICE_NAME = 0x1f | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + // >>> Breaking protocol change. + // ToDo: This message is miscategorized should be ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE + // since it is a header followed by text (the file name) + HCOM_MDOW_REQUEST_GET_INITIAL_FILE_BYTES = 0x20 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_UPLOAD_START_DATA_SEND = 0x21 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_UPLOAD_ABORT_DATA_SEND = 0x22 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + // The file types have the optional data field defined for sending file information + HCOM_MDOW_REQUEST_START_FILE_TRANSFER = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_FILE_START_TYPE, + HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME = 0x02 | ProtocolType.HCOM_PROTOCOL_HEADER_FILE_START_TYPE, + HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_FILE_START_TYPE, + + // These message are a header followed by text, one contains the texts length too + HCOM_MDOW_REQUEST_UPLOAD_FILE_INIT = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_EXEC_DIAG_APP_CMD = 0x02 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + // ToDo HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD doesn't send text, it's a header only message type + HCOM_MDOW_REQUEST_RTC_READ_TIME_CMD = 0x04 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_RTC_WAKEUP_TIME_CMD = 0x05 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + + + // This is a simple type with binary data + HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, + + HCOM_MDOW_REQUEST_DEVELOPER = 0xf8 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_DEVELOPER_1 = 0xf0 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_DEVELOPER_2 = 0xf1 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_DEVELOPER_3 = 0xf2 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_DEVELOPER_4 = 0xf3 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + // Testing QSPI flash + HCOM_MDOW_REQUEST_QSPI_FLASH_INIT = 0xf4 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_QSPI_FLASH_WRITE = 0xf5 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + HCOM_MDOW_REQUEST_QSPI_FLASH_READ = 0xf6 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + HCOM_HOST_REQUEST_UPLOADING_FILE_DATA = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, + +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/ResetDeviceRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/ResetDeviceRequest.cs new file mode 100644 index 00000000..b9529e52 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/ResetDeviceRequest.cs @@ -0,0 +1,16 @@ +namespace Meadow.Hcom +{ + internal class ResetDeviceRequest : Request + { + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_RESTART_PRIMARY_MCU; + + public ResetDeviceRequest() + { + } + // Serialized example: + // message + // 01-00-07-00-12-01-00-00-00-00-00-00" + // encoded + // 00-02-2A-02-06-03-12-01-01-01-01-01-01-01-00 + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/ResetRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/ResetRequest.cs new file mode 100644 index 00000000..3380b2d2 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/ResetRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class ResetRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_RESTART_PRIMARY_MCU; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/RuntimeDisableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/RuntimeDisableRequest.cs new file mode 100644 index 00000000..03a9e347 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/RuntimeDisableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class RuntimeDisableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_MONO_DISABLE; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/RuntimeEnableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/RuntimeEnableRequest.cs new file mode 100644 index 00000000..52796b77 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/RuntimeEnableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class RuntimeEnableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_MONO_ENABLE; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/SetRtcTimeRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/SetRtcTimeRequest.cs new file mode 100644 index 00000000..400a6251 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/SetRtcTimeRequest.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class SetRtcTimeRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD; + + public DateTimeOffset? Time + { + get + { + if (Payload.Length == 0) return null; + + return DateTimeOffset.Parse(Encoding.ASCII.GetString(Payload)); + } + set + { + base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); + } + } + +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/StartFileDataRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/StartFileDataRequest.cs new file mode 100644 index 00000000..eaab3ed8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/StartFileDataRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class StartFileDataRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_UPLOAD_START_DATA_SEND; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/TraceDisableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/TraceDisableRequest.cs new file mode 100644 index 00000000..d984b8cb --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/TraceDisableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class TraceDisableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_HOST; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/TraceEnableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/TraceEnableRequest.cs new file mode 100644 index 00000000..c03ada9c --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/TraceEnableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class TraceEnableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_HOST; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/TraceLevelRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/TraceLevelRequest.cs new file mode 100644 index 00000000..e5e614b9 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/TraceLevelRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class TraceLevelRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_CHANGE_TRACE_LEVEL; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/UartTraceDisableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/UartTraceDisableRequest.cs new file mode 100644 index 00000000..b45b11c6 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/UartTraceDisableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class UartTraceDisableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_NO_TRACE_TO_UART; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/UartTraceEnableRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/UartTraceEnableRequest.cs new file mode 100644 index 00000000..57d1ebb4 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/UartTraceEnableRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class UartTraceEnableRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_SEND_TRACE_TO_UART; +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/DeviceInfoSerialResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/DeviceInfoSerialResponse.cs new file mode 100644 index 00000000..cce2d9a0 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/DeviceInfoSerialResponse.cs @@ -0,0 +1,25 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class DeviceInfoSerialResponse : SerialResponse +{ + public Dictionary Fields { get; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + public string RawText => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal DeviceInfoSerialResponse(byte[] data, int length) + : base(data, length) + { + var rawFields = RawText.Split('~', StringSplitOptions.RemoveEmptyEntries); + foreach (var f in rawFields) + { + var pair = f.Split('|', StringSplitOptions.RemoveEmptyEntries); + + if ((pair.Length == 2) && !Fields.ContainsKey(pair[0])) + { + Fields.Add(pair[0], pair[1]); + } + } + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitFailedResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitFailedResponse.cs new file mode 100644 index 00000000..498011cd --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitFailedResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class FileReadInitFailedResponse : SerialResponse +{ + internal FileReadInitFailedResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs new file mode 100644 index 00000000..e3855f4e --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class FileReadInitOkResponse : SerialResponse +{ + internal FileReadInitOkResponse(byte[] data, int length) + : base(data, length) + { + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitFailedSerialResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitFailedSerialResponse.cs new file mode 100644 index 00000000..653d532b --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitFailedSerialResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class FileWriteInitFailedSerialResponse : SerialResponse +{ + internal FileWriteInitFailedSerialResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitOkSerialResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitOkSerialResponse.cs new file mode 100644 index 00000000..641ea004 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitOkSerialResponse.cs @@ -0,0 +1,10 @@ +namespace Meadow.Hcom; + +internal class FileWriteInitOkSerialResponse : SerialResponse +{ + internal FileWriteInitOkSerialResponse(byte[] data, int length) + : base(data, length) + { + } +} + diff --git a/Source/v2/Meadow.Hcom/Serial Responses/RequestErrorTextResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/RequestErrorTextResponse.cs new file mode 100644 index 00000000..0c723a5e --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/RequestErrorTextResponse.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class ReconnectRequiredResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal ReconnectRequiredResponse(byte[] data, int length) + : base(data, length) + { + } +} + +internal class RequestErrorTextResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal RequestErrorTextResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/ResponseType.cs b/Source/v2/Meadow.Hcom/Serial Responses/ResponseType.cs new file mode 100644 index 00000000..d0106008 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/ResponseType.cs @@ -0,0 +1,36 @@ +namespace Meadow.Hcom; + +public enum ResponseType : ushort +{ + HCOM_HOST_REQUEST_UNDEFINED_REQUEST = 0x00 | ProtocolType.HCOM_PROTOCOL_HEADER_UNDEFINED_TYPE, + + // Only header + HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_ONLY_TYPE, + + // Simple with some text message + HCOM_HOST_REQUEST_TEXT_REJECTED = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_ACCEPTED = 0x02 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_CONCLUDED = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_ERROR = 0x04 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_INFORMATION = 0x05 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_LIST_HEADER = 0x06 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_LIST_MEMBER = 0x07 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_CRC_MEMBER = 0x08 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_MONO_STDOUT = 0x09 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_DEVICE_INFO = 0x0A | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_TRACE_MSG = 0x0B | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_RECONNECT = 0x0C | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_TEXT_MONO_STDERR = 0x0D | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + + HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY = 0x0E | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL = 0x0F | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + + HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY = 0x10 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL = 0x11 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_HOST_REQUEST_DNLD_FAIL_RESEND = 0x12 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + + // Simple with mono debug data + HCOM_HOST_REQUEST_DEBUGGING_MONO_DATA = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, + HCOM_HOST_REQUEST_SEND_INITIAL_FILE_BYTES = 0x02 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, + HCOM_HOST_REQUEST_UPLOADING_FILE_DATA = 0x03 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs new file mode 100644 index 00000000..81925df6 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs @@ -0,0 +1,71 @@ +namespace Meadow.Hcom; + +internal class SerialResponse +{ + private const int HCOM_PROTOCOL_REQUEST_HEADER_SEQ_OFFSET = 0; + private const int HCOM_PROTOCOL_REQUEST_HEADER_VERSION_OFFSET = 2; + private const int HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET = 4; + private const int HCOM_PROTOCOL_REQUEST_HEADER_EXTRA_DATA_OFFSET = 6; + private const int HCOM_PROTOCOL_REQUEST_HEADER_USER_DATA_OFFSET = 8; + protected const int RESPONSE_PAYLOAD_OFFSET = 12; + + protected byte[] _data; + + public ushort SequenceNumber => BitConverter.ToUInt16(_data, HCOM_PROTOCOL_REQUEST_HEADER_SEQ_OFFSET); + public ushort ProtocolVersion => BitConverter.ToUInt16(_data, HCOM_PROTOCOL_REQUEST_HEADER_VERSION_OFFSET); + public ResponseType RequestType => (ResponseType)BitConverter.ToUInt16(_data, HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET); + public ushort ExtraData => BitConverter.ToUInt16(_data, HCOM_PROTOCOL_REQUEST_HEADER_EXTRA_DATA_OFFSET); + public uint UserData => BitConverter.ToUInt32(_data, HCOM_PROTOCOL_REQUEST_HEADER_USER_DATA_OFFSET); + protected int PayloadLength => _data.Length - RESPONSE_PAYLOAD_OFFSET; + + public static SerialResponse Parse(byte[] data, int length) + { + var type = (ResponseType)BitConverter.ToUInt16(data, HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET); + + switch (type) + { + case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR: + return new TextStdErrResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT: + return new TextStdOutResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_INFORMATION: + return new TextInformationResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: + return new TextRequestResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: + return new DeviceInfoSerialResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: + return new TextConcludedResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER: + return new TextListHeaderResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER: + return new TextListMemberResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER: + return new TextCrcMemberResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL: + return new FileReadInitFailedResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY: + return new FileReadInitOkResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA: + return new UploadDataPacketResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED: + return new UploadCompletedResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_ERROR: + return new RequestErrorTextResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_TEXT_RECONNECT: + return new ReconnectRequiredResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL: + return new FileWriteInitFailedSerialResponse(data, length); + case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY: + return new FileWriteInitOkSerialResponse(data, length); + default: + return new SerialResponse(data, length); + } + } + + protected SerialResponse(byte[] data, int length) + { + _data = new byte[length]; + Array.Copy(data, 0, _data, 0, length); + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextAcceptedResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextAcceptedResponse.cs new file mode 100644 index 00000000..54e044c5 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextAcceptedResponse.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class TextAcceptedResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextAcceptedResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextConcludedResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextConcludedResponse.cs new file mode 100644 index 00000000..8e72d4af --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextConcludedResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class TextConcludedResponse : SerialResponse +{ + internal TextConcludedResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextCrcMemberResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextCrcMemberResponse.cs new file mode 100644 index 00000000..ef3d64bb --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextCrcMemberResponse.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class TextCrcMemberResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextCrcMemberResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextInformationResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextInformationResponse.cs new file mode 100644 index 00000000..f18d80ed --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextInformationResponse.cs @@ -0,0 +1,16 @@ +using System.Text; + +namespace Meadow.Hcom; + +/// +/// An unsolicited text response sent by HCOM (i.e. typically a Console.Write) +/// +internal class TextInformationResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextInformationResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextListHeaderResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextListHeaderResponse.cs new file mode 100644 index 00000000..8bca8cdf --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextListHeaderResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class TextListHeaderResponse : SerialResponse +{ + internal TextListHeaderResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextListMemberResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextListMemberResponse.cs new file mode 100644 index 00000000..d6a89661 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextListMemberResponse.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class TextListMemberResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextListMemberResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextRequestResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextRequestResponse.cs new file mode 100644 index 00000000..aee3a25c --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextRequestResponse.cs @@ -0,0 +1,16 @@ +using System.Text; + +namespace Meadow.Hcom; + +/// +/// A text response to a solicited host request +/// +internal class TextRequestResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextRequestResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextStdErrResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextStdErrResponse.cs new file mode 100644 index 00000000..e19173bc --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextStdErrResponse.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class TextStdErrResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextStdErrResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/TextStdOutResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/TextStdOutResponse.cs new file mode 100644 index 00000000..2d40264f --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/TextStdOutResponse.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class TextStdOutResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextStdOutResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/UploadCompletedResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/UploadCompletedResponse.cs new file mode 100644 index 00000000..e6f4378e --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/UploadCompletedResponse.cs @@ -0,0 +1,9 @@ +namespace Meadow.Hcom; + +internal class UploadCompletedResponse : SerialResponse +{ + internal UploadCompletedResponse(byte[] data, int length) + : base(data, length) + { + } +} diff --git a/Source/v2/Meadow.Hcom/Serial Responses/UploadDataPacketResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/UploadDataPacketResponse.cs new file mode 100644 index 00000000..7d6236d8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/UploadDataPacketResponse.cs @@ -0,0 +1,11 @@ +namespace Meadow.Hcom; + +internal class UploadDataPacketResponse : SerialResponse +{ + internal UploadDataPacketResponse(byte[] data, int length) + : base(data, length) + { + } + + public byte[] FileData => _data[RESPONSE_PAYLOAD_OFFSET..]; +} diff --git a/Source/v2/Meadow.SoftwareManager.Unit.Tests/F7CollectionTests.cs b/Source/v2/Meadow.SoftwareManager.Unit.Tests/F7CollectionTests.cs new file mode 100644 index 00000000..fbcb7d04 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager.Unit.Tests/F7CollectionTests.cs @@ -0,0 +1,14 @@ +using Meadow.Software; + +namespace Meadow.SoftwareManager.Unit.Tests; + +public class F7CollectionTests +{ + [Fact] + public void TestCollectionPopulation() + { + var collection = new F7FirmwarePackageCollection(F7FirmwarePackageCollection.DefaultF7FirmwareStoreRoot); + + collection.Refresh(); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj new file mode 100644 index 00000000..8fdf6f7b --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Source/v2/Meadow.SoftwareManager.Unit.Tests/Usings.cs b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager/AssemblyInfo.cs b/Source/v2/Meadow.SoftwareManager/AssemblyInfo.cs new file mode 100644 index 00000000..6e772109 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Meadow.SoftwareManager.Unit.Tests")] diff --git a/Source/v2/Meadow.SoftwareManager/CrcTools.cs b/Source/v2/Meadow.SoftwareManager/CrcTools.cs new file mode 100644 index 00000000..4de3e8ab --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/CrcTools.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.IO.Hashing; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public static class CrcTools +{ + //============================================================== + // The following crc32 table and calculation code was copied from + // '...\nuttx\libs\libc\misc\lib_crc32.c'. Minor changes have been made. + // The file’s title block contains the following text: + //* The logic in this file was developed by Gary S.Brown: + //* COPYRIGHT (C) 1986 Gary S.Brown.You may use this program, or code or tables + //* extracted from it, as desired without restriction. + private static readonly uint[] crc32_tab = + { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + + //--------------------------------------------------------------------- + // Calculate the checksum of a buffer starting at an offset + public static uint Crc32part(byte[] buffer, int length, int offset, uint crc32val) + { + for (int i = offset; i < length + offset; i++) + { + crc32val = crc32_tab[(crc32val & 0xff) ^ buffer[i]] ^ (crc32val >> 8); + } + return crc32val; + } + + //--------------------------------------------------------------------- + // Calculate the checksum of a buffer starting at the beginning + public static uint Crc32part(byte[] buffer, int length, uint crc32val) + { + return Crc32part(buffer, length, 0, crc32val); + } + + public static async Task CalculateCrc32FileHash(string filePath) + { + var crc32 = new Crc32(); + + using (var fs = File.OpenRead(filePath)) + { + await crc32.AppendAsync(fs); + } + + var checkSum = crc32.GetCurrentHash(); + Array.Reverse(checkSum); // make big endian + return BitConverter.ToString(checkSum).Replace("-", "").ToLower(); + } +} diff --git a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs new file mode 100644 index 00000000..da0f1e66 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; + + +namespace Meadow.Software; + +internal class DownloadFileStream : Stream, IDisposable +{ + public event EventHandler DownloadProgress; + + private readonly Stream _stream; + + private long _position; + + public DownloadFileStream(Stream stream) + { + _stream = stream; + + DownloadProgress?.Invoke(this, 0); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => _stream.Length; + public override long Position { get => _position; set => throw new NotImplementedException(); } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var b = _stream.Read(buffer, offset, count); + _position += b; + + DownloadProgress?.Invoke(this, _position); + + return b; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } +} diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs new file mode 100644 index 00000000..f172e97f --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs @@ -0,0 +1,185 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Meadow.Software; + +internal class F7FirmwareDownloadManager +{ + public event EventHandler DownloadProgress = default!; + + private const string VersionCheckUrlRoot = + "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; + + private readonly HttpClient Client = new() + { + Timeout = TimeSpan.FromMinutes(5) + }; + + public async Task GetLatestAvailableVersion() + { + var contents = await GetReleaseMetadata(); + + return contents?.Version ?? string.Empty; + } + + public async Task GetReleaseMetadata(string? version = null) + { + string versionCheckUrl; + if (version is null || string.IsNullOrWhiteSpace(version)) + { + versionCheckUrl = VersionCheckUrlRoot + "latest.json"; + } + else + { + versionCheckUrl = VersionCheckUrlRoot + version + ".json"; + } + + string versionCheckFile; + + try + { + versionCheckFile = await DownloadFile(new Uri(versionCheckUrl)); + } + catch + { + return null; + } + + try + { + var content = JsonSerializer.Deserialize(File.ReadAllText(versionCheckFile)); + + return content; + } + catch + { + return null; + } + } + + public void SetDefaultVersion(string destinationRoot, string version) + { + File.WriteAllText(Path.Combine(destinationRoot, "latest.txt"), version); + } + + public async Task DownloadRelease(string destinationRoot, string version, bool overwrite = false) + { + var downloadManager = new F7FirmwareDownloadManager(); + var meta = await downloadManager.GetReleaseMetadata(version); + if (meta == null) return false; + + CreateFolder(destinationRoot, false); + + //we'll write latest.txt regardless of version if it doesn't exist + SetDefaultVersion(destinationRoot, meta.Version); + + string local_path; + + if (string.IsNullOrWhiteSpace(version)) + { + local_path = Path.Combine(destinationRoot, meta.Version); + version = meta.Version; + } + else + { + local_path = Path.Combine(destinationRoot, version); + } + + if (CreateFolder(local_path, overwrite) == false) + { + throw new Exception($"Firmware version {version} already exists locally"); + } + + try + { + await DownloadAndExtractFile(new Uri(meta.DownloadURL), local_path); + } + catch + { + throw new Exception($"Unable to download OS files for {version}"); + } + + try + { + await DownloadAndExtractFile(new Uri(meta.NetworkDownloadURL), local_path); + } + catch + { + throw new Exception($"Unable to download Coprocessor files for {version}"); + } + + return true; + } + + private async Task DownloadAndExtractFile(Uri uri, string target_path, CancellationToken cancellationToken = default) + { + var downloadFileName = await DownloadFile(uri, cancellationToken); + + ZipFile.ExtractToDirectory( + downloadFileName, + target_path); + + File.Delete(downloadFileName); + } + + private bool CreateFolder(string path, bool eraseIfExists = true) + { + if (Directory.Exists(path)) + { + if (eraseIfExists) + { + CleanPath(path); + } + else + { + return false; + } + } + else + { + Directory.CreateDirectory(path); + } + return true; + } + + private void CleanPath(string path) + { + var di = new DirectoryInfo(path); + foreach (FileInfo file in di.GetFiles()) + { + file.Delete(); + } + foreach (DirectoryInfo dir in di.GetDirectories()) + { + dir.Delete(true); + } + } + + private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri); + using var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + response.EnsureSuccessStatusCode(); + + var downloadFileName = Path.GetTempFileName(); + + using var stream = await response.Content.ReadAsStreamAsync(); + + var contentLength = response.Content.Headers.ContentLength; + + using var downloadFileStream = new DownloadFileStream(stream); + using var firmwareFile = File.OpenWrite(downloadFileName); + + downloadFileStream.DownloadProgress += (s, e) => { DownloadProgress?.Invoke(this, e); }; + + await downloadFileStream.CopyToAsync(firmwareFile); + + return downloadFileName; + } +} diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs new file mode 100644 index 00000000..8a14443a --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public class F7FirmwarePackageCollection : IFirmwarePackageCollection +{ + /// + public event EventHandler DownloadProgress; + + public string PackageFileRoot { get; } + + private List _f7Packages = new(); + + public FirmwarePackage? DefaultPackage { get; private set; } + + public static string DefaultF7FirmwareStoreRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "WildernessLabs", + "Firmware"); + + internal F7FirmwarePackageCollection() + : this(DefaultF7FirmwareStoreRoot) + { + } + + internal F7FirmwarePackageCollection(string rootPath) + { + if (!Directory.Exists(rootPath)) + { + Directory.CreateDirectory(rootPath); + } + + PackageFileRoot = rootPath; + } + + /// + /// Checks the remote (i.e. cloud) store to see if a new firmware package is available. + /// + /// A version number if an update is available, otherwise null + public async Task UpdateAvailable() + { + var downloadManager = new F7FirmwareDownloadManager(); + + var latestVersion = await downloadManager.GetLatestAvailableVersion(); + + var existing = _f7Packages.FirstOrDefault(p => p.Version == latestVersion); + + if (existing == null) + { + return latestVersion; + } + + return null; + } + + public Task DeletePackage(string version) + { + var existing = _f7Packages.FirstOrDefault(p => p.Version == version); + + if (existing == null) + { + throw new ArgumentException($"Version '{version}' not found locally."); + } + + // if we're deleting the default, we need to det another default + var i = _f7Packages.Count - 1; + while (DefaultPackage.Version == _f7Packages[i].Version) + { + i--; + } + var newDefault = _f7Packages[i].Version; + _f7Packages.Remove(DefaultPackage); + SetDefaultPackage(newDefault); + + var path = Path.Combine(PackageFileRoot, version); + + Directory.Delete(path, true); + + return Task.CompletedTask; + } + + public Task SetDefaultPackage(string version) + { + var existing = _f7Packages.FirstOrDefault(p => p.Version == version); + + if (existing == null) + { + throw new ArgumentException($"Version '{version}' not found locally."); + } + + var downloadManager = new F7FirmwareDownloadManager(); + downloadManager.SetDefaultVersion(PackageFileRoot, version); + + return Task.CompletedTask; + } + + public async Task IsVersionAvailableForDownload(string version) + { + var downloadManager = new F7FirmwareDownloadManager(); + + var meta = await downloadManager.GetReleaseMetadata(version); + + if (meta == null) return false; + if (meta.Version != string.Empty) return true; + + return false; + } + + public async Task GetLatestAvailableVersion() + { + var downloadManager = new F7FirmwareDownloadManager(); + + var meta = await downloadManager.GetReleaseMetadata(); + + if (meta == null) return null; + if (meta.Version == string.Empty) return null; + + return meta.Version; + } + + public async Task RetrievePackage(string version, bool overwrite = false) + { + var downloadManager = new F7FirmwareDownloadManager(); + + void ProgressHandler(object sender, long e) + { + DownloadProgress?.Invoke(this, e); + } + + downloadManager.DownloadProgress += ProgressHandler; + try + { + var meta = await downloadManager.GetReleaseMetadata(version); + if (meta == null) return false; + + return await downloadManager.DownloadRelease(PackageFileRoot, version, overwrite); + } + finally + { + downloadManager.DownloadProgress -= ProgressHandler; + } + } + + public Task Refresh() + { + _f7Packages.Clear(); + + foreach (var directory in Directory.GetDirectories(PackageFileRoot)) + { + var hasFiles = false; + + var package = new FirmwarePackage(this) + { + Version = Path.GetFileName(directory) + }; + + foreach (var file in Directory.GetFiles(directory)) + { + var fn = Path.GetFileName(file); + switch (fn) + { + case F7FirmwareFiles.CoprocBootloaderFile: + package.CoprocBootloader = fn; + hasFiles = true; + break; + case F7FirmwareFiles.CoprocPartitionTableFile: + package.CoprocPartitionTable = fn; + hasFiles = true; + break; + case F7FirmwareFiles.CoprocApplicationFile: + package.CoprocApplication = fn; + hasFiles = true; + break; + case F7FirmwareFiles.OSWithBootloaderFile: + package.OSWithBootloader = fn; + hasFiles = true; + break; + case F7FirmwareFiles.OsWithoutBootloaderFile: + package.OsWithoutBootloader = fn; + hasFiles = true; + break; + case F7FirmwareFiles.RuntimeFile: + package.Runtime = fn; + hasFiles = true; + break; + } + } + + if (Directory.Exists(Path.Combine(directory, F7FirmwareFiles.BclFolder))) + { + package.BclFolder = F7FirmwareFiles.BclFolder; + } + + if (hasFiles) + { + _f7Packages.Add(package); + } + } + + var fi = new FileInfo(Path.Combine(PackageFileRoot, "latest.txt")); + if (fi.Exists) + { + // get default + using var reader = fi.OpenText(); + var content = reader.ReadToEnd().Trim(); + + // does it actually exist? + DefaultPackage = _f7Packages.FirstOrDefault(p => p.Version == content); + } + + return Task.CompletedTask; + } + + public IEnumerator GetEnumerator() + { + return _f7Packages.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal static class F7FirmwareFiles + { + public const string CoprocBootloaderFile = "bootloader.bin"; + public const string CoprocPartitionTableFile = "partition-table.bin"; + public const string CoprocApplicationFile = "MeadowComms.bin"; + public const string OSWithBootloaderFile = "Meadow.OS.bin"; + public const string OsWithoutBootloaderFile = "Meadow.OS.Update.bin"; + public const string RuntimeFile = "Meadow.OS.Runtime.bin"; + public const string BclFolder = "meadow_assemblies"; + } +} diff --git a/Source/v2/Meadow.SoftwareManager/F7ReleaseMetadata.cs b/Source/v2/Meadow.SoftwareManager/F7ReleaseMetadata.cs new file mode 100644 index 00000000..d5fa7f22 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/F7ReleaseMetadata.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + + +namespace Meadow.Software; + +public class F7ReleaseMetadata +{ + [JsonPropertyName("version")] + public string Version { get; set; } = default!; + [JsonPropertyName("minCLIVersion")] + public string MinCLIVersion { get; set; } = default!; + [JsonPropertyName("downloadUrl")] + public string DownloadURL { get; set; } = default!; + [JsonPropertyName("networkDownloadUrl")] + public string NetworkDownloadURL { get; set; } = default!; + +} diff --git a/Source/v2/Meadow.SoftwareManager/FileManager.cs b/Source/v2/Meadow.SoftwareManager/FileManager.cs new file mode 100644 index 00000000..9509e698 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/FileManager.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public class FileManager +{ + public static readonly string WildernessTempFolderPath = 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"; + internal static readonly string VersionCheckUrlRoot = + "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; + */ + + public FirmwareStore Firmware { get; } + + public FileManager() + { + Firmware = new FirmwareStore(); + var f7Collection = new F7FirmwarePackageCollection(); + Firmware.AddCollection("Meadow F7", f7Collection); + } + + public async Task Refresh() + { + foreach (var c in Firmware) + { + await c.Refresh(); + } + } + + /* + private void GetAllLocalFirmwareVersions() + { + } + + public bool DownloadRuntimeVersion(string version) + { + } + + public static string? GetLocalPathToRuntimeVersion(string version) + { + } + + public static string[] GetLocalRuntimeVersions() + { + } + */ +} \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs new file mode 100644 index 00000000..ae2c489c --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace Meadow.Software; + +public class FirmwarePackage +{ + internal IFirmwarePackageCollection _collection; + + internal FirmwarePackage(IFirmwarePackageCollection collection) + { + _collection = collection; + } + + public string GetFullyQualifiedPath(string file) + { + return Path.Combine(_collection.PackageFileRoot, Version, file); + } + + public string Version { get; set; } + public string Targets { get; set; } + public string? CoprocBootloader { get; set; } + public string? CoprocPartitionTable { get; set; } + public string? CoprocApplication { get; set; } + public string? OSWithBootloader { get; set; } + public string? OsWithoutBootloader { get; set; } + public string? Runtime { get; set; } + public string? BclFolder { get; set; } +} diff --git a/Source/v2/Meadow.SoftwareManager/FirmwareStore.cs b/Source/v2/Meadow.SoftwareManager/FirmwareStore.cs new file mode 100644 index 00000000..de1d5977 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/FirmwareStore.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + + +namespace Meadow.Software; + +public class FirmwareStore : IEnumerable +{ + private readonly Dictionary _collections = new(); + + public string[] CollectionNames => _collections.Keys.ToArray(); + + internal FirmwareStore() + { + } + + public IFirmwarePackageCollection this[string collectionName] + { + get => _collections[collectionName]; + } + + internal void AddCollection(string name, IFirmwarePackageCollection collection) + { + _collections.Add(name, collection); + } + + public IEnumerator GetEnumerator() + { + return _collections.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs new file mode 100644 index 00000000..5d0cf341 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public interface IFirmwarePackageCollection : IEnumerable +{ + /// + /// Event for download progress. + /// + /// + /// EventArgs are the total number of bytes retrieved + /// + public event EventHandler DownloadProgress; + + FirmwarePackage? DefaultPackage { get; } + Task SetDefaultPackage(string version); + Task DeletePackage(string version); + Task Refresh(); + Task GetLatestAvailableVersion(); + Task UpdateAvailable(); + Task IsVersionAvailableForDownload(string version); + Task RetrievePackage(string version, bool overwrite = false); + + string PackageFileRoot { get; } +} diff --git a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj new file mode 100644 index 00000000..d61863d1 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1 + enable + 10 + + + + + + + + diff --git a/Source/v2/icon.png b/Source/v2/icon.png new file mode 100644 index 00000000..2c87b8f1 Binary files /dev/null and b/Source/v2/icon.png differ