From e27b8163adaeb6eb9156b4dced7e54aae6e7c4c4 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 9 Aug 2023 17:23:16 -0500 Subject: [PATCH 01/22] First pass at v2 --- Source/v2/Meadow.CLI.v2.sln | 66 +++ .../Commands/Current/BaseDeviceCommand.cs | 58 +++ .../Commands/Current/ConfigCommand.cs | 71 +++ .../Commands/Current/DeviceResetCommand.cs | 19 + .../Commands/Current/FileListCommand.cs | 84 ++++ .../Commands/Current/GetDeviceInfoCommand.cs | 62 +++ .../Commands/Current/RuntimeDisableCommand.cs | 23 + .../Commands/Current/RuntimeEnableCommand.cs | 23 + .../Commands/Current/RuntimeStateCommand.cs | 21 + .../Commands/Legacy/MonoDisableCommand.cs | 14 + .../Commands/Legacy/MonoEnableCommand.cs | 14 + Source/v2/Meadow.Cli/ISettingsManager.cs | 10 + Source/v2/Meadow.Cli/Meadow.Cli.csproj | 24 + .../v2/Meadow.Cli/MeadowConnectionManager.cs | 46 ++ Source/v2/Meadow.Cli/Program.cs | 97 ++++ .../Meadow.Cli/Properties/launchSettings.json | 59 +++ Source/v2/Meadow.Cli/SettingsManager.cs | 123 +++++ .../Meadow.HCom.Integration.Tests/CliTests.cs | 39 ++ .../ConnectionManagerTests.cs | 215 ++++++++ .../InMemorySettingsManager.cs | 58 +++ .../Meadow.HCom.Integration.Tests.csproj | 29 ++ .../SerialCommandTests.cs | 147 ++++++ .../SerialConnectionTests.cs | 63 +++ .../TcpCommandTests.cs | 44 ++ .../TcpConnectionTests.cs | 25 + .../TestListener.cs | 56 +++ .../Meadow.HCom.Integration.Tests/Usings.cs | 3 + Source/v2/Meadow.Hcom/AssemblyAttributes.cs | 3 + Source/v2/Meadow.Hcom/CobsTools.cs | 101 ++++ Source/v2/Meadow.Hcom/ConnectionManager.cs | 29 ++ Source/v2/Meadow.Hcom/ConnectionState.cs | 9 + .../Meadow.Hcom/Connections/ConnectionBase.cs | 135 +++++ .../SerialConnection.ListenerProc.cs | 276 ++++++++++ .../Connections/SerialConnection.cs | 472 ++++++++++++++++++ .../Meadow.Hcom/Connections/TcpConnection.cs | 88 ++++ Source/v2/Meadow.Hcom/DeviceInfo.cs | 39 ++ Source/v2/Meadow.Hcom/Extensions.cs | 16 + .../Firmware/DownloadFileStream.cs | 89 ++++ .../Meadow.Hcom/Firmware/DownloadManager.cs | 351 +++++++++++++ .../v2/Meadow.Hcom/Firmware/FirmwareInfo.cs | 51 ++ .../Meadow.Hcom/Firmware/FirmwareManager.cs | 148 ++++++ .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 331 ++++++++++++ .../v2/Meadow.Hcom/Firmware/PackageManager.cs | 48 ++ .../Meadow.Hcom/Firmware/PackageVersions.cs | 9 + .../Meadow.Hcom/Firmware/ReleaseMetadata.cs | 16 + .../Http Responses/DeviceInfoHttpResponse.cs | 62 +++ Source/v2/Meadow.Hcom/IConnectionListener.cs | 14 + Source/v2/Meadow.Hcom/IHcomConnection.cs | 20 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 19 + Source/v2/Meadow.Hcom/Meadow.Hcom.csproj | 24 + Source/v2/Meadow.Hcom/Meadow.Hcom.sln | 37 ++ .../MeadowDevice.ResponseListener.cs | 57 +++ Source/v2/Meadow.Hcom/MeadowDevice.cs | 309 ++++++++++++ Source/v2/Meadow.Hcom/MeadowFileInfo.cs | 33 ++ Source/v2/Meadow.Hcom/Protocol.cs | 38 ++ Source/v2/Meadow.Hcom/ProtocolType.cs | 31 ++ .../Serial Requests/FileReadDataRequest.cs | 6 + .../Serial Requests/GetDeviceInfoRequest.cs | 16 + .../Serial Requests/GetFileListRequest.cs | 15 + .../Serial Requests/GetRtcTimeRequest.cs | 6 + .../Serial Requests/GetRuntimeStateRequest.cs | 6 + .../Serial Requests/InitFileReadRequest.cs | 41 ++ .../v2/Meadow.Hcom/Serial Requests/Request.cs | 84 ++++ .../Serial Requests/RequestBuilder.cs | 19 + .../Serial Requests/RequestType.cs | 84 ++++ .../Serial Requests/ResetDeviceRequest.cs | 16 + .../Serial Requests/ResetRequest.cs | 6 + .../Serial Requests/RuntimeDisableRequest.cs | 6 + .../Serial Requests/RuntimeEnableRequest.cs | 6 + .../Serial Requests/SetRtcTimeRequest.cs | 23 + .../Serial Requests/StartFileDataRequest.cs | 6 + .../DeviceInfoSerialResponse.cs | 25 + .../FileReadInitFailedResponse.cs | 9 + .../FileReadInitOkResponse.cs | 9 + .../RequestErrorTextResponse.cs | 23 + .../Serial Responses/ResponseType.cs | 36 ++ .../Serial Responses/SerialResponse.cs | 67 +++ .../Serial Responses/TextAcceptedResponse.cs | 13 + .../Serial Responses/TextConcludedResponse.cs | 9 + .../Serial Responses/TextCrcMemberResponse.cs | 13 + .../TextInformationResponse.cs | 16 + .../TextListHeaderResponse.cs | 9 + .../TextListMemberResponse.cs | 13 + .../Serial Responses/TextRequestResponse.cs | 16 + .../Serial Responses/TextStdErrResponse.cs | 13 + .../Serial Responses/TextStdOutResponse.cs | 13 + .../UploadCompletedResponse.cs | 9 + .../UploadDataPacketResponse.cs | 11 + 88 files changed, 4992 insertions(+) create mode 100644 Source/v2/Meadow.CLI.v2.sln create mode 100644 Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs create mode 100644 Source/v2/Meadow.Cli/ISettingsManager.cs create mode 100644 Source/v2/Meadow.Cli/Meadow.Cli.csproj create mode 100644 Source/v2/Meadow.Cli/MeadowConnectionManager.cs create mode 100644 Source/v2/Meadow.Cli/Program.cs create mode 100644 Source/v2/Meadow.Cli/Properties/launchSettings.json create mode 100644 Source/v2/Meadow.Cli/SettingsManager.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/CliTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/InMemorySettingsManager.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/TcpConnectionTests.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs create mode 100644 Source/v2/Meadow.HCom.Integration.Tests/Usings.cs create mode 100644 Source/v2/Meadow.Hcom/AssemblyAttributes.cs create mode 100644 Source/v2/Meadow.Hcom/CobsTools.cs create mode 100644 Source/v2/Meadow.Hcom/ConnectionManager.cs create mode 100644 Source/v2/Meadow.Hcom/ConnectionState.cs create mode 100644 Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs create mode 100644 Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs create mode 100644 Source/v2/Meadow.Hcom/Connections/SerialConnection.cs create mode 100644 Source/v2/Meadow.Hcom/Connections/TcpConnection.cs create mode 100644 Source/v2/Meadow.Hcom/DeviceInfo.cs create mode 100644 Source/v2/Meadow.Hcom/Extensions.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/PackageManager.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs create mode 100644 Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs create mode 100644 Source/v2/Meadow.Hcom/Http Responses/DeviceInfoHttpResponse.cs create mode 100644 Source/v2/Meadow.Hcom/IConnectionListener.cs create mode 100644 Source/v2/Meadow.Hcom/IHcomConnection.cs create mode 100644 Source/v2/Meadow.Hcom/IMeadowDevice.cs create mode 100644 Source/v2/Meadow.Hcom/Meadow.Hcom.csproj create mode 100644 Source/v2/Meadow.Hcom/Meadow.Hcom.sln create mode 100644 Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs create mode 100644 Source/v2/Meadow.Hcom/MeadowDevice.cs create mode 100644 Source/v2/Meadow.Hcom/MeadowFileInfo.cs create mode 100644 Source/v2/Meadow.Hcom/Protocol.cs create mode 100644 Source/v2/Meadow.Hcom/ProtocolType.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/FileReadDataRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/GetDeviceInfoRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/GetFileListRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/GetRtcTimeRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/GetRuntimeStateRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/Request.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/RequestBuilder.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/ResetDeviceRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/ResetRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/RuntimeDisableRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/RuntimeEnableRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/SetRtcTimeRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/StartFileDataRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/DeviceInfoSerialResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/FileReadInitFailedResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/RequestErrorTextResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/ResponseType.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextAcceptedResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextConcludedResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextCrcMemberResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextInformationResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextListHeaderResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextListMemberResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextRequestResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextStdErrResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/TextStdOutResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/UploadCompletedResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/UploadDataPacketResponse.cs diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln new file mode 100644 index 00000000..d792cc87 --- /dev/null +++ b/Source/v2/Meadow.CLI.v2.sln @@ -0,0 +1,66 @@ + +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 +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 + 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/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs new file mode 100644 index 00000000..f602f9a7 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -0,0 +1,58 @@ +using CliFx; +using CliFx.Infrastructure; +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(Hcom.IMeadowDevice device, CancellationToken cancellationToken); + + public 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.Device, cancellationToken); + } + } + catch (TimeoutException) + { + Logger.LogError($"Timeout attempting to attach to device on {c.Name}"); + } + } + } +} 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..45756ac5 --- /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 = "Get the device info")] +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/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs new file mode 100644 index 00000000..aad76fa3 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs @@ -0,0 +1,19 @@ +using CliFx.Attributes; +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(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + await device.Reset(); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs new file mode 100644 index 00000000..fabd842d --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs @@ -0,0 +1,84 @@ +using CliFx.Attributes; +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(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/GetDeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs new file mode 100644 index 00000000..46ef2cd3 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs @@ -0,0 +1,62 @@ +using CliFx.Attributes; +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(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."); + } + } + } +} + +[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(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/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs new file mode 100644 index 00000000..fd8303a2 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs @@ -0,0 +1,23 @@ +using CliFx.Attributes; +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(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/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs new file mode 100644 index 00000000..c4d97174 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs @@ -0,0 +1,23 @@ +using CliFx.Attributes; +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(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/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs new file mode 100644 index 00000000..859e8024 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs @@ -0,0 +1,21 @@ +using CliFx.Attributes; +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(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/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..72c97ff1 --- /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"); + } +} 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..a18eed7e --- /dev/null +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs new file mode 100644 index 00000000..7ac1c863 --- /dev/null +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -0,0 +1,46 @@ +using Meadow.Cli; +using Meadow.Hcom; +using System.Net; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public class MeadowConnectionManager +{ + private ISettingsManager _settingsManager; + + 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"); + } + + // 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) + { + return new TcpConnection(uri); + } + return new SerialConnection(route); + } +} diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs new file mode 100644 index 00000000..0e4449e2 --- /dev/null +++ b/Source/v2/Meadow.Cli/Program.cs @@ -0,0 +1,97 @@ +using CliFx; +using Meadow.Cli; +using Meadow.CLI.Commands.DeviceManagement; +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(); + */ + + 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..5cb8f923 --- /dev/null +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -0,0 +1,59 @@ +{ + "profiles": { + "Meadow.Cli": { + "commandName": "Project" + }, + "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 COM8" + }, + "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" + } + } +} \ 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.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..e654a588 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -0,0 +1,147 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class SerialCommandTests + { + public string ValidPortName { get; } = "COM9"; + + [Fact] + public async void TestDeviceReset() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var listener = new TestListener(); + connection.AddListener(listener); + + var command = RequestBuilder.Build(); + command.SequenceNumber = 0; + + // 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 + + ((IMeadowConnection)connection).EnqueueRequest(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 TestGetDeviceInfo() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var listener = new TestListener(); + connection.AddListener(listener); + + var command = RequestBuilder.Build(); + command.SequenceNumber = 0; + + // 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 + + ((IMeadowConnection)connection).EnqueueRequest(command); + + var timeoutSecs = 10; + + while (timeoutSecs-- > 0) + { + if (listener.DeviceInfo.Count > 0) + { + break; + } + + await Task.Delay(1000); + } + + Assert.True(listener.DeviceInfo.Count > 0); + } + } + + [Fact] + public async void TestGetFileListNoCrc() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var listener = new TestListener(); + connection.AddListener(listener); + + var command = RequestBuilder.Build(); + command.SequenceNumber = 0; + + // 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 + + ((IMeadowConnection)connection).EnqueueRequest(command); + + var timeoutSecs = 10; + + while (timeoutSecs-- > 0) + { + if (listener.DeviceInfo.Count > 0) + { + break; + } + + await Task.Delay(1000); + } + + Assert.True(listener.TextList.Count > 0); + } + } + + [Fact] + public async void TestGetFileListWithCrc() + { + using (var connection = new SerialConnection(ValidPortName)) + { + Assert.Equal(ConnectionState.Disconnected, connection.State); + + var listener = new TestListener(); + connection.AddListener(listener); + + var command = RequestBuilder.Build(); + command.IncludeCrcs = true; + + command.SequenceNumber = 0; + + // 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 + + ((IMeadowConnection)connection).EnqueueRequest(command); + + var timeoutSecs = 10; + + while (timeoutSecs-- > 0) + { + if (listener.DeviceInfo.Count > 0) + { + break; + } + + await Task.Delay(1000); + } + + Assert.True(listener.TextList.Count > 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..f9b83856 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -0,0 +1,63 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class SerialConnectionTests + { + public string ValidPortName { get; } = "COM3"; + + [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..524a4f18 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs @@ -0,0 +1,44 @@ +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 listener = new TestListener(); + connection.AddListener(listener); + + var command = RequestBuilder.Build(); + command.SequenceNumber = 0; + + // 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 + + connection.EnqueueRequest(command); + + var timeoutSecs = 10; + + while (timeoutSecs-- > 0) + { + if (listener.DeviceInfo.Count > 0) + { + break; + } + + await Task.Delay(1000); + } + + Assert.True(listener.DeviceInfo.Count > 0); + } + } + + } +} \ 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/TestListener.cs b/Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs new file mode 100644 index 00000000..f6cb6f37 --- /dev/null +++ b/Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs @@ -0,0 +1,56 @@ +using Meadow.Hcom; + +namespace Meadow.HCom.Integration.Tests +{ + public class TestListener : IConnectionListener + { + public List StdOut { get; } = new List(); + public List StdErr { get; } = new List(); + public List Messages { get; } = new List(); + public Dictionary DeviceInfo { get; private set; } = new Dictionary(); + public List TextList { get; } = new List(); + public string? LastError { get; set; } + public int? LastRequestConcluded { get; set; } + + public void OnTextMessageConcluded(int requestType) + { + LastRequestConcluded = requestType; + } + + public void OnStdOutReceived(string message) + { + StdOut.Add(message); + } + + public void OnStdErrReceived(string message) + { + StdErr.Add(message); + } + + public void OnInformationMessageReceived(string message) + { + Messages.Add(message); + } + + public void OnDeviceInformationMessageReceived(Dictionary deviceInfo) + { + DeviceInfo = deviceInfo; + } + + public void OnTextListReceived(string[] list) + { + TextList.Clear(); + TextList.AddRange(list); + } + + public void OnErrorTextReceived(string message) + { + LastError = message; + } + + public void OnFileError() + { + throw new Exception(LastError); + } + } +} \ 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..68948b05 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -0,0 +1,135 @@ +using System.Diagnostics; + +namespace Meadow.Hcom; + +public abstract class ConnectionBase : IMeadowConnection, IDisposable +{ + private readonly Queue _pendingCommands = new(); + private ReadFileInfo? _readFileInfo = null; + private bool _isDisposed; + + protected List ConnectionListeners { get; } = new(); + + public ConnectionState State { get; protected set; } + public IMeadowDevice? Device { get; protected set; } + + public event EventHandler FileReadCompleted; + public event EventHandler FileException; + public event EventHandler ConnectionError; + + internal abstract Task DeliverRequest(IRequest request); + public abstract string Name { get; } + public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + + public ConnectionBase() + { + new Thread(CommandManager) + { + IsBackground = true, + Name = "HCOM Sender" + } + .Start(); + } + + protected void RaiseConnectionError(Exception error) + { + ConnectionError?.Invoke(this, error); + } + + public virtual void AddListener(IConnectionListener listener) + { + lock (ConnectionListeners) + { + ConnectionListeners.Add(listener); + } + } + + public virtual void RemoveListener(IConnectionListener listener) + { + lock (ConnectionListeners) + { + ConnectionListeners.Remove(listener); + } + + // TODO: stop maintaining connection? + } + + public Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private void CommandManager() + { + while (!_isDisposed) + { + while (_pendingCommands.Count > 0) + { + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + + var command = _pendingCommands.Dequeue(); + + var response = DeliverRequest(command); + + // 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!; + } + + 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..307bfd05 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -0,0 +1,276 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace Meadow.Hcom +{ + public partial class SerialConnection + { + private bool _reconnectInProgress = false; + + public event EventHandler FileException = delegate { }; + + public 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}"); + + foreach (var listener in _listeners) + { + listener.OnInformationMessageReceived(tir.Text); + } + } + else if (response is TextStdOutResponse tso) + { + // send the message to any listeners + Debug.WriteLine($"STDOUT> {tso.Text}"); + + foreach (var listener in _listeners) + { + listener.OnStdOutReceived(tso.Text); + } + } + else if (response is TextStdErrResponse tse) + { + // send the message to any listeners + Debug.WriteLine($"STDERR> {tse.Text}"); + + foreach (var listener in _listeners) + { + listener.OnStdErrReceived(tse.Text); + } + } + else if (response is TextListHeaderResponse tlh) + { + // start of a list + _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) + { + foreach (var listener in _listeners) + { + listener.OnTextMessageConcluded((int)tcr.RequestType); + } + + if (_reconnectInProgress) + { + _state = ConnectionState.MeadowAttached; + _reconnectInProgress = false; + } + else + { + foreach (var listener in _listeners) + { + listener.OnTextListReceived(_textList.ToArray()); + } + } + } + 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) + { + foreach (var listener in _listeners) + { + listener.OnDeviceInformationMessageReceived(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(); + + (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) + { + _lastError = ret.Text; + } + 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) + { + // 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..b0431bcd --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -0,0 +1,472 @@ +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 : IDisposable, IMeadowConnection + { + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + + public event EventHandler FileReadCompleted = delegate { }; + public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + public event EventHandler ConnectionError; + + private SerialPort _port; + private ILogger? _logger; + private bool _isDisposed; + private ConnectionState _state; + private readonly CancellationTokenSource _cts; + 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 IMeadowDevice? Device { get; private set; } + public string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) + { + _cts = new CancellationTokenSource(); + + 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) + { + _port.Open(); + } + State = ConnectionState.Connected; + } + + private void Close() + { + if (_port.IsOpen) + { + _port.Close(); + } + + State = ConnectionState.Disconnected; + } + + public 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; + + var payload = command.Serialize(); + EncodeAndSendPacket(payload); + + // TODO: re-queue on fail? + } + + Thread.Sleep(1000); + } + } + + private class ReadFileInfo + { + private string? _localFileName; + + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get + { + if (_localFileName != null) return _localFileName; + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); + } + set => _localFileName = value; + } + public FileStream FileStream { get; set; } = default!; + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private void EncodeAndSendPacket(byte[] messageBytes) + { + Debug.WriteLine($"+EncodeAndSendPacket({messageBytes.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. + encodedBytes = new byte[Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE]; + + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, messageBytes.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.WriteLine($"Sending {encodedToSend} bytes..."); + _port.Write(encodedBytes, 0, encodedToSend); + Debug.WriteLine($"sent"); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); + throw; + } + } + catch (Exception except) + { + // DID YOU RESTART MEADOW? + // This should drop the connection and retry + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + throw; + } + } + + + private class SerialMessage + { + private readonly IList> _segments; + + public SerialMessage(Memory segment) + { + _segments = new List>(); + _segments.Add(segment); + } + + public void AddSegment(Memory segment) + { + _segments.Add(segment); + } + + public byte[] ToArray() + { + using var ms = new MemoryStream(); + foreach (var segment in _segments) + { + // We could just call ToArray on the `Memory` but that will result in an uncontrolled allocation. + var tmp = ArrayPool.Shared.Rent(segment.Length); + segment.CopyTo(tmp); + ms.Write(tmp, 0, segment.Length); + ArrayPool.Shared.Return(tmp); + } + return ms.ToArray(); + } + } + + private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) + { + var decodedBuffer = ArrayPool.Shared.Rent(8192); + var packetLength = packetBuffer.Length; + // It's possible that we may find a series of 0x00 values in the buffer. + // This is because when the sender is blocked (because this code isn't + // running) it will attempt to send a single 0x00 before the full message. + // This allows it to test for a connection. When the connection is + // unblocked this 0x00 is sent and gets put into the buffer along with + // any others that were queued along the usb serial pipe line. + if (packetLength == 1) + { + //_logger.LogTrace("Throwing out 0x00 from buffer"); + return false; + } + + var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); + + /* + // If a message is too short it is ignored + if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + { + return false; + } + + Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + + // Process the received packet + ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), + cancellationToken); + + */ + ArrayPool.Shared.Return(decodedBuffer); + return true; + } + + protected 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/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs new file mode 100644 index 00000000..73a4b016 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -0,0 +1,88 @@ +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? + } + + internal override async Task DeliverRequest(IRequest request) + { + if (request is GetDeviceInfoRequest) + { + try + { + var response = await _client.GetAsync($"{_baseUri}/api/info"); + + if (response.IsSuccessStatusCode) + { + var r = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); + var d = r.ToDictionary(); + + foreach (var listener in ConnectionListeners) + { + listener.OnDeviceInformationMessageReceived(d); + } + } + else + { + RaiseConnectionError(new Exception($"API responded with {response.StatusCode}")); + } + } + catch (Exception ex) + { + RaiseConnectionError(ex); + } + } + else + { + throw new NotImplementedException(); + } + } + + public Task WaitForMeadowAttach(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..65e69436 --- /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) + { + _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..9ff97efe --- /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/IHcomConnection.cs b/Source/v2/Meadow.Hcom/IHcomConnection.cs new file mode 100644 index 00000000..c48596ec --- /dev/null +++ b/Source/v2/Meadow.Hcom/IHcomConnection.cs @@ -0,0 +1,20 @@ +namespace Meadow.Hcom +{ + public interface IMeadowConnection + { + event EventHandler FileReadCompleted; + event EventHandler FileException; + event EventHandler ConnectionError; + + string Name { get; } + IMeadowDevice? Device { get; } + Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); + ConnectionState State { get; } + + // internal stuff that probably needs to get moved to anotehr interface + void AddListener(IConnectionListener listener); + void RemoveListener(IConnectionListener listener); + void EnqueueRequest(IRequest command); + } +} \ 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..657c9ab8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -0,0 +1,19 @@ +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 FlashOS(string requestedversion, CancellationToken? cancellationToken = null); + Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null); + Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null); + Task GetRtcTime(CancellationToken? cancellationToken = null); + Task SetRtcTime(DateTimeOffset dateTime, 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.ResponseListener.cs b/Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs new file mode 100644 index 00000000..6f1d712a --- /dev/null +++ b/Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs @@ -0,0 +1,57 @@ +namespace Meadow.Hcom +{ + public partial class MeadowDevice + { + internal class ResponseListener : IConnectionListener + { + public List StdOut { get; } = new List(); + public List StdErr { get; } = new List(); + public List Information { get; } = new List(); + public Dictionary DeviceInfo { get; private set; } = new Dictionary(); + public List TextList { get; } = new List(); + public string? LastError { get; set; } + public int? LastRequestConcluded { get; set; } + + public void OnTextMessageConcluded(int requestType) + { + LastRequestConcluded = requestType; + } + + public void OnStdOutReceived(string message) + { + StdOut.Add(message); + } + + public void OnStdErrReceived(string message) + { + StdErr.Add(message); + } + + public void OnInformationMessageReceived(string message) + { + Information.Add(message); + } + + public void OnDeviceInformationMessageReceived(Dictionary deviceInfo) + { + DeviceInfo = deviceInfo; + } + + public void OnTextListReceived(string[] list) + { + TextList.Clear(); + TextList.AddRange(list); + } + + public void OnErrorTextReceived(string message) + { + LastError = message; + } + + public void OnFileError() + { + throw new Exception(LastError); + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs new file mode 100644 index 00000000..d8bddf9f --- /dev/null +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -0,0 +1,309 @@ +namespace Meadow.Hcom +{ + public partial class MeadowDevice : IMeadowDevice + { + private IMeadowConnection _connection; + private ResponseListener _listener; + private Exception? _lastException; + + public int CommandTimeoutSeconds { get; set; } = 30; + + internal MeadowDevice(IMeadowConnection connection) + { + _connection = connection; + _connection.AddListener(_listener = new ResponseListener()); + _connection.ConnectionError += (s, e) => _lastException = e; + } + + 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; + } + + public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _listener.Information.Clear(); + + _connection.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 (_listener.Information.Count > 0) + { + var m = _listener.Information.FirstOrDefault(i => i.Contains("Mono is")); + if (m != null) + { + return m == "Mono is enabled"; + } + } + + await Task.Delay(500); + } + return false; + } + + public async Task Reset(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _connection.EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + await _connection.WaitForMeadowAttach(cancellationToken); + } + + public async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _listener.Information.Clear(); + + _connection.EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + var success = await WaitForResult(() => + { + if (_listener.Information.Count > 0) + { + var m = _listener.Information.FirstOrDefault(i => i.Contains("Mono is disabled")); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + + if (!success) throw new Exception("Unable to disable runtime"); + } + + public async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _listener.Information.Clear(); + + _connection.EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + var success = await WaitForResult(() => + { + if (_listener.Information.Count > 0) + { + var m = _listener.Information.FirstOrDefault(i => i.Contains("Meadow successfully started MONO")); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + + if (!success) throw new Exception("Unable to enable runtime"); + } + + public async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _listener.DeviceInfo.Clear(); + + _lastException = null; + _connection.EnqueueRequest(command); + + if (!await WaitForResult( + () => _listener.DeviceInfo.Count > 0, + cancellationToken)) + { + return null; + } + + return new DeviceInfo(_listener.DeviceInfo); + } + + public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.IncludeCrcs = includeCrcs; + + _listener.DeviceInfo.Clear(); + + _connection.EnqueueRequest(command); + + if (!await WaitForResult( + () => _listener.TextList.Count > 0, + cancellationToken)) + { + return null; + } + + var list = new List(); + + foreach (var candidate in _listener.TextList) + { + // TODO: should this be part of the connection? A serial response might be different than a future response (TCP or whatever) + var fi = MeadowFileInfo.Parse(candidate); + if (fi != null) + { + list.Add(fi); + } + } + + return list.ToArray(); + + } + + public 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 + { + _connection.FileReadCompleted += OnFileReadCompleted; + _connection.FileException += OnFileError; + + _connection.EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return completed; + }, + cancellationToken)) + { + return false; + } + + return true; + } + finally + { + _connection.FileReadCompleted -= OnFileReadCompleted; + _connection.FileException -= OnFileError; + } + } + + public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.LocalFileName = localFileName; + command.MeadowFileName = meadowFileName; + + _connection.EnqueueRequest(command); + return false; + } + + public Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _listener.Information.Clear(); + + _connection.EnqueueRequest(command); + + DateTimeOffset? now = null; + + var success = await WaitForResult(() => + { + var token = "UTC time:"; + + if (_listener.Information.Count > 0) + { + var m = _listener.Information.FirstOrDefault(i => i.Contains(token)); + if (m != null) + { + var timeString = m.Substring(m.IndexOf(token) + token.Length); + now = DateTimeOffset.Parse(timeString); + return true; + } + } + + return false; + }, cancellationToken); + + return now; + } + + public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.Time = dateTime; + + _listener.LastRequestConcluded = null; + + _connection.EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_listener.LastRequestConcluded != null && _listener.LastRequestConcluded == 0x303) + { + return true; + } + + return false; + }, 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..990d5acc --- /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 = 512; + public const int HCOM_PROTOCOL_ENCODED_MAX_SIZE = HCOM_PROTOCOL_PACKET_MAX_SIZE + 8; + + // 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/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..0d95fb09 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs @@ -0,0 +1,41 @@ +using System.Text; + +namespace Meadow.Hcom; + +internal class InitFileWriteRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER; + + 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); + } + } +} + +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/Request.cs b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs new file mode 100644 index 00000000..d5daf3c8 --- /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; } // TODO: what is this for? + public uint UserData { get; set; } // TODO: what is this for? + + public Request() + { + } + + public byte[]? Payload { get; protected set; } + + public 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..ccdcef86 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs @@ -0,0 +1,84 @@ +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, + + // Only used for testing + 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 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..e22c4cca --- /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) + { + } +} 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..b0963ebc --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs @@ -0,0 +1,67 @@ +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); + 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..]; +} From 9d48dbcfb8840b501357e442db9e2b0890e54001 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Tue, 15 Aug 2023 12:34:14 -0500 Subject: [PATCH 02/22] v2 write file is working --- Meadow.CLI.Core/Devices/MeadowLocalDevice.cs | 758 +++++++++-------- Meadow.CLI/Properties/launchSettings.json | 4 +- .../Commands/Current/DeviceClockCommand.cs | 43 + .../Commands/Current/FileReadCommand.cs | 35 + .../Commands/Current/FileWriteCommand.cs | 75 ++ .../Commands/Current/GetDeviceInfoCommand.cs | 39 - Source/v2/Meadow.Cli/Meadow.Cli.csproj | 1 + .../Meadow.Cli/Properties/launchSettings.json | 10 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 8 + .../SerialConnection.ListenerProc.cs | 17 +- .../Connections/SerialConnection.cs | 780 ++++++++++-------- ...HcomConnection.cs => IMeadowConnection.cs} | 7 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 1 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 7 +- .../Serial Requests/EndFileWriteRequest.cs | 6 + .../Serial Requests/InitFileReadRequest.cs | 19 - .../Serial Requests/InitFileWriteRequest.cs | 103 +++ .../v2/Meadow.Hcom/Serial Requests/Request.cs | 2 +- .../FileReadInitOkResponse.cs | 2 +- .../FileWriteInitFailedSerialResponse.cs | 9 + .../FileWriteInitOkSerialResponse.cs | 10 + .../Serial Responses/SerialResponse.cs | 4 + 22 files changed, 1151 insertions(+), 789 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs rename Source/v2/Meadow.Hcom/{IHcomConnection.cs => IMeadowConnection.cs} (81%) create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitFailedSerialResponse.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Responses/FileWriteInitOkSerialResponse.cs 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/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json index 974f8c8a..28210340 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", @@ -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/Commands/Current/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs new file mode 100644 index 00000000..a74cbeee --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs @@ -0,0 +1,43 @@ +using CliFx.Attributes; +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(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/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs new file mode 100644 index 00000000..545422fd --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs @@ -0,0 +1,35 @@ +using CliFx.Attributes; +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(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/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs new file mode 100644 index 00000000..fcf6e4a8 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs @@ -0,0 +1,75 @@ +using CliFx.Attributes; +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(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; + } + + 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/GetDeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs index 46ef2cd3..f7c43558 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs @@ -3,45 +3,6 @@ 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(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."); - } - } - } -} - [Command("device info", Description = "Get the device info")] public class DeviceInfoCommand : BaseDeviceCommand { diff --git a/Source/v2/Meadow.Cli/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index a18eed7e..777c1f43 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -5,6 +5,7 @@ net6.0 enable enable + 10 diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 5cb8f923..d428137c 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -21,7 +21,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM8" + "commandLineArgs": "config route COM10" }, "Config: Set Route TCP": { "commandName": "Project", @@ -54,6 +54,14 @@ "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\"" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 68948b05..168740a1 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -16,6 +16,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public event EventHandler FileReadCompleted; public event EventHandler FileException; public event EventHandler ConnectionError; + public event EventHandler FileWriteAccepted; internal abstract Task DeliverRequest(IRequest request); public abstract string Name { get; } @@ -71,6 +72,9 @@ public void EnqueueRequest(IRequest command) LocalFileName = sfr.LocalFileName, }; } + else if (command is InitFileWriteRequest fwr) + { + } _pendingCommands.Enqueue(command); } @@ -132,4 +136,8 @@ public void Dispose() GC.SuppressFinalize(this); } + public Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 307bfd05..1f7a8322 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -190,8 +190,8 @@ private async Task ListenerProc() _readFileInfo.FileStream = File.Create(_readFileInfo.LocalFileName); var uploadRequest = RequestBuilder.Build(); - - (this as IMeadowConnection).EnqueueRequest(uploadRequest); + EncodeAndSendPacket(uploadRequest.Serialize()); + // (this as IMeadowConnection).EnqueueRequest(uploadRequest); } else if (response is UploadDataPacketResponse udp) { @@ -226,6 +226,15 @@ private async Task ListenerProc() { _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}"); @@ -239,8 +248,10 @@ private async Task ListenerProc() { FileException?.Invoke(this, dnf); } - catch (IOException) + catch (IOException ioe) { + + FileException?.Invoke(this, ioe); // attempt to read timed out (i.e. there's just no data) // NOP } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index b0431bcd..ef3e670a 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -3,470 +3,576 @@ using System.Diagnostics; using System.IO.Ports; -namespace Meadow.Hcom -{ - public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); +namespace Meadow.Hcom; + +public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); - public partial class SerialConnection : IDisposable, IMeadowConnection +public partial class SerialConnection : IDisposable, IMeadowConnection +{ + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + + public event EventHandler FileReadCompleted = delegate { }; + public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + public event EventHandler ConnectionError; + public event EventHandler FileWriteAccepted; + + private SerialPort _port; + private ILogger? _logger; + private bool _isDisposed; + private ConnectionState _state; + private readonly CancellationTokenSource _cts; + 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 IMeadowDevice? Device { get; private set; } + public string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) { - public const int DefaultBaudRate = 115200; - public const int ReadBufferSizeBytes = 0x2000; - - public event EventHandler FileReadCompleted = delegate { }; - public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; - public event EventHandler ConnectionError; - - private SerialPort _port; - private ILogger? _logger; - private bool _isDisposed; - private ConnectionState _state; - private readonly CancellationTokenSource _cts; - 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 IMeadowDevice? Device { get; private set; } - public string Name { get; } - - public SerialConnection(string port, ILogger? logger = default) - { - _cts = new CancellationTokenSource(); + _cts = new CancellationTokenSource(); - if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) - { - throw new ArgumentException($"Serial Port '{port}' not found."); - } + 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; + Name = port; + State = ConnectionState.Disconnected; + _logger = logger; + _port = new SerialPort(port); + _port.ReadTimeout = _port.WriteTimeout = 5000; - new Task( - () => _ = ListenerProc(), - TaskCreationOptions.LongRunning) - .Start(); + new Task( + () => _ = ListenerProc(), + TaskCreationOptions.LongRunning) + .Start(); - new Thread(CommandManager) - { - IsBackground = true, - Name = "HCOM Sender" - } - .Start(); + new Thread(CommandManager) + { + IsBackground = true, + Name = "HCOM Sender" } + .Start(); + } - private bool MaintainConnection + private bool MaintainConnection + { + get => _maintainConnection; + set { - get => _maintainConnection; - set - { - if (value == MaintainConnection) return; + if (value == MaintainConnection) return; - _maintainConnection = value; + _maintainConnection = value; - if (value) + if (value) + { + if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) { - if (_connectionManager == null || _connectionManager.ThreadState != System.Threading.ThreadState.Running) + _connectionManager = new Thread(ConnectionManagerProc) { - _connectionManager = new Thread(ConnectionManagerProc) - { - IsBackground = true, - Name = "HCOM Connection Manager" - }; - _connectionManager.Start(); - - } + IsBackground = true, + Name = "HCOM Connection Manager" + }; + _connectionManager.Start(); + } } } + } - private void ConnectionManagerProc() + private void ConnectionManagerProc() + { + while (_maintainConnection) { - while (_maintainConnection) + if (!_port.IsOpen) { - if (!_port.IsOpen) + try { - try - { - Debug.WriteLine("Opening COM port..."); - _port.Open(); - Debug.WriteLine("Opened COM port"); - } - catch (Exception ex) - { - Debug.WriteLine($"{ex.Message}"); - Thread.Sleep(1000); - } + Debug.WriteLine("Opening COM port..."); + _port.Open(); + Debug.WriteLine("Opened COM port"); } - else + catch (Exception ex) { + Debug.WriteLine($"{ex.Message}"); Thread.Sleep(1000); } } - } - - public void AddListener(IConnectionListener listener) - { - lock (_listeners) + else { - _listeners.Add(listener); + Thread.Sleep(1000); } - - Open(); - - MaintainConnection = true; } + } - public void RemoveListener(IConnectionListener listener) + public void AddListener(IConnectionListener listener) + { + lock (_listeners) { - lock (_listeners) - { - _listeners.Remove(listener); - } - - // TODO: stop maintaining connection? + _listeners.Add(listener); } - public ConnectionState State - { - get => _state; - private set - { - if (value == State) return; + Open(); - var old = _state; - _state = value; - ConnectionStateChanged?.Invoke(this, old, State); - } - } + MaintainConnection = true; + } - private void Open() + public void RemoveListener(IConnectionListener listener) + { + lock (_listeners) { - if (!_port.IsOpen) - { - _port.Open(); - } - State = ConnectionState.Connected; + _listeners.Remove(listener); } - private void Close() + // TODO: stop maintaining connection? + } + + public ConnectionState State + { + get => _state; + private set { - if (_port.IsOpen) - { - _port.Close(); - } + if (value == State) return; - State = ConnectionState.Disconnected; + var old = _state; + _state = value; + ConnectionStateChanged?.Invoke(this, old, State); } + } - public async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + private void Open() + { + if (!_port.IsOpen) { - try - { - // ensure the port is open - Open(); + _port.Open(); + } + State = ConnectionState.Connected; + } - // search for the device via HCOM - we'll use a simple command since we don't have a "ping" - var command = RequestBuilder.Build(); + private void Close() + { + if (_port.IsOpen) + { + _port.Close(); + } - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang + State = ConnectionState.Disconnected; + } - _port.DiscardInBuffer(); + public async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + try + { + // ensure the port is open + Open(); - // wait for a response - var timeout = timeoutSeconds * 2; - var dataReceived = false; + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" + var command = RequestBuilder.Build(); - // local function so we can unsubscribe - var count = _messageCount; + // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang - _pendingCommands.Enqueue(command); + _port.DiscardInBuffer(); - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); + // wait for a response + var timeout = timeoutSeconds * 2; + var dataReceived = false; - if (count != _messageCount) - { - dataReceived = true; - break; - } + // local function so we can unsubscribe + var count = _messageCount; - await Task.Delay(500); - } + _pendingCommands.Enqueue(command); - // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); - // create the device instance - if (dataReceived) + if (count != _messageCount) { - Device = new MeadowDevice(this); + dataReceived = true; + break; } - return Device; + await Task.Delay(500); } - catch (Exception e) + + // 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) { - _logger?.LogError(e, "Failed to connect"); - throw; + Device = new MeadowDevice(this); } + + return Device; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to connect"); + throw; } + } - private void CommandManager() + private void CommandManager() + { + while (!_isDisposed) { - while (!_isDisposed) + while (_pendingCommands.Count > 0) { - while (_pendingCommands.Count > 0) - { - Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); - var command = _pendingCommands.Dequeue() as Request; + var command = _pendingCommands.Dequeue() as Request; - var payload = command.Serialize(); - EncodeAndSendPacket(payload); + // if this is a file write, we need to packetize for progress - // TODO: re-queue on fail? - } + var payload = command.Serialize(); + EncodeAndSendPacket(payload); - Thread.Sleep(1000); + // TODO: re-queue on fail? } + + Thread.Sleep(1000); } + } - private class ReadFileInfo - { - private string? _localFileName; + private class ReadFileInfo + { + private string? _localFileName; - public string MeadowFileName { get; set; } = default!; - public string? LocalFileName + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get { - get - { - if (_localFileName != null) return _localFileName; + if (_localFileName != null) return _localFileName; - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); - } - set => _localFileName = value; + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); } - public FileStream FileStream { get; set; } = default!; + set => _localFileName = value; } + public FileStream FileStream { get; set; } = default!; + } - public void EnqueueRequest(IRequest command) - { - // TODO: verify we're connected + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected - if (command is InitFileReadRequest sfr) + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo { - _readFileInfo = new ReadFileInfo - { - MeadowFileName = sfr.MeadowFileName, - LocalFileName = sfr.LocalFileName, - }; - } - - _pendingCommands.Enqueue(command); + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; } - private void EncodeAndSendPacket(byte[] messageBytes) - { - Debug.WriteLine($"+EncodeAndSendPacket({messageBytes.Length} bytes)"); + _pendingCommands.Enqueue(command); + } - while (!_port.IsOpen) - { - _state = ConnectionState.Disconnected; - Thread.Sleep(100); - // wait for the port to open - } + private void EncodeAndSendPacket(byte[] messageBytes) + { + EncodeAndSendPacket(messageBytes, messageBytes.Length); + } - _state = ConnectionState.Connected; + private void EncodeAndSendPacket(byte[] messageBytes, int length) + { + Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - try - { - int encodedToSend; - byte[] encodedBytes; + while (!_port.IsOpen) + { + _state = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } - // For file download this is a LOT of messages - // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + _state = ConnectionState.Connected; - // For testing calculate the crc including the sequence number - //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); - try - { - // The encoded size using COBS is just a bit more than the original size adding 1 byte - // every 254 bytes plus 1 and need room for beginning and ending delimiters. - encodedBytes = new byte[Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE]; + try + { + int encodedToSend; + byte[] encodedBytes; - // Skip over first byte so it can be a start delimiter - encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, messageBytes.Length, ref encodedBytes, 1); + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); - // DEBUG TESTING - if (encodedToSend == -1) - { - _logger?.LogError($"Error - encodedToSend == -1"); - return; - } + // 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. + encodedBytes = new byte[Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE]; - 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; - } + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); - // Add delimiters to packet boundaries - try - { - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; - } - catch (Exception encodedBytesEx) + // DEBUG TESTING + if (encodedToSend == -1) { - // This should drop the connection and retry - Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); - Thread.Sleep(500); // Place for break point - throw; + _logger?.LogError($"Error - encodedToSend == -1"); + return; } - try - { - // Send the data to Meadow - Debug.WriteLine($"Sending {encodedToSend} bytes..."); - _port.Write(encodedBytes, 0, encodedToSend); - Debug.WriteLine($"sent"); - } - catch (InvalidOperationException ioe) // Port not opened - { - string msg = string.Format("Write but port not opened. Exception: {0}", ioe); - _logger?.LogError(msg); - throw; - } - catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer - { - string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); - _logger?.LogError(msg); - throw; - } - catch (ArgumentException ae) // offset plus count > buffer length + if (_port == null) { - 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; + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); } } catch (Exception except) { - // DID YOU RESTART MEADOW? + 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 - _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; + } + + try + { + // Send the data to Meadow + Debug.WriteLine($"Sending {encodedToSend} bytes..."); + _port.Write(encodedBytes, 0, encodedToSend); + Debug.WriteLine($"sent"); + } + catch (InvalidOperationException ioe) // Port not opened + { + string msg = string.Format("Write but port not opened. Exception: {0}", ioe); + _logger?.LogError(msg); + throw; + } + catch (ArgumentOutOfRangeException aore) // offset or count don't match buffer + { + string msg = string.Format("Write buffer, offset and count don't line up. Exception: {0}", aore); + _logger?.LogError(msg); + throw; + } + catch (ArgumentException ae) // offset plus count > buffer length + { + string msg = string.Format($"Write offset plus count > buffer length. Exception: {0}", ae); + _logger?.LogError(msg); + throw; + } + catch (TimeoutException te) // Took too long to send + { + string msg = string.Format("Write took too long to send. Exception: {0}", te); + _logger?.LogError(msg); throw; } } + catch (Exception except) + { + // DID YOU RESTART MEADOW? + // This should drop the connection and retry + _logger?.LogError($"EncodeAndSendPacket threw: {except}"); + throw; + } + } - private class SerialMessage + private class SerialMessage + { + private readonly IList> _segments; + + public SerialMessage(Memory segment) { - private readonly IList> _segments; + _segments = new List>(); + _segments.Add(segment); + } - public SerialMessage(Memory segment) - { - _segments = new List>(); - _segments.Add(segment); - } + public void AddSegment(Memory segment) + { + _segments.Add(segment); + } - public void AddSegment(Memory segment) + public byte[] ToArray() + { + using var ms = new MemoryStream(); + foreach (var segment in _segments) { - _segments.Add(segment); + // 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(); + } + } - 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; } - private bool DecodeAndProcessPacket(Memory packetBuffer, CancellationToken cancellationToken) + 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) { - 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) + if (disposing) { - //_logger.LogTrace("Throwing out 0x00 from buffer"); - return false; + Close(); + _port.Dispose(); } - var decodedSize = CobsTools.CobsDecoding(packetBuffer.ToArray(), packetLength, ref decodedBuffer); + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // ---------------------------------------------- + // ---------------------------------------------- + // ---------------------------------------------- + + private Exception? _lastException; + + public int CommandTimeoutSeconds { get; set; } = 30; - /* - // If a message is too short it is ignored - if (decodedSize < MeadowDeviceManager.ProtocolHeaderSize) + 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()) { - return false; + break; } - Debug.Assert(decodedSize <= MeadowDeviceManager.MaxAllowableMsgPacketLength); + await Task.Delay(500); + } - // Process the received packet - ParseAndProcessReceivedPacket(decodedBuffer.AsSpan(0, decodedSize).ToArray(), - cancellationToken); + return true; + } - */ - ArrayPool.Shared.Return(decodedBuffer); - return true; - } + public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.SetParameters(localFileName, meadowFileName ?? Path.GetFileName(localFileName)); - protected virtual void Dispose(bool disposing) + var accepted = false; + Exception? ex = null; + + void OnFileWriteAccepted(object? sender, EventArgs a) { - if (!_isDisposed) - { - if (disposing) - { - Close(); - _port.Dispose(); - } + accepted = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } - _isDisposed = true; - } + FileWriteAccepted += OnFileWriteAccepted; + FileException += OnFileError; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return accepted; + }, + cancellationToken)) + { + return false; } - public void Dispose() + // 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; + + while (true) { - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) + { + return false; + } + + sequenceNumber++; + + // sequenc 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; + EncodeAndSendPacket(packet, bytesRead + 2); + } + + // finish with an "end" message - not enqued because this is all a serial operation + var request = RequestBuilder.Build(); + var p = request.Serialize(); + EncodeAndSendPacket(p); + + return true; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IHcomConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs similarity index 81% rename from Source/v2/Meadow.Hcom/IHcomConnection.cs rename to Source/v2/Meadow.Hcom/IMeadowConnection.cs index c48596ec..11901ea2 100644 --- a/Source/v2/Meadow.Hcom/IHcomConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -5,6 +5,7 @@ public interface IMeadowConnection event EventHandler FileReadCompleted; event EventHandler FileException; event EventHandler ConnectionError; + event EventHandler FileWriteAccepted; string Name { get; } IMeadowDevice? Device { get; } @@ -16,5 +17,11 @@ public interface IMeadowConnection void AddListener(IConnectionListener listener); void RemoveListener(IConnectionListener listener); void EnqueueRequest(IRequest command); + + + + Task WriteFile(string localFileName, string? meadowFileName = null, 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 index 657c9ab8..1bf0b834 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -9,6 +9,7 @@ public interface IMeadowDevice 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 FlashOS(string requestedversion, CancellationToken? cancellationToken = null); Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index d8bddf9f..91d10683 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -232,12 +232,7 @@ void OnFileError(object? sender, Exception exception) public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - command.LocalFileName = localFileName; - command.MeadowFileName = meadowFileName; - - _connection.EnqueueRequest(command); - return false; + return await _connection.WriteFile(localFileName, meadowFileName, cancellationToken); } public Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null) 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..75199cc8 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs @@ -0,0 +1,6 @@ +namespace Meadow.Hcom; + +internal class EndFileWriteRequest : Request +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER; +} diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs index 0d95fb09..44f1c9d1 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs @@ -2,25 +2,6 @@ namespace Meadow.Hcom; -internal class InitFileWriteRequest : Request -{ - public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER; - - 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); - } - } -} - internal class InitFileReadRequest : Request { public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_UPLOAD_FILE_INIT; 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..995d9da7 --- /dev/null +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs @@ -0,0 +1,103 @@ +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 +{ + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER; + + 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) + { + // file write has additional header payload that's sent with the request, build it here + + var source = new FileInfo(localFile); + + if (!source.Exists) throw new FileNotFoundException(); + + LocalFileName = localFile; + MeadowFileName = meadowFileName; + + var nameBytes = Encoding.ASCII.GetBytes(meadowFileName); + var data = File.ReadAllBytes(source.FullName); + 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(0), 0, Payload, 8, 4); // TODO: flash address + Array.Copy(espHash, 0, Payload, 12, espHash.Length); // TODO: ESP hash + 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 index d5daf3c8..1f8aa7d6 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/Request.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs @@ -19,7 +19,7 @@ public Request() public byte[]? Payload { get; protected set; } - public byte[] Serialize() + public virtual byte[] Serialize() { var messageBytes = new byte[2 + 2 + 2 + 2 + 4 + (Payload?.Length ?? 0)]; diff --git a/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs index e22c4cca..e3855f4e 100644 --- a/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs +++ b/Source/v2/Meadow.Hcom/Serial Responses/FileReadInitOkResponse.cs @@ -6,4 +6,4 @@ 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/SerialResponse.cs b/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs index b0963ebc..81925df6 100644 --- a/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs +++ b/Source/v2/Meadow.Hcom/Serial Responses/SerialResponse.cs @@ -54,6 +54,10 @@ public static SerialResponse Parse(byte[] data, int length) 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); } From b9a1813534a970a5ed615722f9202cd110c677fd Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 16 Aug 2023 11:27:16 -0500 Subject: [PATCH 03/22] major v2 refactor --- .../Meadow.Hcom/Connections/ConnectionBase.cs | 19 +- .../SerialConnection.ListenerProc.cs | 30 +- .../Connections/SerialConnection.cs | 261 +++++++++++++++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 60 ++++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 14 +- .../MeadowDevice.ResponseListener.cs | 57 ---- Source/v2/Meadow.Hcom/MeadowDevice.cs | 259 ++--------------- 7 files changed, 361 insertions(+), 339 deletions(-) delete mode 100644 Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 168740a1..294afed1 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -13,14 +13,22 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public ConnectionState State { get; protected set; } public IMeadowDevice? Device { get; protected set; } - public event EventHandler FileReadCompleted; - public event EventHandler FileException; public event EventHandler ConnectionError; - public event EventHandler FileWriteAccepted; internal abstract Task DeliverRequest(IRequest request); public abstract string Name { get; } + 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 Reset(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 ConnectionBase() { @@ -135,9 +143,4 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - public Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) - { - throw new NotImplementedException(); - } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 1f7a8322..f102a371 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -93,34 +93,26 @@ private async Task ListenerProc() // send the message to any listeners Debug.WriteLine($"INFO> {tir.Text}"); - foreach (var listener in _listeners) - { - listener.OnInformationMessageReceived(tir.Text); - } + InfoMessages.Add(tir.Text); } else if (response is TextStdOutResponse tso) { // send the message to any listeners Debug.WriteLine($"STDOUT> {tso.Text}"); - foreach (var listener in _listeners) - { - listener.OnStdOutReceived(tso.Text); - } + StdOut.Add(tso.Text); } else if (response is TextStdErrResponse tse) { // send the message to any listeners Debug.WriteLine($"STDERR> {tse.Text}"); - foreach (var listener in _listeners) - { - listener.OnStdErrReceived(tse.Text); - } + StdErr.Add(tse.Text); } else if (response is TextListHeaderResponse tlh) { // start of a list + _textListComplete = false; _textList.Clear(); } else if (response is TextListMemberResponse tlm) @@ -133,10 +125,7 @@ private async Task ListenerProc() } else if (response is TextConcludedResponse tcr) { - foreach (var listener in _listeners) - { - listener.OnTextMessageConcluded((int)tcr.RequestType); - } + _lastRequestConcluded = (int)tcr.RequestType; if (_reconnectInProgress) { @@ -145,9 +134,9 @@ private async Task ListenerProc() } else { - foreach (var listener in _listeners) + if (_textListComplete != null) { - listener.OnTextListReceived(_textList.ToArray()); + _textListComplete = true; } } } @@ -158,10 +147,7 @@ private async Task ListenerProc() } else if (response is DeviceInfoSerialResponse dir) { - foreach (var listener in _listeners) - { - listener.OnDeviceInformationMessageReceived(dir.Fields); - } + _deviceInfo = new DeviceInfo(dir.Fields); } else if (response is ReconnectRequiredResponse rrr) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index ef3e670a..f576ea1f 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -12,16 +12,16 @@ public partial class SerialConnection : IDisposable, IMeadowConnection public const int DefaultBaudRate = 115200; public const int ReadBufferSizeBytes = 0x2000; - public event EventHandler FileReadCompleted = delegate { }; + private event EventHandler FileReadCompleted = delegate { }; + private event EventHandler FileWriteAccepted; + public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; public event EventHandler ConnectionError; - public event EventHandler FileWriteAccepted; private SerialPort _port; private ILogger? _logger; private bool _isDisposed; private ConnectionState _state; - private readonly CancellationTokenSource _cts; private List _listeners = new List(); private Queue _pendingCommands = new Queue(); private bool _maintainConnection; @@ -36,8 +36,6 @@ public partial class SerialConnection : IDisposable, IMeadowConnection public SerialConnection(string port, ILogger? logger = default) { - _cts = new CancellationTokenSource(); - if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) { throw new ArgumentException($"Serial Port '{port}' not found."); @@ -482,6 +480,7 @@ public void Dispose() // ---------------------------------------------- private Exception? _lastException; + private bool? _textListComplete; public int CommandTimeoutSeconds { get; set; } = 30; @@ -507,6 +506,213 @@ private async Task WaitForResult(Func checkAction, CancellationToken return true; } + private DeviceInfo? _deviceInfo; + private int? _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 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 == 0x303) + { + return true; + } + + return false; + }, cancellationToken); + } + + public 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 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 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 WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeSucessfullyEnabledToken)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + + if (!success) throw new Exception("Unable to enable runtime"); + } + + public 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 WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeSucessfullyDisabledToken)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + + if (!success) throw new Exception("Unable to disable runtime"); + } + + public async Task Reset(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 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 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 async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -575,4 +781,49 @@ void OnFileError(object? sender, Exception exception) return true; } + + public 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 index 73a4b016..54e847b5 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -47,6 +47,21 @@ public TcpConnection(string uri) // 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}"); + } + } + internal override async Task DeliverRequest(IRequest request) { if (request is GetDeviceInfoRequest) @@ -85,4 +100,49 @@ public Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } + + public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task Reset(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(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 11901ea2..b541a593 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -2,10 +2,7 @@ { public interface IMeadowConnection { - event EventHandler FileReadCompleted; - event EventHandler FileException; event EventHandler ConnectionError; - event EventHandler FileWriteAccepted; string Name { get; } IMeadowDevice? Device { get; } @@ -20,8 +17,17 @@ public interface IMeadowConnection - Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); + 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 Reset(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); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs b/Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs deleted file mode 100644 index 6f1d712a..00000000 --- a/Source/v2/Meadow.Hcom/MeadowDevice.ResponseListener.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace Meadow.Hcom -{ - public partial class MeadowDevice - { - internal class ResponseListener : IConnectionListener - { - public List StdOut { get; } = new List(); - public List StdErr { get; } = new List(); - public List Information { get; } = new List(); - public Dictionary DeviceInfo { get; private set; } = new Dictionary(); - public List TextList { get; } = new List(); - public string? LastError { get; set; } - public int? LastRequestConcluded { get; set; } - - public void OnTextMessageConcluded(int requestType) - { - LastRequestConcluded = requestType; - } - - public void OnStdOutReceived(string message) - { - StdOut.Add(message); - } - - public void OnStdErrReceived(string message) - { - StdErr.Add(message); - } - - public void OnInformationMessageReceived(string message) - { - Information.Add(message); - } - - public void OnDeviceInformationMessageReceived(Dictionary deviceInfo) - { - DeviceInfo = deviceInfo; - } - - public void OnTextListReceived(string[] list) - { - TextList.Clear(); - TextList.AddRange(list); - } - - public void OnErrorTextReceived(string message) - { - LastError = message; - } - - public void OnFileError() - { - throw new Exception(LastError); - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 91d10683..5230c875 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -3,231 +3,45 @@ public partial class MeadowDevice : IMeadowDevice { private IMeadowConnection _connection; - private ResponseListener _listener; - private Exception? _lastException; - - public int CommandTimeoutSeconds { get; set; } = 30; internal MeadowDevice(IMeadowConnection connection) { _connection = connection; - _connection.AddListener(_listener = new ResponseListener()); - _connection.ConnectionError += (s, e) => _lastException = e; - } - - 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; } public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _listener.Information.Clear(); - - _connection.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 (_listener.Information.Count > 0) - { - var m = _listener.Information.FirstOrDefault(i => i.Contains("Mono is")); - if (m != null) - { - return m == "Mono is enabled"; - } - } - - await Task.Delay(500); - } - return false; + return await _connection.IsRuntimeEnabled(cancellationToken); } public async Task Reset(CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _connection.EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - await _connection.WaitForMeadowAttach(cancellationToken); + await _connection.Reset(cancellationToken); } public async Task RuntimeDisable(CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _listener.Information.Clear(); - - _connection.EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - var success = await WaitForResult(() => - { - if (_listener.Information.Count > 0) - { - var m = _listener.Information.FirstOrDefault(i => i.Contains("Mono is disabled")); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); - - if (!success) throw new Exception("Unable to disable runtime"); + await _connection.RuntimeDisable(cancellationToken); } public async Task RuntimeEnable(CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _listener.Information.Clear(); - - _connection.EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - var success = await WaitForResult(() => - { - if (_listener.Information.Count > 0) - { - var m = _listener.Information.FirstOrDefault(i => i.Contains("Meadow successfully started MONO")); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); - - if (!success) throw new Exception("Unable to enable runtime"); + await _connection.RuntimeEnable(cancellationToken); } public async Task GetDeviceInfo(CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _listener.DeviceInfo.Clear(); - - _lastException = null; - _connection.EnqueueRequest(command); - - if (!await WaitForResult( - () => _listener.DeviceInfo.Count > 0, - cancellationToken)) - { - return null; - } - - return new DeviceInfo(_listener.DeviceInfo); + return await _connection.GetDeviceInfo(cancellationToken); } public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - command.IncludeCrcs = includeCrcs; - - _listener.DeviceInfo.Clear(); - - _connection.EnqueueRequest(command); - - if (!await WaitForResult( - () => _listener.TextList.Count > 0, - cancellationToken)) - { - return null; - } - - var list = new List(); - - foreach (var candidate in _listener.TextList) - { - // TODO: should this be part of the connection? A serial response might be different than a future response (TCP or whatever) - var fi = MeadowFileInfo.Parse(candidate); - if (fi != null) - { - list.Add(fi); - } - } - - return list.ToArray(); - + return await _connection.GetFileList(includeCrcs, cancellationToken); } public 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 - { - _connection.FileReadCompleted += OnFileReadCompleted; - _connection.FileException += OnFileError; - - _connection.EnqueueRequest(command); - - if (!await WaitForResult( - () => - { - if (ex != null) throw ex; - return completed; - }, - cancellationToken)) - { - return false; - } - - return true; - } - finally - { - _connection.FileReadCompleted -= OnFileReadCompleted; - _connection.FileException -= OnFileError; - } + return await _connection.ReadFile(meadowFileName, localFileName, cancellationToken); } public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) @@ -235,70 +49,29 @@ public async Task WriteFile(string localFileName, string? meadowFileName = return await _connection.WriteFile(localFileName, meadowFileName, cancellationToken); } - public Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null) + public async Task GetRtcTime(CancellationToken? cancellationToken = null) { - throw new NotImplementedException(); + return await _connection.GetRtcTime(cancellationToken); } - public Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null) + public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) { - throw new NotImplementedException(); + await _connection.SetRtcTime(dateTime, cancellationToken); } - public Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null) + public Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } - public async Task GetRtcTime(CancellationToken? cancellationToken = null) + public Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - - _listener.Information.Clear(); - - _connection.EnqueueRequest(command); - - DateTimeOffset? now = null; - - var success = await WaitForResult(() => - { - var token = "UTC time:"; - - if (_listener.Information.Count > 0) - { - var m = _listener.Information.FirstOrDefault(i => i.Contains(token)); - if (m != null) - { - var timeString = m.Substring(m.IndexOf(token) + token.Length); - now = DateTimeOffset.Parse(timeString); - return true; - } - } - - return false; - }, cancellationToken); - - return now; + throw new NotImplementedException(); } - public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + public Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null) { - var command = RequestBuilder.Build(); - command.Time = dateTime; - - _listener.LastRequestConcluded = null; - - _connection.EnqueueRequest(command); - - var success = await WaitForResult(() => - { - if (_listener.LastRequestConcluded != null && _listener.LastRequestConcluded == 0x303) - { - return true; - } - - return false; - }, cancellationToken); + throw new NotImplementedException(); } } } \ No newline at end of file From d4021cdd40d13c8ad1cf394f8e448723d46bb7a1 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 16 Aug 2023 12:32:00 -0500 Subject: [PATCH 04/22] loads more code cleanup --- .../Commands/Current/ListenCommand.cs | 49 ++++++++ .../v2/Meadow.Cli/MeadowConnectionManager.cs | 13 ++- .../Meadow.Cli/Properties/launchSettings.json | 8 ++ .../SerialCommandTests.cs | 106 ++---------------- .../SerialConnectionTests.cs | 4 +- .../TcpCommandTests.cs | 25 +---- .../TestListener.cs | 56 --------- .../Meadow.Hcom/Connections/ConnectionBase.cs | 103 ++--------------- .../SerialConnection.ListenerProc.cs | 5 +- .../Connections/SerialConnection.cs | 28 +++-- .../Meadow.Hcom/Connections/TcpConnection.cs | 38 +------ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 12 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 2 +- 13 files changed, 115 insertions(+), 334 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs delete mode 100644 Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs 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..4f12d5a9 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -0,0 +1,49 @@ +using CliFx.Attributes; +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(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger.LogInformation($"Cancelled."); + } +} diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 7ac1c863..1b4ea9e6 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -7,6 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class MeadowConnectionManager { private ISettingsManager _settingsManager; + private IMeadowConnection? _currentConnection; public MeadowConnectionManager(ISettingsManager settingsManager) { @@ -22,6 +23,9 @@ public MeadowConnectionManager(ISettingsManager settingsManager) 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")) @@ -39,8 +43,13 @@ public MeadowConnectionManager(ISettingsManager settingsManager) if (uri != null) { - return new TcpConnection(uri); + _currentConnection = new TcpConnection(uri); + } + else + { + _currentConnection = new SerialConnection(route); } - return new SerialConnection(route); + + return _currentConnection; } } diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index d428137c..3709ec4e 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -3,6 +3,14 @@ "Meadow.Cli": { "commandName": "Project" }, + "Listen": { + "commandName": "Project", + "commandLineArgs": "listen" + }, + "Listen (no prefix)": { + "commandName": "Project", + "commandLineArgs": "listen -n" + }, "Device Info": { "commandName": "Project", "commandLineArgs": "device info" diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs index e654a588..d60dc865 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -4,7 +4,7 @@ namespace Meadow.HCom.Integration.Tests { public class SerialCommandTests { - public string ValidPortName { get; } = "COM9"; + public string ValidPortName { get; } = "COM10"; [Fact] public async void TestDeviceReset() @@ -13,30 +13,9 @@ public async void TestDeviceReset() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var listener = new TestListener(); - connection.AddListener(listener); + await connection.ResetDevice(); - var command = RequestBuilder.Build(); - command.SequenceNumber = 0; - - // 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 - - ((IMeadowConnection)connection).EnqueueRequest(command); - - var timeoutSecs = 10; - - while (timeoutSecs-- > 0) - { - if (listener.Messages.Count > 0) - { - break; - } - - await Task.Delay(1000); - } - - Assert.True(listener.Messages.Count > 0); + // TODO: find a way to verify device reset } } @@ -47,30 +26,9 @@ public async void TestGetDeviceInfo() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var listener = new TestListener(); - connection.AddListener(listener); - - var command = RequestBuilder.Build(); - command.SequenceNumber = 0; + var info = await connection.GetDeviceInfo(); - // 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 - - ((IMeadowConnection)connection).EnqueueRequest(command); - - var timeoutSecs = 10; - - while (timeoutSecs-- > 0) - { - if (listener.DeviceInfo.Count > 0) - { - break; - } - - await Task.Delay(1000); - } - - Assert.True(listener.DeviceInfo.Count > 0); + Assert.NotNull(info); } } @@ -81,30 +39,10 @@ public async void TestGetFileListNoCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var listener = new TestListener(); - connection.AddListener(listener); - - var command = RequestBuilder.Build(); - command.SequenceNumber = 0; + var files = await connection.GetFileList(false); - // 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 - - ((IMeadowConnection)connection).EnqueueRequest(command); - - var timeoutSecs = 10; - - while (timeoutSecs-- > 0) - { - if (listener.DeviceInfo.Count > 0) - { - break; - } - - await Task.Delay(1000); - } - - Assert.True(listener.TextList.Count > 0); + Assert.NotNull(files); + Assert.True(files.Length > 0); } } @@ -115,32 +53,10 @@ public async void TestGetFileListWithCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var listener = new TestListener(); - connection.AddListener(listener); - - var command = RequestBuilder.Build(); - command.IncludeCrcs = true; - - command.SequenceNumber = 0; - - // 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 - - ((IMeadowConnection)connection).EnqueueRequest(command); - - var timeoutSecs = 10; - - while (timeoutSecs-- > 0) - { - if (listener.DeviceInfo.Count > 0) - { - break; - } - - await Task.Delay(1000); - } + var files = await connection.GetFileList(true); - Assert.True(listener.TextList.Count > 0); + Assert.NotNull(files); + Assert.True(files.Length > 0); } } } diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs index f9b83856..92467a24 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -4,7 +4,7 @@ namespace Meadow.HCom.Integration.Tests { public class SerialConnectionTests { - public string ValidPortName { get; } = "COM3"; + public string ValidPortName { get; } = "COM10"; [Fact] public void TestInvalidPortName() @@ -20,6 +20,7 @@ public async void TestListen() { using (var connection = new SerialConnection(ValidPortName)) { + /* Assert.Equal(ConnectionState.Disconnected, connection.State); var listener = new TestListener(); @@ -41,6 +42,7 @@ public async void TestListen() } Assert.True(listener.Messages.Count > 0); + */ } } diff --git a/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs index 524a4f18..f98ea581 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/TcpCommandTests.cs @@ -13,30 +13,9 @@ public async void TestGetDeviceInfo() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var listener = new TestListener(); - connection.AddListener(listener); + var info = await connection.GetDeviceInfo(); - var command = RequestBuilder.Build(); - command.SequenceNumber = 0; - - // 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 - - connection.EnqueueRequest(command); - - var timeoutSecs = 10; - - while (timeoutSecs-- > 0) - { - if (listener.DeviceInfo.Count > 0) - { - break; - } - - await Task.Delay(1000); - } - - Assert.True(listener.DeviceInfo.Count > 0); + Assert.NotNull(info); } } diff --git a/Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs b/Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs deleted file mode 100644 index f6cb6f37..00000000 --- a/Source/v2/Meadow.HCom.Integration.Tests/TestListener.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Meadow.Hcom; - -namespace Meadow.HCom.Integration.Tests -{ - public class TestListener : IConnectionListener - { - public List StdOut { get; } = new List(); - public List StdErr { get; } = new List(); - public List Messages { get; } = new List(); - public Dictionary DeviceInfo { get; private set; } = new Dictionary(); - public List TextList { get; } = new List(); - public string? LastError { get; set; } - public int? LastRequestConcluded { get; set; } - - public void OnTextMessageConcluded(int requestType) - { - LastRequestConcluded = requestType; - } - - public void OnStdOutReceived(string message) - { - StdOut.Add(message); - } - - public void OnStdErrReceived(string message) - { - StdErr.Add(message); - } - - public void OnInformationMessageReceived(string message) - { - Messages.Add(message); - } - - public void OnDeviceInformationMessageReceived(Dictionary deviceInfo) - { - DeviceInfo = deviceInfo; - } - - public void OnTextListReceived(string[] list) - { - TextList.Clear(); - TextList.AddRange(list); - } - - public void OnErrorTextReceived(string message) - { - LastError = message; - } - - public void OnFileError() - { - throw new Exception(LastError); - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 294afed1..4f8d41a0 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -1,29 +1,24 @@ -using System.Diagnostics; - -namespace Meadow.Hcom; +namespace Meadow.Hcom; public abstract class ConnectionBase : IMeadowConnection, IDisposable { - private readonly Queue _pendingCommands = new(); - private ReadFileInfo? _readFileInfo = null; private bool _isDisposed; - protected List ConnectionListeners { get; } = new(); - public ConnectionState State { get; protected set; } public IMeadowDevice? Device { get; protected set; } - public event EventHandler ConnectionError; + public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; + public event EventHandler ConnectionError = default!; - internal abstract Task DeliverRequest(IRequest request); 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 Reset(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); @@ -32,96 +27,16 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public ConnectionBase() { - new Thread(CommandManager) - { - IsBackground = true, - Name = "HCOM Sender" - } - .Start(); } - protected void RaiseConnectionError(Exception error) + protected void RaiseDeviceMessageReceived(string message, string? source) { - ConnectionError?.Invoke(this, error); + DeviceMessageReceived?.Invoke(this, (message, source)); } - public virtual void AddListener(IConnectionListener listener) - { - lock (ConnectionListeners) - { - ConnectionListeners.Add(listener); - } - } - - public virtual void RemoveListener(IConnectionListener listener) - { - lock (ConnectionListeners) - { - ConnectionListeners.Remove(listener); - } - - // TODO: stop maintaining connection? - } - - public Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) - { - throw new NotImplementedException(); - } - - public void EnqueueRequest(IRequest command) - { - // TODO: verify we're connected - - if (command is InitFileReadRequest sfr) - { - _readFileInfo = new ReadFileInfo - { - MeadowFileName = sfr.MeadowFileName, - LocalFileName = sfr.LocalFileName, - }; - } - else if (command is InitFileWriteRequest fwr) - { - } - - _pendingCommands.Enqueue(command); - } - - private void CommandManager() - { - while (!_isDisposed) - { - while (_pendingCommands.Count > 0) - { - Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); - - var command = _pendingCommands.Dequeue(); - - var response = DeliverRequest(command); - - // TODO: re-queue on fail? - } - - Thread.Sleep(1000); - } - } - - private class ReadFileInfo + protected void RaiseConnectionError(Exception error) { - 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!; + ConnectionError?.Invoke(this, error); } protected virtual void Dispose(bool disposing) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index f102a371..93ec2f82 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -9,7 +9,7 @@ public partial class SerialConnection public event EventHandler FileException = delegate { }; - public async Task WaitForMeadowAttach(CancellationToken? cancellationToken) + public override async Task WaitForMeadowAttach(CancellationToken? cancellationToken) { var timeout = 20; @@ -94,6 +94,7 @@ private async Task ListenerProc() Debug.WriteLine($"INFO> {tir.Text}"); InfoMessages.Add(tir.Text); + base.RaiseDeviceMessageReceived(tir.Text, "info"); } else if (response is TextStdOutResponse tso) { @@ -101,6 +102,7 @@ private async Task ListenerProc() Debug.WriteLine($"STDOUT> {tso.Text}"); StdOut.Add(tso.Text); + base.RaiseDeviceMessageReceived(tso.Text, "stdout"); } else if (response is TextStdErrResponse tse) { @@ -108,6 +110,7 @@ private async Task ListenerProc() Debug.WriteLine($"STDERR> {tse.Text}"); StdErr.Add(tse.Text); + base.RaiseDeviceMessageReceived(tse.Text, "stderr"); } else if (response is TextListHeaderResponse tlh) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index f576ea1f..dbf6b6f2 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -7,7 +7,7 @@ namespace Meadow.Hcom; public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); -public partial class SerialConnection : IDisposable, IMeadowConnection +public partial class SerialConnection : ConnectionBase, IDisposable { public const int DefaultBaudRate = 115200; public const int ReadBufferSizeBytes = 0x2000; @@ -16,7 +16,6 @@ public partial class SerialConnection : IDisposable, IMeadowConnection private event EventHandler FileWriteAccepted; public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; - public event EventHandler ConnectionError; private SerialPort _port; private ILogger? _logger; @@ -31,8 +30,7 @@ public partial class SerialConnection : IDisposable, IMeadowConnection private ReadFileInfo? _readFileInfo = null; private string? _lastError = null; - public IMeadowDevice? Device { get; private set; } - public string Name { get; } + public override string Name { get; } public SerialConnection(string port, ILogger? logger = default) { @@ -164,7 +162,7 @@ private void Close() State = ConnectionState.Disconnected; } - public async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { try { @@ -518,7 +516,7 @@ private async Task WaitForResult(Func checkAction, CancellationToken private const string RuntimeIsEnabledToken = "Mono is enabled"; private const string RtcRetrievalToken = "UTC time:"; - public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.Time = dateTime; @@ -538,7 +536,7 @@ public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancell }, cancellationToken); } - public async Task GetRtcTime(CancellationToken? cancellationToken = null) + public override async Task GetRtcTime(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -567,7 +565,7 @@ public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancell return now; } - public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -596,7 +594,7 @@ public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = return false; } - public async Task RuntimeEnable(CancellationToken? cancellationToken = null) + public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -624,7 +622,7 @@ public async Task RuntimeEnable(CancellationToken? cancellationToken = null) if (!success) throw new Exception("Unable to enable runtime"); } - public async Task RuntimeDisable(CancellationToken? cancellationToken = null) + public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -652,7 +650,7 @@ public async Task RuntimeDisable(CancellationToken? cancellationToken = null) if (!success) throw new Exception("Unable to disable runtime"); } - public async Task Reset(CancellationToken? cancellationToken = null) + public override async Task ResetDevice(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -664,7 +662,7 @@ public async Task Reset(CancellationToken? cancellationToken = null) await WaitForMeadowAttach(cancellationToken); } - public async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); @@ -683,7 +681,7 @@ public async Task Reset(CancellationToken? cancellationToken = null) return _deviceInfo; } - public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.IncludeCrcs = includeCrcs; @@ -713,7 +711,7 @@ public async Task Reset(CancellationToken? cancellationToken = null) return list.ToArray(); } - public async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + public override async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.SetParameters(localFileName, meadowFileName ?? Path.GetFileName(localFileName)); @@ -782,7 +780,7 @@ void OnFileError(object? sender, Exception exception) } - public async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); command.MeadowFileName = meadowFileName; diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 54e847b5..805f287f 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -62,41 +62,7 @@ public TcpConnection(string uri) } } - internal override async Task DeliverRequest(IRequest request) - { - if (request is GetDeviceInfoRequest) - { - try - { - var response = await _client.GetAsync($"{_baseUri}/api/info"); - - if (response.IsSuccessStatusCode) - { - var r = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); - var d = r.ToDictionary(); - - foreach (var listener in ConnectionListeners) - { - listener.OnDeviceInformationMessageReceived(d); - } - } - else - { - RaiseConnectionError(new Exception($"API responded with {response.StatusCode}")); - } - } - catch (Exception ex) - { - RaiseConnectionError(ex); - } - } - else - { - throw new NotImplementedException(); - } - } - - public Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) + public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } @@ -106,7 +72,7 @@ public Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task Reset(CancellationToken? cancellationToken = null) + public override Task ResetDevice(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index b541a593..0821cb75 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -2,6 +2,7 @@ { public interface IMeadowConnection { + event EventHandler<(string message, string? source)> DeviceMessageReceived; event EventHandler ConnectionError; string Name { get; } @@ -10,20 +11,11 @@ public interface IMeadowConnection Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); ConnectionState State { get; } - // internal stuff that probably needs to get moved to anotehr interface - void AddListener(IConnectionListener listener); - void RemoveListener(IConnectionListener listener); - void EnqueueRequest(IRequest command); - - - - - 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 Reset(CancellationToken? cancellationToken = null); + Task ResetDevice(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task RuntimeDisable(CancellationToken? cancellationToken = null); Task RuntimeEnable(CancellationToken? cancellationToken = null); diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 5230c875..765d770b 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -16,7 +16,7 @@ public async Task IsRuntimeEnabled(CancellationToken? cancellationToken = public async Task Reset(CancellationToken? cancellationToken = null) { - await _connection.Reset(cancellationToken); + await _connection.ResetDevice(cancellationToken); } public async Task RuntimeDisable(CancellationToken? cancellationToken = null) From 51904c0944194c526f656b8035acaeeaf7b5acfd Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 18 Aug 2023 11:37:04 -0500 Subject: [PATCH 05/22] firmware list command --- Source/v2/Meadow.CLI.v2.sln | 12 ++ .../Commands/Current/ConfigCommand.cs | 2 +- .../Commands/Current/FirmwareListCommand.cs | 114 ++++++++++++++ Source/v2/Meadow.Cli/Meadow.Cli.csproj | 1 + .../Meadow.Cli/Properties/launchSettings.json | 8 + .../Meadow.Hcom/Connections/ConnectionBase.cs | 5 + Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 6 + .../F7CollectionTests.cs | 14 ++ .../Meadow.SoftwareManager.Unit.Tests.csproj | 28 ++++ .../Usings.cs | 1 + .../v2/Meadow.SoftwareManager/AssemblyInfo.cs | 2 + .../F7FirmwareDownloadManager.cs | 143 +++++++++++++++++ .../F7FirmwarePackageCollection.cs | 148 ++++++++++++++++++ .../v2/Meadow.SoftwareManager/FileManager.cs | 52 ++++++ .../Meadow.SoftwareManager/FirmwarePackage.cs | 14 ++ .../Meadow.SoftwareManager/FirmwareStore.cs | 37 +++++ .../IFirmwarePackageCollection.cs | 12 ++ .../Meadow.SoftwareManager.csproj | 13 ++ 19 files changed, 613 insertions(+), 1 deletion(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FirmwareListCommand.cs create mode 100644 Source/v2/Meadow.SoftwareManager.Unit.Tests/F7CollectionTests.cs create mode 100644 Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj create mode 100644 Source/v2/Meadow.SoftwareManager.Unit.Tests/Usings.cs create mode 100644 Source/v2/Meadow.SoftwareManager/AssemblyInfo.cs create mode 100644 Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs create mode 100644 Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs create mode 100644 Source/v2/Meadow.SoftwareManager/FileManager.cs create mode 100644 Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs create mode 100644 Source/v2/Meadow.SoftwareManager/FirmwareStore.cs create mode 100644 Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs create mode 100644 Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln index d792cc87..52244504 100644 --- a/Source/v2/Meadow.CLI.v2.sln +++ b/Source/v2/Meadow.CLI.v2.sln @@ -17,6 +17,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Logging", "..\..\..\ 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 @@ -51,6 +55,14 @@ Global {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 diff --git a/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs index 45756ac5..3c07f021 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ConfigCommand.cs @@ -7,7 +7,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("config", Description = "Get the device info")] +[Command("config", Description = "Read or modify the meadow CLI configuration")] public class ConfigCommand : ICommand { private readonly ISettingsManager _settingsManager; diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareListCommand.cs new file mode 100644 index 00000000..d5221705 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/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; } + + public FirmwareListCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + { + _settingsManager = settingsManager; + _logger = loggerFactory?.CreateLogger(); + } + + public async ValueTask ExecuteAsync(IConsole console) + { + + var manager = new FileManager(); + + await manager.Refresh(); + + if (Verbose) + { + await DisplayVerboseResults(manager); + } + else + { + await DisplayTerseResults(manager); + } + } + + 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/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index 777c1f43..644c36f4 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -20,6 +20,7 @@ + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 3709ec4e..ae3ee0cc 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -70,6 +70,14 @@ "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" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 4f8d41a0..fa9df3d2 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -25,6 +25,11 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public abstract Task GetRtcTime(CancellationToken? cancellationToken = null); public abstract Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + public Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + public ConnectionBase() { } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 0821cb75..70e47e67 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -21,5 +21,7 @@ public interface IMeadowConnection Task RuntimeEnable(CancellationToken? cancellationToken = null); Task GetRtcTime(CancellationToken? cancellationToken = null); Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + + Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 765d770b..37a3190c 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -71,6 +71,12 @@ public Task FlashCoprocessor(string requestedversion, CancellationToken? cancell public Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null) { + // TODO: do we have the version locally? + + // TODO: download the version + + // TODO: send the file + throw new NotImplementedException(); } } 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/F7FirmwareDownloadManager.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs new file mode 100644 index 00000000..5c22b9e7 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +internal class DownloadFileStream : Stream, IDisposable +{ + private readonly Stream _stream; + + private long _position; + + public DownloadFileStream(Stream stream) + { + _stream = stream; + } + + 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; + 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); + } +} + +public class ReleaseMetadata +{ + [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!; + +} + +internal class F7FirmwareDownloadManager +{ + 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; + } + + private 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; + } + } + + 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(); + using var downloadFileStream = new DownloadFileStream(stream); + using var firmwareFile = File.OpenWrite(downloadFileName); + 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..58e204e1 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -0,0 +1,148 @@ +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 +{ + private readonly string _rootPath; + 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); + } + + _rootPath = 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 Refresh() + { + _f7Packages.Clear(); + + foreach (var directory in Directory.GetDirectories(_rootPath)) + { + var hasFiles = false; + + var package = new FirmwarePackage + { + 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(_rootPath, "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/FileManager.cs b/Source/v2/Meadow.SoftwareManager/FileManager.cs new file mode 100644 index 00000000..d829f50f --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/FileManager.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public class FileManager +{ + /* + 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..3176d9a3 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -0,0 +1,14 @@ +namespace Meadow.Software; + +public class FirmwarePackage +{ + 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..8bd7a5b0 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Meadow.Software; + +public interface IFirmwarePackageCollection : IEnumerable +{ + FirmwarePackage? DefaultPackage { get; } + Task Refresh(); + Task UpdateAvailable(); +} diff --git a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj new file mode 100644 index 00000000..2908c075 --- /dev/null +++ b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + enable + 10 + + + + + + + From 0d93652114ba09c2fb21847ac44b778a8bcac987 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 18 Aug 2023 12:54:08 -0500 Subject: [PATCH 06/22] F7 firmware download --- .../Current/FirmwareDownloadCommand.cs | 92 ++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 8 + .../DownloadFileStream.cs | 62 +++++++ .../F7FirmwareDownloadManager.cs | 170 +++++++++++------- .../F7FirmwarePackageCollection.cs | 50 ++++++ .../F7ReleaseMetadata.cs | 17 ++ .../IFirmwarePackageCollection.cs | 14 +- 7 files changed, 345 insertions(+), 68 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs create mode 100644 Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs create mode 100644 Source/v2/Meadow.SoftwareManager/F7ReleaseMetadata.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs new file mode 100644 index 00000000..e100cfa4 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs @@ -0,0 +1,92 @@ +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 download", Description = "Download a firmware package")] +public class FirmwareDownloadCommand : ICommand +{ + private readonly ISettingsManager _settingsManager; + private readonly ILogger? _logger; + + public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + { + _settingsManager = settingsManager; + _logger = loggerFactory?.CreateLogger(); + } + + [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!; + + public async ValueTask ExecuteAsync(IConsole console) + { + var manager = new FileManager(); + + await manager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = manager.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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index ae3ee0cc..6d4dbaa9 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -78,6 +78,14 @@ "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.2.0.1 --force" } } } \ No newline at end of file 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 index 5c22b9e7..6e77075e 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs @@ -1,79 +1,17 @@ using System; using System.IO; +using System.IO.Compression; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; - namespace Meadow.Software; -internal class DownloadFileStream : Stream, IDisposable -{ - private readonly Stream _stream; - - private long _position; - - public DownloadFileStream(Stream stream) - { - _stream = stream; - } - - 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; - 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); - } -} - -public class ReleaseMetadata -{ - [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!; - -} - internal class F7FirmwareDownloadManager { + public event EventHandler DownloadProgress = default!; + private const string VersionCheckUrlRoot = "https://s3-us-west-2.amazonaws.com/downloads.wildernesslabs.co/Meadow_Beta/"; @@ -89,7 +27,7 @@ public async Task GetLatestAvailableVersion() return contents?.Version ?? string.Empty; } - private async Task GetReleaseMetadata(string? version = null) + public async Task GetReleaseMetadata(string? version = null) { string versionCheckUrl; if (version is null || string.IsNullOrWhiteSpace(version)) @@ -114,7 +52,7 @@ public async Task GetLatestAvailableVersion() try { - var content = JsonSerializer.Deserialize(File.ReadAllText(versionCheckFile)); + var content = JsonSerializer.Deserialize(File.ReadAllText(versionCheckFile)); return content; } @@ -124,6 +62,98 @@ public async Task GetLatestAvailableVersion() } } + 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 + File.WriteAllText(Path.Combine(destinationRoot, "latest.txt"), 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); @@ -134,8 +164,14 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT 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 index 58e204e1..d18c4700 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -10,6 +10,9 @@ namespace Meadow.Software; public class F7FirmwarePackageCollection : IFirmwarePackageCollection { + /// + public event EventHandler DownloadProgress; + private readonly string _rootPath; private List _f7Packages = new(); @@ -55,6 +58,53 @@ internal F7FirmwarePackageCollection(string rootPath) return null; } + 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(_rootPath, version, overwrite); + } + finally + { + downloadManager.DownloadProgress -= ProgressHandler; + } + } + public Task Refresh() { _f7Packages.Clear(); 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/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index 8bd7a5b0..7c4450ef 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; @@ -6,7 +7,18 @@ 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 Refresh(); + Task GetLatestAvailableVersion(); Task UpdateAvailable(); + Task IsVersionAvailableForDownload(string version); + Task RetrievePackage(string version, bool overwrite = false); } From 6ea7e718322e80be5037322996226fc1f6182d67 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 18 Aug 2023 17:32:02 -0500 Subject: [PATCH 07/22] added firmware delete and default commands --- .../Commands/Current/BaseCommand.cs | 34 +++++++++++++++ .../Current/FirmwareDefaultCommand.cs | 37 +++++++++++++++++ .../Commands/Current/FirmwareDeleteCommand.cs | 41 +++++++++++++++++++ .../Current/FirmwareDownloadCommand.cs | 32 ++++++--------- .../Meadow.Cli/Properties/launchSettings.json | 10 ++++- .../F7FirmwareDownloadManager.cs | 8 +++- .../F7FirmwarePackageCollection.cs | 41 +++++++++++++++++++ .../IFirmwarePackageCollection.cs | 2 + 8 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FirmwareDefaultCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs 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/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDefaultCommand.cs new file mode 100644 index 00000000..dd72c04e --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDefaultCommand.cs @@ -0,0 +1,37 @@ +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 : BaseCommand +{ + public FirmwareDefaultCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(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) + { + var manager = new FileManager(); + + await manager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = manager.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/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs new file mode 100644 index 00000000..18473421 --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs @@ -0,0 +1,41 @@ +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 : BaseCommand +{ + public FirmwareDeleteCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) + { + } + + [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] + public string Version { get; set; } = default!; + + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + { + var manager = new FileManager(); + + await manager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = manager.Firmware["Meadow F7"]; + + var existing = collection.FirstOrDefault(p => p.Version == Version); + + if (existing == null) + { + } + + Logger?.LogInformation($"Deleting firmware '{Version}'..."); + + await collection.DeletePackage(Version); + + Logger?.LogInformation($"Done."); + } +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs index e100cfa4..559c6079 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs @@ -1,6 +1,4 @@ -using CliFx; -using CliFx.Attributes; -using CliFx.Infrastructure; +using CliFx.Attributes; using Meadow.Cli; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -8,15 +6,11 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("firmware download", Description = "Download a firmware package")] -public class FirmwareDownloadCommand : ICommand +public class FirmwareDownloadCommand : BaseCommand { - private readonly ISettingsManager _settingsManager; - private readonly ILogger? _logger; - - public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(settingsManager, loggerFactory) { - _settingsManager = settingsManager; - _logger = loggerFactory?.CreateLogger(); } [CommandOption("force", 'f', IsRequired = false)] @@ -25,7 +19,7 @@ public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory? [CommandParameter(0, Name = "Version number to download", IsRequired = false)] public string? Version { get; set; } = default!; - public async ValueTask ExecuteAsync(IConsole console) + protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) { var manager = new FileManager(); @@ -41,27 +35,27 @@ public async ValueTask ExecuteAsync(IConsole console) if (latest == null) { - _logger?.LogError($"Unable to get latest version information."); + Logger?.LogError($"Unable to get latest version information."); return; } - _logger?.LogInformation($"Latest available version is '{latest}'..."); + Logger?.LogInformation($"Latest available version is '{latest}'..."); Version = latest; } else { - _logger?.LogInformation($"Checking for firmware package '{Version}'..."); + Logger?.LogInformation($"Checking for firmware package '{Version}'..."); } var isAvailable = await collection.IsVersionAvailableForDownload(Version); if (!isAvailable) { - _logger?.LogError($"Requested package version '{Version}' is not available."); + Logger?.LogError($"Requested package version '{Version}' is not available."); return; } - _logger?.LogInformation($"Downloading firmware package '{Version}'..."); + Logger?.LogInformation($"Downloading firmware package '{Version}'..."); try @@ -72,16 +66,16 @@ public async ValueTask ExecuteAsync(IConsole console) if (!result) { - _logger?.LogError($"Unable to download package '{Version}'."); + Logger?.LogError($"Unable to download package '{Version}'."); } else { - _logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); } } catch (Exception ex) { - _logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); } } diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 6d4dbaa9..6f3d272d 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -85,7 +85,15 @@ }, "Firmware Download version": { "commandName": "Project", - "commandLineArgs": "firmware download 1.2.0.1 --force" + "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" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs index 6e77075e..f172e97f 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwareDownloadManager.cs @@ -62,6 +62,11 @@ public async Task GetLatestAvailableVersion() } } + 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(); @@ -69,8 +74,9 @@ public async Task DownloadRelease(string destinationRoot, string version, if (meta == null) return false; CreateFolder(destinationRoot, false); + //we'll write latest.txt regardless of version if it doesn't exist - File.WriteAllText(Path.Combine(destinationRoot, "latest.txt"), meta.Version); + SetDefaultVersion(destinationRoot, meta.Version); string local_path; diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index d18c4700..a1ba76b1 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -58,6 +58,47 @@ internal F7FirmwarePackageCollection(string rootPath) 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(_rootPath, 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(_rootPath, version); + + return Task.CompletedTask; + } + public async Task IsVersionAvailableForDownload(string version) { var downloadManager = new F7FirmwareDownloadManager(); diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index 7c4450ef..60c31504 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -16,6 +16,8 @@ public interface IFirmwarePackageCollection : IEnumerable public event EventHandler DownloadProgress; FirmwarePackage? DefaultPackage { get; } + Task SetDefaultPackage(string version); + Task DeletePackage(string version); Task Refresh(); Task GetLatestAvailableVersion(); Task UpdateAvailable(); From 53872a90756f65aee1cea13bf2fa6c734a5bdaaf Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 18 Aug 2023 17:59:58 -0500 Subject: [PATCH 08/22] working on firmware write --- .../Commands/Current/BaseDeviceCommand.cs | 4 ++ .../Commands/Current/FirmwareWriteCommand.cs | 52 +++++++++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 14 ++++- .../Connections/SerialConnection.cs | 9 +++- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index f602f9a7..1e5052e2 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -53,6 +53,10 @@ public async ValueTask ExecuteAsync(IConsole console) { 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/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs new file mode 100644 index 00000000..5b0f455f --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -0,0 +1,52 @@ +using CliFx.Attributes; +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; } + + [CommandParameter(0, Name = "Files to write", IsRequired = false)] + public FirmwareType[]? Files { get; set; } = default!; + + public FirmwareWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { + } + + protected override ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + if (Files == null) + { + Logger.LogInformation("Writing all firmware..."); + } + else + { + if (Files.Contains(FirmwareType.OS)) + { + Logger.LogInformation("Writing OS..."); + } + if (Files.Contains(FirmwareType.Runtime)) + { + Logger.LogInformation("Writing Runtime..."); + } + if (Files.Contains(FirmwareType.ESP)) + { + Logger.LogInformation("Writing ESP..."); + } + } + + return ValueTask.CompletedTask; + } +} + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 6f3d272d..fc864591 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -29,7 +29,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM10" + "commandLineArgs": "config route COM7" }, "Config: Set Route TCP": { "commandName": "Project", @@ -94,6 +94,18 @@ "Firmware Delete": { "commandName": "Project", "commandLineArgs": "firmware delete 1.2.0.1" + }, + "Firmware Write all": { + "commandName": "Project", + "commandLineArgs": "firmware write" + }, + "Firmware Write version": { + "commandName": "Project", + "commandLineArgs": "firmware write -v 1.2.0.1" + }, + "Firmware Write os": { + "commandName": "Project", + "commandLineArgs": "firmware write os esp" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index dbf6b6f2..c00e3cf8 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -147,7 +147,14 @@ private void Open() { if (!_port.IsOpen) { - _port.Open(); + try + { + _port.Open(); + } + catch (FileNotFoundException) + { + throw new Exception($"Serial port '{_port.PortName}' not found"); + } } State = ConnectionState.Connected; } From 2ce9f223b5d46af58b1abdce900daecea42a5c03 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 23 Aug 2023 09:54:27 -0500 Subject: [PATCH 09/22] runtime flashing over serial --- .../Commands/Current/BaseDeviceCommand.cs | 5 +- .../Commands/Current/DeviceClockCommand.cs | 3 +- .../Commands/Current/DeviceResetCommand.cs | 3 +- .../Commands/Current/FileListCommand.cs | 3 +- .../Commands/Current/FileReadCommand.cs | 3 +- .../Commands/Current/FileWriteCommand.cs | 9 ++- .../Commands/Current/FirmwareDeleteCommand.cs | 6 -- .../Commands/Current/FirmwareWriteCommand.cs | 75 +++++++++++++++++-- .../Commands/Current/GetDeviceInfoCommand.cs | 3 +- .../Commands/Current/ListenCommand.cs | 3 +- .../Commands/Current/RuntimeDisableCommand.cs | 3 +- .../Commands/Current/RuntimeEnableCommand.cs | 3 +- .../Commands/Current/RuntimeStateCommand.cs | 3 +- .../Meadow.Cli/Properties/launchSettings.json | 4 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 7 ++ .../Connections/SerialConnection.cs | 43 ++++++++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 5 ++ .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 3 +- Source/v2/Meadow.Hcom/IMeadowDevice.cs | 7 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 16 ++-- .../Serial Requests/EndFileWriteRequest.cs | 9 ++- .../Serial Requests/InitFileWriteRequest.cs | 10 ++- .../F7FirmwarePackageCollection.cs | 17 +++-- .../Meadow.SoftwareManager/FirmwarePackage.cs | 16 +++- .../IFirmwarePackageCollection.cs | 2 + 26 files changed, 207 insertions(+), 56 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index 1e5052e2..2ea75806 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -1,5 +1,6 @@ using CliFx; using CliFx.Infrastructure; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -15,7 +16,7 @@ public BaseDeviceCommand(MeadowConnectionManager connectionManager, ILoggerFacto ConnectionManager = connectionManager; } - protected abstract ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken); + protected abstract ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken); public async ValueTask ExecuteAsync(IConsole console) { @@ -46,7 +47,7 @@ public async ValueTask ExecuteAsync(IConsole console) } else { - await ExecuteCommand(c.Device, cancellationToken); + await ExecuteCommand(c, c.Device, cancellationToken); } } catch (TimeoutException) diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs index a74cbeee..7980c712 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -14,7 +15,7 @@ public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFact { } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { if (Time == null) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs index aad76fa3..a8cb9e64 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -12,7 +13,7 @@ public DeviceResetCommand(MeadowConnectionManager connectionManager, ILoggerFact Logger.LogInformation($"Resetting the device..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { await device.Reset(); } diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs index fabd842d..5e3f1bdf 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -17,7 +18,7 @@ public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory Logger.LogInformation($"Getting file list..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { var files = await device.GetFileList(Verbose, cancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs index 545422fd..f475987e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -17,7 +18,7 @@ public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory { } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { Logger.LogInformation($"Getting file '{MeadowFile}' from device..."); diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs index fcf6e4a8..e769da09 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -24,7 +25,7 @@ public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactor { } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { if (TargetFileNames.Any() && Files.Count != TargetFileNames.Count) { @@ -34,6 +35,12 @@ protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, Can return; } + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + 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++) diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs index 18473421..08ae586e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs @@ -26,12 +26,6 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation // TODO: add switch and support for other platforms var collection = manager.Firmware["Meadow F7"]; - var existing = collection.FirstOrDefault(p => p.Version == Version); - - if (existing == null) - { - } - Logger?.LogInformation($"Deleting firmware '{Version}'..."); await collection.DeletePackage(Version); diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index 5b0f455f..d41a360c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -1,4 +1,6 @@ using CliFx.Attributes; +using Meadow.Hcom; +using Meadow.Software; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -24,29 +26,90 @@ public FirmwareWriteCommand(MeadowConnectionManager connectionManager, ILoggerFa { } - protected override ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { + var manager = new FileManager(); + await manager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = manager.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; + } + package = existing; + } + else + { + Version = collection.DefaultPackage?.Version ?? + throw new Exception("No default version set"); + + package = collection.DefaultPackage; + } + + 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 == null) { - Logger.LogInformation("Writing all firmware..."); + Logger.LogInformation($"Writing all firmware for version '{Version}'..."); } else { if (Files.Contains(FirmwareType.OS)) { - Logger.LogInformation("Writing OS..."); + Logger.LogInformation($"{Environment.NewLine}Writing OS..."); } if (Files.Contains(FirmwareType.Runtime)) { - Logger.LogInformation("Writing 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("Writing ESP..."); + Logger.LogInformation($"{Environment.NewLine}Writing ESP..."); } } - return ValueTask.CompletedTask; + Logger.LogInformation($"{Environment.NewLine}"); + + if (wasRuntimeEnabled) + { + await device.RuntimeEnable(); + } + + // TODO: if we're an F7 device, we need to reset + } + + private void Connection_FileWriteProgress(object? sender, (long completed, long total) e) + { + throw new NotImplementedException(); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs index f7c43558..a9a17f0b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -12,7 +13,7 @@ public DeviceInfoCommand(MeadowConnectionManager connectionManager, ILoggerFacto Logger.LogInformation($"Getting device info..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { var deviceInfo = await device.GetDeviceInfo(cancellationToken); if (deviceInfo != null) diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index 4f12d5a9..6971c0d0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -37,7 +38,7 @@ private void OnDeviceMessageReceived(object? sender, (string message, string? so } } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs index fd8303a2..2c36869f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -12,7 +13,7 @@ public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerF Logger.LogInformation($"Disabling runtime..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { await device.RuntimeDisable(cancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs index c4d97174..7cfb8a27 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -12,7 +13,7 @@ public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFa Logger.LogInformation($"Enabling runtime..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { await device.RuntimeEnable(cancellationToken); diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs index 859e8024..de465c7a 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using Meadow.Hcom; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -12,7 +13,7 @@ public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFac Logger.LogInformation($"Querying runtime state..."); } - protected override async ValueTask ExecuteCommand(Hcom.IMeadowDevice device, CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { var state = await device.IsRuntimeEnabled(cancellationToken); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index fc864591..4c421d07 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -103,9 +103,9 @@ "commandName": "Project", "commandLineArgs": "firmware write -v 1.2.0.1" }, - "Firmware Write os": { + "Firmware Write runtime": { "commandName": "Project", - "commandLineArgs": "firmware write os esp" + "commandLineArgs": "firmware write runtime" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index fa9df3d2..133b3fa5 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -9,6 +9,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; public event EventHandler ConnectionError = default!; + public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress; public abstract string Name { get; } @@ -24,6 +25,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable 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 Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null) { @@ -34,6 +36,11 @@ public ConnectionBase() { } + 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)); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index c00e3cf8..8ad0a9fa 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -719,9 +719,37 @@ public override async Task ResetDevice(CancellationToken? cancellationToken = nu } 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, + cancellationToken); + } + + public override async Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, + RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, + cancellationToken); + + // TODO: after writing the runtime, the device will erase flash and move the binary + // we need to wait for that to complete + return status; + } + + private async Task WriteFile( + string localFileName, + string? meadowFileName, + RequestType initialRequestType, + RequestType endRequestType, + CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); - command.SetParameters(localFileName, meadowFileName ?? Path.GetFileName(localFileName)); + command.SetParameters( + localFileName, + meadowFileName ?? Path.GetFileName(localFileName), + initialRequestType); var accepted = false; Exception? ex = null; @@ -760,6 +788,13 @@ void OnFileError(object? sender, Exception exception) 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) @@ -775,11 +810,15 @@ void OnFileError(object? sender, Exception exception) bytesRead = fs.Read(packet, 2, packet.Length - 2); if (bytesRead <= 0) break; EncodeAndSendPacket(packet, bytesRead + 2); - + 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(); EncodeAndSendPacket(p); diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 805f287f..26a23d1b 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -111,4 +111,9 @@ public override Task ReadFile(string meadowFileName, string? localFileName { throw new NotImplementedException(); } + + public override Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index 9ff97efe..c3ec31b5 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -181,7 +181,7 @@ private async void StateMachine() info = await _connection.Device.GetDeviceInfo(); } - await _connection.Device.FlashRuntime(RequestedVersion); + // await _connection.Device.FlashRuntime(RequestedVersion); } catch (Exception ex) { diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 70e47e67..3185b310 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -4,6 +4,7 @@ public interface IMeadowConnection { event EventHandler<(string message, string? source)> DeviceMessageReceived; event EventHandler ConnectionError; + event EventHandler<(string fileName, long completed, long total)> FileWriteProgress; string Name { get; } IMeadowDevice? Device { get; } @@ -22,6 +23,6 @@ public interface IMeadowConnection Task GetRtcTime(CancellationToken? cancellationToken = null); Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); - Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null); + Task WriteRuntime(string localFileName, 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 index 1bf0b834..cf739345 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -10,11 +10,12 @@ public interface IMeadowDevice 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 FlashOS(string requestedversion, CancellationToken? cancellationToken = null); Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null); - Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null); - Task GetRtcTime(CancellationToken? cancellationToken = null); - Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); + // Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 37a3190c..19cc4abb 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -49,6 +49,11 @@ public async Task WriteFile(string localFileName, string? meadowFileName = 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 GetRtcTime(CancellationToken? cancellationToken = null) { return await _connection.GetRtcTime(cancellationToken); @@ -68,16 +73,5 @@ public Task FlashCoprocessor(string requestedversion, CancellationToken? cancell { throw new NotImplementedException(); } - - public Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null) - { - // TODO: do we have the version locally? - - // TODO: download the version - - // TODO: send the file - - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs index 75199cc8..50aedf47 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/EndFileWriteRequest.cs @@ -2,5 +2,12 @@ internal class EndFileWriteRequest : Request { - public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER; + 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/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs index 995d9da7..8c99bb6c 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs @@ -68,7 +68,9 @@ public static uint Crc32part(byte[] buffer, uint length, uint crc32val) internal class InitFileWriteRequest : Request { - public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER; + private RequestType _requestType; + + public override RequestType RequestType => _requestType; public uint FileSize { get; set; } public uint CheckSum { get; set; } @@ -78,9 +80,13 @@ internal class InitFileWriteRequest : Request public string LocalFileName { get; private set; } = default!; public string MeadowFileName { get; private set; } - public void SetParameters(string localFile, string meadowFileName) + public void SetParameters( + string localFile, + string meadowFileName, + 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); diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index a1ba76b1..8a14443a 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -13,7 +13,8 @@ public class F7FirmwarePackageCollection : IFirmwarePackageCollection /// public event EventHandler DownloadProgress; - private readonly string _rootPath; + public string PackageFileRoot { get; } + private List _f7Packages = new(); public FirmwarePackage? DefaultPackage { get; private set; } @@ -35,7 +36,7 @@ internal F7FirmwarePackageCollection(string rootPath) Directory.CreateDirectory(rootPath); } - _rootPath = rootPath; + PackageFileRoot = rootPath; } /// @@ -77,7 +78,7 @@ public Task DeletePackage(string version) _f7Packages.Remove(DefaultPackage); SetDefaultPackage(newDefault); - var path = Path.Combine(_rootPath, version); + var path = Path.Combine(PackageFileRoot, version); Directory.Delete(path, true); @@ -94,7 +95,7 @@ public Task SetDefaultPackage(string version) } var downloadManager = new F7FirmwareDownloadManager(); - downloadManager.SetDefaultVersion(_rootPath, version); + downloadManager.SetDefaultVersion(PackageFileRoot, version); return Task.CompletedTask; } @@ -138,7 +139,7 @@ void ProgressHandler(object sender, long e) var meta = await downloadManager.GetReleaseMetadata(version); if (meta == null) return false; - return await downloadManager.DownloadRelease(_rootPath, version, overwrite); + return await downloadManager.DownloadRelease(PackageFileRoot, version, overwrite); } finally { @@ -150,11 +151,11 @@ public Task Refresh() { _f7Packages.Clear(); - foreach (var directory in Directory.GetDirectories(_rootPath)) + foreach (var directory in Directory.GetDirectories(PackageFileRoot)) { var hasFiles = false; - var package = new FirmwarePackage + var package = new FirmwarePackage(this) { Version = Path.GetFileName(directory) }; @@ -202,7 +203,7 @@ public Task Refresh() } } - var fi = new FileInfo(Path.Combine(_rootPath, "latest.txt")); + var fi = new FileInfo(Path.Combine(PackageFileRoot, "latest.txt")); if (fi.Exists) { // get default diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs index 3176d9a3..ae2c489c 100644 --- a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -1,7 +1,21 @@ -namespace Meadow.Software; +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; } diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index 60c31504..5d0cf341 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -23,4 +23,6 @@ public interface IFirmwarePackageCollection : IEnumerable Task UpdateAvailable(); Task IsVersionAvailableForDownload(string version); Task RetrievePackage(string version, bool overwrite = false); + + string PackageFileRoot { get; } } From d64759c47eca6a183f3bf0e5f9762b868fa03574 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 23 Aug 2023 12:44:57 -0500 Subject: [PATCH 10/22] working on ESP file write --- .../Commands/Current/FirmwareWriteCommand.cs | 11 ++++- .../Meadow.Cli/Properties/launchSettings.json | 4 ++ .../Meadow.Hcom/Connections/ConnectionBase.cs | 1 + .../Connections/SerialConnection.cs | 32 +++++++++++++-- .../Meadow.Hcom/Connections/TcpConnection.cs | 5 +++ .../Meadow.Hcom/Firmware/FirmwareUpdater.cs | 2 +- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 1 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 3 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 41 ++++++++++++++++--- .../Serial Requests/InitFileWriteRequest.cs | 6 +-- 10 files changed, 90 insertions(+), 16 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index d41a360c..bff221b6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -93,7 +93,16 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, } if (Files.Contains(FirmwareType.ESP)) { - Logger.LogInformation($"{Environment.NewLine}Writing 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); } } diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 4c421d07..6553f941 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -106,6 +106,10 @@ "Firmware Write runtime": { "commandName": "Project", "commandLineArgs": "firmware write runtime" + }, + "Firmware Write esp": { + "commandName": "Project", + "commandLineArgs": "firmware write esp" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 133b3fa5..edad4e02 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -26,6 +26,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable 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 Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null) { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 8ad0a9fa..12837e86 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -355,7 +355,8 @@ private void EncodeAndSendPacket(byte[] messageBytes, int length) try { // Send the data to Meadow - Debug.WriteLine($"Sending {encodedToSend} bytes..."); + Debug.Write($"Sending {encodedToSend} bytes..."); + _port.WriteTimeout = 50000; _port.Write(encodedBytes, 0, encodedToSend); Debug.WriteLine($"sent"); } @@ -718,19 +719,26 @@ public override async Task ResetDevice(CancellationToken? cancellationToken = nu return list.ToArray(); } - public override async Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + 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) + public override async Task WriteRuntime( + string localFileName, + CancellationToken? cancellationToken = null) { var status = await WriteFile(localFileName, "Meadow.OS.Runtime.bin", RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_RUNTIME, RequestType.HCOM_MDOW_REQUEST_MONO_UPDATE_FILE_END, + 0, cancellationToken); // TODO: after writing the runtime, the device will erase flash and move the binary @@ -738,17 +746,31 @@ public override async Task WriteRuntime(string localFileName, Cancellation return status; } + public override async Task WriteCoprocessorFile( + string localFileName, + int destinationAddress, + CancellationToken? cancellationToken = null) + { + return await WriteFile(localFileName, null, + RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, + destinationAddress, + cancellationToken); + } + 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; @@ -768,6 +790,7 @@ void OnFileError(object? sender, Exception exception) EnqueueRequest(command); + // this will wait for a "file write accepted" from the target if (!await WaitForResult( () => { @@ -804,12 +827,13 @@ void OnFileError(object? sender, Exception exception) sequenceNumber++; - // sequenc number at the start of the packet + // 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; EncodeAndSendPacket(packet, bytesRead + 2); + await Task.Delay(10); progress += bytesRead; base.RaiseFileWriteProgress(fileName, progress, expected); } diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 26a23d1b..a4d54214 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -116,4 +116,9 @@ public override Task WriteRuntime(string localFileName, CancellationToken? { throw new NotImplementedException(); } + + public override Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs index c3ec31b5..03c70cd9 100644 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ b/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs @@ -228,7 +228,7 @@ private async void StateMachine() } Debug.WriteLine(">> flashing ESP"); - await _connection.Device.FlashCoprocessor(RequestedVersion); + //await _connection.Device.FlashCoprocessor(RequestedVersion); // await _connection.Device.FlashCoprocessor(DownloadManager.FirmwareDownloadsFilePath, RequestedVersion); } catch (Exception ex) diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 3185b310..6ce70969 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -24,5 +24,6 @@ public interface IMeadowConnection Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null); + Task WriteCoprocessorFile(string localFileName, int destinationAddress, 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 index cf739345..1a2c2034 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -13,9 +13,8 @@ public interface IMeadowDevice 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 FlashOS(string requestedversion, CancellationToken? cancellationToken = null); - Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null); - // Task FlashRuntime(string requestedversion, CancellationToken? cancellationToken = null); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 19cc4abb..ef86d4f0 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -54,6 +54,42 @@ public async Task WriteRuntime(string localFileName, CancellationToken? ca 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; + } + + 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); @@ -68,10 +104,5 @@ public Task FlashOS(string requestedversion, CancellationToken? cancellationToke { throw new NotImplementedException(); } - - public Task FlashCoprocessor(string requestedversion, CancellationToken? cancellationToken = null) - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs index 8c99bb6c..2035f349 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileWriteRequest.cs @@ -83,6 +83,7 @@ internal class InitFileWriteRequest : Request 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 @@ -96,14 +97,13 @@ public void SetParameters( MeadowFileName = meadowFileName; var nameBytes = Encoding.ASCII.GetBytes(meadowFileName); - var data = File.ReadAllBytes(source.FullName); 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(0), 0, Payload, 8, 4); // TODO: flash address - Array.Copy(espHash, 0, Payload, 12, espHash.Length); // TODO: ESP hash + 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 From 1dc22ed1b85be85406527e940fb57c2be02dacea Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Thu, 24 Aug 2023 15:47:54 -0500 Subject: [PATCH 11/22] Added support for DFU write of OS --- .../Commands/Current/BaseDeviceCommand.cs | 2 +- .../Commands/Current/FirmwareWriteCommand.cs | 131 ++- Source/v2/Meadow.Cli/DFU/DfuContext.cs | 50 ++ Source/v2/Meadow.Cli/DFU/DfuSharp.cs | 827 ++++++++++++++++++ Source/v2/Meadow.Cli/DFU/DfuUtils.cs | 299 +++++++ Source/v2/Meadow.Cli/Meadow.Cli.csproj | 1 + .../Meadow.Cli/Properties/launchSettings.json | 6 +- 7 files changed, 1286 insertions(+), 30 deletions(-) create mode 100644 Source/v2/Meadow.Cli/DFU/DfuContext.cs create mode 100644 Source/v2/Meadow.Cli/DFU/DfuSharp.cs create mode 100644 Source/v2/Meadow.Cli/DFU/DfuUtils.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index 2ea75806..b1cef16e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -18,7 +18,7 @@ public BaseDeviceCommand(MeadowConnectionManager connectionManager, ILoggerFacto protected abstract ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken); - public async ValueTask ExecuteAsync(IConsole console) + public virtual async ValueTask ExecuteAsync(IConsole console) { var cancellationToken = console.RegisterCancellationHandler(); var c = ConnectionManager.GetCurrentConnection(); diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index bff221b6..384230bc 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -1,4 +1,6 @@ using CliFx.Attributes; +using CliFx.Infrastructure; +using Meadow.CLI.Core.Internals.Dfu; using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -18,6 +20,9 @@ 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!; @@ -26,13 +31,61 @@ public FirmwareWriteCommand(MeadowConnectionManager connectionManager, ILoggerFa { } - protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + 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 (UseDfu) + { + if (!Files.Contains(FirmwareType.OS)) + { + Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); + return; + } + + // 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); + } + } + else + { + await base.ExecuteAsync(console); + } + } + + private async Task GetSelectedPackage() { var manager = new FileManager(); await manager.Refresh(); - // for now we only support F7 - // TODO: add switch and support for other platforms var collection = manager.Firmware["Meadow F7"]; FirmwarePackage package; @@ -44,7 +97,7 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, if (existing == null) { Logger.LogError($"Requested version '{Version}' not found."); - return; + return null; } package = existing; } @@ -56,6 +109,13 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, package = collection.DefaultPackage; } + return package; + } + + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) + { + var package = await GetSelectedPackage(); + var wasRuntimeEnabled = await device.IsRuntimeEnabled(cancellationToken); if (wasRuntimeEnabled) @@ -70,40 +130,52 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Console.Write($"Writing {e.fileName}: {p:0}% \r"); }; - if (Files == null) - { - Logger.LogInformation($"Writing all firmware for version '{Version}'..."); - } - else + if (Files.Contains(FirmwareType.OS)) { - if (Files.Contains(FirmwareType.OS)) + bool useDfu = false; + bool deviceSupportsOta = false; // TODO: get this based on device OS version + + if (package.OsWithoutBootloader == null + || !deviceSupportsOta + || UseDfu) { - Logger.LogInformation($"{Environment.NewLine}Writing OS..."); + useDfu = true; } - if (Files.Contains(FirmwareType.Runtime)) + + if (useDfu) { - Logger.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + // this would have already happened before now (in ExecuteAsync) so ignore + } + else + { + Logger.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - // get the path to the runtime file - var rtpath = package.GetFullyQualifiedPath(package.Runtime); + throw new NotSupportedException("OtA writes for the OS are not yet supported"); + } + } + if (Files.Contains(FirmwareType.Runtime)) + { + Logger.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); - // TODO: for serial, we must wait for the flash to complete + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); - await device.WriteRuntime(rtpath, cancellationToken); - } - if (Files.Contains(FirmwareType.ESP)) - { - Logger.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + // TODO: for serial, we must wait for the flash to complete - var fileList = new string[] - { + 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); - } + await device.WriteCoprocessorFiles(fileList, cancellationToken); } Logger.LogInformation($"{Environment.NewLine}"); @@ -116,9 +188,12 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, // TODO: if we're an F7 device, we need to reset } - private void Connection_FileWriteProgress(object? sender, (long completed, long total) e) + private async Task WriteOsWithDfu(string osFile) { - throw new NotImplementedException(); + await DfuUtils.FlashFile( + osFile, + logger: Logger, + format: DfuUtils.DfuFlashFormat.ConsoleOut); } } 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..89b7f3fc --- /dev/null +++ b/Source/v2/Meadow.Cli/DFU/DfuUtils.cs @@ -0,0 +1,299 @@ +using LibUsbDotNet.LibUsb; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.ComponentModel; +using System.Diagnostics; +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; + } + } + } +} diff --git a/Source/v2/Meadow.Cli/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index 644c36f4..3b5267e9 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -16,6 +16,7 @@ + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 6553f941..4705dfa9 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -29,7 +29,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM7" + "commandLineArgs": "config route COM4" }, "Config: Set Route TCP": { "commandName": "Project", @@ -110,6 +110,10 @@ "Firmware Write esp": { "commandName": "Project", "commandLineArgs": "firmware write esp" + }, + "Firmware Write os DFU": { + "commandName": "Project", + "commandLineArgs": "firmware write os -v 1.2.0.1 -d" } } } \ No newline at end of file From 7c8bf58a9128626dc4a64c1843284c45e8da90df Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 25 Aug 2023 16:40:39 -0500 Subject: [PATCH 12/22] DFU default fix --- .../Commands/Current/FirmwareWriteCommand.cs | 22 +++++------ Source/v2/Meadow.Cli/Meadow.Cli.csproj | 35 +++++++++++++++--- .../Meadow.Cli/Properties/launchSettings.json | 6 ++- Source/v2/icon.png | Bin 0 -> 4055 bytes 4 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 Source/v2/icon.png diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index 384230bc..3019e74d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -47,6 +47,16 @@ public override async ValueTask ExecuteAsync(IConsole console) }; } + bool deviceSupportsOta = false; // TODO: get this based on device OS version + + if (package.OsWithoutBootloader == null + || !deviceSupportsOta + || UseDfu) + { + UseDfu = true; + } + + if (UseDfu) { if (!Files.Contains(FirmwareType.OS)) @@ -132,17 +142,7 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, if (Files.Contains(FirmwareType.OS)) { - bool useDfu = false; - bool deviceSupportsOta = false; // TODO: get this based on device OS version - - if (package.OsWithoutBootloader == null - || !deviceSupportsOta - || UseDfu) - { - useDfu = true; - } - - if (useDfu) + if (UseDfu) { // this would have already happened before now (in ExecuteAsync) so ignore } diff --git a/Source/v2/Meadow.Cli/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index 3b5267e9..74684db5 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -1,11 +1,30 @@ - + - Exe - net6.0 - enable - enable - 10 + 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 @@ -24,4 +43,8 @@ + + + + diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 4705dfa9..9ea526e2 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -3,6 +3,10 @@ "Meadow.Cli": { "commandName": "Project" }, + "Version": { + "commandName": "Project", + "commandLineArgs": "--version" + }, "Listen": { "commandName": "Project", "commandLineArgs": "listen" @@ -113,7 +117,7 @@ }, "Firmware Write os DFU": { "commandName": "Project", - "commandLineArgs": "firmware write os -v 1.2.0.1 -d" + "commandLineArgs": "firmware write os -v 1.2.0.1" } } } \ No newline at end of file diff --git a/Source/v2/icon.png b/Source/v2/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2c87b8f1c5d6571b83925ade3326473b548d3a7c GIT binary patch literal 4055 zcmds4X*|?j8~)EQ*+R)65+cuov{_RalwHa)G{~Cl`x4TOM?_;wSwi8FWg2V7Hex7S zJ(jVJWo)IwV1}|~%*;F9-}~|X_I`Um{D0>@=e|CibKTdu{^w4ESSDY6B0DcqW z>s9~&Vuv7r`v`k*45{#B4?ID}cR~Sx@5JE;0`dyP4k4jdM*2YY;Hee%g!8_hg&qLZ zrSl!QLI6O}&*ZwE%>&T-tZwby8z*}=Pe>mZlGd}3aXsQ`%sWyfKsm}ULez|hyby-{ zks473yZUGCsNvPxBK_Rr;ggy27M?GJ%aU*W>C%^~-}P9R$23WkkRSDNJ0xPLFmi~h zLw~5Nl44-|wJmapPSzfp`n8r8;~ce%7C@P4gaI>W0HBu}=(q$1f>HqBl(0!WcK9->=3qZgTO;DaLt2`^qvL?UwTN9RwSy`V!^($)_qlYA1F55HtUc|R zzX?m_PXcKRCqTsJa)CMV0)`z9yxe*NM(0K)CQuo4FK7^XP*U=t3?Qz4&l^b}l)x8C z&XGGs!9}<87`6xb`dG+mzI}Z_)hEC;z4x#7V*r1i3cY5-p2&vit?H2I;~dWA(#xD6 zVs5x63%Y)6jO2<-2fX_E1mCngVP`l%#EciawCHntYfVw-CbhstGK6NNbIyot$21hK6Me_3ZpxQ&xb(Nus`wo(=F`QW;N1S~M>s)p1yGkbMn*bHy>^~uvR8#_ z-P_Ye>_BYr8tMVwICOM9X?vG*OW51oqO{u0hQ}Bw9Dks7olFN&-?~YLO`q_lG`ztW8YRg8AJ4^9Ko@Q5*4@Jw5K&z=?u!}rEbj4V`V;7F zhtJ1O9OkqmsPmn=zWxh{T!x*G-zgz-rEn|!u)f@=$rR18{K1iql~wQcd(ZRKFMFRE z*2&B$ZbvFtgUvZ%Yhdxw%Hhu)E56?McIMw^a~Ot8CiLKI_L`i~SQLfB#izS5Jt0nM zb~)6f;nYQ~ohhWb?|%~N9$pKkO5;DS=8VvIy7rB_o+8N&jl30Fzz?_|{V2cVr$?Yt zf!}?Ytr&3K`1qKL00IHAkpx1O8fEGtIZYn{Z3QiM6x6W2F60%nBv#HEhvThK2S``T@rXL&75wH(rTZY_4#Y+RScUr;B;KX=4Um&t&kBe+EnNyp zl9z4^Bi;X%Jz}iaGz}#2Pn!kCjtrE>&KBFksP{>8Y&(&>nCF|A0%dK$zzEM9k-`sT8>Za{IpzDY-yidZ6 z!x=xHKf(5kFHMJsE8HY8kFO@p>f3YgQNYr@MVOxsY(Od8+3e1~cAR}jnHJRniz0gb zZ40Z+aEhmZU1t3W?%ro_`#@caT@3}ySOwNP^z>v4`G!c->hnN$#pI4c3-3lWZQh70 z<^1hqrzm@^H#Y-YGoOFLzvV@pRceeiJz8+SUr4kIpDd0yq+tSvFUTdQ+NDT(O&fl29p!2nbeRvYaQVn{-4lD*JibZ)q-|d6d=b;Ox zs&u0N=&y2}_e>hXWF?0ADH$1%_#yJ{{NzyxLx-V};1+LH?mu4@ zi=F~~ck0$vRD%>E!uYjkiMti7@qUSEC-ojhb1S5A=#17IO&8V1Oh=#JgscWz81%59 zlUypLlO0tt;C|v7Vf({aKUR=bFy)}q#9SMRQ!ty=svOnysMyO{jOdwGEP4P`j&5q4 z<)4vQ`k>M^6DOeCU!h)z`PyQ?XuQ)WqKYqSdGIXgvvt2$mNdJPKKeFHMYQhUR58_Lg&I{}L ztOwsZl+lNbd>;OKgQLi!TV3v6d5(R_5GFS8k1GT}>>7E_C)a zqu2h54bBUAQO&+r9i_{=>SN2HgIsojuf5fZkTzlY^xPT|EIB@-jw_{%vX`HiP|}Tt z!1$r$3}Gy)xTJrAr#bxe36i1i>f|cU9o+Qobfy^F{oFve%(@$g@J`N8@?!1pHkOw; zymT%Un7EiYcfxpfQ=?%zUF3ogpl0i5K)M6z`e4zJD&9`WTUM7Qxu}pdJ(AN13gdJ7 z?~Q>mIJ>fu3%QJ?zx)P*0ZY z+dRSbA_vBMv+eX;EXfu|0xH_v$oJPe`28JScchG-!jw| zOl6D`uWuYUFQ!AnCC3lMuDtnox>nQcvT6|fMWu_!Bu^p5t2o{3_h_dNJcrqSP~i{F zPzFLG4=nt>=A)CSH(oNz(QOOD^o%k10lB)-7S;&VvaP*DFCP!Ft0yGTstlvgs5boR z=)PHFX@NzZ1G!YWNn8$BAoy%2M7hy;wlH-`Gv#F^-SwO>4Etc+H{)jqt!l@8mbK&7 zeIBNqvifKE>m6_rsj1;dlB9AwG=ZSHReV~-#A~MsH~sdz*Jd|IQCRQ%n41y0<|4gZ zP2OCYgz(WsJlq8Oq6{4b3>b5dr*xGqgzI)Gq3UcB5=**8`B6gmao>-v?kfs2N65no zN{iVXOHg+n34*JmHDf*t2ZsfDID@=@Wh8%aF!I%**Basz1!%M5vyDo~ zVH*~qSX+62A*)~+H!t@+5&tcJOoPz;%jtOZk$F*Ie*puMT_Yy62V?rzM|zmpP9RBg z{oZk?!hh?sDlq`J_WcPRz@>81Q`z6f$HW?Rj8fiX(w2K>tj&}#hfsP~28PA&uzwWQ zf|(J1-gJK~W3l#>lM!X(%}Hn0@^tjq2gQ;Be5UPCF_fe*2=xEm&;2hQDqYI{{LjMo U4DV`o9}bw@xOu%=-{r4=0Z!*v7ytkO literal 0 HcmV?d00001 From 79d09c4be0c51ff6d7d24e4069585b20e3e2514a Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Mon, 28 Aug 2023 16:18:08 -0500 Subject: [PATCH 13/22] increase HCOM packet size --- .../Commands/Current/FirmwareWriteCommand.cs | 14 +++--- .../Meadow.Cli/Properties/launchSettings.json | 2 +- .../Connections/SerialConnection.cs | 44 ++++++++++++------- Source/v2/Meadow.Hcom/Protocol.cs | 4 +- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index 3019e74d..be3e699d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -47,6 +47,12 @@ public override async ValueTask ExecuteAsync(IConsole console) }; } + 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 @@ -57,14 +63,8 @@ public override async ValueTask ExecuteAsync(IConsole console) } - if (UseDfu) + if (UseDfu && Files.Contains(FirmwareType.OS)) { - if (!Files.Contains(FirmwareType.OS)) - { - Logger.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); - return; - } - // no connection is required here - in fact one won't exist // unless maybe we add a "DFUConnection"? await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader)); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 9ea526e2..2617c367 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -33,7 +33,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM4" + "commandLineArgs": "config route COM7" }, "Config: Set Route TCP": { "commandName": "Project", diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 12837e86..20432e13 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -279,12 +279,12 @@ public void EnqueueRequest(IRequest command) _pendingCommands.Enqueue(command); } - private void EncodeAndSendPacket(byte[] messageBytes) + private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) { - EncodeAndSendPacket(messageBytes, messageBytes.Length); + await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); } - private void EncodeAndSendPacket(byte[] messageBytes, int length) + private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = null) { Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); @@ -311,7 +311,8 @@ private void EncodeAndSendPacket(byte[] messageBytes, int length) { // 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. - encodedBytes = new byte[Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE]; + 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); @@ -355,10 +356,9 @@ private void EncodeAndSendPacket(byte[] messageBytes, int length) try { // Send the data to Meadow - Debug.Write($"Sending {encodedToSend} bytes..."); - _port.WriteTimeout = 50000; - _port.Write(encodedBytes, 0, encodedToSend); - Debug.WriteLine($"sent"); + // Debug.Write($"Sending {encodedToSend} bytes..."); + await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken ?? CancellationToken.None); + // Debug.WriteLine($"sent"); } catch (InvalidOperationException ioe) // Port not opened { @@ -751,11 +751,21 @@ public override async Task WriteCoprocessorFile( int destinationAddress, CancellationToken? cancellationToken = null) { - return await WriteFile(localFileName, null, + // 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); + + // now wait for the STM32 to finish writing to the ESP32 + return await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + Console.WriteLine(m); + return m.Contains("TransferComplete"); + }, + cancellationToken); } private async Task WriteFile( @@ -792,12 +802,12 @@ void OnFileError(object? sender, Exception exception) // this will wait for a "file write accepted" from the target if (!await WaitForResult( - () => - { - if (ex != null) throw ex; - return accepted; - }, - cancellationToken)) + () => + { + if (ex != null) throw ex; + return accepted; + }, + cancellationToken)) { return false; } @@ -832,7 +842,7 @@ void OnFileError(object? sender, Exception exception) // followed by the file data bytesRead = fs.Read(packet, 2, packet.Length - 2); if (bytesRead <= 0) break; - EncodeAndSendPacket(packet, bytesRead + 2); + await EncodeAndSendPacket(packet, bytesRead + 2, cancellationToken); await Task.Delay(10); progress += bytesRead; base.RaiseFileWriteProgress(fileName, progress, expected); @@ -844,7 +854,7 @@ void OnFileError(object? sender, Exception exception) var request = RequestBuilder.Build(); request.SetRequestType(endRequestType); var p = request.Serialize(); - EncodeAndSendPacket(p); + await EncodeAndSendPacket(p, cancellationToken); return true; diff --git a/Source/v2/Meadow.Hcom/Protocol.cs b/Source/v2/Meadow.Hcom/Protocol.cs index 990d5acc..88e6ae07 100644 --- a/Source/v2/Meadow.Hcom/Protocol.cs +++ b/Source/v2/Meadow.Hcom/Protocol.cs @@ -19,8 +19,8 @@ internal class Protocol // 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 = 512; - public const int HCOM_PROTOCOL_ENCODED_MAX_SIZE = HCOM_PROTOCOL_PACKET_MAX_SIZE + 8; + 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; From 35760dd1a1bc3b06e30e2e34c436d5856320e025 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Mon, 28 Aug 2023 17:03:55 -0500 Subject: [PATCH 14/22] improving runtime flash process --- .../Connections/SerialConnection.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 20432e13..11845c49 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -735,14 +735,51 @@ public override async Task WriteRuntime( string localFileName, CancellationToken? cancellationToken = null) { + CommandTimeoutSeconds = 120; + + 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); - // TODO: after writing the runtime, the device will erase flash and move the binary - // we need to wait for that to complete + Console.WriteLine("\nErasing runtime flash blocks..."); + + status = await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); + + InfoMessages.Clear(); + + Console.WriteLine("Moving runtime to flash..."); + + status = await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); + + InfoMessages.Clear(); + + Console.WriteLine("Verifying..."); + + status = await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono runtime successfully flashed."); + }, + cancellationToken); + + Console.WriteLine("Done."); + + CommandTimeoutSeconds = 30; + return status; } From 40d69f68bdab906c8eeb3dca184409d2382d042b Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Tue, 29 Aug 2023 16:31:24 -0500 Subject: [PATCH 15/22] coprocessor transfer work --- .../Commands/Current/BaseDeviceCommand.cs | 1 + .../Commands/Current/FirmwareWriteCommand.cs | 6 + .../Meadow.Cli/Properties/launchSettings.json | 2 +- .../Meadow.Hcom/Connections/ConnectionBase.cs | 6 + .../SerialConnection.ListenerProc.cs | 9 +- .../Connections/SerialConnection.cs | 137 +++++++++++------- Source/v2/Meadow.Hcom/IMeadowConnection.cs | 1 + 7 files changed, 106 insertions(+), 56 deletions(-) diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index b1cef16e..f9201b1e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -48,6 +48,7 @@ public virtual async ValueTask ExecuteAsync(IConsole console) else { await ExecuteCommand(c, c.Device, cancellationToken); + Logger.LogInformation($"Done."); } } catch (TimeoutException) diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index be3e699d..ef4afa4e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -124,6 +124,12 @@ public override async ValueTask ExecuteAsync(IConsole console) 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.ConnectionMessage += (s, m) => + { + Logger.LogInformation(m); + }; + var package = await GetSelectedPackage(); var wasRuntimeEnabled = await device.IsRuntimeEnabled(cancellationToken); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 2617c367..9ea526e2 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -33,7 +33,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM7" + "commandLineArgs": "config route COM4" }, "Config: Set Route TCP": { "commandName": "Project", diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index edad4e02..83e29065 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -10,6 +10,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; public event EventHandler ConnectionError = default!; public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress; + public event EventHandler ConnectionMessage = default!; public abstract string Name { get; } @@ -37,6 +38,11 @@ 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)); diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 93ec2f82..05d8fdb7 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -128,19 +128,16 @@ private async Task ListenerProc() } else if (response is TextConcludedResponse tcr) { - _lastRequestConcluded = (int)tcr.RequestType; + _lastRequestConcluded = (RequestType)tcr.RequestType; if (_reconnectInProgress) { _state = ConnectionState.MeadowAttached; _reconnectInProgress = false; } - else + else if (_textListComplete != null) { - if (_textListComplete != null) - { - _textListComplete = true; - } + _textListComplete = true; } } else if (response is TextRequestResponse trr) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 11845c49..f0da07f4 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -513,7 +513,7 @@ private async Task WaitForResult(Func checkAction, CancellationToken } private DeviceInfo? _deviceInfo; - private int? _lastRequestConcluded = null; + private RequestType? _lastRequestConcluded = null; private List StdOut { get; } = new List(); private List StdErr { get; } = new List(); private List InfoMessages { get; } = new List(); @@ -535,7 +535,7 @@ public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken var success = await WaitForResult(() => { - if (_lastRequestConcluded != null && _lastRequestConcluded == 0x303) + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) { return true; } @@ -735,52 +735,63 @@ public override async Task WriteRuntime( string localFileName, CancellationToken? cancellationToken = null) { - CommandTimeoutSeconds = 120; + var commandTimeout = CommandTimeoutSeconds; - 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); - - Console.WriteLine("\nErasing runtime flash blocks..."); + CommandTimeoutSeconds = 120; + _lastRequestConcluded = null; - status = await WaitForResult(() => + try { - var m = string.Join('\n', InfoMessages); - return m.Contains("Mono memory erase success"); - }, - cancellationToken); + InfoMessages.Clear(); - 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); - Console.WriteLine("Moving runtime to flash..."); + RaiseConnectionMessage("\nErasing runtime flash blocks..."); - status = await WaitForResult(() => - { - var m = string.Join('\n', InfoMessages); - return m.Contains("Verifying runtime flash operation."); - }, - cancellationToken); + status = await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); - InfoMessages.Clear(); + InfoMessages.Clear(); - Console.WriteLine("Verifying..."); + RaiseConnectionMessage("Moving runtime to flash..."); - status = await WaitForResult(() => - { - var m = string.Join('\n', InfoMessages); - return m.Contains("Mono runtime successfully flashed."); - }, - cancellationToken); + status = await WaitForResult(() => + { + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); - Console.WriteLine("Done."); + InfoMessages.Clear(); + + RaiseConnectionMessage("Verifying..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } - CommandTimeoutSeconds = 30; + return false; + }, + cancellationToken); - return status; + return status; + } + finally + { + CommandTimeoutSeconds = commandTimeout; + } } public override async Task WriteCoprocessorFile( @@ -788,21 +799,50 @@ public override async Task WriteCoprocessorFile( int destinationAddress, CancellationToken? cancellationToken = null) { - // 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); + // 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 && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } - // now wait for the STM32 to finish writing to the ESP32 - return await WaitForResult(() => + return false; + }, + cancellationToken); + } + finally { - var m = string.Join('\n', InfoMessages); - Console.WriteLine(m); - return m.Contains("TransferComplete"); - }, - cancellationToken); + _port.ReadTimeout = readTimeout; + CommandTimeoutSeconds = commandTimeout; + } } private async Task WriteFile( @@ -880,7 +920,6 @@ void OnFileError(object? sender, Exception exception) bytesRead = fs.Read(packet, 2, packet.Length - 2); if (bytesRead <= 0) break; await EncodeAndSendPacket(packet, bytesRead + 2, cancellationToken); - await Task.Delay(10); progress += bytesRead; base.RaiseFileWriteProgress(fileName, progress, expected); } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 6ce70969..ccc9ea95 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -4,6 +4,7 @@ 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; } From a15a99d89b2f69a62f13d2d7fd9802f168c19e25 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Tue, 29 Aug 2023 17:02:58 -0500 Subject: [PATCH 16/22] added port list --- .../Commands/Current/FirmwareWriteCommand.cs | 1 + .../Commands/Current/PortListCommand.cs | 22 ++ Source/v2/Meadow.Cli/Meadow.Cli.csproj | 1 + .../v2/Meadow.Cli/MeadowConnectionManager.cs | 195 ++++++++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 4 + 5 files changed, 223 insertions(+) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/PortListCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs index ef4afa4e..480c311e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs @@ -84,6 +84,7 @@ public override async ValueTask ExecuteAsync(IConsole console) var cancellationToken = console.RegisterCancellationHandler(); await ExecuteCommand(connection, connection.Device, cancellationToken); } + Logger.LogInformation($"Done."); } else { 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/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index 74684db5..9ce48f5c 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -36,6 +36,7 @@ + diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 1b4ea9e6..361c4b09 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,11 +1,18 @@ 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; @@ -52,4 +59,192 @@ public MeadowConnectionManager(ISettingsManager settingsManager) 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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 9ea526e2..d97f07cf 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -7,6 +7,10 @@ "commandName": "Project", "commandLineArgs": "--version" }, + "Port List": { + "commandName": "Project", + "commandLineArgs": "port list" + }, "Listen": { "commandName": "Project", "commandLineArgs": "listen" From 8a565a6d63bca4d40fa7f36c2a8ea71f2e412748 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 30 Aug 2023 10:11:35 -0500 Subject: [PATCH 17/22] added trace commands --- .../{ => Device}/DeviceClockCommand.cs | 0 .../DeviceInfoCommand.cs} | 0 .../{ => Device}/DeviceResetCommand.cs | 0 .../Current/{ => File}/FileListCommand.cs | 0 .../Current/{ => File}/FileReadCommand.cs | 0 .../Current/{ => File}/FileWriteCommand.cs | 0 .../{ => Firmware}/FirmwareDefaultCommand.cs | 0 .../{ => Firmware}/FirmwareDeleteCommand.cs | 0 .../{ => Firmware}/FirmwareDownloadCommand.cs | 0 .../{ => Firmware}/FirmwareListCommand.cs | 0 .../{ => Firmware}/FirmwareWriteCommand.cs | 0 .../{ => Runtime}/RuntimeDisableCommand.cs | 0 .../{ => Runtime}/RuntimeEnableCommand.cs | 0 .../{ => Runtime}/RuntimeStateCommand.cs | 0 .../Current/Trace/TraceLevelCommand.cs | 92 ++++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 12 ++ .../Meadow.Hcom/Connections/ConnectionBase.cs | 10 +- .../Connections/SerialConnection.cs | 114 ++++++++++++------ .../Meadow.Hcom/Connections/TcpConnection.cs | 15 +++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 4 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 5 +- Source/v2/Meadow.Hcom/MeadowDevice.cs | 14 ++- .../v2/Meadow.Hcom/Serial Requests/Request.cs | 2 +- .../Serial Requests/TraceDisableRequest.cs | 6 + .../Serial Requests/TraceEnableRequest.cs | 6 + .../Serial Requests/TraceLevelRequest.cs | 6 + 26 files changed, 239 insertions(+), 47 deletions(-) rename Source/v2/Meadow.Cli/Commands/Current/{ => Device}/DeviceClockCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{GetDeviceInfoCommand.cs => Device/DeviceInfoCommand.cs} (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Device}/DeviceResetCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => File}/FileListCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => File}/FileReadCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => File}/FileWriteCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Firmware}/FirmwareDefaultCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Firmware}/FirmwareDeleteCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Firmware}/FirmwareDownloadCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Firmware}/FirmwareListCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Firmware}/FirmwareWriteCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Runtime}/RuntimeDisableCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Runtime}/RuntimeEnableCommand.cs (100%) rename Source/v2/Meadow.Cli/Commands/Current/{ => Runtime}/RuntimeStateCommand.cs (100%) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/TraceDisableRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/TraceEnableRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/TraceLevelRequest.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/DeviceClockCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/GetDeviceInfoCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/DeviceResetCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FileListCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FileReadCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FileWriteCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FirmwareDefaultCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FirmwareDeleteCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FirmwareDownloadCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FirmwareListCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/FirmwareWriteCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/RuntimeDisableCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/RuntimeEnableCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs diff --git a/Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs similarity index 100% rename from Source/v2/Meadow.Cli/Commands/Current/RuntimeStateCommand.cs rename to Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs 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..e2955d3d --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -0,0 +1,92 @@ +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); + } + } +} + +[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); + } +} + +[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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index d97f07cf..58474218 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -122,6 +122,18 @@ "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" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 83e29065..f6cf3c05 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -9,7 +9,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; public event EventHandler ConnectionError = default!; - public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress; + public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress = default!; public event EventHandler ConnectionMessage = default!; public abstract string Name { get; } @@ -28,11 +28,9 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable 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 Task UpdateRuntime(string localFileName, CancellationToken? cancellationToken = null) - { - throw new NotImplementedException(); - } + public abstract Task TraceEnable(CancellationToken? cancellationToken = null); + public abstract Task TraceDisable(CancellationToken? cancellationToken = null); + public abstract Task SetTraceLevel(int level, CancellationToken? cancellationToken = null); public ConnectionBase() { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index f0da07f4..6c3266f4 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -487,6 +487,17 @@ public void Dispose() 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; @@ -512,17 +523,38 @@ private async Task WaitForResult(Func checkAction, CancellationToken return true; } - 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 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; + } + } - 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:"; + 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) { @@ -613,19 +645,7 @@ public override async Task RuntimeEnable(CancellationToken? cancellationToken = // we have to give time for the device to actually reset await Task.Delay(500); - var success = await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeSucessfullyEnabledToken)); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); + var success = await WaitForResponseText(RuntimeSucessfullyEnabledToken); if (!success) throw new Exception("Unable to enable runtime"); } @@ -641,23 +661,45 @@ public override async Task RuntimeDisable(CancellationToken? cancellationToken = // we have to give time for the device to actually reset await Task.Delay(500); - var success = await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeSucessfullyDisabledToken)); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); + 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 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 ResetDevice(CancellationToken? cancellationToken = null) { var command = RequestBuilder.Build(); diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index a4d54214..6c8b5d16 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -121,4 +121,19 @@ public override Task WriteCoprocessorFile(string localFileName, int destin { 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(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index ccc9ea95..d0148a70 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -26,5 +26,9 @@ public interface IMeadowConnection 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); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index 1a2c2034..39bc4716 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -14,7 +14,8 @@ public interface IMeadowDevice Task GetRtcTime(CancellationToken? cancellationToken = null); Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null); Task WriteCoprocessorFiles(string[] localFileNames, CancellationToken? cancellationToken = null); - - Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null); + Task TraceEnable(CancellationToken? cancellationToken = null); + Task TraceDisable(CancellationToken? cancellationToken = null); + Task SetTraceLevel(int level, CancellationToken? cancellationToken = null); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index ef86d4f0..fd3dd95d 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -100,9 +100,19 @@ public async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancell await _connection.SetRtcTime(dateTime, cancellationToken); } - public Task FlashOS(string requestedversion, CancellationToken? cancellationToken = null) + public async Task TraceEnable(CancellationToken? cancellationToken = null) { - throw new NotImplementedException(); + 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); } } } \ 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 index 1f8aa7d6..c84dd7f5 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/Request.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs @@ -11,7 +11,7 @@ public abstract class Request : IRequest public ushort SequenceNumber { get; set; } public ushort ProtocolVersion { get; set; } public ushort ExtraData { get; set; } // TODO: what is this for? - public uint UserData { get; set; } // TODO: what is this for? + public uint UserData { get; set; } public Request() { 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; +} From 6308166c7e9762fd3db9e277b2e383e52acafa6e Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 30 Aug 2023 12:01:18 -0500 Subject: [PATCH 18/22] added developer command --- .../Commands/Current/DeveloperCommand.cs | 37 ++++++++++++++ .../Current/Trace/TraceDisableCommand.cs | 27 ++++++++++ .../Current/Trace/TraceEnableCommand.cs | 36 +++++++++++++ .../Current/Trace/TraceLevelCommand.cs | 51 ------------------- .../Meadow.Cli/Properties/launchSettings.json | 4 ++ .../Meadow.Hcom/Connections/ConnectionBase.cs | 1 + .../SerialConnection.ListenerProc.cs | 1 + .../Connections/SerialConnection.cs | 13 +++++ .../Meadow.Hcom/Connections/TcpConnection.cs | 5 ++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 2 + Source/v2/Meadow.Hcom/IMeadowDevice.cs | 1 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 5 ++ .../Serial Requests/DeveloperRequest.cs | 6 +++ .../v2/Meadow.Hcom/Serial Requests/Request.cs | 2 +- .../Serial Requests/RequestType.cs | 3 +- 15 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/DeveloperRequest.cs 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/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 index e2955d3d..0d139c7b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -39,54 +39,3 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, } } -[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); - } -} - -[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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 58474218..8ce0c611 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -134,6 +134,10 @@ "Trace level": { "commandName": "Project", "commandLineArgs": "trace level 2" + }, + "Developer": { + "commandName": "Project", + "commandLineArgs": "developer -p 2 -v 20" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index f6cf3c05..1dc01218 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -31,6 +31,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable 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 ConnectionBase() { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 05d8fdb7..b8162443 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -210,6 +210,7 @@ private async Task ListenerProc() } else if (response is RequestErrorTextResponse ret) { + RaiseDeviceMessageReceived(ret.Text, "hcom"); _lastError = ret.Text; } else if (response is FileWriteInitFailedSerialResponse fwf) diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index 6c3266f4..fbcae7a1 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -700,6 +700,19 @@ public override async Task SetTraceLevel(int level, CancellationToken? cancellat 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(); diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 6c8b5d16..18801fc3 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -136,4 +136,9 @@ public override Task SetTraceLevel(int level, CancellationToken? cancellationTok { throw new NotImplementedException(); } + + public override Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index d0148a70..0e722fce 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -30,5 +30,7 @@ public interface IMeadowConnection 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); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index 39bc4716..045775d7 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -17,5 +17,6 @@ public interface IMeadowDevice 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); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index fd3dd95d..5587f1ee 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -114,5 +114,10 @@ public async Task SetTraceLevel(int level, CancellationToken? cancellationToken { 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/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/Request.cs b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs index c84dd7f5..229ea4fa 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/Request.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/Request.cs @@ -10,7 +10,7 @@ public abstract class Request : IRequest public ushort SequenceNumber { get; set; } public ushort ProtocolVersion { get; set; } - public ushort ExtraData { get; set; } // TODO: what is this for? + public ushort ExtraData { get; set; } public uint UserData { get; set; } public Request() diff --git a/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs b/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs index ccdcef86..ad98c3cc 100644 --- a/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs +++ b/Source/v2/Meadow.Hcom/Serial Requests/RequestType.cs @@ -69,11 +69,12 @@ public enum RequestType : ushort // This is a simple type with binary data HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA = 0x01 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_BINARY_TYPE, - // Only used for testing + 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, From 139ff1105aa869f5102854a375935dafb54fe608 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Tue, 5 Sep 2023 11:52:21 -0500 Subject: [PATCH 19/22] uart trace commands --- .../Devices/MeadowLocalDevice.Comms.cs | 8 ++-- Meadow.CLI/Properties/launchSettings.json | 2 +- .../Current/Firmware/FirmwareWriteCommand.cs | 4 +- .../Current/Trace/TraceLevelCommand.cs | 1 - .../Current/Uart/UartTraceDisableCommand.cs | 26 +++++++++++++ .../Current/Uart/UartTraceEnableCommand.cs | 26 +++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 10 ++++- .../Meadow.Hcom/Connections/ConnectionBase.cs | 2 + .../Connections/SerialConnection.cs | 38 ++++++++++++++++++- .../Meadow.Hcom/Connections/TcpConnection.cs | 10 +++++ Source/v2/Meadow.Hcom/IMeadowConnection.cs | 3 ++ Source/v2/Meadow.Hcom/IMeadowDevice.cs | 2 + Source/v2/Meadow.Hcom/MeadowDevice.cs | 10 +++++ .../UartTraceDisableRequest.cs | 6 +++ .../Serial Requests/UartTraceEnableRequest.cs | 6 +++ 15 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/UartTraceDisableRequest.cs create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/UartTraceEnableRequest.cs 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/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json index 77b3d94d..a9bc47bc 100644 --- a/Meadow.CLI/Properties/launchSettings.json +++ b/Meadow.CLI/Properties/launchSettings.json @@ -46,7 +46,7 @@ }, "FlashOS": { "commandName": "Project", - "commandLineArgs": "flash os" + "commandLineArgs": "flash os -d -k" }, "Help": { "commandName": "Project", diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 480c311e..341e4019 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -126,9 +126,9 @@ public override async ValueTask ExecuteAsync(IConsole console) 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.ConnectionMessage += (s, m) => + connection.DeviceMessageReceived += (s, e) => { - Logger.LogInformation(m); + Logger.LogInformation(e.message); }; var package = await GetSelectedPackage(); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index 0d139c7b..3c7300a5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -38,4 +38,3 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, } } } - 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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 8ce0c611..30c63606 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -105,7 +105,7 @@ }, "Firmware Write all": { "commandName": "Project", - "commandLineArgs": "firmware write" + "commandLineArgs": "firmware write runtime esp" }, "Firmware Write version": { "commandName": "Project", @@ -138,6 +138,14 @@ "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" } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 1dc01218..ec7984e8 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -32,6 +32,8 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable 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() { diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index fbcae7a1..61eff517 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -688,6 +688,28 @@ public override async Task TraceDisable(CancellationToken? cancellationToken = n 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(); @@ -810,6 +832,12 @@ public override async Task WriteRuntime( status = await WaitForResult(() => { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + var m = string.Join('\n', InfoMessages); return m.Contains("Mono memory erase success"); }, @@ -821,6 +849,12 @@ public override async Task WriteRuntime( status = await WaitForResult(() => { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + var m = string.Join('\n', InfoMessages); return m.Contains("Verifying runtime flash operation."); }, @@ -832,7 +866,7 @@ public override async Task WriteRuntime( status = await WaitForResult(() => { - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + if (_lastRequestConcluded != null) { return true; } @@ -884,7 +918,7 @@ await WriteFile(localFileName, null, RaiseConnectionMessage(InfoMessages.Last()); } - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + if (_lastRequestConcluded != null) { return true; } diff --git a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs index 18801fc3..49eeffd3 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -141,4 +141,14 @@ public override Task SetDeveloperParameter(ushort parameter, uint value, Cancell { 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/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 0e722fce..959cce8c 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -32,5 +32,8 @@ public interface IMeadowConnection 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 index 045775d7..fce2fec5 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -18,5 +18,7 @@ public interface IMeadowDevice 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/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 5587f1ee..9e9bacaf 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -72,6 +72,16 @@ public async Task WriteCoprocessorFiles(string[] localFileNames, Cancellat 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 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; +} From 0b6f713cc4a13b935e5f4b884da5a3d7b8f72409 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Fri, 22 Sep 2023 12:06:24 -0500 Subject: [PATCH 20/22] added commands to build and trim applications --- .../Commands/Current/App/AppBuildCommand.cs | 115 ++++++++++ .../Commands/Current/BaseFileCommand.cs | 16 ++ .../Firmware/FirmwareDefaultCommand.cs | 12 +- .../Current/Firmware/FirmwareDeleteCommand.cs | 12 +- .../Firmware/FirmwareDownloadCommand.cs | 12 +- .../Current/Firmware/FirmwareListCommand.cs | 14 +- .../Current/Firmware/FirmwareWriteCommand.cs | 10 +- Source/v2/Meadow.Cli/Meadow.Cli.csproj | 18 ++ .../PackageManager.AssemblyManager.cs | 211 ++++++++++++++++++ .../Meadow.Cli/PackageManager.BuildOptions.cs | 15 ++ Source/v2/Meadow.Cli/PackageManager.cs | 152 +++++++++++++ Source/v2/Meadow.Cli/Program.cs | 4 + .../Meadow.Cli/Properties/launchSettings.json | 12 + Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll | Bin 0 -> 114176 bytes Source/v2/Meadow.Cli/lib/Mono.Cecil.dll | Bin 0 -> 499200 bytes Source/v2/Meadow.Cli/lib/illink.deps.json | 112 ++++++++++ Source/v2/Meadow.Cli/lib/illink.dll | Bin 0 -> 466944 bytes .../Meadow.Cli/lib/illink.runtimeconfig.json | 10 + Source/v2/Meadow.Cli/lib/meadow_link.xml | 11 + 19 files changed, 704 insertions(+), 32 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs create mode 100644 Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs create mode 100644 Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs create mode 100644 Source/v2/Meadow.Cli/PackageManager.cs create mode 100644 Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll create mode 100644 Source/v2/Meadow.Cli/lib/Mono.Cecil.dll create mode 100644 Source/v2/Meadow.Cli/lib/illink.deps.json create mode 100644 Source/v2/Meadow.Cli/lib/illink.dll create mode 100644 Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json create mode 100644 Source/v2/Meadow.Cli/lib/meadow_link.xml 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..de1b208e --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -0,0 +1,115 @@ +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."); + } + } +} + +[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/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/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index dd72c04e..37687713 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -6,10 +6,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("firmware default", Description = "Sets the current default firmware package")] -public class FirmwareDefaultCommand : BaseCommand +public class FirmwareDefaultCommand : BaseFileCommand { - public FirmwareDefaultCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) - : base(settingsManager, loggerFactory) + public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) { } @@ -18,13 +18,11 @@ public FirmwareDefaultCommand(ISettingsManager settingsManager, ILoggerFactory l protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) { - var manager = new FileManager(); - - await manager.Refresh(); + await FileManager.Refresh(); // for now we only support F7 // TODO: add switch and support for other platforms - var collection = manager.Firmware["Meadow F7"]; + var collection = FileManager.Firmware["Meadow F7"]; var existing = collection.FirstOrDefault(p => p.Version == Version); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 08ae586e..511a0cc5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -6,10 +6,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("firmware delete", Description = "Delete a local firmware package")] -public class FirmwareDeleteCommand : BaseCommand +public class FirmwareDeleteCommand : BaseFileCommand { - public FirmwareDeleteCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) - : base(settingsManager, loggerFactory) + public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) { } @@ -18,13 +18,11 @@ public FirmwareDeleteCommand(ISettingsManager settingsManager, ILoggerFactory lo protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) { - var manager = new FileManager(); - - await manager.Refresh(); + await FileManager.Refresh(); // for now we only support F7 // TODO: add switch and support for other platforms - var collection = manager.Firmware["Meadow F7"]; + var collection = FileManager.Firmware["Meadow F7"]; Logger?.LogInformation($"Deleting firmware '{Version}'..."); diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index 559c6079..65fb7283 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -6,10 +6,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("firmware download", Description = "Download a firmware package")] -public class FirmwareDownloadCommand : BaseCommand +public class FirmwareDownloadCommand : BaseFileCommand { - public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) - : base(settingsManager, loggerFactory) + public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) + : base(fileManager, settingsManager, loggerFactory) { } @@ -21,13 +21,11 @@ public FirmwareDownloadCommand(ISettingsManager settingsManager, ILoggerFactory protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) { - var manager = new FileManager(); - - await manager.Refresh(); + await FileManager.Refresh(); // for now we only support F7 // TODO: add switch and support for other platforms - var collection = manager.Firmware["Meadow F7"]; + var collection = FileManager.Firmware["Meadow F7"]; if (Version == null) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index d5221705..87f08c44 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -16,26 +16,26 @@ public class FirmwareListCommand : ICommand [CommandOption("verbose", 'v', IsRequired = false)] public bool Verbose { get; set; } - public FirmwareListCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) + 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) { - - var manager = new FileManager(); - - await manager.Refresh(); + await FileManager.Refresh(); if (Verbose) { - await DisplayVerboseResults(manager); + await DisplayVerboseResults(FileManager); } else { - await DisplayTerseResults(manager); + await DisplayTerseResults(FileManager); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 341e4019..b6fe8a46 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -26,9 +26,12 @@ public class FirmwareWriteCommand : BaseDeviceCommand [CommandParameter(0, Name = "Files to write", IsRequired = false)] public FirmwareType[]? Files { get; set; } = default!; - public FirmwareWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + private FileManager FileManager { get; } + + public FirmwareWriteCommand(FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + FileManager = fileManager; } public override async ValueTask ExecuteAsync(IConsole console) @@ -94,10 +97,9 @@ public override async ValueTask ExecuteAsync(IConsole console) private async Task GetSelectedPackage() { - var manager = new FileManager(); - await manager.Refresh(); + await FileManager.Refresh(); - var collection = manager.Firmware["Meadow F7"]; + var collection = FileManager.Firmware["Meadow F7"]; FirmwarePackage package; if (Version != null) diff --git a/Source/v2/Meadow.Cli/Meadow.Cli.csproj b/Source/v2/Meadow.Cli/Meadow.Cli.csproj index 9ce48f5c..172cfa33 100644 --- a/Source/v2/Meadow.Cli/Meadow.Cli.csproj +++ b/Source/v2/Meadow.Cli/Meadow.Cli.csproj @@ -25,6 +25,7 @@ meadow latest Copyright 2020-2022 Wilderness Labs + enable @@ -37,6 +38,8 @@ + + @@ -48,4 +51,19 @@ + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + 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..9679a840 --- /dev/null +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -0,0 +1,152 @@ +using Meadow.Software; +using System.Diagnostics; +using YamlDotNet.Serialization; + +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); +} + +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 index 0e4449e2..aa956ca3 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -1,6 +1,7 @@ using CliFx; using Meadow.Cli; using Meadow.CLI.Commands.DeviceManagement; +using Meadow.Software; using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Events; @@ -42,7 +43,10 @@ public static async Task Main(string[] args) }); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + /* services.AddSingleton(); services.AddSingleton(); diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 30c63606..23db90c6 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -146,6 +146,18 @@ "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" } } } \ 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 0000000000000000000000000000000000000000..d6f5ee58c08204959957f7168aefc85fea9769cb GIT binary patch literal 114176 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 RWV^t>eWm|j;QvAg{sSvy`wRd8 literal 0 HcmV?d00001 diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll b/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll new file mode 100644 index 0000000000000000000000000000000000000000..682aef81ff4b3bab63113e63c8ae9e53b8077d19 GIT binary patch literal 499200 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%0APFdTIg%$_o8)Vy*rl#4%(MJpX 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 literal 0 HcmV?d00001 diff --git a/Source/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 0000000000000000000000000000000000000000..f392213b13c7b453f5b1671a037526ea7363c162 GIT binary patch literal 466944 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

oWoE0{;WQWKS2#dqHO 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%0DbE7Pb(JM;gls>Rd~_TL`* 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 literal 0 HcmV?d00001 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 @@ + + + + + + + + + + + From c553be9da4a21ef2e3be0a09ece6412e9fde6735 Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 27 Sep 2023 09:37:42 -0500 Subject: [PATCH 21/22] Dfu install --- Meadow.CLI.Core/Managers/PackageManager.cs | 20 ++--- .../Commands/Current/App/AppBuildCommand.cs | 60 +------------- .../Commands/Current/App/AppDeployCommand.cs | 55 ++++++++++++ .../Commands/Current/App/AppTrimCommand.cs | 63 ++++++++++++++ .../Commands/Current/Dfu/DfuInstallCommand.cs | 83 +++++++++++++++++++ .../Commands/Legacy/InstallDfuCommand.cs | 16 ++++ .../Commands/Legacy/MonoEnableCommand.cs | 2 +- Source/v2/Meadow.Cli/DFU/DfuUtils.cs | 81 ++++++++++++++++++ .../Meadow.Cli/Properties/launchSettings.json | 12 +++ .../Firmware/DownloadFileStream.cs | 10 +-- .../v2/Meadow.SoftwareManager/FileManager.cs | 8 +- 11 files changed, 333 insertions(+), 77 deletions(-) create mode 100644 Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs create mode 100644 Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs 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/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index de1b208e..1296f80e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -54,62 +54,4 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation Logger.LogError($"Build success."); } } -} - -[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); - } -} +} \ 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..c764855b --- /dev/null +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -0,0 +1,55 @@ +using CliFx.Attributes; +using Meadow.Cli; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("app deploy", Description = "Deploys a built Meadow application to a target device")] +public class AppDeployCommand : BaseCommand +{ + private IPackageManager _packageManager; + + [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] + public string? Path { get; set; } = default!; + + public AppDeployCommand(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; + } + } + else + { + // TODO: only deploy if it's App.dll + } + + // TODO: send files + + 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/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/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/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs index 72c97ff1..4fa4860a 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -11,4 +11,4 @@ public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFacto { Logger.LogWarning($"Deprecated command. Use `runtime enable` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/DFU/DfuUtils.cs b/Source/v2/Meadow.Cli/DFU/DfuUtils.cs index 89b7f3fc..1a020ab7 100644 --- a/Source/v2/Meadow.Cli/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.Cli/DFU/DfuUtils.cs @@ -1,8 +1,10 @@ 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; @@ -296,4 +298,83 @@ private static string GetDfuUtilVersion() } } } + + 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/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index 23db90c6..25caa7bc 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -3,6 +3,10 @@ "Meadow.Cli": { "commandName": "Project" }, + "Help": { + "commandName": "Project", + "commandLineArgs": "--help" + }, "Version": { "commandName": "Project", "commandLineArgs": "--version" @@ -158,6 +162,14 @@ "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.Hcom/Firmware/DownloadFileStream.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs index 65e69436..4c37a41a 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadFileStream.cs @@ -4,14 +4,14 @@ namespace Meadow.Hcom; public class DownloadFileStream : Stream, IDisposable { - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly Stream _stream; private long _position; private DateTimeOffset _lastLog; private long _lastPosition; - public DownloadFileStream(Stream stream, ILogger logger) + public DownloadFileStream(Stream stream, ILogger? logger = null) { _stream = stream; _logger = logger; @@ -72,17 +72,17 @@ private void LogPosition() if (_position < 1024) { - _logger.LogInformation("Downloaded {position} bytes", _position); + _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)); + _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)); + _logger?.LogInformation("Downloaded {position} MiB", Math.Round(_position / 1024M / 1024M, 2, MidpointRounding.ToEven)); _lastPosition = _position; } } diff --git a/Source/v2/Meadow.SoftwareManager/FileManager.cs b/Source/v2/Meadow.SoftwareManager/FileManager.cs index d829f50f..9509e698 100644 --- a/Source/v2/Meadow.SoftwareManager/FileManager.cs +++ b/Source/v2/Meadow.SoftwareManager/FileManager.cs @@ -1,10 +1,16 @@ -using System.Threading.Tasks; +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"; From d45e93bc7f451b82c2764c49932d639562b8a9dd Mon Sep 17 00:00:00 2001 From: Chris Tacke Date: Wed, 27 Sep 2023 10:36:40 -0500 Subject: [PATCH 22/22] working on app deploy --- Source/v2/Meadow.Cli/AppManager.cs | 90 +++++++++++++++++++ .../Commands/Current/App/AppDeployCommand.cs | 18 ++-- .../Commands/Current/File/FileWriteCommand.cs | 2 + Source/v2/Meadow.Cli/IPackageManager.cs | 11 +++ Source/v2/Meadow.Cli/PackageManager.cs | 10 --- Source/v2/Meadow.SoftwareManager/CrcTools.cs | 85 ++++++++++++++++++ .../Meadow.SoftwareManager.csproj | 1 + 7 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 Source/v2/Meadow.Cli/AppManager.cs create mode 100644 Source/v2/Meadow.Cli/IPackageManager.cs create mode 100644 Source/v2/Meadow.SoftwareManager/CrcTools.cs 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/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs index c764855b..3ac340d5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,24 +1,22 @@ 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 : BaseCommand +public class AppDeployCommand : BaseDeviceCommand { - private IPackageManager _packageManager; - [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] public string? Path { get; set; } = default!; - public AppDeployCommand(IPackageManager packageManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) - : base(settingsManager, loggerFactory) + public AppDeployCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) { - _packageManager = packageManager; } - protected override async ValueTask ExecuteCommand(CancellationToken cancellationToken) + protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken) { string path = Path == null ? AppDomain.CurrentDomain.BaseDirectory @@ -39,7 +37,11 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation // TODO: only deploy if it's App.dll } - // TODO: send files + // 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; diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index e769da09..a6360e80 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -38,6 +38,8 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection, 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"); }; 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/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs index 9679a840..480b959c 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.Cli/PackageManager.cs @@ -4,16 +4,6 @@ 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); -} - public partial class PackageManager : IPackageManager { public const string BuildOptionsFileName = "app.build.yaml"; 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/Meadow.SoftwareManager.csproj b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj index 2908c075..d61863d1 100644 --- a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj +++ b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj @@ -8,6 +8,7 @@ +