diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml new file mode 100644 index 00000000..3b5b3975 --- /dev/null +++ b/.github/workflows/dotnet-ci.yml @@ -0,0 +1,54 @@ +name: Develop Build + +on: + workflow_dispatch: + pull_request: + branches: [ develop ] + push: + branches: [ develop ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - name: Checkout Meadow.Logging + uses: actions/checkout@v3 + with: + repository: WildernessLabs/Meadow.Logging + path: Meadow.Logging + ref: develop + + - name: Checkout Meadow.Units + uses: actions/checkout@v3 + with: + repository: WildernessLabs/Meadow.Units + path: Meadow.Units + ref: develop + + - name: Checkout Meadow.Contracts + uses: actions/checkout@v3 + with: + repository: WildernessLabs/Meadow.Contracts + path: Meadow.Contracts + ref: develop + + - name: Checkout Meadow.CLI + uses: actions/checkout@v3 + with: + path: Meadow.CLI + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: + 8.0.x + + - name: Build CLI v1 + run: dotnet build -c Release Meadow.CLI/MeadowCLI.sln + + - name: Build CLI v2 + run: dotnet build -c Release Meadow.CLI/Source/v2/Meadow.CLI.v2.sln + + diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 300b7506..31b83240 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,4 +1,4 @@ -name: Meadow.CLI +name: Meadow.CLI Packaging env: CLI_RELEASE_VERSION: 1.5.0.0 IDE_TOOLS_RELEASE_VERSION: 1.5.0 @@ -7,13 +7,9 @@ env: VS_MAC_2022_VERSION: 17.6 on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -# Allows you to run this workflow manually from the Actions tab workflow_dispatch: + push: + branches: [ main ] jobs: build-and-optionally-publish-nuget: @@ -129,68 +125,68 @@ jobs: # run: | # nuget push main\Source\v2\Meadow.CLI\bin\Release\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} - build-vswin-2019: - runs-on: windows-2019 - needs: [build-and-optionally-publish-nuget] - name: Build Win 2019 Extension - - steps: - - name: Checkout Meadow.CLI.Core side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/Meadow.CLI - path: Meadow.CLI - - - if: ${{ github.ref == 'refs/heads/main' }} - name: Checkout Win Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Win_Meadow_Extension - path: vs-win - ref: main - - - if: ${{ github.ref != 'refs/heads/main' }} - name: Checkout Win Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Win_Meadow_Extension - path: vs-win - - - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 5.0.x - 6.0.x - 7.0.x - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Add MSBuild to Path - uses: microsoft/setup-msbuild@v1.1 - - - if: ${{ github.event_name == 'workflow_dispatch' }} - name: Update VS2019 Version Numbers - run: | - $content = Get-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest | Out-String - $newcontent = $content -replace 'Version="1.*" Language="en-US" Publisher="Wilderness Labs"', 'Version="${{ ENV.IDE_TOOLS_RELEASE_VERSION }}" Language="en-US" Publisher="Wilderness Labs"' - $newcontent | Set-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest - - - name: Restore VS2019 dependencies - run: dotnet restore vs-win/VS_Meadow_Extension.2019.sln /p:Configuration=Release - - - name: Build VS2019 Extension - id: VS2019-Extension - run: msbuild vs-win/VS_Meadow_Extension.2019.sln /t:Rebuild /p:Configuration=Release - env: - DevEnvDir: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE' - - - name: Upload VS2019 VSIX Artifacts - uses: actions/upload-artifact@v2 - with: - name: Meadow.Win.VS2019.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} - path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2019\bin\Release\*.vsix' + # build-vswin-2019: + # runs-on: windows-2019 + # needs: [build-and-optionally-publish-nuget] + # name: Build Win 2019 Extension + + # steps: + # - name: Checkout Meadow.CLI.Core side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/Meadow.CLI + # path: Meadow.CLI + + # - if: ${{ github.ref == 'refs/heads/main' }} + # name: Checkout Win Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Win_Meadow_Extension + # path: vs-win + # ref: main + + # - if: ${{ github.ref != 'refs/heads/main' }} + # name: Checkout Win Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Win_Meadow_Extension + # path: vs-win + + # - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: | + # 5.0.x + # 6.0.x + # 7.0.x + + # - name: Setup NuGet + # uses: NuGet/setup-nuget@v1.0.5 + + # - name: Add MSBuild to Path + # uses: microsoft/setup-msbuild@v1.1 + + # - if: ${{ github.event_name == 'workflow_dispatch' }} + # name: Update VS2019 Version Numbers + # run: | + # $content = Get-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest | Out-String + # $newcontent = $content -replace 'Version="1.*" Language="en-US" Publisher="Wilderness Labs"', 'Version="${{ ENV.IDE_TOOLS_RELEASE_VERSION }}" Language="en-US" Publisher="Wilderness Labs"' + # $newcontent | Set-Content vs-win/VS_Meadow_Extension/VS_Meadow_Extension.2019/source.extension.vsixmanifest + + # - name: Restore VS2019 dependencies + # run: dotnet restore vs-win/VS_Meadow_Extension.2019.sln /p:Configuration=Release + + # - name: Build VS2019 Extension + # id: VS2019-Extension + # run: msbuild vs-win/VS_Meadow_Extension.2019.sln /t:Rebuild /p:Configuration=Release + # env: + # DevEnvDir: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE' + + # - name: Upload VS2019 VSIX Artifacts + # uses: actions/upload-artifact@v2 + # with: + # name: Meadow.Win.VS2019.vsix.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} + # path: 'vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2019\bin\Release\*.vsix' #- if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} # name: Publish VS2019 Extension @@ -265,72 +261,72 @@ jobs: run: | & "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\VSSDK\VisualStudioIntegration\Tools\Bin\VsixPublisher.exe" publish -payload "vs-win\VS_Meadow_Extension\VS_Meadow_Extension.2022\bin\Release\Meadow.2022.vsix" -publishManifest "vs-win\publishManifest.2022.json" -ignoreWarnings "None" -personalAccessToken "${{secrets.MARKETPLACE_PUBLISH_PAT}}" - build-vsmac-2019: - name: Build Mac 2019 Extension - needs: [build-and-optionally-publish-nuget] - runs-on: macos-11 - - steps: - - name: Checkout Meadow.CLI.Core side-by-side - uses: actions/checkout@v2 - with: - path: Meadow.CLI - - - if: ${{ github.ref == 'refs/heads/main' }} - name: Checkout Mac Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Mac_Meadow_Extension - path: vs-mac - ref: main - - - if: ${{ github.ref != 'refs/heads/main' }} - name: Checkout Mac Extension side-by-side - uses: actions/checkout@v2 - with: - repository: WildernessLabs/VS_Mac_Meadow_Extension - path: vs-mac - - - name: Set default Xcode 13.0 - run: | - XCODE_ROOT=/Applications/Xcode_13.0.0.app - echo "MD_APPLE_SDK_ROOT=$XCODE_ROOT" >> $GITHUB_ENV - sudo xcode-select -s $XCODE_ROOT - - - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 5.0.x - 6.0.x - 7.0.x - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Work around so that VS2019 is picked up. - run: | - mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" - mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" - - - if: ${{ github.event_name == 'workflow_dispatch' }} - name: Update VS2019 Version Numbers - run: | - sed -i "" "s/Version = \"1.*\"/Version = \"${{ENV.IDE_TOOLS_RELEASE_VERSION}}\"/" vs-mac/VS4Mac_Meadow_Extension/Properties/AddinInfo.cs - - - name: Restore our VS2019 project - run: | - msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Restore /p:Configuration=Release - - - name: Build and Package the VS2019 Extension - run: | - msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Build /p:Configuration=Release /p:CreatePackage=true - - - name: Upload Mac VS2019 mpack Artifacts - uses: actions/upload-artifact@v2 - with: - name: Meadow.Mac.2019.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} - path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net472/*.mpack' + # build-vsmac-2019: + # name: Build Mac 2019 Extension + # needs: [build-and-optionally-publish-nuget] + # runs-on: macos-11 + + # steps: + # - name: Checkout Meadow.CLI.Core side-by-side + # uses: actions/checkout@v2 + # with: + # path: Meadow.CLI + + # - if: ${{ github.ref == 'refs/heads/main' }} + # name: Checkout Mac Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Mac_Meadow_Extension + # path: vs-mac + # ref: main + + # - if: ${{ github.ref != 'refs/heads/main' }} + # name: Checkout Mac Extension side-by-side + # uses: actions/checkout@v2 + # with: + # repository: WildernessLabs/VS_Mac_Meadow_Extension + # path: vs-mac + + # - name: Set default Xcode 13.0 + # run: | + # XCODE_ROOT=/Applications/Xcode_13.0.0.app + # echo "MD_APPLE_SDK_ROOT=$XCODE_ROOT" >> $GITHUB_ENV + # sudo xcode-select -s $XCODE_ROOT + + # - name: Setup .NET Core SDK 5.0.x, 6.0.x & 7.0.x + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: | + # 5.0.x + # 6.0.x + # 7.0.x + + # - name: Setup NuGet + # uses: NuGet/setup-nuget@v1.0.5 + + # - name: Work around so that VS2019 is picked up. + # run: | + # mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" + # mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" + + # - if: ${{ github.event_name == 'workflow_dispatch' }} + # name: Update VS2019 Version Numbers + # run: | + # sed -i "" "s/Version = \"1.*\"/Version = \"${{ENV.IDE_TOOLS_RELEASE_VERSION}}\"/" vs-mac/VS4Mac_Meadow_Extension/Properties/AddinInfo.cs + + # - name: Restore our VS2019 project + # run: | + # msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Restore /p:Configuration=Release + + # - name: Build and Package the VS2019 Extension + # run: | + # msbuild vs-mac/VS4Mac_Meadow_Extension.sln /t:Build /p:Configuration=Release /p:CreatePackage=true + + # - name: Upload Mac VS2019 mpack Artifacts + # uses: actions/upload-artifact@v2 + # with: + # name: Meadow.Mac.2019.mpack.${{ ENV.IDE_TOOLS_RELEASE_VERSION }} + # path: 'vs-mac/VS4Mac_Meadow_Extension/bin/Release/net472/*.mpack' #- if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }} # name: Get Commit Messages @@ -592,4 +588,4 @@ jobs: name: Publish VSCode Extension run: | cd vs-code - vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} \ No newline at end of file + vsce publish -p ${{ secrets.MARKETPLACE_PUBLISH_PAT }} diff --git a/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs b/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs index 434515da..cfda7164 100644 --- a/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs +++ b/Meadow.CLI.Core/Devices/MeadowDeviceHelper.cs @@ -243,7 +243,7 @@ public Task QspiInit(int value, CancellationToken cancellationToken = default) return _meadowDevice.QspiInit(value, cancellationToken); } - public async Task DeployApp(string fileName, bool includePdbs = true, CancellationToken cancellationToken = default, bool verbose = false, IList? noLink = null) + public async Task DeployApp(string fileName, bool includePdbs = false, CancellationToken cancellationToken = default, bool verbose = false, IList? noLink = null) { try { diff --git a/Meadow.CLI.Core/Globals.cs b/Meadow.CLI.Core/Globals.cs index 766d71a2..26f1b897 100644 --- a/Meadow.CLI.Core/Globals.cs +++ b/Meadow.CLI.Core/Globals.cs @@ -1,6 +1,2 @@ -#if VS2019 -global using Meadow.CLI.Core.Logging; -#else -global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Logging.Abstractions; -#endif \ No newline at end of file +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Abstractions; \ No newline at end of file diff --git a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj index f796bb81..10fed43f 100644 --- a/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj +++ b/Meadow.CLI.Core/Meadow.CLI.Core.VS2019.csproj @@ -63,8 +63,6 @@ - - diff --git a/Meadow.CLI.Core/lib/meadow_link.xml b/Meadow.CLI.Core/lib/meadow_link.xml index 0fe29752..bd47ba33 100644 --- a/Meadow.CLI.Core/lib/meadow_link.xml +++ b/Meadow.CLI.Core/lib/meadow_link.xml @@ -8,4 +8,5 @@ + diff --git a/Meadow.CLI/Commands/App/DeployAppCommand.cs b/Meadow.CLI/Commands/App/DeployAppCommand.cs index a72da6ca..d15a2423 100644 --- a/Meadow.CLI/Commands/App/DeployAppCommand.cs +++ b/Meadow.CLI/Commands/App/DeployAppCommand.cs @@ -27,7 +27,7 @@ public class DeployAppCommand : MeadowSerialCommand public IList NoLink { get; init; } = null; [CommandOption("includePdbs", 'i', Description = "Include the PDB files on deploy to enable debugging", IsRequired = false)] - public bool IncludePdbs { get; init; } = true; + public bool IncludePdbs { get; init; } = false; public DeployAppCommand(DownloadManager downloadManager, ILoggerFactory loggerFactory, MeadowDeviceManager meadowDeviceManager) : base(downloadManager, loggerFactory, meadowDeviceManager) diff --git a/README.md b/README.md index 739eaa31..f7b52339 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ -Meadow CLI project banner stating Meadow's Command-Line-Interface to interact with the board and perform functions via a terminal/command-line window. - ## Build Status [![Build](https://github.com/WildernessLabs/Meadow.CLI/actions/workflows/dotnet.yml/badge.svg)](https://github.com/WildernessLabs/Meadow.CLI/actions) ## Getting Started -The CLI tool supports DFU flashing for `nuttx.bin` and `nuttx_user.bin`. When the application is run with `-d`, it looks for `nuttx.bin` and `nuttx_user.bin` in the application directory and if not found, it will abort. Optionally, paths for the files can be specific with `--osFile` and `--userFile`. - -The CLI tool also supports device and file management including file transfers, flash partitioning, and MCU reset. - To install the latest Meadow.CLI release, run the .NET tool install command to get the latest package from NuGet. ```console @@ -25,183 +19,25 @@ Once installed, run the Meadow.CLI from a command line with `meadow`. To see the options, run the application with the --help arg. -## Running Commands - -### Specifying the Serial Port -File and device commands require you to specify the serial port (`-s` or `--SerialPort`). You can determine the serial port name in Windows by viewing the Device Manager. The CLI will remember the last Serial Port used, so you only need to specify it if you need to change the value. - -On Mac and Linux, the serial port will show up in the **/dev** folder, generally with the prefix **tty.usb**. You can likely find the serial port name by running the command `ls /dev/tty.usb`. - -### Setting the Log Verbosity -Appending `-v` or `-vv` to any command will increase the logging verbosity to `Debug` and `Trace` respectively. `Trace` should only be necessary when debugging issues with the CLI. - -### Available Commands - -```console -meadow v1.0.0 - -USAGE - meadow [options] - meadow [command] [...] +## Useful commands -OPTIONS - -h|--help Shows help text. - --version Shows version information. +### Download Meadow OS -COMMANDS - app deploy Deploy the specified app to the Meadow - cloud login Log into the Meadow Service - cloud logout Logout of the Meadow Service - debug Debug a Meadow Application - device info Get the device info - device mac Read the ESP32's MAC address - device name Get the name of the Meadow - device provision Registers and prepares connected device for use with Meadow Cloud - download os Downloads the latest Meadow.OS to the host PC - esp32 file write Write files to the ESP File System - esp32 restart Restart the ESP32 - file delete Delete files from the Meadow File System Subcommands: file delete all. - file initial Get the initial bytes from a file - file list List files in the on-board filesystem - file write Write files to the Meadow File System - flash erase Erase the flash on the Meadow Board - flash esp Flash the ESP co-processor - flash os Update the OS on the Meadow Board - flash verify Verify the contents of the flash were deleted - fs renew Create a File System on the Meadow Board - install dfu-util Install the DfuUtil utility - list ports List available COM ports - listen Listen for console output from Meadow - mono disable Sets mono to NOT run on the Meadow board then resets it - mono enable Sets mono to run on the Meadow board and then resets it - mono flash Uploads the mono runtime file to the Meadow device. Does NOT move it into place - mono state Returns whether or not mono is enabled or disabled on the Meadow device - mono update rt Uploads the mono runtime files to the Meadow device and moves it into place - nsh disable Disables NSH on the Meadow device - nsh enable Enables NSH on the Meadow device - package create Create Meadow Package - package list List Meadow Packages - package publish List Meadow Packages - package upload Upload Meadow Package - qspi init Init the QSPI on the Meadow - qspi read Read a QSPI value from the Meadow - qspi write Write a QSPI value to the Meadow - set developer Set developer value - trace disable Disable Trace Logging on the Meadow - trace enable Enable trace logging on the Meadow - trace level Enable trace logging on the Meadow - uart trace Configure trace logs to go to UART - use port Set the preferred serial port ``` - -### Getting Help - -Specifying `--help` with no command will output the list of available commands. Specifying `--help` after a command (e.g., `meadow file delete --help`) will output command specific help. - -```console -meadow v1.0.0 - -USAGE - meadow file delete --files [options] - meadow file delete [command] [...] - -DESCRIPTION - Delete files from the Meadow File System - -OPTIONS -* -f|--files The file(s) to delete from the Meadow Files System - -s|--SerialPort Meadow COM port Default: "COM10". - -g|--LogVerbosity Log verbosity - -h|--help Shows help text. - -COMMANDS - all Delete all files from the Meadow File System - -You can run `meadow file delete [command] --help` to show help on a specific command. -Done! +meadow firmware download ``` -## Useful commands - ### Update the Meadow OS + ``` -meadow flash os +meadow firmware write ``` -#### Meadow.CLI download location - -If you need to find or clear out any of the OS download files retrieved by Meadow.CLI, they are located in a WildernessLabs folder in the user directory. - -macOS: `~/.local/share/WildernessLabs/Firmware/` -Windows: `%LOCALAPPDATA%\WildernessLabs\Firmware` - ### Listen for Meadow Console.WriteLine ``` meadow listen ``` -### Set the trace level - -You can set the debug trace level to values 0, 1, 2, or 3. 2 is the most useful. -``` -meadow trace enable --level 2 -``` - -### File transfers -``` -meadow files write -f [NameOfFile] -``` -You may specify multiple instances of `-f` to send multiple files - -### List files in flash -``` -meadow files list -``` - -### Delete a File - -``` -meadow files delete -f [NameOfFile] -``` -You may specify multiple instances of `-f` to send multiple files - -### Stop/start the installed application from running automatically -``` -meadow mono disable -meadow mono enable -``` -### Useful utilities -``` -meadow device info -meadow device name -``` - -### Debugging -**NOTE THIS IS NOT YET FULLY IMPLEMENTED, IT WILL NOT WORK** -``` -meadow debug --DebugPort XXXX -``` -This starts listening on the specified port for a debugger to attach - -Note: you can use SDB command line debugger from https://github.com/mono/sdb. Just build it according to its readme, run the above command and then: - -``` -sdb "connect 127.0.0.1 XXXX" -``` -Substitute XXXX for the same port number as above - -## Running applications - -You'll typically need at least 5 files installed to the Meadow flash to run a Meadow app: - -1. System.dll -2. System.Core.dll -3. mscorlib.dll -4. Meadow.Core.dll -5. App.exe (your app) - -It's a good idea to disable mono first, copy the files, and then enable mono - ## Uninstall the Meadow.CLI tool If you ever need to remove the Meadow.CLI tool, you can remove it through the .NET command-line tool as you would any other global tool. @@ -243,18 +79,9 @@ dotnet tool uninstall WildernessLabs.Meadow.CLI --global dotnet tool install WildernessLabs.Meadow.CLI --global ``` -# License +### Meadow.CLI download location -Copyright Wilderness Labs Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +If you need to find or clear out any of the OS download files retrieved by Meadow.CLI, they are located in a WildernessLabs folder in the user directory. + +macOS: `~/.local/share/WildernessLabs/Firmware/` +Windows: `%LOCALAPPDATA%\WildernessLabs\Firmware` diff --git a/Source/TestApps/LinkerTest/LinkerTest.sln b/Source/TestApps/LinkerTest/LinkerTest.sln new file mode 100644 index 00000000..6d8b8ee0 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkerTest", "LinkerTest\LinkerTest.csproj", "{F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F18B502F-1D67-4F9A-8F1F-6A3C91C942E9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8AF82077-F626-42F0-81B2-B92439C770DA} + EndGlobalSection +EndGlobal diff --git a/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs b/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs new file mode 100644 index 00000000..825aee14 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/ILLinker.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace LinkerTest +{ + internal class ILLinker + { + readonly ILogger? _logger; + + public ILLinker(ILogger? logger = null) + { + _logger = logger; + } + + public async Task RunILLink( + string illinkerDllPath, + string descriptorXmlPath, + string noLinkArgs, + string prelinkAppPath, + string prelinkDir, + string postlinkDir) + { + if (!File.Exists(illinkerDllPath)) + { + throw new FileNotFoundException("Cannot run trimming operation, illink.dll not found"); + } + + //original + //var monolinker_args = $"\"{illinkerDllPath}\" -x \"{descriptorXmlPath}\" {noLinkArgs} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlinkDir}\" -r \"{prelinkAppPath}\" -a \"{prelink_os}\" -d \"{prelinkDir}\""; + + var monolinker_args = $"\"{illinkerDllPath}\"" + + $" -x \"{descriptorXmlPath}\" " + //link files in the descriptor file (needed) + $"{noLinkArgs} " + //arguments to skip linking - will be blank if we are linking + $"-r \"{prelinkAppPath}\" " + //link the app in the prelink folder (needed) + $"--skip-unresolved true " + //skip unresolved references (needed -hangs without) + $"--deterministic true " + //make deterministic (to avoid pushing unchanged files to the device) + $"--keep-facades true " + //keep facades (needed - will skip key libs without) + $"-b true " + //Update debug symbols for each linked module (needed - will skip key libs without) + $"-o \"{postlinkDir}\" " + //output directory + + + //old + //$"--ignore-descriptors false " + //ignore descriptors (doesn't appear to impact behavior) + //$"-c link " + //link framework assemblies + //$"-d \"{prelinkDir}\"" //additional folder to link (not needed) + + //experimental + //$"--explicit-reflection true " + //enable explicit reflection (throws an exception with it) + //$"--keep-dep-attributes true " + //keep dependency attributes (files are slightly larger with, doesn't fix dependency issue) + ""; + + _logger?.Log(LogLevel.Information, "Trimming assemblies"); + + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Directory.GetDirectoryRoot(illinkerDllPath); + 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(); + + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + + _logger?.Log(LogLevel.Debug, "StandardOutput Contains: " + stdOutReaderResult); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + _logger?.Log(LogLevel.Debug, $"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + throw new Exception("Trimming failed"); + } + } + } + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj b/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj new file mode 100644 index 00000000..5f9d9e86 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/LinkerTest.csproj @@ -0,0 +1,47 @@ + + + + Exe + net6.0 + enable + enable + preview + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs b/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs new file mode 100644 index 00000000..8ebcf4a3 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/MeadowLinker.cs @@ -0,0 +1,175 @@ +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; +using System.Reflection; + +namespace LinkerTest; + +public class MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) +{ + private const string IL_LINKER_DIR = "lib"; + private const string IL_LINKER_DLL = "illink.dll"; + private const string MEADOW_LINK_XML = "meadow_link.xml"; + + public const string PostLinkDirectoryName = "postlink_bin"; + public const string PreLinkDirectoryName = "prelink_bin"; + + readonly ILLinker _linker = new ILLinker(logger); + readonly ILogger? _logger = logger; + + private readonly string _meadowAssembliesPath = meadowAssembliesPath; + + public async Task Trim( + FileInfo meadowAppFile, + bool includePdbs = false, + IList? noLink = null) + { + var dependencies = MapDependencies(meadowAppFile); + + CopyDependenciesToPreLinkFolder(meadowAppFile, dependencies, includePdbs); + + //run the _linker against the dependencies + await TrimMeadowApp(meadowAppFile, noLink); + } + + List MapDependencies(FileInfo meadowAppFile) + { + //get all dependencies in meadowAppFile and exclude the Meadow App + var dependencyMap = new List(); + + var appRefs = GetAssemblyReferences(meadowAppFile.FullName); + return GetDependencies(meadowAppFile.FullName, appRefs, dependencyMap, meadowAppFile.DirectoryName); + } + + public void CopyDependenciesToPreLinkFolder( + FileInfo meadowApp, + List dependencies, + bool includePdbs) + { + //set up the paths + var prelinkDir = Path.Combine(meadowApp.DirectoryName!, PreLinkDirectoryName); + var postlinkDir = Path.Combine(meadowApp.DirectoryName!, PostLinkDirectoryName); + + //create output directories + CreateEmptyDirectory(prelinkDir); + CreateEmptyDirectory(postlinkDir); + + //copy meadow app + File.Copy(meadowApp.FullName, Path.Combine(prelinkDir, meadowApp.Name), overwrite: true); + + //copy dependencies and optional pdbs from the local folder and the meadow assemblies folder + foreach (var dependency in dependencies) + { + var destination = Path.Combine(prelinkDir, Path.GetFileName(dependency)); + File.Copy(dependency, destination, overwrite: true); + + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(dependency, "pdb"); + if (File.Exists(pdbFile)) + { + destination = Path.ChangeExtension(destination, "pdb"); + File.Copy(pdbFile, destination, overwrite: true); + } + } + } + } + + public async Task?> TrimMeadowApp( + FileInfo file, + IList? noLink) + { + //set up the paths + var prelink_dir = Path.Combine(file.DirectoryName!, PreLinkDirectoryName); + var postlink_dir = Path.Combine(file.DirectoryName!, PostLinkDirectoryName); + var prelink_app = Path.Combine(prelink_dir, file.Name); + var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var illinker_path = Path.Combine(base_path!, IL_LINKER_DIR, IL_LINKER_DLL); + var descriptor_path = Path.Combine(base_path!, IL_LINKER_DIR, MEADOW_LINK_XML); + + //prepare _linker arguments + var no_link_args = noLink != null ? string.Join(" ", noLink.Select(o => $"-p copy \"{o}\"")) : string.Empty; + + try + { + //link the apps + await _linker.RunILLink(illinker_path, descriptor_path, no_link_args, prelink_app, prelink_dir, postlink_dir); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error trimming Meadow app"); + } + + return Directory.EnumerateFiles(postlink_dir); + } + + /// + /// This method recursively gets all dependencies for the given assembly + /// + private List GetDependencies(string assemblyPath, Collection assemblyReferences, List dependencyMap, string appDir) + { + if (dependencyMap.Contains(assemblyPath)) + { //already have this assembly mapped + return dependencyMap; + } + + dependencyMap.Add(assemblyPath); + + foreach (var reference in assemblyReferences) + { + var fullPath = FindAssemblyFullPath(reference.Name, appDir, _meadowAssembliesPath); + + Collection namedRefs = default!; + + if (fullPath == null) + { + continue; + } + namedRefs = GetAssemblyReferences(fullPath); + + //recursive! + dependencyMap = GetDependencies(fullPath!, namedRefs!, dependencyMap, appDir); + } + + return dependencyMap.Where(x => x.Contains("App.") == false).ToList(); + } + + static string? FindAssemblyFullPath(string fileName, string localPath, string meadowAssembliesPath) + { + //Assembly may not have a file extension, add .dll if it doesn't + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") + { + fileName += ".dll"; + } + + //meadow assemblies path + if (File.Exists(Path.Combine(meadowAssembliesPath, fileName))) + { + return Path.Combine(meadowAssembliesPath, fileName); + } + + //localPath + if (File.Exists(Path.Combine(localPath, fileName))) + { + return Path.Combine(localPath, fileName); + } + + return null; + } + + private Collection GetAssemblyReferences(string assemblyPath) + { + using var definition = AssemblyDefinition.ReadAssembly(assemblyPath); + return definition.MainModule.AssemblyReferences; + } + + private void CreateEmptyDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, recursive: true); + } + Directory.CreateDirectory(directoryPath); + } +} \ No newline at end of file diff --git a/Source/TestApps/LinkerTest/LinkerTest/Program.cs b/Source/TestApps/LinkerTest/LinkerTest/Program.cs new file mode 100644 index 00000000..f0f60768 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/Program.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; + +namespace LinkerTest +{ + internal class Program + { + private static readonly string _meadowAssembliesPath = @"C:\Users\adria\AppData\Local\WildernessLabs\Firmware\1.6.0.1\meadow_assemblies\"; + + static async Task Main(string[] args) + { + Console.WriteLine("Hello, World!"); + + // await OtherLink(); + + // return; + + var linker = new MeadowLinker(_meadowAssembliesPath); + + string fileToLink = @"H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\App.dll"; + + await linker.Trim(new FileInfo(fileToLink), true); + } + + static async Task OtherLink() + { + var monolinker_args = @"""H:\WL\Meadow.CLI\Meadow.CLI.Classic\bin\Debug\lib\illink.dll"" -x ""H:\WL\Meadow.CLI\Meadow.CLI.Classic\bin\Debug\lib\meadow_link.xml"" --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\postlink_bin"" -r ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin\App.dll"" -a ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin\Meadow.dll"" -d ""H:\WL\Meadow.ProjectLab\Source\ProjectLab_Demo\bin\Debug\netstandard2.1\prelink_bin"""; + + Console.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(); + Console.WriteLine("StandardOutput Contains: " + stdOutReaderResult); + } + } + } + } +} diff --git a/Source/TestApps/LinkerTest/LinkerTest/Readme.md b/Source/TestApps/LinkerTest/LinkerTest/Readme.md new file mode 100644 index 00000000..52c7eaf0 --- /dev/null +++ b/Source/TestApps/LinkerTest/LinkerTest/Readme.md @@ -0,0 +1,41 @@ +Attempting to diagnose why adding nuget packages is changing the behavior of illink when trimming libraries. + +Notes: +It appears the Mono.Cecil libraries are version 0.11.2 +We do need the nugets to access the Cecil APIs (could dynamically load the lib), I vote we keep the nuget and the libs in sync + +Safe nugets: +Mono.Cecil 0.11.2 +Newtonsoft.Json 13.0.3 +System.Runtime.CompilerServices.Unsafe 6.0.0 +Microsoft.Extensions.DependencyInjection +System.Management + + +Unsafe nugets: +Microsoft.Extensions.Configuration.Json + +Impacts all extension libraries, netstandard.dll, system.core.dll, system.dll +This is probably the one that breaks the world + + +Serilog - impacts Microsoft.Extensions.Primitives + + +Microsoft.Extensions.Logging 7.0.0 +Microsoft.Extensions.Logging.Abstractions 8.0.0 + +Together change the linking behavior of: +Microsoft.Extensions.Configuration +Microsoft.Extensions.Configuration.Abstractions +Microsoft.Extensions.Configuration.FileExtensions +Microsoft.Extensions.Configuration.Primitives + + +Adding a reference to Meadow.CLI.Core also impacts the linking behavior of the above libraries and causes three libraries to be linked out completely + +Microsoft.Bcl.AsyncInterfaces +System.Buffers +System.Threading.Tasks.Extensions + + diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.Pdb.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/Mono.Cecil.Pdb.dll rename to Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.Pdb.dll diff --git a/Source/v2/Meadow.Cli/lib/Mono.Cecil.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/Mono.Cecil.dll rename to Source/TestApps/LinkerTest/LinkerTest/lib/Mono.Cecil.dll diff --git a/Source/v2/Meadow.Cli/lib/illink.deps.json b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.deps.json rename to Source/TestApps/LinkerTest/LinkerTest/lib/illink.deps.json diff --git a/Source/v2/Meadow.Cli/lib/illink.dll b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.dll similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.dll rename to Source/TestApps/LinkerTest/LinkerTest/lib/illink.dll diff --git a/Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json b/Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json similarity index 100% rename from Source/v2/Meadow.Cli/lib/illink.runtimeconfig.json rename to Source/TestApps/LinkerTest/LinkerTest/lib/illink.runtimeconfig.json diff --git a/Source/v2/Meadow.Cli/lib/meadow_link.xml b/Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml similarity index 100% rename from Source/v2/Meadow.Cli/lib/meadow_link.xml rename to Source/TestApps/LinkerTest/LinkerTest/lib/meadow_link.xml diff --git a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs similarity index 95% rename from Source/v2/Meadow.Cli/DFU/DfuSharp.cs rename to Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs index 62f502f0..13faa55f 100644 --- a/Source/v2/Meadow.Cli/DFU/DfuSharp.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuSharp.cs @@ -1,9 +1,5 @@ -using System; -using System.IO; -using System.Threading; -using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.InteropServices; -using System.Diagnostics; namespace DfuSharp { @@ -23,7 +19,6 @@ public enum LogLevel public delegate void HotplugCallback(IntPtr ctx, IntPtr device, HotplugEventType eventType, IntPtr userData); - class NativeMethods { @@ -199,7 +194,6 @@ struct DeviceDescriptor struct ConfigDescriptor { -#pragma warning disable CS0649 public byte bLength; public byte bDescriptorType; public ushort wTotalLength; @@ -211,15 +205,12 @@ struct ConfigDescriptor public IntPtr interfaces; public IntPtr extra; public int extra_length; -#pragma warning restore CS0649 } struct @Interface { -#pragma warning disable CS0649 public IntPtr altsetting; public int num_altsetting; -#pragma warning restore CS0649 public InterfaceDescriptor[] Altsetting { @@ -270,7 +261,7 @@ public class UploadingEventArgs : EventArgs public UploadingEventArgs(int bytesUploaded) { - this.BytesUploaded = bytesUploaded; + BytesUploaded = bytesUploaded; } } @@ -281,7 +272,7 @@ public class DfuDevice : IDisposable const int transfer_size = 0x800; const int address = 0x08000000; - IntPtr handle; + readonly IntPtr handle; InterfaceDescriptor interface_descriptor; DfuFunctionDescriptor dfu_descriptor; @@ -289,16 +280,18 @@ public DfuDevice(IntPtr device, InterfaceDescriptor interface_descriptor, DfuFun { 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; + public event UploadingEventHandler Uploading = default!; protected virtual void OnUploading(UploadingEventArgs e) { - if (Uploading != null) - Uploading(this, e); + Uploading?.Invoke(this, e); } public void ClaimInterface() { @@ -415,6 +408,7 @@ public void Download(FileStream file) { int count = 0; ushort transaction = 2; + using (var writer = new BinaryWriter(file)) { while (count < flash_size) @@ -663,7 +657,7 @@ public class Context : IDisposable IntPtr _callbackHandle = IntPtr.Zero; - IntPtr handle; + readonly IntPtr handle; public Context(LogLevel debug_level = LogLevel.None) { var ret = NativeMethods.libusb_init(ref handle); @@ -673,12 +667,12 @@ public Context(LogLevel debug_level = LogLevel.None) throw new Exception(string.Format("Error: {0} while trying to initialize libusb", ret)); // instantiate our callback handler - this._hotplugCallbackHandler = new HotplugCallback(HandleHotplugCallback); + _hotplugCallbackHandler = new HotplugCallback(HandleHotplugCallback); } public void Dispose() { - //this.StopListeningForHotplugEvents(); // not needed, they're automatically deregistered in libusb_exit. + //StopListeningForHotplugEvents(); // not needed, they're automatically deregistered in libusb_exit. NativeMethods.libusb_exit(handle); } @@ -711,8 +705,10 @@ public List GetDfuDevices(List idVendors) 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)); + } + //throw new Exception(string.Format("Error: {0} while trying to get the config descriptor", ret2)); var config_descriptor = Marshal.PtrToStructure(ptr); @@ -721,7 +717,9 @@ public List GetDfuDevices(List idVendors) 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++) @@ -730,11 +728,16 @@ public List GetDfuDevices(List idVendors) // 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)); + } } } } @@ -767,7 +770,7 @@ public List GetDfuDevices(List idVendors) public bool HasCapability(Capabilities caps) { - return NativeMethods.libusb_has_capability(caps) == 0 ? false : true; + return NativeMethods.libusb_has_capability(caps) != 0; } public void BeginListeningForHotplugEvents() @@ -795,8 +798,8 @@ public void BeginListeningForHotplugEvents() 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); + ErrorCodes success = NativeMethods.libusb_hotplug_register_callback(handle, HotplugEventType.DeviceArrived | HotplugEventType.DeviceLeft, HotplugFlags.DefaultNoFlags, + vendorID, productID, deviceClass, _hotplugCallbackHandler, userData, out _callbackHandle); if (success == ErrorCodes.Success) { @@ -817,15 +820,14 @@ public void StopListeningForHotplugEvents() return; } - NativeMethods.libusb_hotplug_deregister_callback(this.handle, this._callbackHandle); - + NativeMethods.libusb_hotplug_deregister_callback(handle, _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()); + 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.Core/DFU/DfuUtils.cs similarity index 89% rename from Source/v2/Meadow.Cli/DFU/DfuUtils.cs rename to Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs index 3a6e2079..50d0401c 100644 --- a/Source/v2/Meadow.Cli/DFU/DfuUtils.cs +++ b/Source/v2/Meadow.CLI.Core/DFU/DfuUtils.cs @@ -11,9 +11,7 @@ namespace Meadow.CLI.Core.Internals.Dfu; public static class DfuUtils { - private static int _osAddress = 0x08000000; - - // public static string LastSerialNumber { get; private set; } = ""; + private static readonly int _osAddress = 0x08000000; public enum DfuFlashFormat { @@ -41,10 +39,9 @@ private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFor { 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); @@ -57,6 +54,7 @@ private static void FormatDfuOutput(string logLine, ILogger? logger, DfuFlashFor } else //Console out { + Debug.WriteLine(logLine); Console.Write(logLine); Console.Write(logLine.Contains("%") ? "\r" : "\r\n"); @@ -98,15 +96,15 @@ public static async Task FlashFile(string fileName, string dfuSerialNumber { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - logger.LogError("dfu-util update required. To update, run in administrator mode: `meadow install dfu-util`"); + logger.LogError("dfu-util update required - to update, run in administrator mode: `meadow install dfu-util`"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - logger.LogError("dfu-util update required. To update, run: `brew upgrade dfu-util`"); + logger.LogError("dfu-util update required - to update, run: `brew upgrade dfu-util`"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - logger.LogError("dfu-util update required. To update , run: `apt upgrade dfu-util` or the equivalent for your Linux distribution"); + logger.LogError("dfu-util update required - to update , run: `apt upgrade dfu-util` or the equivalent for your Linux distribution"); } else { @@ -275,24 +273,10 @@ public static async Task InstallDfuUtil( var targetDir = is64Bit ? Environment.GetFolderPath(Environment.SpecialFolder.System) - : Environment.GetFolderPath( - Environment.SpecialFolder.SystemX86); + : 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 { @@ -302,5 +286,4 @@ public static async Task InstallDfuUtil( } } } - -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs new file mode 100644 index 00000000..29e8c45c --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/Firmware/FirmwareWriter.cs @@ -0,0 +1,74 @@ +using Meadow.CLI.Core.Internals.Dfu; +using Meadow.Hcom; +using Meadow.LibUsb; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace MeadowCLI; + +public class FirmwareWriter +{ + public IEnumerable GetLibUsbDevices(bool useLegacyLibUsb = false) + { + ILibUsbProvider provider; + + if (useLegacyLibUsb) + { + provider = new ClassicLibUsbProvider(); + } + else + { + provider = new LibUsbProvider(); + } + + return provider.GetDevicesInBootloaderMode(); + } + + public bool IsDfuDeviceAvailable(bool useLegacyLibUsb = false) + { + try + { + return GetLibUsbDevices(useLegacyLibUsb).Count() > 0; + } + catch + { + return false; + } + } + + public Task WriteOsWithDfu(string osFile, ILogger? logger = null, bool useLegacyLibUsb = false) + { + var devices = GetLibUsbDevices(useLegacyLibUsb); + + switch (devices.Count()) + { + case 0: throw new Exception("No device found in bootloader mode"); + case 1: break; + default: throw new Exception("Multiple devices found in bootloader mode - only connect one device"); + } + + var serialNumber = devices.First().GetDeviceSerialNumber(); + + Debug.WriteLine($"DFU Writing file {osFile}"); + + return DfuUtils.FlashFile( + osFile, + serialNumber, + logger: logger, + format: DfuUtils.DfuFlashFormat.ConsoleOut); + } + + public Task WriteRuntimeWithHcom(IMeadowConnection connection, string firmwareFile, ILogger? logger = null) + { + if (connection.Device == null) throw new Exception("No connected device"); + + return connection.Device.WriteRuntime(firmwareFile); + } + + public Task WriteCoprocessorFilesWithHcom(IMeadowConnection connection, string[] files, ILogger? logger = null) + { + if (connection.Device == null) throw new Exception("No connected device"); + + return connection.Device.WriteCoprocessorFiles(files); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs b/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs new file mode 100644 index 00000000..29c14943 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/Linker/ILLinker.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace LinkerTest +{ + internal class ILLinker + { + readonly ILogger? _logger; + + public ILLinker(ILogger? logger = null) + { + _logger = logger; + } + + public async Task RunILLink( + string illinkerDllPath, + string descriptorXmlPath, + string noLinkArgs, + string prelinkAppPath, + string prelinkDir, + string postlinkDir) + { + if (!File.Exists(illinkerDllPath)) + { + throw new FileNotFoundException("Cannot run trimming operation, illink.dll not found"); + } + + //original + //var monolinker_args = $"\"{illinkerDllPath}\" -x \"{descriptorXmlPath}\" {noLinkArgs} --skip-unresolved --deterministic --keep-facades true --ignore-descriptors true -b true -c link -o \"{postlinkDir}\" -r \"{prelinkAppPath}\" -a \"{prelink_os}\" -d \"{prelinkDir}\""; + + var monolinker_args = $"\"{illinkerDllPath}\"" + + $" -x \"{descriptorXmlPath}\" " + //link files in the descriptor file (needed) + $"{noLinkArgs} " + //arguments to skip linking - will be blank if we are linking + $"-r \"{prelinkAppPath}\" " + //link the app in the prelink folder (needed) + $"--skip-unresolved true " + //skip unresolved references (needed -hangs without) + $"--deterministic true " + //make deterministic (to avoid pushing unchanged files to the device) + $"--keep-facades true " + //keep facades (needed - will skip key libs without) + $"-b true " + //Update debug symbols for each linked module (needed - will skip key libs without) + $"-o \"{postlinkDir}\" " + //output directory + + + //old + //$"--ignore-descriptors false " + //ignore descriptors (doesn't appear to impact behavior) + //$"-c link " + //link framework assemblies + //$"-d \"{prelinkDir}\"" //additional folder to link (not needed) + + //experimental + //$"--explicit-reflection true " + //enable explicit reflection (throws an exception with it) + //$"--keep-dep-attributes true " + //keep dependency attributes (files are slightly larger with, doesn't fix dependency issue) + ""; + + _logger?.Log(LogLevel.Information, "Trimming assemblies"); + + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Directory.GetDirectoryRoot(illinkerDllPath); + 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(); + + _logger?.Log(LogLevel.Debug, "StandardOutput Contains: " + stdOutReaderResult); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + _logger?.Log(LogLevel.Debug, $"Trimming failed - ILLinker execution error!\nProcess Info: {process.StartInfo.FileName} {process.StartInfo.Arguments} \nExit Code: {process.ExitCode}"); + throw new Exception("Trimming failed"); + } + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Linker/MeadowLinker.cs b/Source/v2/Meadow.CLI.Core/Linker/MeadowLinker.cs new file mode 100644 index 00000000..89ff3540 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/Linker/MeadowLinker.cs @@ -0,0 +1,182 @@ +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; +using System.Reflection; + +namespace LinkerTest; + +public class MeadowLinker +{ + private const string IL_LINKER_DIR = "lib"; + private const string IL_LINKER_DLL = "illink.dll"; + private const string MEADOW_LINK_XML = "meadow_link.xml"; + + private const string PostLinkDirectoryName = "postlink_bin"; + private const string PreLinkDirectoryName = "prelink_bin"; + + readonly ILLinker _linker; + readonly ILogger? _logger; + + //ToDo ... might need to make this a property or pass it in when used + private readonly string _meadowAssembliesPath; + + public MeadowLinker(string meadowAssembliesPath, ILogger? logger = null) + { + _meadowAssembliesPath = meadowAssembliesPath; + _logger = logger; + _linker = new ILLinker(logger); + } + + public async Task Trim( + FileInfo meadowAppFile, + bool includePdbs = false, + IList? noLink = null) + { + var dependencies = MapDependencies(meadowAppFile); + + CopyDependenciesToPreLinkFolder(meadowAppFile, dependencies, includePdbs); + + await TrimMeadowApp(meadowAppFile, noLink); + } + + public List MapDependencies(FileInfo meadowAppFile) + { + //get all dependencies in meadowAppFile and exclude the Meadow App + var dependencyMap = new List(); + + var appRefs = GetAssemblyReferences(meadowAppFile.FullName); + return GetDependencies(meadowAppFile.FullName, appRefs, dependencyMap, meadowAppFile.DirectoryName); + } + + public void CopyDependenciesToPreLinkFolder( + FileInfo meadowApp, + List dependencies, + bool includePdbs) + { + //set up the paths + var prelinkDir = Path.Combine(meadowApp.DirectoryName!, PreLinkDirectoryName); + var postlinkDir = Path.Combine(meadowApp.DirectoryName!, PostLinkDirectoryName); + + //create output directories + CreateEmptyDirectory(prelinkDir); + CreateEmptyDirectory(postlinkDir); + + //copy meadow app + File.Copy(meadowApp.FullName, Path.Combine(prelinkDir, meadowApp.Name), overwrite: true); + + //copy dependencies and optional pdbs from the local folder and the meadow assemblies folder + foreach (var dependency in dependencies) + { + var destination = Path.Combine(prelinkDir, Path.GetFileName(dependency)); + File.Copy(dependency, destination, overwrite: true); + + if (includePdbs) + { + var pdbFile = Path.ChangeExtension(dependency, "pdb"); + if (File.Exists(pdbFile)) + { + destination = Path.ChangeExtension(destination, "pdb"); + File.Copy(pdbFile, destination, overwrite: true); + } + } + } + } + + private async Task> TrimMeadowApp( + FileInfo file, + IList? noLink) + { + //set up the paths + var prelink_dir = Path.Combine(file.DirectoryName!, PreLinkDirectoryName); + var postlink_dir = Path.Combine(file.DirectoryName!, PostLinkDirectoryName); + var prelink_app = Path.Combine(prelink_dir, file.Name); + var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var illinker_path = Path.Combine(base_path!, IL_LINKER_DIR, IL_LINKER_DLL); + var descriptor_path = Path.Combine(base_path!, IL_LINKER_DIR, MEADOW_LINK_XML); + + //prepare _linker arguments + var no_link_args = noLink != null ? string.Join(" ", noLink.Select(o => $"-p copy \"{o}\"")) : string.Empty; + + try + { + //link the apps + await _linker.RunILLink(illinker_path, descriptor_path, no_link_args, prelink_app, prelink_dir, postlink_dir); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error trimming Meadow app"); + } + + return Directory.EnumerateFiles(postlink_dir); + } + + /// + /// This method recursively gets all dependencies for the given assembly + /// + private List GetDependencies(string assemblyPath, Collection assemblyReferences, List dependencyMap, string appDir) + { + if (dependencyMap.Contains(assemblyPath)) + { //already have this assembly mapped + return dependencyMap; + } + + dependencyMap.Add(assemblyPath); + + foreach (var reference in assemblyReferences) + { + var fullPath = FindAssemblyFullPath(reference.Name, appDir, _meadowAssembliesPath); + + Collection namedRefs = default!; + + if (fullPath == null) + { + continue; + } + namedRefs = GetAssemblyReferences(fullPath); + + //recursive! + dependencyMap = GetDependencies(fullPath!, namedRefs!, dependencyMap, appDir); + } + + return dependencyMap.Where(x => x.Contains("App.") == false).ToList(); + } + + static string? FindAssemblyFullPath(string fileName, string localPath, string meadowAssembliesPath) + { + //Assembly may not have a file extension, add .dll if it doesn't + if (Path.GetExtension(fileName) != ".exe" && + Path.GetExtension(fileName) != ".dll") + { + fileName += ".dll"; + } + + //meadow assemblies path + if (File.Exists(Path.Combine(meadowAssembliesPath, fileName))) + { + return Path.Combine(meadowAssembliesPath, fileName); + } + + //local path + if (File.Exists(Path.Combine(localPath, fileName))) + { + return Path.Combine(localPath, fileName); + } + + return null; + } + + private Collection GetAssemblyReferences(string assemblyPath) + { + using var definition = AssemblyDefinition.ReadAssembly(assemblyPath); + return definition.MainModule.AssemblyReferences; + } + + private void CreateEmptyDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, recursive: true); + } + Directory.CreateDirectory(directoryPath); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj index 8e75c458..c2c1bd24 100644 --- a/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj +++ b/Source/v2/Meadow.CLI.Core/Meadow.CLI.Core.csproj @@ -6,12 +6,35 @@ enable - - 4 - true - - + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs similarity index 64% rename from Source/v2/Meadow.Cli/IPackageManager.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs index 05e37243..389e69cf 100644 --- a/Source/v2/Meadow.Cli/IPackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/IPackageManager.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI; +namespace Meadow.CLI; public interface IPackageManager { @@ -10,14 +8,12 @@ bool BuildApplication( string projectFilePath, string configuration = "Release", bool clean = true, - ILogger? logger = null, CancellationToken? cancellationToken = null); Task TrimApplication( FileInfo applicationFilePath, bool includePdbs = false, IList? noLink = null, - ILogger? logger = null, CancellationToken? cancellationToken = null); Task AssemblePackage( @@ -26,14 +22,5 @@ Task AssemblePackage( string osVersion, string filter = "*", bool overwrite = false, - ILogger? logger = null, CancellationToken? cancellationToken = null); - - List? AssemblyDependencies { get; set; } - - IEnumerable? TrimmedDependencies { get; set; } - bool Trimmed { get; set; } - - string? RuntimeVersion { get; set; } - string? MeadowAssembliesPath { get; } } \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs new file mode 100644 index 00000000..2df69f04 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.AssemblyManager.cs @@ -0,0 +1,36 @@ +namespace Meadow.CLI; + +public partial class PackageManager +{ + private const string PreLinkDirectoryName = "prelink_bin"; + public const string PostLinkDirectoryName = "postlink_bin"; + public const string PackageOutputDirectoryName = "mpak"; + + private string _meadowAssembliesPath = string.Empty; + + private string MeadowAssembliesPath + { + get + { + if (string.IsNullOrWhiteSpace(_meadowAssembliesPath)) + { // for now we only support F7 + // TODO: add switch and support for other platforms + var store = _fileManager.Firmware["Meadow F7"]; + if (store != null) + { + store.Refresh(); + if (store.DefaultPackage != null && store.DefaultPackage.BclFolder != null) + { + _meadowAssembliesPath = store.DefaultPackage.GetFullyQualifiedPath(store.DefaultPackage.BclFolder); + } + } + } + return _meadowAssembliesPath; + } + } + + public List GetDependencies(FileInfo file) + { + return _meadowLinker.MapDependencies(file); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs similarity index 66% rename from Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs index 5bec4adc..412c1c5a 100644 --- a/Source/v2/Meadow.Cli/PackageManager.BuildOptions.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.BuildOptions.cs @@ -4,12 +4,12 @@ public partial class PackageManager { private record BuildOptions { - public DeployOptions? Deploy { get; set; } + public DeployOptions Deploy { get; set; } public record DeployOptions { - public List? NoLink { get; set; } + public List NoLink { get; set; } public bool? IncludePDBs { get; set; } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs similarity index 83% rename from Source/v2/Meadow.Cli/PackageManager.cs rename to Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs index 4caaef23..39fbb307 100644 --- a/Source/v2/Meadow.Cli/PackageManager.cs +++ b/Source/v2/Meadow.CLI.Core/PackageManager/PackageManager.cs @@ -1,7 +1,7 @@ using GlobExpressions; +using LinkerTest; using Meadow.Cloud; using Meadow.Software; -using Microsoft.Extensions.Logging; using System.Diagnostics; using System.IO.Compression; using System.Runtime.InteropServices; @@ -15,10 +15,13 @@ public partial class PackageManager : IPackageManager public const string BuildOptionsFileName = "app.build.yaml"; private FileManager _fileManager; + private MeadowLinker _meadowLinker; public PackageManager(FileManager fileManager) { _fileManager = fileManager; + + _meadowLinker = new MeadowLinker(MeadowAssembliesPath, null); } private bool CleanApplication(string projectFilePath, string configuration = "Release", CancellationToken? cancellationToken = null) @@ -67,7 +70,7 @@ private bool CleanApplication(string projectFilePath, string configuration = "Re return success; } - public bool BuildApplication(string projectFilePath, string configuration = "Release", bool clean = true, ILogger? logger = null, CancellationToken? cancellationToken = null) + public bool BuildApplication(string projectFilePath, string configuration = "Release", bool clean = true, CancellationToken? cancellationToken = null) { if (clean && !CleanApplication(projectFilePath, configuration, cancellationToken)) { @@ -99,7 +102,6 @@ public bool BuildApplication(string projectFilePath, string configuration = "Rel Debug.WriteLine(dataLine.Data); if (dataLine.Data.Contains("Build FAILED", StringComparison.InvariantCultureIgnoreCase)) { - logger?.LogError(dataLine.Data); Debug.WriteLine("Build failed"); success = false; } @@ -119,11 +121,10 @@ public bool BuildApplication(string projectFilePath, string configuration = "Rel return success; } - public async Task TrimApplication( + public Task TrimApplication( FileInfo applicationFilePath, bool includePdbs = false, IList? noLink = null, - ILogger? logger = null, CancellationToken? cancellationToken = null) { if (!applicationFilePath.Exists) @@ -131,52 +132,32 @@ public async Task TrimApplication( throw new FileNotFoundException($"{applicationFilePath} not found"); } - Trimmed = false; - - // does an app.build.yaml file exist? + // does a meadow.build.yml file exist? var buildOptionsFile = Path.Combine( applicationFilePath.DirectoryName ?? string.Empty, BuildOptionsFileName); if (File.Exists(buildOptionsFile)) { - logger?.LogInformation($"'{BuildOptionsFileName}' is present"); 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) + if (opts.Deploy.NoLink != null && opts.Deploy.NoLink.Count > 0) { noLink = opts.Deploy.NoLink; } - if (opts.Deploy?.IncludePDBs != null) + if (opts.Deploy.IncludePDBs != null) { includePdbs = opts.Deploy.IncludePDBs.Value; } } - AssemblyDependencies = GetDependencies(applicationFilePath) - .ToList(); + var linker = new MeadowLinker(MeadowAssembliesPath); - try - { - TrimmedDependencies = await TrimDependencies( - applicationFilePath, - AssemblyDependencies, - noLink, - logger, - includePdbs, - verbose: false); - } - catch (Exception) - { - logger?.LogError($"Trimming FAILED. Falling back to untrimmed dependencies"); - Trimmed = false; - } - - Trimmed = false; + return linker.Trim(applicationFilePath, includePdbs, noLink); } public const string PackageMetadataFileName = "info.json"; @@ -187,7 +168,6 @@ public Task AssemblePackage( string osVersion, string filter = "*", bool overwrite = false, - ILogger? logger = null, CancellationToken? cancellationToken = null) { var di = new DirectoryInfo(outputFolder); @@ -196,7 +176,7 @@ public Task AssemblePackage( di.Create(); } - var mpakName = Path.Combine(outputFolder, $"{DateTime.UtcNow.ToString("yyyyMMddff")}.mpak"); + var mpakName = Path.Combine(outputFolder, $"{DateTime.UtcNow:yyyyMMddff}.mpak"); if (File.Exists(mpakName)) { @@ -219,7 +199,7 @@ public Task AssemblePackage( // write a metadata file info.json in the mpak // TODO: we need to see what is necessary and meaningful here and pass it in via param (or the entire file via param?) - PackageInfo info = new PackageInfo() + PackageInfo info = new() { Version = "1.0", OsVersion = osVersion @@ -246,11 +226,17 @@ private void CreateEntry(ZipArchive archive, string fromFile, string entryPath) public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, string appName = "App.dll") { - if (!Directory.Exists(rootFolder)) throw new FileNotFoundException(); + if (!Directory.Exists(rootFolder)) { throw new FileNotFoundException(); } + + //see if this is a fully qualified path to the app.dll + if (File.Exists(Path.Combine(rootFolder, appName))) + { + return new FileInfo[] { new(Path.Combine(rootFolder, appName)) }; + } // look for a 'bin' folder var path = Path.Combine(rootFolder, "bin"); - if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); + if (!Directory.Exists(path)) throw new FileNotFoundException($"No 'bin' directory found under {rootFolder}. Have you compiled?"); var files = new List(); FindApp(path, files); @@ -259,9 +245,9 @@ void FindApp(string directory, List fileList) { foreach (var dir in Directory.GetDirectories(directory)) { - var shortname = System.IO.Path.GetFileName(dir); + var shortname = Path.GetFileName(dir); - if (shortname == PackageManager.PostLinkDirectoryName || shortname == PackageManager.PreLinkDirectoryName) + if (shortname == PostLinkDirectoryName || shortname == PreLinkDirectoryName) { continue; } @@ -273,10 +259,9 @@ void FindApp(string directory, List fileList) } FindApp(dir, fileList); - } } return files.ToArray(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.Pdb.dll b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.Pdb.dll new file mode 100644 index 00000000..d6f5ee58 Binary files /dev/null and b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.Pdb.dll differ diff --git a/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.dll b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.dll new file mode 100644 index 00000000..682aef81 Binary files /dev/null and b/Source/v2/Meadow.CLI.Core/lib/Mono.Cecil.dll differ diff --git a/Source/v2/Meadow.CLI.Core/lib/illink.deps.json b/Source/v2/Meadow.CLI.Core/lib/illink.deps.json new file mode 100644 index 00000000..d7b91143 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/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.Core/lib/illink.dll b/Source/v2/Meadow.CLI.Core/lib/illink.dll new file mode 100644 index 00000000..f392213b Binary files /dev/null and b/Source/v2/Meadow.CLI.Core/lib/illink.dll differ diff --git a/Source/v2/Meadow.CLI.Core/lib/illink.runtimeconfig.json b/Source/v2/Meadow.CLI.Core/lib/illink.runtimeconfig.json new file mode 100644 index 00000000..617ab505 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/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.Core/lib/meadow_link.xml b/Source/v2/Meadow.CLI.Core/lib/meadow_link.xml new file mode 100644 index 00000000..0fe29752 --- /dev/null +++ b/Source/v2/Meadow.CLI.Core/lib/meadow_link.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Source/v2/Meadow.CLI.v2.sln b/Source/v2/Meadow.CLI.v2.sln index 4f733098..81933c1e 100644 --- a/Source/v2/Meadow.CLI.v2.sln +++ b/Source/v2/Meadow.CLI.v2.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 @@ -27,7 +26,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.UsbLibClassic", "Mea EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.CLI.Core", "Meadow.CLI.Core\Meadow.CLI.Core.csproj", "{677B1C15-8936-4807-8A4F-4F5219BBDB7C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cloud.Client", "Meadow.Cloud.Client\Meadow.Cloud.Client.csproj", "{A71A3C98-2B11-46FE-AB7A-EAD9271862AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Cloud.Client", "Meadow.Cloud.Client\Meadow.Cloud.Client.csproj", "{A71A3C98-2B11-46FE-AB7A-EAD9271862AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.UsbLib.Core", "Meadow.UsbLib.Core\Meadow.UsbLib.Core.csproj", "{F02ADBEF-4D52-4A71-9D95-74F45D68B43B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +88,10 @@ Global {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A71A3C98-2B11-46FE-AB7A-EAD9271862AA}.Release|Any CPU.Build.0 = Release|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F02ADBEF-4D52-4A71-9D95-74F45D68B43B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs deleted file mode 100644 index 2c0f9d66..00000000 --- a/Source/v2/Meadow.CLI/Commands/Current/App/BaseAppCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseAppCommand : BaseDeviceCommand -{ - protected IPackageManager _packageManager; - - public BaseAppCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - _packageManager = packageManager; - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs new file mode 100644 index 00000000..adba5992 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyCreateCommand.cs @@ -0,0 +1,73 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey create", Description = "Create a Meadow.Cloud API key")] +public class CloudApiKeyCreateCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name of the API key", IsRequired = true, Name = "NAME")] + public string Name { get; init; } = default!; + + [CommandOption("duration", 'd', Description = "The duration of the API key, in days", IsRequired = true)] + public int Duration { get; init; } = default!; + + [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key", IsRequired = true)] + public string[] Scopes { get; init; } = default!; + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyCreateCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + if (Duration < 1 || Duration > 90) + { + throw new CommandException("Duration (-d|--duration) must be between 1 and 90 days.", showHelp: true); + } + + Host ??= DefaultHost; + + Logger?.LogInformation($"Creating an API key on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var request = new CreateApiTokenRequest(Name!, Duration, Scopes!); + var response = await ApiTokenService.CreateApiToken(request, Host, CancellationToken); + + Logger?.LogInformation($"Your API key '{response.Name}' (expiring {response.ExpiresAt:G} UTC) is:"); + Logger?.LogInformation($"\n{response.Token}\n"); + Logger?.LogInformation("Make sure to copy this key now as you will not be able to see this again."); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs new file mode 100644 index 00000000..206acf01 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyDeleteCommand.cs @@ -0,0 +1,65 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey delete", Description = "Delete a Meadow.Cloud API key")] +public class CloudApiKeyDeleteCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] + public string NameOrId { get; init; } = default!; + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyDeleteCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Deleting API key `{NameOrId}` on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var getRequest = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiKey = getRequest.FirstOrDefault(x => x.Id == NameOrId || string.Equals(x.Name, NameOrId, StringComparison.OrdinalIgnoreCase)); + + if (apiKey == null) + { + throw new CommandException($"API key `{NameOrId}` not found."); + } + + await ApiTokenService.DeleteApiToken(apiKey.Id, Host, CancellationToken); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs new file mode 100644 index 00000000..692c40c5 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyListCommand.cs @@ -0,0 +1,71 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey list", Description = "List your Meadow.Cloud API keys")] +public class CloudApiKeyListCommand : BaseCloudCommand +{ + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyListCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected override async ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Retrieving your API keys from Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var response = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiTokens = response.OrderBy(a => a.Name); + + if (!apiTokens.Any()) + { + Logger?.LogInformation("You have no API keys."); + return; + } + + var table = new ConsoleTable("Id", "Name", $"Expires (UTC)", "Scopes"); + foreach (var apiToken in apiTokens) + { + table.AddRow(apiToken.Id, apiToken.Name, $"{apiToken.ExpiresAt:G}", string.Join(", ", apiToken.Scopes.OrderBy(t => t))); + } + + Logger?.LogInformation(table); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Get API keys command failed: {ex.Message}", innerException: ex); + } + } +} + + diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs new file mode 100644 index 00000000..88b00336 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ApiKey/CloudApiKeyUpdateCommand.cs @@ -0,0 +1,75 @@ +using CliFx.Attributes; +using CliFx.Exceptions; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud apikey update", Description = "Update a Meadow.Cloud API key")] +public class CloudApiKeyUpdateCommand : BaseCloudCommand +{ + [CommandParameter(0, Description = "The name or ID of the API key", IsRequired = true, Name = "NAME_OR_ID")] + public string NameOrId { get; init; } = default!; + + [CommandOption("name", 'n', Description = "The new name to use for the API key", IsRequired = false)] + public string? NewName { get; set; } + + [CommandOption("scopes", 's', Description = "The list of scopes (permissions) to grant the API key", IsRequired = false)] + public string[]? Scopes { get; set; } + + [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] + public string? Host { get; set; } + + private ApiTokenService ApiTokenService { get; } + + public CloudApiKeyUpdateCommand( + ApiTokenService apiTokenService, + CollectionService collectionService, + DeviceService deviceService, + IdentityManager identityManager, + UserService userService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + ApiTokenService = apiTokenService; + } + + protected async override ValueTask ExecuteCommand() + { + Host ??= DefaultHost; + + Logger?.LogInformation($"Updating API key `{NameOrId}` on Meadow.Cloud{(Host != DefaultHost ? $" ({Host.ToLowerInvariant()})" : string.Empty)}..."); + + var token = await IdentityManager.GetAccessToken(CancellationToken); + if (string.IsNullOrWhiteSpace(token)) + { + throw new CommandException("You must be signed into Meadow.Cloud to execute this command. Run 'meadow cloud login' to do so."); + } + + try + { + var getRequest = await ApiTokenService.GetApiTokens(Host, CancellationToken); + var apiKey = getRequest.FirstOrDefault(x => x.Id == NameOrId || string.Equals(x.Name, NameOrId, StringComparison.OrdinalIgnoreCase)); + + if (apiKey == null) + { + throw new CommandException($"API key `{NameOrId}` not found."); + } + + NewName ??= apiKey.Name; + Scopes ??= apiKey.Scopes; + + var updateRequest = new UpdateApiTokenRequest(NewName!, Scopes!); + await ApiTokenService.UpdateApiToken(apiKey.Id, updateRequest, Host, CancellationToken); + } + catch (MeadowCloudAuthException ex) + { + throw new CommandException("You must be signed in to execute this command.", innerException: ex); + } + catch (MeadowCloudException ex) + { + throw new CommandException($"Create API key command failed: {ex.Message}", innerException: ex); + } + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs new file mode 100644 index 00000000..40be5333 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/Cloud/ConsoleTable.cs @@ -0,0 +1,107 @@ +using System.Text; + +namespace Meadow.CLI; + +public class ConsoleTable +{ + private readonly string[] _columns; + private IList _rows; + + public ConsoleTable(params string[] columns) + { + _columns = columns; + _rows = new List(); + } + + public void AddRow(params object[] values) + { + if (values.Length != _columns.Length) + { + throw new InvalidOperationException("The number of values for the given row does not match the number of columns."); + } + + _rows.Add(values.Select(v => Convert.ToString(v) ?? string.Empty).ToArray()); + } + + public static implicit operator string(ConsoleTable table) => table.Render(); + + public string Render() + { + var maxWidths = new int[_columns.Length]; + for (var i = 0; i < _columns.Length; i++) + { + maxWidths[i] = _columns[i].Length; + } + + for (var i = 0; i < _rows.Count; i++) + { + for (var j = 0; j < _rows[i].Length; j++) + { + maxWidths[j] = Math.Max(maxWidths[j], _rows[i][j].Length); + } + } + + var sb = new StringBuilder(); + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-+-"); + } + } + + // Header + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(_columns[i].PadRight(maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append(" | "); + } + } + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-|-"); + } + } + + // Rows + for (var i = 0; i < _rows.Count; i++) + { + sb.AppendLine(); + for (var j = 0; j < _rows[i].Length; j++) + { + sb.Append(_rows[i][j].PadRight(maxWidths[j])); + if (j < _rows[i].Length - 1) + { + sb.Append(" | "); + } + } + } + + // Divider + sb.AppendLine(); + for (var i = 0; i < _columns.Length; i++) + { + sb.Append(new string('-', maxWidths[i])); + if (i < _columns.Length - 1) + { + sb.Append("-+-"); + } + } + + sb.AppendLine(); + return sb.ToString(); + } +} diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs b/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs deleted file mode 100644 index 744a62e3..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/ConsoleSpinner.cs +++ /dev/null @@ -1,27 +0,0 @@ - -using CliFx.Infrastructure; - -namespace Meadow.CLI -{ - public class ConsoleSpinner - { - private int counter = 0; - private char[] sequence = { '|', '/', '-', '\\' }; - private IConsole console; - - public ConsoleSpinner(IConsole console) - { - this.console = console; - } - - public async Task Turn(int delay = 100, CancellationToken cancellationToken = default) - { - while (!cancellationToken.IsCancellationRequested) - { - counter++; - console?.Output.WriteAsync($"{sequence[counter % 4]} \r"); - await Task.Delay(delay, CancellationToken.None); // Not propogating the token intentionally. - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs b/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs deleted file mode 100644 index 65275656..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/ExtensionMethods.cs +++ /dev/null @@ -1,124 +0,0 @@ - -namespace Meadow.CLI -{ - public static class ExtensionMethods - { - public const string ConsoleColourBlack = "\u001b[30m"; - public const string ConsoleColourBlue = "\u001b[34m"; - public const string ConsoleColourCyan = "\u001b[36m"; - public const string ConsoleColourGreen = "\u001b[32m"; - public const string ConsoleColourMagenta = "\u001b[35m"; - public const string ConsoleColourRed = "\u001b[31m"; - public const string ConsoleColourReset = "\u001b[0m"; - public const string ConsoleColourWhite = "\u001b[37m"; - public const string ConsoleColourYellow = "\u001b[33m"; - - public static string ColourConsoleText(this string textToColour, string textColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return textColour + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextBlack(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourBlack + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextCyan(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourCyan + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextBlue(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourBlue + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextGreen(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourGreen + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextMagenta(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourMagenta + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextRed(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourRed + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextWhite(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourWhite + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - - public static string ColourConsoleTextYellow(this string textToColour) - { - if (!string.IsNullOrEmpty(textToColour)) - { - return ConsoleColourYellow + textToColour + ConsoleColourReset; - } - else - { - return string.Empty; - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs b/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs deleted file mode 100644 index 68fbca39..00000000 --- a/Source/v2/Meadow.CLI/Commands/Helper/UITaskExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CliFx.Infrastructure; - -namespace Meadow.CLI -{ - public static class UITaskExtensions - { - public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) - { - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(console); - - var consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); - - try - { - await task; - } - finally - { - // Cancel the spinner when the original task completes - spinnerCancellationTokenSource.Cancel(); - - // Let's wait for the spinner to finish - await consoleSpinnerTask; - } - } - - public static async Task WithSpinner(this Task task, IConsole console, int delay = 100, CancellationToken cancellationToken = default) - { - // Get our spinner read - var spinnerCancellationTokenSource = new CancellationTokenSource(); - var consoleSpinner = new ConsoleSpinner(console); - - Task consoleSpinnerTask = consoleSpinner.Turn(delay, spinnerCancellationTokenSource.Token); - - try - { - return await task; - } - finally - { - // Cancel the spinner when the original task completes - spinnerCancellationTokenSource.Cancel(); - - // Let's wait for the spinner to finish - await consoleSpinnerTask; - } - } - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/ConsoleSpinner.cs b/Source/v2/Meadow.CLI/ConsoleSpinner.cs new file mode 100644 index 00000000..40547722 --- /dev/null +++ b/Source/v2/Meadow.CLI/ConsoleSpinner.cs @@ -0,0 +1,37 @@ +using CliFx.Infrastructure; + +namespace Meadow.CLI +{ + public static class ConsoleSpinner + { + private static readonly char[] sequence = { '|', '/', '-', '\\' }; + + private static CancellationToken? token; + + public static void Spin(IConsole? console, int udpateInterval_ms = 100, CancellationToken cancellationToken = default) + { + if (console == null) + { + throw new ArgumentNullException(nameof(console)); + } + + if (token != null) + { + throw new InvalidOperationException("A spinner is already running"); + } + token = cancellationToken; + + _ = Task.Run(async () => + { + int index = 0; + + while (cancellationToken.IsCancellationRequested == false) + { + index++; + console?.Output.WriteAsync($"{sequence[index % 4]} \r"); + await Task.Delay(udpateInterval_ms, CancellationToken.None); + } + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Meadow.CLI.csproj b/Source/v2/Meadow.CLI/Meadow.CLI.csproj index 275726c4..fc9b39dc 100644 --- a/Source/v2/Meadow.CLI/Meadow.CLI.csproj +++ b/Source/v2/Meadow.CLI/Meadow.CLI.csproj @@ -5,17 +5,16 @@ net6.0 enable true - Wilderness Labs, Inc meadow WildernessLabs.Meadow.CLI - Chris Tacke, Peter Moody, Adrian Stevens, Brian Kim, Pete Garafano, Dominique Louis + Wilderness Labs, Inc Wilderness Labs, Inc true - 2.0.0-alpha.3 + 2.0.0.10 AnyCPU - http://developer.wildernesslabs.co/Meadow/Meadow.Foundation/ - icon.png + http://developer.wildernesslabs.co/Meadow/Meadow.CLI/ https://github.com/WildernessLabs/Meadow.CLI + icon.png Meadow, Meadow.Foundation, Meadow.CLI Command-line interface for Meadow false @@ -23,40 +22,29 @@ false false meadow - latest - Copyright 2020-2023 Wilderness Labs + 11.0 + Copyright 2020-2024 Wilderness Labs enable Apache-2.0 README.md - - 4 - true - 1701;1702 - - - - - - + - - @@ -67,18 +55,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs index d5168f49..9f0a3caf 100644 --- a/Source/v2/Meadow.Cli/AppManager.cs +++ b/Source/v2/Meadow.Cli/AppManager.cs @@ -1,226 +1,116 @@ -using System.Drawing; -using System.Threading; -using Meadow.Hcom; -using Meadow.Software; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI; - -public static class AppManager -{ - static string[] dllLinkIngoreList = { "System.Threading.Tasks.Extensions.dll" };//, "Microsoft.Extensions.Primitives.dll" }; - static string[] pdbLinkIngoreList = { "System.Threading.Tasks.Extensions.pdb" };//, "Microsoft.Extensions.Primitives.pdb" }; - - 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> GenerateDeployList(IPackageManager packageManager, - string localBinaryDirectory, - bool includePdbs, - bool includeXmlDocs, - ILogger? logger, - CancellationToken cancellationToken) - { - // TODO: add sub-folder support when HCOM supports it - - logger?.LogInformation($"Generating the list of files to deploy from {localBinaryDirectory}..."); - - var localFiles = new Dictionary(); - - var auxiliary = Directory.EnumerateFiles(localBinaryDirectory, "*.*", SearchOption.TopDirectoryOnly) - .Where(s => new FileInfo(s).Extension != ".dll") - .Where(s => new FileInfo(s).Extension != ".pdb") - .Where(s => !s.Contains(".DS_Store")); - - foreach (var item in auxiliary) - { - var file = Path.Combine(localBinaryDirectory, item); - if (File.Exists(file)) - { - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - } - - if (packageManager.Trimmed && packageManager.TrimmedDependencies != null) - { - var trimmedDependencies = packageManager.TrimmedDependencies - .Where(x => dllLinkIngoreList.Any(f => x.Contains(f)) == false) - .Where(x => pdbLinkIngoreList.Any(f => x.Contains(f)) == false) - .ToList(); - - // Crawl trimmed dependencies - foreach (var file in trimmedDependencies) - { - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - - // Add the Dlls from the TrimmingIgnorelist - for (int i = 0; i < dllLinkIngoreList.Length; i++) - { - //add the files from the dll link ignore list - if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(dllLinkIngoreList[i]))) - { - var dllfound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(dllLinkIngoreList[i])); - if (!string.IsNullOrEmpty(dllfound)) - { - await AddToLocalFiles(localFiles, dllfound, includePdbs, includeXmlDocs, cancellationToken); - } - } - } - - if (includePdbs) - { - for (int i = 0; i < pdbLinkIngoreList.Length; i++) - { - //add the files from the pdb link ignore list - if (packageManager.AssemblyDependencies!.Exists(f => f.Contains(pdbLinkIngoreList[i]))) - { - var pdbFound = packageManager.AssemblyDependencies!.FirstOrDefault(f => f.Contains(pdbLinkIngoreList[i])); - if (!string.IsNullOrEmpty(pdbFound)) - { - await AddToLocalFiles(localFiles, pdbFound, includePdbs, includeXmlDocs, cancellationToken); - } - } - } - } - } - else - { - foreach (var file in packageManager.AssemblyDependencies!) - { - // TODO: add any other filtering capability here - - //Populate out LocalFile Dictionary with this entry - await AddToLocalFiles(localFiles, file, includePdbs, includeXmlDocs, cancellationToken); - } - } - - if (localFiles.Count() == 0) - { - logger?.LogInformation($"No new files to deploy"); - } - - logger?.LogInformation("Done."); - - return localFiles; - } - - public static async Task DeployApplication( - IMeadowConnection connection, - Dictionary localFiles, - ILogger logger, - CancellationToken cancellationToken) - { - // get a list of files on-device, with CRCs - var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty(); - - // get a list of files of the device files that are not in the list we intend to deploy - var removeFiles = deviceFiles - .Select(f => Path.GetFileName(f.Name)) - .Except(localFiles.Keys - .Select(f => Path.GetFileName(f))); - - if (removeFiles.Count() == 0) - { - logger.LogInformation($"No files to delete"); - } - - // delete those files - foreach (var file in removeFiles) - { - logger.LogInformation($"Deleting file '{file}'..."); - await connection.DeleteFile(file, cancellationToken); - } - - // now send all files with differing CRCs - foreach (var localFile in localFiles) - { - if (!File.Exists(localFile.Key)) - { - logger.LogInformation($"{localFile.Key} not found" + Environment.NewLine); - continue; - } - - var filename = Path.GetFileName(localFile.Key); - - var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == filename); - - if (existing != null && existing.Crc != null) - { - var remoteCrc = uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber); - var localCrc = localFile.Value; - - // do the file name and CRC match? - if (remoteCrc == localCrc) - { - // exists and has a matching CRC, skip it - logger.LogInformation($"Skipping file (hash match): {filename}" + Environment.NewLine); - continue; - } - } - - bool success; - - do - { - try - { - if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) - { - logger.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}'. Retrying."); - await Task.Delay(100); - success = false; - } - else - { - success = true; - } - } - catch (Exception ex) - { - logger.LogWarning($"Error sending '{Path.GetFileName(localFile.Key)}' ({ex.Message}). Retrying."); - await Task.Delay(100); - success = false; - } - - } while (!success); - } - } - - private static async Task AddToLocalFiles(Dictionary localFiles, string file, bool includePdbs, bool includeXmlDocs, CancellationToken cancellationToken) - { - if (!includePdbs && IsPdb(file)) - return; - if (!includeXmlDocs && IsXmlDoc(file)) - return; - - // 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); - - if (!localFiles.ContainsKey(file)) - localFiles.Add(file, crc); - } +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( + IPackageManager packageManager, + IMeadowConnection connection, + string localBinaryDirectory, + bool includePdbs, + bool includeXmlDocs, + ILogger? logger, + CancellationToken cancellationToken) + { + // TODO: add sub-folder support when HCOM supports it + + var localFiles = new Dictionary(); + + // get a list of files to send + var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll"))); + dependencies.Add(Path.Combine(localBinaryDirectory, "App.dll")); + + logger?.LogInformation("Generating list of files to deploy..."); + foreach (var file in dependencies) + { + // 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); + } + + if (localFiles.Count() == 0) + { + logger?.LogInformation($"No new files to deploy"); + } + + // get a list of files on-device, with CRCs + var deviceFiles = await connection.GetFileList("/meadow0/", true, cancellationToken) ?? Array.Empty(); + + // get a list of files of the device files that are not in the list we intend to deploy + var removeFiles = deviceFiles + .Select(f => Path.GetFileName(f.Name)) + .Except(localFiles.Keys + .Select(f => Path.GetFileName(f))); + + if (!removeFiles.Any()) + { + logger?.LogInformation($"No files to delete"); + } + + // delete those files + foreach (var file in removeFiles) + { + logger?.LogInformation($"Deleting file '{file}'..."); + await connection.DeleteFile(file, cancellationToken); + } + + // now send all files with differing CRCs + foreach (var localFile in localFiles) + { + var existing = deviceFiles.FirstOrDefault(f => Path.GetFileName(f.Name) == Path.GetFileName(localFile.Key)); + + if (existing != null && existing.Crc != null) + { + if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value) + { + // exists and has a matching CRC, skip it + continue; + } + } + + send_file: + + if (!await connection.WriteFile(localFile.Key, null, cancellationToken)) + { + logger?.LogWarning($"Error sending'{Path.GetFileName(localFile.Key)}'. Retrying."); + await Task.Delay(100); + goto send_file; + } + } + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs index b3cc8cce..9e169169 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs @@ -1,19 +1,18 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app build", Description = "Compiles a Meadow application")] +[Command("app build", Description = "Compile a Meadow application")] public class AppBuildCommand : BaseCommand { - private IPackageManager _packageManager; + private readonly 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 string? Path { get; init; } public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) : base(loggerFactory) @@ -21,42 +20,36 @@ public AppBuildCommand(IPackageManager packageManager, ILoggerFactory loggerFact _packageManager = packageManager; } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { - await Task.Run(async () => - { - string path = Path == null - ? Environment.CurrentDirectory - : Path; + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; - // is the path a file? - if (!File.Exists(path)) + // is the path a file? + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) { - // is it a valid directory? - if (!Directory.Exists(path)) - { - Logger?.LogError($"Invalid application path '{path}'"); - return; - } + Logger?.LogError($"Invalid application path '{path}'"); + return ValueTask.CompletedTask; } + } - if (Configuration == null) - Configuration = "Release"; + Configuration ??= "Release"; - Logger?.LogInformation($"Building {Configuration} configuration of {path} (this may take a few seconds)..."); + Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); - // TODO: enable cancellation of this call - var success = await Task.FromResult(_packageManager.BuildApplication(path, Configuration)) - .WithSpinner(Console!, 250); + // TODO: enable cancellation of this call + var success = _packageManager.BuildApplication(path, Configuration); - if (!success) - { - Logger?.LogError($"Build failed!"); - } - else - { - Logger?.LogError($"Build success."); - } - }); + if (!success) + { + Logger?.LogError($"Build failed!"); + } + else + { + Logger?.LogInformation($"Build successful"); + } + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs index 6ec201cf..50190451 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDebugCommand.cs @@ -3,34 +3,39 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("app debug", Description = "Debugs a running application")] +[Command("app debug", Description = "Debug a running application")] public class AppDebugCommand : BaseDeviceCommand { // VS 2019 - 4024 // VS 2017 - 4022 // VS 2015 - 4020 - [CommandOption("Port", 'p', Description = "The port to run the debug server on")] + [CommandOption("Port", 'p', Description = "The port to run the debug server on", IsRequired = false)] public int Port { get; init; } = 4024; public AppDebugCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + using var server = await connection.StartDebuggingSession(Port, Logger, CancellationToken); - if (Connection != null) + if (Console != null) { - using (var server = await Connection.StartDebuggingSession(Port, Logger, CancellationToken)) - { - if (Console != null) - { - Logger?.LogInformation("Debugging server started. Press Enter to exit"); - await Console.Input.ReadLineAsync(); - } - } + Logger?.LogInformation("Debugging server started - press Enter to exit"); + await Console.Input.ReadLineAsync(); } } } \ 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 index 438f97f7..5a3b70d6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs @@ -1,120 +1,104 @@ using CliFx.Attributes; + 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 : BaseAppCommand +[Command("app deploy", Description = "Deploy a built Meadow application to a target device")] +public class AppDeployCommand : BaseDeviceCommand { - private string lastFile = string.Empty; + private readonly IPackageManager _packageManager; [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) + : base(connectionManager, loggerFactory) { + _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - string path = Path == null - ? Environment.CurrentDirectory - : Path; + return; + } - // is the path a file? - FileInfo file; + string path = Path ?? Environment.CurrentDirectory; - lastFile = string.Empty; + // is the path a file? + FileInfo file; - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); + var lastFile = string.Empty; - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); - - await Connection.RuntimeDisable(CancellationToken); - } + // in order to deploy, the runtime must be disabled + var isRuntimeEnabled = await connection.IsRuntimeEnabled(); - if (!File.Exists(path)) - { - // is it a valid directory? - if (!Directory.Exists(path)) - { - Logger?.LogError($"Invalid application path '{path}'"); - return; - } + if (isRuntimeEnabled) + { + Logger?.LogInformation("Disabling runtime..."); - // does the directory have an App.dll in it? - file = new FileInfo(System.IO.Path.Combine(path, "App.dll")); - if (!file.Exists) - { - // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) - var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + await connection.RuntimeDisable(CancellationToken); + } - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{path}'"); - return; - } + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; - file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - } - } - else + if (e.fileName != lastFile) { - // TODO: only deploy if it's App.dll - file = new FileInfo(path); + Console?.Output.WriteAsync("\n"); + lastFile = e.fileName; } - var targetDirectory = file.DirectoryName; + // Console instead of Logger due to line breaking for progress bar + Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + }; - if (Logger != null && !string.IsNullOrEmpty(targetDirectory)) + if (!File.Exists(path)) + { + // is it a valid directory? + if (!Directory.Exists(path)) { - var trimApplicationCommand = new AppTrimCommand(_packageManager, ConnectionManager, LoggerFactory!) - { - Path = path, - }; - await trimApplicationCommand.ExecuteAsync(Console!); - - var localFiles = await AppManager.GenerateDeployList(_packageManager, targetDirectory, targetDirectory.Contains("Debug"), false, Logger, CancellationToken) - .WithSpinner(Console!, 250); - Console?.Output.WriteAsync("\n"); - - Connection.FileWriteProgress += Connection_FileWriteProgress; - - await AppManager.DeployApplication(Connection, localFiles, Logger, CancellationToken); - Console?.Output.WriteAsync("\n"); - - Connection.FileWriteProgress -= Connection_FileWriteProgress; + Logger?.LogError($"Invalid application path '{path}'"); + return; } - if (wasRuntimeEnabled) + // does the directory have an App.dll in it? + file = new FileInfo(System.IO.Path.Combine(path, "App.dll")); + if (!file.Exists) { - // restore runtime state - Logger?.LogInformation("Enabling runtime..."); + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); - await Connection.RuntimeEnable(CancellationToken); + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return; + } + + file = candidates.OrderByDescending(c => c.LastWriteTime).First(); } } - } + else + { + // TODO: only deploy if it's App.dll + file = new FileInfo(path); + } - private void Connection_FileWriteProgress(object? sender, (string fileName, long completed, long total) e) - { - var p = (e.completed / (double)e.total) * 100d; + var targetDirectory = file.DirectoryName!; + + await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, CancellationToken); - if (e.fileName != lastFile) + if (isRuntimeEnabled) { - Console?.Output.WriteAsync("\n"); - lastFile = e.fileName; - } + // restore runtime state + Logger?.LogInformation("Enabling runtime..."); - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); + await connection.RuntimeEnable(CancellationToken); + } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs index 9ebf65dd..c21b1798 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs @@ -5,81 +5,157 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("app run", Description = "Builds, trims and deploys a Meadow application to a target device")] -public class AppRunCommand : BaseAppCommand +public class AppRunCommand : BaseDeviceCommand { - [CommandOption("no-prefix", 'n', IsRequired = false, Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'")] - public bool NoPrefix { get; set; } + private readonly IPackageManager _packageManager; + private string? _lastFile; + + [CommandOption("no-prefix", 'n', Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed during 'listen'", IsRequired = false)] + public bool NoPrefix { get; init; } [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] public string? Configuration { get; set; } [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)] - public string? Path { get; set; } = default!; + public string? Path { get; init; } public AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) + : base(connectionManager, loggerFactory) { + _packageManager = packageManager; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - string path = Path == null - ? Environment.CurrentDirectory - : Path; + if (connection == null) + { + return; + } + + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; if (!Directory.Exists(path)) { - Logger?.LogError($"Target directory '{path}' not found."); + Logger?.LogError($"Target directory '{path}' not found"); return; } var lastFile = string.Empty; - var buildmApplicationCommand = new AppBuildCommand(_packageManager, LoggerFactory!) + // in order to deploy, the runtime must be disabled + var wasRuntimeEnabled = await connection.IsRuntimeEnabled(); + if (wasRuntimeEnabled) { - Path = path - }; - await buildmApplicationCommand.ExecuteAsync(Console!); + Logger?.LogInformation("Disabling runtime..."); + await connection.RuntimeDisable(CancellationToken); + } - if (Connection != null) + if (!await BuildApplication(path, CancellationToken)) { - // illink returns before all files are actually written. That's not fun, but we must just wait a little while. - // disabling the runtime provides us that time + return; + } - // in order to deploy, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); + if (!await TrimApplication(path, CancellationToken)) + { + return; + } - Logger?.LogInformation("Disabling runtime..."); + // illink returns before all files are written - attempt a delay of 1s + await Task.Delay(1000); + + if (!await DeployApplication(connection, path, CancellationToken)) + { + return; + } + + Logger?.LogInformation("Enabling the runtime..."); + await connection.RuntimeEnable(CancellationToken); + + Logger?.LogInformation("Listening for messages from Meadow...\n"); + connection.DeviceMessageReceived += OnDeviceMessageReceived; + + while (!CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); + } + + Logger?.LogInformation("Listen cancelled..."); + } + + private Task BuildApplication(string path, CancellationToken cancellationToken) + { + if (Configuration == null) { Configuration = "Debug"; } + + Logger?.LogInformation($"Building {Configuration} configuration of {path}..."); - await Connection.RuntimeDisable(CancellationToken); + // TODO: enable cancellation of this call + return Task.FromResult(_packageManager.BuildApplication(path, Configuration)); + } - if (Connection is SerialConnection s) - { - s.CommandTimeoutSeconds = 60; - } + private async Task TrimApplication(string path, CancellationToken cancellationToken) + { + // it's a directory - we need to determine the latest build (they might have a Debug and a Release config) + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return false; + } - var deployApplication = new AppDeployCommand(_packageManager, ConnectionManager, LoggerFactory!) - { - Path = path - }; - await deployApplication.ExecuteAsync(Console!); + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); - Logger?.LogInformation("Enabling the runtime..."); - await Connection.RuntimeEnable(CancellationToken); + // if no configuration was provided, find the most recently built + Logger?.LogInformation($"Trimming {file.FullName}"); + Logger?.LogInformation("This may take a few seconds..."); - Logger?.LogInformation("Listening for messages from Meadow...\n"); - Connection.DeviceMessageReceived += OnDeviceMessageReceived; + var cts = new CancellationTokenSource(); + ConsoleSpinner.Spin(Console, cancellationToken: cts.Token); - while (!CancellationToken.IsCancellationRequested) - { - await Task.Delay(1000); - } + await _packageManager.TrimApplication(file, false, null, CancellationToken); + cts.Cancel(); - Logger?.LogInformation("Listen cancelled..."); + return true; + } + + private async Task DeployApplication(IMeadowConnection connection, string path, CancellationToken cancellationToken) + { + connection.FileWriteProgress += OnFileWriteProgress; + + var candidates = PackageManager.GetAvailableBuiltConfigurations(path, "App.dll"); + + if (candidates.Length == 0) + { + Logger?.LogError($"Cannot find a compiled application at '{path}'"); + return false; } + + var file = candidates.OrderByDescending(c => c.LastWriteTime).First(); + + Logger?.LogInformation($"Deploying app from {file.DirectoryName}..."); + + await AppManager.DeployApplication(_packageManager, connection, file.DirectoryName!, true, false, Logger, CancellationToken); + + connection.FileWriteProgress -= OnFileWriteProgress; + + return true; + } + + private void OnFileWriteProgress(object? sender, (string fileName, long completed, long total) e) + { + var p = e.completed / (double)e.total * 100d; + + if (e.fileName != _lastFile) + { + Console?.Output.Write("\n"); + _lastFile = e.fileName; + } + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); } private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) @@ -93,4 +169,4 @@ private void OnDeviceMessageReceived(object? sender, (string message, string? so Logger?.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs index 917e24ca..56c4cea9 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs @@ -1,75 +1,65 @@ -using System.Threading; -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 : BaseAppCommand -{ - [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, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(packageManager, connectionManager, loggerFactory) - { - } - - protected override async ValueTask ExecuteCommand() - { - await base.ExecuteCommand(); - - string path = Path == null - ? Environment.CurrentDirectory - : 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 a 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); - } - - // Find RuntimeVersion - if (Connection != null) - { - var info = await Connection.GetDeviceInfo(CancellationToken); - - _packageManager.RuntimeVersion = info?.RuntimeVersion; - - Logger?.LogInformation($"Using runtime files from {_packageManager.MeadowAssembliesPath}"); - - // Avoid double reporting. - DetachMessageHandlers(Connection); - } - - // TODO: support `nolink` command line args - await _packageManager.TrimApplication(file, false, null, Logger, CancellationToken) - .WithSpinner(Console!, 250); - } +using CliFx.Attributes; +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 readonly IPackageManager _packageManager; + + [CommandOption('c', Description = "The build configuration to trim", IsRequired = false)] + public string? Configuration { get; init; } + + [CommandParameter(0, Name = "Path to project file", IsRequired = false)] + public string? Path { get; init; } + + public AppTrimCommand(IPackageManager packageManager, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _packageManager = packageManager; + } + + protected override async ValueTask ExecuteCommand() + { + string path = Path ?? AppDomain.CurrentDomain.BaseDirectory; + + // 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 a 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}"); + Logger?.LogInformation("This may take a few seconds..."); + + var cts = new CancellationTokenSource(); + ConsoleSpinner.Spin(Console, cancellationToken: cts.Token); + + await _packageManager.TrimApplication(file, false, null, CancellationToken); + cts.Cancel(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs index 3482fef1..618585b0 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCloudCommand.cs @@ -1,72 +1,68 @@ -using System.Configuration; -using Meadow.Cloud; -using Meadow.Cloud.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseCloudCommand : BaseCommand -{ - public const string DefaultHost = "https://www.meadowcloud.co"; - - protected IdentityManager IdentityManager { get; } - protected UserService UserService { get; } - protected DeviceService DeviceService { get; } - protected CollectionService CollectionService { get; } - - public BaseCloudCommand( - IdentityManager identityManager, - UserService userService, - DeviceService deviceService, - CollectionService collectionService, - ILoggerFactory? loggerFactory) - : base(loggerFactory) - { - IdentityManager = identityManager; - UserService = userService; - DeviceService = deviceService; - CollectionService = collectionService; - } - - protected async Task ValidateOrg(string host, string? orgNameOrId = null, CancellationToken? cancellationToken = null) - { - UserOrg? org = null; - - try - { - Logger?.LogInformation("Retrieving your user and organization information..."); - - var userOrgs = await UserService.GetUserOrgs(host, cancellationToken) - .WithSpinner(Console!, 250); - - if (!userOrgs.Any()) - { - Logger?.LogInformation($"Please visit {host} to register your account."); - } - else if (userOrgs.Count() > 1 && string.IsNullOrEmpty(orgNameOrId)) - { - Logger?.LogInformation($"You are a member of more than 1 organization. Please specify the desired orgId for this device provisioning."); - } - else if (userOrgs.Count() == 1 && string.IsNullOrEmpty(orgNameOrId)) - { - org = userOrgs.First(); - } - else - { - org = userOrgs.FirstOrDefault(o => o.Id == orgNameOrId || o.Name == orgNameOrId); - if (org == null) - { - Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{orgNameOrId}'"); - } - } - } - catch (MeadowCloudAuthException) - { - Logger?.LogError($"You must be signed in to execute this command."); - Logger?.LogError($"Please run \"meadow cloud login\" to sign in to Meadow.Cloud."); - } - - return org; - } +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +public abstract class BaseCloudCommand : BaseCommand +{ + public const string DefaultHost = "https://www.meadowcloud.co"; + + protected IdentityManager IdentityManager { get; } + protected UserService UserService { get; } + protected DeviceService DeviceService { get; } + protected CollectionService CollectionService { get; } + + public BaseCloudCommand( + IdentityManager identityManager, + UserService userService, + DeviceService deviceService, + CollectionService collectionService, + ILoggerFactory? loggerFactory) + : base(loggerFactory) + { + IdentityManager = identityManager; + UserService = userService; + DeviceService = deviceService; + CollectionService = collectionService; + } + + protected async Task ValidateOrg(string host, string? orgNameOrId = null, CancellationToken? cancellationToken = null) + { + UserOrg? org = null; + + try + { + Logger?.LogInformation("Retrieving your user and organization information..."); + + var userOrgs = await UserService.GetUserOrgs(host, cancellationToken).ConfigureAwait(false); + if (!userOrgs.Any()) + { + Logger?.LogInformation($"Please visit {host} to register your account."); + } + else if (userOrgs.Count() > 1 && string.IsNullOrEmpty(orgNameOrId)) + { + Logger?.LogInformation($"You are a member of more than 1 organization. Please specify the desired orgId for this device provisioning."); + } + else if (userOrgs.Count() == 1 && string.IsNullOrEmpty(orgNameOrId)) + { + orgNameOrId = userOrgs.First().Id; + } + else + { + org = userOrgs.FirstOrDefault(o => o.Id == orgNameOrId || o.Name == orgNameOrId); + if (org == null) + { + Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{orgNameOrId}'"); + } + } + } + catch (MeadowCloudAuthException) + { + Logger?.LogError($"You must be signed in to execute this command."); + Logger?.LogError($"Please run \"meadow cloud login\" to sign in to Meadow.Cloud."); + } + + return org; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index 076709b7..e0bc422f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -1,8 +1,6 @@ using CliFx; -using CliFx.Attributes; using CliFx.Infrastructure; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace Meadow.CLI.Commands.DeviceManagement; @@ -13,9 +11,6 @@ public abstract class BaseCommand : ICommand protected IConsole? Console { get; private set; } protected CancellationToken CancellationToken { get; private set; } - [CommandOption("verbose", IsRequired = false)] - public bool Verbose { get; set; } - public BaseCommand(ILoggerFactory? loggerFactory) { LoggerFactory = loggerFactory; @@ -41,12 +36,7 @@ public async ValueTask ExecuteAsync(IConsole console) if (CancellationToken.IsCancellationRequested) { - Logger?.LogInformation($"Cancelled."); - } - else - { - Logger?.LogInformation($"Done."); + Logger?.LogInformation($"Cancelled"); } } - } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs index b343f2f3..6645588b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseDeviceCommand.cs @@ -6,25 +6,22 @@ namespace Meadow.CLI.Commands.DeviceManagement; public abstract class BaseDeviceCommand : BaseCommand { protected MeadowConnectionManager ConnectionManager { get; } - public IMeadowConnection? Connection { get; private set; } public BaseDeviceCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(loggerFactory) { ConnectionManager = connectionManager; } - protected override async ValueTask ExecuteCommand() - { - Connection = await GetCurrentConnection(); - } - protected async Task GetCurrentConnection() { var connection = ConnectionManager.GetCurrentConnection(); if (connection != null) { - AttachMessageHandlers(connection); + connection.ConnectionError += (s, e) => + { + Logger?.LogError(e.Message); + }; try { @@ -54,61 +51,9 @@ protected override async ValueTask ExecuteCommand() } else { - Logger?.LogError("Current Connnection Unavailable"); // No connection path is defined ?? + Logger?.LogError("Current Connnection Unavailable"); } return null; } - - private void AttachMessageHandlers(IMeadowConnection? connection) - { - if (connection != null) - { - connection.ConnectionError += Connection_ConnectionError; - - connection.ConnectionMessage += Connection_ConnectionMessage; - - // the connection passes messages back to us (info about actions happening on-device) - connection.DeviceMessageReceived += Connection_DeviceMessageReceived; - } - } - - private void Connection_DeviceMessageReceived(object? sender, (string message, string? source) e) - { - if (e.message.Contains("% downloaded")) - { - // don't echo this, as we're already reporting % written - } - else if(e.source != null && e.source.Contains("stdout")) - { - // don't echo this, as we're already reporting it higher up - } - else - { - Logger?.LogInformation(e.message); - } - } - - private void Connection_ConnectionMessage(object? sender, string message) - { - Logger?.LogInformation(message); - } - - private void Connection_ConnectionError(object? sender, Exception e) - { - Logger?.LogError(e.Message); - } - - public void DetachMessageHandlers(IMeadowConnection? connection) - { - if (connection != null) - { - connection.ConnectionError -= Connection_ConnectionError; - - connection.ConnectionMessage -= Connection_ConnectionMessage; - - // the connection passes messages back to us (info about actions happening on-device) - connection.DeviceMessageReceived -= Connection_DeviceMessageReceived; - } - } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs index e995f156..6edc65ef 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseFileCommand.cs @@ -1,5 +1,4 @@ -using Meadow.CLI; -using Meadow.Software; +using Meadow.Software; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -7,20 +6,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public abstract class BaseFileCommand : BaseSettingsCommand { protected FileManager FileManager { get; } - protected IFirmwarePackageCollection? Collection { get; private set; } public BaseFileCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory) { FileManager = fileManager; } - - protected override async ValueTask ExecuteCommand() - { - await FileManager.Refresh(); - - // for now we only support F7 - // TODO: add switch and support for other platforms - Collection = FileManager.Firmware["Meadow F7"]; - } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs index 0d617e8d..fa4ca8fd 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLoginCommand.cs @@ -5,7 +5,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud login", Description = "Log into the Meadow Service")] +[Command("cloud login", Description = "Log in to Meadow.Cloud")] public class CloudLoginCommand : BaseCloudCommand { [CommandOption("host", Description = $"Optionally set a host (default is {DefaultHost})", IsRequired = false)] @@ -18,12 +18,11 @@ public CloudLoginCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Host == null) Host = DefaultHost; + Host ??= DefaultHost; Logger?.LogInformation($"Logging into {Host}..."); @@ -37,4 +36,4 @@ protected override async ValueTask ExecuteCommand() : "There was a problem retrieving your account information."); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs index bea4c3ca..23161fd6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/CloudLogoutCommand.cs @@ -5,7 +5,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud logout", Description = "Log out of the Meadow Service")] +[Command("cloud logout", Description = "Log out of Meadow.Cloud")] public class CloudLogoutCommand : BaseCloudCommand { public CloudLogoutCommand( @@ -15,16 +15,14 @@ public CloudLogoutCommand( CollectionService collectionService, ILoggerFactory? loggerFactory) : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { - await Task.Run(() => - { - Logger?.LogInformation($"Logging out of Meadow.Cloud..."); + Logger?.LogInformation($"Logging out of Meadow.Cloud..."); + + IdentityManager.Logout(); - IdentityManager.Logout(); - }); + return ValueTask.CompletedTask; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs index f690b8c7..684e917d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/CloudCollectionListCommand.cs @@ -10,8 +10,9 @@ public class CloudCollectionListCommand : BaseCloudCommand { - private const string InvalidArg = "Provided argument is not valid JSON:"; - public override JsonDocument Convert(string? rawValue) { try { - if (rawValue != null) - return JsonDocument.Parse(rawValue); - else - throw new CommandException($"{InvalidArg}"); + return JsonDocument.Parse(rawValue!); } catch (JsonException ex) { - throw new CommandException($"{InvalidArg} {ex.Message}", showHelp: false, innerException: ex); + throw new CommandException($"Provided argument is not valid JSON: {ex.Message}", showHelp: false, innerException: ex); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs index 8aa8e094..7e824fd5 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Collection/QualityOfService.cs @@ -5,4 +5,4 @@ public enum QualityOfService AtLeastOnce = 0, AtMostOnce = 1, ExactlyOnce = 2 -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs index 1a445dd0..f36cff56 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Command/CloudCommandPublishCommand.cs @@ -7,25 +7,25 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud command publish", Description = "Publish a command to Meadow devices via the Meadow Service")] +[Command("cloud command publish", Description = "Publish a command to Meadow devices via Meadow.Cloud")] public class CloudCommandPublishCommand : BaseCloudCommand { [CommandParameter(0, Description = "The name of the command", IsRequired = true, Name = "COMMAND_NAME")] - public string? CommandName { get; set; } + public string CommandName { get; init; } = default!; - [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command")] - public string? CollectionId { get; set; } + [CommandOption("collectionId", 'c', Description = "The target collection for publishing the command", IsRequired = false)] + public string? CollectionId { get; init; } - [CommandOption("deviceIds", 'd', Description = "The target devices for publishing the command")] - public string[]? DeviceIds { get; set; } + [CommandOption("deviceIds", 'd', Description = "The target devices for publishing the command", IsRequired = false)] + public string[]? DeviceIds { get; init; } - [CommandOption("args", 'a', Description = "The arguments for the command as a JSON string", Converter = typeof(JsonDocumentBindingConverter))] - public JsonDocument? Arguments { get; set; } + [CommandOption("args", 'a', Description = "The arguments for the command as a JSON string", Converter = typeof(JsonDocumentBindingConverter), IsRequired = false)] + public JsonDocument? Arguments { get; init; } - [CommandOption("qos", 'q', Description = "The MQTT-defined quality of service for the command")] - public QualityOfService QualityOfService { get; set; } = QualityOfService.AtLeastOnce; + [CommandOption("qos", 'q', Description = "The MQTT-defined quality of service for the command", IsRequired = false)] + public QualityOfService QualityOfService { get; init; } = QualityOfService.AtLeastOnce; - [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)")] + [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } private CommandService CommandService { get; } @@ -54,8 +54,7 @@ protected override async ValueTask ExecuteCommand() throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed.", showHelp: true); } - if (Host == null) - Host = DefaultHost; + Host ??= DefaultHost; var token = await IdentityManager.GetAccessToken(CancellationToken); if (string.IsNullOrWhiteSpace(token)) @@ -65,23 +64,19 @@ protected override async ValueTask ExecuteCommand() try { - if (!string.IsNullOrEmpty(CommandName)) + if (!string.IsNullOrWhiteSpace(CollectionId)) { - Logger?.LogInformation($"Publishing '{CommandName}' command to Meadow.Cloud. Please wait..."); - if (!string.IsNullOrWhiteSpace(CollectionId)) - { - await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); - } - else if (DeviceIds != null && DeviceIds.Any()) - { - await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); - } - else - { - throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed."); - } - Logger?.LogInformation("Publish command successful."); + await CommandService.PublishCommandForCollection(CollectionId, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); } + else if (DeviceIds?.Length > 0) + { + await CommandService.PublishCommandForDevices(DeviceIds, CommandName, Arguments, (int)QualityOfService, Host, CancellationToken); + } + else + { + throw new CommandException("Cannot specify both a collection ID (-c|--collectionId) and list of device IDs (-d|--deviceIds). Only one is allowed."); + } + Logger?.LogInformation("Publish command successful."); } catch (MeadowCloudAuthException ex) { diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs index 7f27e138..34a8601f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageCreateCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Cloud; using Meadow.Cloud.Identity; using Meadow.Software; @@ -7,24 +6,24 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("cloud package create", Description = "Builds, trims and creates a Meadow Package (MPAK)")] +[Command("cloud package create", Description = "Create a Meadow Package (MPAK)")] public class CloudPackageCreateCommand : BaseCloudCommand { [CommandParameter(0, Name = "Path to project file", IsRequired = false)] - public string? ProjectPath { get; set; } = default!; + public string? ProjectPath { get; set; } [CommandOption('c', Description = "The build configuration to compile", IsRequired = false)] - public string Configuration { get; set; } = "Release"; + public string Configuration { get; init; } = "Release"; [CommandOption("name", 'n', Description = "Name of the mpak file to be created", IsRequired = false)] - public string? MpakName { get; init; } = default!; + public string? MpakName { get; init; } [CommandOption("filter", 'f', Description = "Glob pattern to filter files. ex ('app.dll', 'app*','{app.dll,meadow.dll}')", IsRequired = false)] public string Filter { get; init; } = "*"; - private IPackageManager _packageManager; - private FileManager _fileManager; + private readonly IPackageManager _packageManager; + private readonly FileManager _fileManager; public CloudPackageCreateCommand( IdentityManager identityManager, @@ -42,14 +41,11 @@ public CloudPackageCreateCommand( protected override async ValueTask ExecuteCommand() { - if (ProjectPath == null) - { - ProjectPath = Environment.CurrentDirectory; - } + ProjectPath ??= AppDomain.CurrentDomain.BaseDirectory; // build Logger?.LogInformation($"Building {Configuration} version of application..."); - if (!_packageManager.BuildApplication(ProjectPath, Configuration, true, Logger, CancellationToken)) + if (!_packageManager.BuildApplication(ProjectPath, Configuration, true, CancellationToken)) { return; } @@ -72,10 +68,11 @@ protected override async ValueTask ExecuteCommand() // package var packageDir = Path.Combine(file.Directory?.FullName ?? string.Empty, PackageManager.PackageOutputDirectoryName); + //TODO - properly manage shared paths var postlinkDir = Path.Combine(file.Directory?.FullName ?? string.Empty, PackageManager.PostLinkDirectoryName); Logger?.LogInformation($"Assembling the MPAK..."); - var packagePath = await _packageManager.AssemblePackage(postlinkDir, packageDir, osVersion, Filter, true, Logger, CancellationToken); + var packagePath = await _packageManager.AssemblePackage(postlinkDir, packageDir, osVersion, Filter, true, CancellationToken); if (packagePath != null) { @@ -85,6 +82,5 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Package assembly failed."); } - } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs index 22d3f03c..2bdbf2be 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageListCommand.cs @@ -8,10 +8,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package list", Description = "Lists all Meadow Packages (MPAK)")] public class CloudPackageListCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandOption("orgId", 'o', Description = "Optional organization ID", IsRequired = false)] - public string? OrgId { get; set; } + public string? OrgId { get; init; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } @@ -30,30 +30,24 @@ public CloudPackageListCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) - Host = DefaultHost; - + Host ??= DefaultHost; var org = await ValidateOrg(Host, OrgId, CancellationToken); - if (org == null) - return; + if (org == null) { return; } - if (!string.IsNullOrEmpty(org.Id)) - { - var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); + var packages = await _packageService.GetOrgPackages(org.Id, Host, CancellationToken); - if (packages == null || packages.Count == 0) - { - Logger?.LogInformation("No packages found."); - } - else + if (packages == null || packages.Count == 0) + { + Logger?.LogInformation("No packages found"); + } + else + { + Logger?.LogInformation("packages:"); + foreach (var package in packages) { - Logger?.LogInformation("packages:"); - foreach (var package in packages) - { - Logger?.LogInformation($" {package.Id} | {package.Name}"); - } + Logger?.LogInformation($" {package.Id} | {package.Name}"); } } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs index 931141c1..d4232729 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackagePublishCommand.cs @@ -8,16 +8,16 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("cloud package publish", Description = "Publishes a Meadow Package (MPAK)")] public class CloudPackagePublishCommand : BaseCloudCommand { - private PackageService _packageService; + private readonly PackageService _packageService; [CommandParameter(0, Name = "PackageID", Description = "ID of the package to publish", IsRequired = true)] - public string? PackageId { get; init; } + public string PackageId { get; init; } = default!; [CommandOption("collectionId", 'c', Description = "The target collection for publishing", IsRequired = true)] - public string? CollectionId { get; set; } + public string CollectionId { get; init; } = default!; [CommandOption("metadata", 'm', Description = "Pass through metadata", IsRequired = false)] - public string? Metadata { get; set; } + public string? Metadata { get; init; } [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } @@ -36,24 +36,18 @@ public CloudPackagePublishCommand( protected override async ValueTask ExecuteCommand() { - if (Host == null) - Host = DefaultHost; + Host ??= DefaultHost; try { - if (!string.IsNullOrEmpty(PackageId) - && !string.IsNullOrEmpty(CollectionId) - && !string.IsNullOrEmpty(Metadata)) - { - Logger?.LogInformation($"Publishing package {PackageId} to collection {CollectionId}..."); - - await _packageService.PublishPackage(PackageId, CollectionId, Metadata, Host, CancellationToken); - Logger?.LogInformation("Publish successful."); - } + Logger?.LogInformation($"Publishing package {PackageId} to collection {CollectionId}..."); + + await _packageService.PublishPackage(PackageId, CollectionId, Metadata ?? string.Empty, Host, CancellationToken); + Logger?.LogInformation("Publish successful."); } catch (MeadowCloudException mex) { Logger?.LogError($"Publish failed: {mex.Message}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs index 13948a45..cc5f0946 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Cloud/Package/CloudPackageUploadCommand.cs @@ -1,101 +1,62 @@ -using System.Configuration; -using CliFx.Attributes; -using Meadow.Cloud; -using Meadow.Cloud.Identity; -using Meadow.Hcom; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("cloud package upload", Description = "Upload a Meadow Package (MPAK) to Meadow.Cloud")] -public class CloudPackageUploadCommand : BaseCloudCommand -{ - [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = false)] - public string? MpakPath { get; set; } - - [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] - public string? OrgId { get; set; } - - [CommandOption("description", 'd', Description = "Description of the package", IsRequired = false)] - public string? Description { get; set; } - - [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] - public string? Host { get; set; } - - private PackageService _packageService; - - public CloudPackageUploadCommand( - IdentityManager identityManager, - UserService userService, - DeviceService deviceService, - CollectionService collectionService, - PackageService packageService, - ILoggerFactory? loggerFactory) - : base(identityManager, userService, deviceService, collectionService, loggerFactory) - { - _packageService = packageService; - } - - protected override async ValueTask ExecuteCommand() - { - if (string.IsNullOrEmpty(MpakPath)) - { - var candidates = PackageManager.GetAvailableBuiltConfigurations(Environment.CurrentDirectory, "App.dll"); - - if (candidates.Length == 0) - { - Logger?.LogError($"Cannot find a compiled application at '{Environment.CurrentDirectory}'"); - return; - } - - var appDll = candidates.OrderByDescending(c => c.LastWriteTime).First(); - var packageDir = Path.Combine(appDll.Directory?.FullName ?? string.Empty, PackageManager.PackageOutputDirectoryName); - var files = Directory.EnumerateFiles(packageDir, "*.*", SearchOption.TopDirectoryOnly); - - var fileInfoList = new List(); - foreach (var file in files) - { - fileInfoList.Add(new FileInfo(file)); - } - - MpakPath = fileInfoList.OrderByDescending(f => f.LastWriteTime).First().FullName; - } - - if (!File.Exists(MpakPath)) - { - Logger?.LogError($"Package {MpakPath} does not exist"); - return; - } - - if (Host == null) - Host = DefaultHost; - - var org = await ValidateOrg(Host, OrgId, CancellationToken); - - if (org == null || string.IsNullOrEmpty(org.Id)) - { - Logger?.LogError($"Invalid Org"); - return; - } - - if (string.IsNullOrEmpty(Description)) - { - Description = string.Empty; - } - - try - { - Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); - - var package = await _packageService.UploadPackage(MpakPath, org.Id, Description, Host, CancellationToken) - .WithSpinner(Console!, 250); - - Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); - } - catch (MeadowCloudException mex) - { - Logger?.LogError($"Upload failed: {mex.Message}"); - } - - } +using CliFx.Attributes; +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("cloud package upload", Description = "Upload a Meadow Package (MPAK) to Meadow.Cloud")] +public class CloudPackageUploadCommand : BaseCloudCommand +{ + [CommandParameter(0, Name = "MpakPath", Description = "The full path of the mpak file", IsRequired = true)] + public string MpakPath { get; init; } = default!; + + [CommandOption("orgId", 'o', Description = "OrgId to upload to", IsRequired = false)] + public string? OrgId { get; init; } + + [CommandOption("description", 'd', Description = "Description of the package", IsRequired = false)] + public string? Description { get; init; } + + [CommandOption("host", Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] + public string? Host { get; set; } + + private readonly PackageService _packageService; + + public CloudPackageUploadCommand( + IdentityManager identityManager, + UserService userService, + DeviceService deviceService, + CollectionService collectionService, + PackageService packageService, + ILoggerFactory? loggerFactory) + : base(identityManager, userService, deviceService, collectionService, loggerFactory) + { + _packageService = packageService; + } + + protected override async ValueTask ExecuteCommand() + { + if (!File.Exists(MpakPath)) + { + Logger?.LogError($"Package {MpakPath} does not exist"); + return; + } + + Host ??= DefaultHost; + var org = await ValidateOrg(Host, OrgId, CancellationToken); + + if (org == null) { return; } + + try + { + Logger?.LogInformation($"Uploading package {Path.GetFileName(MpakPath)}..."); + + var package = await _packageService.UploadPackage(MpakPath, org.Id, Description ?? string.Empty, Host, CancellationToken); + Logger?.LogInformation($"Upload complete. Package Id: {package.Id}"); + } + catch (MeadowCloudException mex) + { + Logger?.LogError($"Upload failed: {mex.Message}"); + } + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs index 589c4c4e..b1e47b1f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Config/ConfigCommand.cs @@ -1,8 +1,5 @@ -using CliFx; -using CliFx.Attributes; +using CliFx.Attributes; using CliFx.Exceptions; -using CliFx.Infrastructure; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -11,61 +8,58 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class ConfigCommand : BaseSettingsCommand { [CommandOption("list", IsRequired = false)] - public bool List { get; set; } + public bool List { get; init; } [CommandParameter(0, Name = "Settings", IsRequired = false)] - public string[]? Settings { get; set; } + public string[]? Settings { get; init; } public ConfigCommand(ISettingsManager settingsManager, ILoggerFactory? loggerFactory) : base(settingsManager, loggerFactory) - { - - } + { } - protected override async ValueTask ExecuteCommand() + protected override ValueTask ExecuteCommand() { - await Task.Run(() => + if (List) { - if (List) - { - Logger?.LogInformation($"Current CLI configuration"); + 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}"); - } - } + // display all current config + var settings = SettingsManager.GetPublicSettings(); + if (settings.Count == 0) + { + Logger?.LogInformation($" "); } else { - switch (Settings?.Length) + foreach (var kvp in SettingsManager.GetPublicSettings()) { - case 0: - // not valid - throw new CommandException($"No setting provided"); - case 1: - // erase a setting - Logger?.LogInformation($"{Environment.NewLine}Deleting Setting {Settings[0]}"); - SettingsManager.DeleteSetting(Settings[0]); - break; - case 2: - // set a setting - Logger?.LogInformation($"{Environment.NewLine}Setting {Settings[0]}={Settings[1]}"); - SettingsManager.SaveSetting(Settings[0], Settings[1]); - break; - default: - // not valid; - throw new CommandException($"Too many parameters provided"); + 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($"{Environment.NewLine}Deleting Setting {Settings[0]}"); + SettingsManager.DeleteSetting(Settings[0]); + break; + case 2: + // set a setting + Logger?.LogInformation($"{Environment.NewLine}Setting {Settings[0]}={Settings[1]}"); + SettingsManager.SaveSetting(Settings[0], Settings[1]); + break; + default: + // not valid; + throw new CommandException($"Too many parameters provided"); + } + } + + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs index f94910f2..15fa96a1 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/DeveloperCommand.cs @@ -6,28 +6,36 @@ 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("param", 'p', Description = "The parameter to set.", IsRequired = false)] + public ushort Parameter { get; init; } - [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; } + [CommandOption("value", 'v', Description = "The value to apply to the parameter. Valid values are 0 to 4,294,967,295", IsRequired = false)] + public uint Value { get; init; } public DeveloperCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - Logger?.LogInformation($"Setting developer parameter {Parameter} to {Value}"); - await Connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); - } + return; } + + 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 connection.Device.SetDeveloperParameter(Parameter, Value, CancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs index 7e657a44..b0e73ba7 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceClockCommand.cs @@ -7,41 +7,42 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class DeviceClockCommand : BaseDeviceCommand { [CommandParameter(0, Name = "Time", IsRequired = false)] - public string? Time { get; set; } + public string? Time { get; init; } public DeviceClockCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } - if (Connection != null && Connection.Device != null) + if (Time == null) { - if (Time == null) + Logger?.LogInformation($"Getting device clock..."); + var deviceTime = await connection.Device.GetRtcTime(CancellationToken); + Logger?.LogInformation($"{deviceTime.Value:s}Z"); + } + else + { + if (Time == "now") + { + Logger?.LogInformation($"Setting device clock..."); + await connection.Device.SetRtcTime(DateTimeOffset.UtcNow, CancellationToken); + } + else if (DateTimeOffset.TryParse(Time, out DateTimeOffset dto)) { - Logger?.LogInformation($"Getting device clock..."); - var deviceTime = await Connection.Device.GetRtcTime(CancellationToken); - Logger?.LogInformation($"{deviceTime.Value:s}Z"); + Logger?.LogInformation($"Setting device clock..."); + await connection.Device.SetRtcTime(dto, CancellationToken); } else { - if (Time == "now") - { - Logger?.LogInformation($"Setting device clock..."); - await Connection.Device.SetRtcTime(DateTimeOffset.UtcNow, CancellationToken); - } - else if (DateTimeOffset.TryParse(Time, out DateTimeOffset dto)) - { - Logger?.LogInformation($"Setting device clock..."); - await Connection.Device.SetRtcTime(dto, CancellationToken); - } - else - { - Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); - } + Logger?.LogInformation($"Unable to parse '{Time}' to a valid time."); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs index a1b55bd8..ec412c4f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceInfoCommand.cs @@ -9,20 +9,22 @@ public class DeviceInfoCommand : BaseDeviceCommand public DeviceInfoCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Getting device info..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Getting device info..."); - var deviceInfo = await Connection.Device.GetDeviceInfo(CancellationToken); - if (deviceInfo != null) - { - Logger?.LogInformation(deviceInfo.ToString()); - } + return; + } + + var deviceInfo = await connection.Device.GetDeviceInfo(CancellationToken); + if (deviceInfo != null) + { + Logger?.LogInformation(deviceInfo.ToString()); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs index d51bd1cc..11362377 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceProvisionCommand.cs @@ -1,6 +1,5 @@ using CliFx.Attributes; using Meadow.Cloud; -using Meadow.Cloud.Identity; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -8,7 +7,8 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("device provision", Description = "Registers and prepares connected device for use with Meadow Cloud")] public class DeviceProvisionCommand : BaseDeviceCommand { - private DeviceService _deviceService; + private readonly DeviceService _deviceService; + private readonly UserService _userService; public const string DefaultHost = "https://www.meadowcloud.co"; @@ -16,34 +16,34 @@ public class DeviceProvisionCommand : BaseDeviceCommand public string? OrgId { get; set; } [CommandOption("collectionId", 'c', Description = "The target collection for device registration", IsRequired = false)] - public string? CollectionId { get; set; } + public string? CollectionId { get; init; } [CommandOption("name", 'n', Description = "Device friendly name", IsRequired = false)] - public string? Name { get; set; } + public string? Name { get; init; } [CommandOption("host", 'h', Description = "Optionally set a host (default is https://www.meadowcloud.co)", IsRequired = false)] public string? Host { get; set; } - public DeviceProvisionCommand(DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public DeviceProvisionCommand(UserService userService, DeviceService deviceService, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { _deviceService = deviceService; + _userService = userService; } protected override async ValueTask ExecuteCommand() { UserOrg? org; + try { - if (Host == null) Host = DefaultHost; - - var identityManager = new IdentityManager(Logger); - var _userService = new UserService(identityManager); + Host ??= DefaultHost; Logger?.LogInformation("Retrieving your user and organization information..."); var userOrgs = await _userService.GetUserOrgs(Host, CancellationToken).ConfigureAwait(false); - if (!userOrgs.Any()) + + if (userOrgs == null || !userOrgs.Any()) { Logger?.LogInformation($"Please visit {Host} to register your account."); return; @@ -59,6 +59,7 @@ protected override async ValueTask ExecuteCommand() } org = userOrgs.FirstOrDefault(o => o.Id == OrgId || o.Name == OrgId); + if (org == null) { Logger?.LogInformation($"Unable to find an organization with a Name or ID matching '{OrgId}'"); @@ -74,23 +75,48 @@ protected override async ValueTask ExecuteCommand() var connection = await GetCurrentConnection(); - if (connection == null) + if (connection == null || connection.Device == null) { - Logger?.LogError($"No connection path is defined"); return; } - var info = await connection.Device!.GetDeviceInfo(CancellationToken); + var info = await connection.Device.GetDeviceInfo(CancellationToken); Logger?.LogInformation("Requesting device public key (this will take a minute)..."); var publicKey = await connection.Device.GetPublicKey(CancellationToken); - var delim = "-----END PUBLIC KEY-----\n"; - publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + if (string.IsNullOrWhiteSpace(publicKey)) + { + Logger?.LogError("Could not retrieve device's public key."); + return; + } + + var delimiters = new string[] + { + "-----END PUBLIC KEY-----\n", // F7 delimiter + "-----END RSA PUBLIC KEY-----\n" // linux/mac/windows delimiter + }; + var valid = false; - Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); + foreach (var delim in delimiters) + { + var index = publicKey.IndexOf(delim); + if (index > 0) + { + valid = true; + publicKey = publicKey.Substring(0, publicKey.IndexOf(delim) + delim.Length); + break; + } + } + + if (!valid) + { + Logger?.LogError("Device returned an invali dpublic key"); + return; + } + Logger?.LogInformation("Provisioning device with Meadow.Cloud..."); var provisioningID = !string.IsNullOrWhiteSpace(info?.ProcessorId) ? info.ProcessorId : info?.SerialNumber; var provisioningName = !string.IsNullOrWhiteSpace(Name) ? Name : info?.DeviceName; @@ -104,7 +130,5 @@ protected override async ValueTask ExecuteCommand() { Logger?.LogError($"Failed to provision device: {result.message}"); } - - return; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs index dadea321..6baf6e7c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Device/DeviceResetCommand.cs @@ -9,16 +9,18 @@ public class DeviceResetCommand : BaseDeviceCommand public DeviceResetCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { + Logger?.LogInformation($"Resetting the device..."); } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Resetting the device..."); - await Connection.Device.Reset(); + return; } + + await connection.Device.Reset(); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs index 5c1b173e..f9708726 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Dfu/DfuInstallCommand.cs @@ -1,8 +1,5 @@ using CliFx.Attributes; -using CliFx.Infrastructure; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; -using Meadow.Hcom; using Meadow.Software; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; @@ -26,15 +23,11 @@ protected DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory log public DfuInstallCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (Version == null) - { - Version = DefaultVersion; - } + Version ??= DefaultVersion; switch (Version) { @@ -43,7 +36,7 @@ protected override async ValueTask ExecuteCommand() // valid break; default: - Logger?.LogError("Only versions 0.10 and 0.11 are supported."); + Logger?.LogError("Only DFU versions 0.10 and 0.11 are supported"); return; } @@ -64,8 +57,7 @@ protected override async ValueTask ExecuteCommand() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Logger?.LogWarning( - "To install DFU on Linux, use the package manager to install the dfu-util package"); + Logger?.LogWarning("To install DFU on Linux, use the package manager to install the dfu-util package"); } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs index d8d207c2..4b5ba86b 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs @@ -1,107 +1,74 @@ -using System; -using System.Diagnostics.Metrics; -using CliFx.Attributes; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("file delete", Description = "Deletes a file from the device")] -public class FileDeleteCommand : BaseDeviceCommand -{ - [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; - - public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - } - - protected override async ValueTask ExecuteCommand() - { - await base.ExecuteCommand(); - - if (Connection != null) - { - // in order to delete, the runtime must be disabled - var wasRuntimeEnabled = await Connection.IsRuntimeEnabled(); - - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling runtime..."); - - await Connection.RuntimeDisable(CancellationToken); - } - - try - { - var fileList = await Connection.GetFileList(false); - - if (MeadowFile == "all") - { - if (Console != null) - { - Logger?.LogInformation($"{Environment.NewLine}Are you sure you want to delete ALL files from this device (Y/N)?"); - - var reply = await Console.Input.ReadLineAsync(); - if ((!string.IsNullOrEmpty(reply) && reply.ToLower() != "y") || string.IsNullOrEmpty(reply)) - { - return; - } - } - - if (fileList != null) - { - if (fileList.Length > 0) - { - foreach (var f in fileList) - { - if (Connection.Device != null) - { - var p = Path.GetFileName(f.Name); - - Console?.Output.WriteAsync($"Deleting file '{p}' from device... \r"); - await Connection.Device.DeleteFile(p, CancellationToken); - } - else - { - Logger?.LogError($"No Device Found."); - } - } - } - else - { - Logger?.LogInformation($"No files to delete."); - } - } - } - else - { - var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false; - - if (!exists) - { - Logger?.LogError($"File '{MeadowFile}' not found on device."); - } - else - { - if (Connection.Device != null) - { - Console?.Output.WriteAsync($"Deleting file '{MeadowFile}' from device... \r"); - await Connection.Device.DeleteFile(MeadowFile, CancellationToken); - } - } - } - } - finally - { - if (wasRuntimeEnabled) - { - // restore runtime state - Logger?.LogInformation("Enabling runtime..."); - - await Connection.RuntimeEnable(CancellationToken); - } - } - } - } +using CliFx.Attributes; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("file delete", Description = "Deletes a file from the device")] +public class FileDeleteCommand : BaseDeviceCommand +{ + [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] + public string MeadowFile { get; init; } = default!; + + public FileDeleteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + : base(connectionManager, loggerFactory) + { } + + protected override async ValueTask ExecuteCommand() + { + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } + + // get a list of files in the target folder + var folder = Path.GetDirectoryName(MeadowFile)!.Replace(Path.DirectorySeparatorChar, '/'); + if (string.IsNullOrWhiteSpace(folder)) + { + folder = "/meadow0"; + } + + var fileList = await connection.GetFileList($"{folder}/", false); + + if (fileList == null || fileList.Length == 0) + { + Logger?.LogError($"File delete failed, no files found"); + return; + } + + if (MeadowFile == "all") + { + foreach (var f in fileList) + { + var p = Path.GetFileName(f.Name); + Logger?.LogInformation($"Deleting file '{p}' from device..."); + await connection.Device.DeleteFile(p, CancellationToken); + } + } + else + { + var requested = Path.GetFileName(MeadowFile); + + var exists = fileList?.Any(f => Path.GetFileName(f.Name) == requested) ?? false; + + if (!exists) + { + Logger?.LogError($"File '{MeadowFile}' not found on device."); + } + else + { + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + + if (wasRuntimeEnabled) + { + Logger?.LogError($"The runtime must be disabled before doing any file management. Use 'meadow runtime disable' first."); + return; + } + + Logger?.LogInformation($"Deleting file '{MeadowFile}' from device..."); + await connection.Device.DeleteFile(MeadowFile, CancellationToken); + } + } + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs index 44115518..24067f2f 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileInitialCommand.cs @@ -7,27 +7,31 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FileInitialCommand : BaseDeviceCommand { [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; + public string MeadowFile { get; init; } = default!; public FileInitialCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - Logger?.LogInformation($"Reading file '{MeadowFile}' from device...\n"); + return; + } - var data = await Connection.Device.ReadFileString(MeadowFile, CancellationToken); + Logger?.LogInformation($"Reading file '{MeadowFile}' from device...\n"); - Logger?.LogInformation(data); - } + var data = await connection.Device.ReadFileString(MeadowFile, CancellationToken); + + if (data == null) + { + Logger?.LogError($"Failed to retrieve file"); + return; } + + Logger?.LogInformation(data); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs index 9325db79..8358fb2e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileListCommand.cs @@ -8,77 +8,95 @@ public class FileListCommand : BaseDeviceCommand { public const int FileSystemBlockSize = 4096; + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; init; } + + [CommandParameter(0, Name = "Folder", IsRequired = false)] + public string? Folder { get; set; } + public FileListCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation($"Getting file list..."); - var files = await Connection.Device.GetFileList(Verbose, CancellationToken); + return; + } - if (files == null || files.Length == 0) + if (Folder != null) + { + if (Folder.EndsWith('/') == false) { - Logger?.LogInformation($"No files found"); + Folder += "/"; } - else + + Logger?.LogInformation($"Getting file list from '{Folder}'..."); + } + else + { + Logger?.LogInformation($"Getting file list..."); + } + + var files = await connection.Device.GetFileList(Folder ?? "/meadow0/", Verbose, CancellationToken); + + if (files == null || files.Length == 0) + { + Logger?.LogInformation($"No files found"); + } + else + { + if (Verbose) { - 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) { - var longestFileName = files.Select(x => x.Name.Length) - .OrderByDescending(x => x) - .FirstOrDefault(); + totalBytesUsed += file.Size ?? 0; + totalBlocksUsed += ((file.Size ?? 0) / FileSystemBlockSize) + 1; - var totalBytesUsed = 0L; - var totalBlocksUsed = 0L; + var line = $"{file.Name.PadRight(longestFileName)}"; + line = $"{line}\t{file.Crc:x8}"; - foreach (var file in files) + if (file.Size > 1000000) { - 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); + line = $"{line}\t{file.Size / 1000000d,7:0.0} MB "; } - - 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) + else if (file.Size > 1000) + { + line = $"{line}\t{file.Size / 1000,7:0} kB "; + } + else { - Logger?.LogInformation(file.Name); + line = $"{line}\t{file.Size,7} bytes"; } - Logger?.LogInformation( - $"\nSummary:\n" + - $"\t{files.Length} files"); + Logger?.LogInformation(line); + } + + Logger?.LogInformation( + $"\nSummary:\n" + + $"\t{files.Length} file(s)\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($"\t{files.Length} file(s)"); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs index 7538cef5..c816c473 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileReadCommand.cs @@ -7,34 +7,35 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FileReadCommand : BaseDeviceCommand { [CommandParameter(0, Name = "MeadowFile", IsRequired = true)] - public string MeadowFile { get; set; } = default!; + public string MeadowFile { get; init; } = default!; [CommandParameter(1, Name = "LocalFile", IsRequired = false)] - public string? LocalFile { get; set; } + public string? LocalFile { get; init; } public FileReadCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } - if (Connection != null && Connection.Device != null) + Logger?.LogInformation($"Getting file '{MeadowFile}' from device..."); + + var success = await connection.Device.ReadFile(MeadowFile, LocalFile, CancellationToken); + + if (success) + { + Logger?.LogInformation($"Success"); + } + else { - Logger?.LogInformation($"Getting file '{MeadowFile}' from device..."); - - var success = await Connection.Device.ReadFile(MeadowFile, LocalFile, CancellationToken); - - if (success) - { - Logger?.LogInformation($"Success"); - } - else - { - Logger?.LogInformation($"Failed to retrieve file"); - } + Logger?.LogInformation($"Failed to retrieve file"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs index b9995901..aff9cc96 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileWriteCommand.cs @@ -16,61 +16,60 @@ public class FileWriteCommand : BaseDeviceCommand [CommandOption( "targetFiles", 't', - Description = "The filename(s) to use on the Meadow File System")] + Description = "The filename(s) to use on the Meadow File System", + IsRequired = false)] public IList TargetFileNames { get; init; } = Array.Empty(); public FileWriteCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - 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; + } - return; - } + 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})."); - Connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; + return; + } - // Console instead of Logger due to line breaking for progress bar - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + connection.FileWriteProgress += (s, e) => + { + var p = e.completed / (double)e.total * 100d; + + // Console instead of Logger due to line breaking for progress bar + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; - Logger?.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); + Logger?.LogInformation($"Writing {Files.Count} file{(Files.Count > 1 ? "s" : "")} to device..."); - for (var i = 0; i < Files.Count; i++) + for (var i = 0; i < Files.Count; i++) + { + if (!File.Exists(Files[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 { - Logger?.LogError($"Cannot find file '{Files[i]}'. Skippping"); + await connection.Device.WriteFile(Files[i], targetFileName, CancellationToken); } - else + catch (Exception ex) { - try - { - if (Connection.Device != null) - { - var targetFileName = GetTargetFileName(i); - - Logger?.LogInformation($"Writing '{Files[i]}' as '{targetFileName}' to device"); - - await Connection.Device.WriteFile(Files[i], targetFileName, CancellationToken); - } - } - catch (Exception ex) - { - Logger?.LogError($"Error writing file: {ex.Message}"); - } + Logger?.LogError($"Error writing file: {ex.Message}"); } } } @@ -87,4 +86,4 @@ private string GetTargetFileName(int i) return new FileInfo(Files[i]).Name; } -} \ No newline at end of file +} diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs index 4de18847..8fb3c02e 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDefaultCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,30 +9,30 @@ public class FirmwareDefaultCommand : BaseFileCommand { public FirmwareDefaultCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to use as default", IsRequired = false)] - public string? Version { get; set; } = null; + public string? Version { get; init; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); - if (Collection != null) + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; + + if (Version == null) { - if (Version == null) - { - Logger?.LogInformation($"Default firmware is '{Collection.DefaultPackage?.Version}'."); - } - else - { - var existing = Collection.FirstOrDefault(p => p.Version == Version); - - Logger?.LogInformation($"Setting default firmware to '{Version}'..."); - - await Collection.SetDefaultPackage(Version); - } + Logger?.LogInformation($"Default firmware is '{collection?.DefaultPackage?.Version}'."); + } + else + { + var existing = collection.FirstOrDefault(p => p.Version == Version); + + Logger?.LogInformation($"Setting default firmware to '{Version}'..."); + + await collection.SetDefaultPackage(Version); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs index 77059364..32188417 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDeleteCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,19 +9,21 @@ public class FirmwareDeleteCommand : BaseFileCommand { public FirmwareDeleteCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandParameter(0, Name = "Version number to delete", IsRequired = true)] - public string Version { get; set; } = default!; + public string Version { get; init; } = default!; protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); + + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; Logger?.LogInformation($"Deleting firmware '{Version}'..."); - if (Collection != null) - await Collection.DeletePackage(Version); + await collection.DeletePackage(Version); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs index d959fd0d..d12e4928 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareDownloadCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -10,82 +9,80 @@ public class FirmwareDownloadCommand : BaseFileCommand { public FirmwareDownloadCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) - { - } + { } [CommandOption("force", 'f', IsRequired = false)] - public bool Force { get; set; } + public bool Force { get; init; } [CommandOption("version", 'v', IsRequired = false)] - public string? Version { get; set; } = default!; + public string? Version { get; set; } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + await FileManager.Refresh(); - if (Collection != null) - { - bool explicitVersion; + // for now we only support F7 + // TODO: add switch and support for other platforms + var collection = FileManager.Firmware["Meadow F7"]; - if (Version == null) - { - explicitVersion = false; - var latest = await Collection.GetLatestAvailableVersion(); + bool explicitVersion; - if (latest == null) - { - Logger?.LogError($"Unable to get latest version information."); - return; - } + if (Version == null) + { + explicitVersion = false; + var latest = await collection.GetLatestAvailableVersion(); - Logger?.LogInformation($"Latest available version is '{latest}'..."); - Version = latest; - } - else + if (latest == null) { - explicitVersion = true; - Logger?.LogInformation($"Checking for firmware package '{Version}'..."); + Logger?.LogError($"Unable to get latest version information"); + return; } - var isAvailable = await Collection.IsVersionAvailableForDownload(Version); + Logger?.LogInformation($"Latest available version is '{latest}'..."); + Version = latest; + } + else + { + explicitVersion = true; + Logger?.LogInformation($"Checking for firmware package '{Version}'..."); + } - if (!isAvailable) - { - Logger?.LogError($"Requested package version '{Version}' is not available."); - return; - } + var isAvailable = await collection.IsVersionAvailableForDownload(Version); - Logger?.LogInformation($"Downloading firmware package '{Version}'..."); + if (!isAvailable) + { + Logger?.LogError($"Requested package version '{Version}' is not available."); + return; + } - try - { - Collection.DownloadProgress += OnDownloadProgress; + Logger?.LogInformation($"Downloading firmware package '{Version}'..."); - var result = await Collection.RetrievePackage(Version, Force); + try + { + collection.DownloadProgress += OnDownloadProgress; - if (!result) - { - Logger?.LogError($"Unable to download package '{Version}'."); - } - else - { - Logger?.LogError($"{Environment.NewLine} Firmware package '{Version}' downloaded."); + var result = await collection.RetrievePackage(Version, Force); - if (!explicitVersion) - { - await Collection.SetDefaultPackage(Version); - } - } + if (!result) + { + Logger?.LogError($"Unable to download package '{Version}'"); } - catch (Exception ex) + else { - Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + Logger?.LogInformation($"Firmware package '{Version}' downloaded"); + + if (explicitVersion == false) + { + await collection.SetDefaultPackage(Version); + } } } + catch (Exception ex) + { + Logger?.LogError($"Unable to download package '{Version}': {ex.Message}"); + } } - // TODO private long _lastProgress = 0; - private void OnDownloadProgress(object? sender, long e) { // use Console so we can Write instead of Logger which only supports WriteLine diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs index 4f328b6a..7140fb31 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareListCommand.cs @@ -1,108 +1,107 @@ -using System.Linq; -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 : BaseCommand -{ - private FileManager FileManager { get; } - - public FirmwareListCommand(FileManager fileManager, ILoggerFactory? loggerFactory) - : base(loggerFactory) - { - FileManager = fileManager; - } - - protected override async ValueTask ExecuteCommand() - { - await FileManager.Refresh(); - - if (Verbose) - { - await DisplayVerboseResults(FileManager); - } - else - { - await DisplayTerseResults(FileManager); - } - } - - private async Task DisplayVerboseResults(FileManager manager) - { - Logger?.LogInformation($" (D== Default, OSB==OS without bootloader, RT==Runtime, CP==Coprocessor){Environment.NewLine}"); - Logger?.LogInformation($" D VERSION OS OSB RT CP BCL"); - - Logger?.LogInformation($"------------------------------------------"); - - foreach (var name in manager.Firmware.CollectionNames) - { - Logger?.LogInformation($" {name}"); - var collection = manager.Firmware[name.ToString()]; - - foreach (var package in collection.OrderByDescending(s=> s.Version)) - { - if (package == collection.DefaultPackage) - { - var detailedInformation = $" * {package.Version?.PadRight(18)} " + - $"{(package.OSWithBootloader != null ? "X " : " ")}" + - $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + - $"{(package.Runtime != null ? "X " : " ")}" + - $"{(package.CoprocApplication != null ? "X " : " ")}" + - $"{(package.BclFolder != null ? "X " : " ")}" + - " (default)"; - Logger?.LogInformation(detailedInformation.ColourConsoleTextGreen()); - } - 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.ToString()]; - - foreach (var package in collection.OrderByDescending(s => s.Version)) - { - if (package == collection.DefaultPackage) - { - Logger?.LogInformation($" * {package.Version} (default)".ColourConsoleTextGreen()); - } - else - { - Logger?.LogInformation($" {package.Version}"); - } - } - - var update = await collection.UpdateAvailable(); - if (update != null) - { - Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); - } - } - } -} +using CliFx.Attributes; +using Meadow.Software; +using Microsoft.Extensions.Logging; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("firmware list", Description = "List locally available firmware")] +public class FirmwareListCommand : BaseCommand +{ + [CommandOption("verbose", 'v', IsRequired = false)] + public bool Verbose { get; init; } + + private FileManager FileManager { get; } + + public FirmwareListCommand(FileManager fileManager, ILoggerFactory? loggerFactory) + : base(loggerFactory) + { + FileManager = fileManager; + } + + protected override async ValueTask ExecuteCommand() + { + await FileManager.Refresh(); + + if (Verbose) + { + await DisplayVerboseResults(FileManager); + } + else + { + await DisplayTerseResults(FileManager); + } + } + + private async Task DisplayVerboseResults(FileManager manager) + { + Logger?.LogInformation($" (D== Default, OSB==OS without bootloader, RT==Runtime, CP==Coprocessor){Environment.NewLine}"); + Logger?.LogInformation($" D VERSION OS OSB RT CP BCL"); + + Logger?.LogInformation($"------------------------------------------"); + + foreach (var name in manager.Firmware.CollectionNames) + { + Logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + Logger?.LogInformation( + $" * {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + else + { + Logger?.LogInformation( + $" {package.Version.PadRight(18)} " + + $"{(package.OSWithBootloader != null ? "X " : " ")}" + + $"{(package.OsWithoutBootloader != null ? " X " : " ")}" + + $"{(package.Runtime != null ? "X " : " ")}" + + $"{(package.CoprocApplication != null ? "X " : " ")}" + + $"{(package.BclFolder != null ? "X " : " ")}" + ); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } + + private async Task DisplayTerseResults(FileManager manager) + { + foreach (var name in manager.Firmware.CollectionNames) + { + Logger?.LogInformation($" {name}"); + var collection = manager.Firmware[name]; + + foreach (var package in collection) + { + if (package == collection.DefaultPackage) + { + Logger?.LogInformation($" * {package.Version} (default)"); + } + else + { + Logger?.LogInformation($" {package.Version}"); + } + } + + var update = await collection.UpdateAvailable(); + if (update != null) + { + Logger?.LogInformation($"{Environment.NewLine} ! {update} IS AVAILABLE FOR DOWNLOAD"); + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs index 8f76945f..296de248 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Firmware/FirmwareWriteCommand.cs @@ -1,8 +1,5 @@ -using System.Collections.Concurrent; -using CliFx.Attributes; -using Meadow.CLI; +using CliFx.Attributes; using Meadow.CLI.Core.Internals.Dfu; -using Meadow.Cloud; using Meadow.Hcom; using Meadow.LibUsb; using Meadow.Software; @@ -27,22 +24,12 @@ public class FirmwareWriteCommand : BaseDeviceCommand public bool UseDfu { get; set; } [CommandParameter(0, Name = "Files to write", IsRequired = false)] - public FirmwareType[]? Files { get; set; } = default!; - - [CommandOption("file", 'f', IsRequired = false, Description = "Path to OS, Runtime or ESP file")] - public string? Path { get; set; } = default!; - - [CommandOption("address", 'a', IsRequired = false, Description = "Address location to write the file to")] - public int? Address { get; set; } = default!; + public FirmwareType[]? FirmwareFileTypes { get; set; } = default!; private FileManager FileManager { get; } private ISettingsManager Settings { get; } - private const string FileWriteComplete = "Firmware Write Complete!"; - private ILibUsbDevice[]? _libUsbDevices; - private IMeadowConnection? connection; - - // TODO private bool _fileWriteError = false; + private ILibUsbDevice? _libUsbDevice; public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) @@ -53,201 +40,108 @@ public FirmwareWriteCommand(ISettingsManager settingsManager, FileManager fileMa protected override async ValueTask ExecuteCommand() { - // do not call the base implementation - it will look for a route, and with DFU we don't have (or need) one - var package = await GetSelectedPackage(); - if (Files == null && package != null) + if (package == null) { - Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); - - Files = new FirmwareType[] - { - FirmwareType.OS, - FirmwareType.Runtime, - FirmwareType.ESP - }; + Logger?.LogError($"Firware write failed - No package selected"); + return; } - if (Files != null && !Files.Contains(FirmwareType.OS) && UseDfu) + if (FirmwareFileTypes == null) { - Logger?.LogError($"DFU is only used for OS files. Select an OS file or remove the DFU option"); - return; + Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); + + FirmwareFileTypes = new FirmwareType[] + { + FirmwareType.OS, + FirmwareType.Runtime, + FirmwareType.ESP + }; } bool deviceSupportsOta = false; // TODO: get this based on device OS version - if ((Files != null && Files.Contains(FirmwareType.OS)) && (package != null && package.OsWithoutBootloader == null + if (package.OsWithoutBootloader == null || !deviceSupportsOta - || UseDfu)) + || UseDfu) { UseDfu = true; } - if (Files != null && package != null) + if (!FirmwareFileTypes.Contains(FirmwareType.OS) && UseDfu) { - // get the device's serial number via DFU - we'll need it to find the device after it resets - try - { - if (UseDfu) - { - GetLibUsbDevicesInBootloaderModeForCurrentEnvironment(); - } - } - catch (Exception ex) - { - Logger?.LogError(ex.Message); - return; - } + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); + return; + } - var flashStatus = new ConcurrentDictionary(); + if (UseDfu && FirmwareFileTypes.Contains(FirmwareType.OS)) + { + var osFile = package.GetFullyQualifiedPath(package.OSWithBootloader); - if (_libUsbDevices != null) + if (osFile == null) { - if (_libUsbDevices.Length > 1) - { - await Console!.Output.WriteLineAsync($"Found {_libUsbDevices.Length} devices in bootloader mode.{Environment.NewLine}Would you like to flash them all (Y/N)?"); - var yesOrNo = await Console!.Input.ReadLineAsync(); - if (!string.IsNullOrEmpty(yesOrNo)) - { - if (yesOrNo.ToLower() == "n") - { - Logger?.LogInformation("User elected not to proceed."); - return; - } - } - } - - foreach (var libUsbDevice in _libUsbDevices) - { - var serialNumber = libUsbDevice.GetDeviceSerialNumber(); - - // no connection is required here - in fact one won't exist - // unless maybe we add a "DFUConnection"? - - try - { - if (package != null && package.OSWithBootloader != null && Files.Contains(FirmwareType.OS)) - { - flashStatus[serialNumber] = "WritingOS"; - await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serialNumber); - } - } - catch (Exception ex) - { - flashStatus[serialNumber] = ex.Message; - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - - Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); - continue; - } - - var newPort = await MeadowConnectionManager.GetPortFromSerialNumber(serialNumber); - - if (!string.IsNullOrEmpty(newPort)) - { - Logger?.LogInformation($"Meadow found at {newPort}"); - - // configure the route to that port for the user - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); - - await WriteNonOSToDevice(Files, package, flashStatus); - - flashStatus[serialNumber] = FileWriteComplete; - } - - } - - Logger?.LogInformation($"{Environment.NewLine}Firmware Write Status:"); - foreach (var item in flashStatus) - { - var textColour = ExtensionMethods.ConsoleColourRed; - if (item.Value.Contains(FileWriteComplete)) { - textColour = ExtensionMethods.ConsoleColourGreen; - } - Logger?.LogInformation($"Serial Number: {item.Key} - {item.Value}".ColourConsoleText(textColour)); - } + Logger?.LogError($"OS file not found for version '{package.Version}'"); + return; } - else + if (await WriteOsWithDfu(osFile) == false) { - await WriteNonOSToDevice(Files, package, flashStatus); - - if (connection != null) - flashStatus[connection.Name] = FileWriteComplete; + return; } + //remove from collection to enable writing of other files - ToDo rework this logic + FirmwareFileTypes = FirmwareFileTypes.Where(t => t != FirmwareType.OS).ToArray(); } - } - private async Task WriteNonOSToDevice(FirmwareType[] files, FirmwarePackage? package, ConcurrentDictionary flashStatus) - { - // get the connection associated with that route + IMeadowConnection? connection = null; + connection = await GetCurrentConnection(); - try + if (connection == null || connection.Device == null) { - if (connection != null && files.Any(f => f != FirmwareType.OS)) - { - await connection.WaitForMeadowAttach(); + return; + } - if (CancellationToken.IsCancellationRequested) - { - return; - } + await WriteFiles(connection, FirmwareFileTypes); - flashStatus[connection.Name] = "WriteFiles"; - await WriteFiles(package, connection); - } - } - catch (Exception ex) - { - if (connection != null) - flashStatus[connection.Name] = ex.Message; - // Log the exception but move onto the next device - Logger?.LogError($"{Environment.NewLine}Exception type: {ex.GetType().Name}", ex); + await connection.ResetDevice(CancellationToken); + await connection.WaitForMeadowAttach(); - return; - } - finally + var deviceInfo = await connection.Device.GetDeviceInfo(CancellationToken); + + if (deviceInfo != null) { - // Needed to avoid double messages - DetachMessageHandlers(connection); + Logger?.LogInformation(deviceInfo.ToString()); } } - private void GetLibUsbDevicesInBootloaderModeForCurrentEnvironment() + private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() { - // Clear it out each - if (_libUsbDevices != null && _libUsbDevices.Length > 0) + if (_libUsbDevice == null) { - _libUsbDevices = Array.Empty(); - } - - ILibUsbProvider provider; + ILibUsbProvider provider; - // TODO: read the settings manager to decide which provider to use (default to non-classic) - var setting = Settings.GetAppSetting(SettingsManager.PublicSettings.LibUsb); - if (setting == "classic") - { - provider = new ClassicLibUsbProvider(); - } - else - { - provider = new LibUsbProvider(); - } + // TODO: read the settings manager to decide which provider to use (default to non-classic) + var setting = Settings.GetAppSetting(SettingsManager.PublicSettings.LibUsb); + if (setting == "classic") + { + provider = new ClassicLibUsbProvider(); + } + else + { + provider = new LibUsbProvider(); + } - var devices = provider.GetDevicesInBootloaderMode(); + var devices = provider.GetDevicesInBootloaderMode(); - switch (devices.Count) - { - case 0: - throw new Exception("No devices found in bootloader mode."); + _libUsbDevice = devices.Count switch + { + 0 => throw new Exception("No device found in bootloader mode"), + 1 => devices[0], + _ => throw new Exception("Multiple devices found in bootloader mode - only connect one device"), + }; } - _libUsbDevices = devices.ToArray(); + return _libUsbDevice; } private async Task GetSelectedPackage() @@ -264,7 +158,7 @@ private void GetLibUsbDevicesInBootloaderModeForCurrentEnvironment() if (existing == null) { - Logger?.LogError($"Requested version '{Version}' not found."); + Logger?.LogError($"Requested version '{Version}' not found"); return null; } package = existing; @@ -280,148 +174,175 @@ private void GetLibUsbDevicesInBootloaderModeForCurrentEnvironment() return package; } - private async ValueTask WriteFiles(FirmwarePackage? package, IMeadowConnection connection) + private async ValueTask WriteFiles(IMeadowConnection connection, FirmwareType[] firmwareFileTypes) { + // the connection passes messages back to us (info about actions happening on-device + connection.DeviceMessageReceived += (s, e) => + { + if (e.message.Contains("% downloaded")) + { // don't echo this, as we're already reporting % written + } + else + { + Logger?.LogInformation(e.message); + } + }; + connection.ConnectionMessage += (s, message) => + { + Logger?.LogInformation(message); + }; connection.FileWriteFailed += (s, e) => { - Logger?.LogError($"WriteFiles FAILED!!"); - // TODO _fileWriteError = true; + Logger?.LogError("Error writing file"); }; - if (Files != null - && connection.Device != null - && package != null) + var package = await GetSelectedPackage(); + + if (package == null) { - var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + Logger?.LogError($"Firware write failed - unable to find selected package"); + return; + } - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling device runtime..."); - await connection.Device.RuntimeDisable(); - } + var wasRuntimeEnabled = await connection!.Device!.IsRuntimeEnabled(CancellationToken); - connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; - if (p == 100.0) - { - Console?.Output.WriteAsync($"{Environment.NewLine}"); - } - else - { - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - } - }; + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling device runtime..."); + await connection.Device.RuntimeDisable(); + } - if (Files.Contains(FirmwareType.OS)) - { - if (UseDfu) - { - // this would have already happened before now (in ExecuteAsync) so ignore - } - else - { - Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; - throw new NotSupportedException("OtA writes for the OS are not yet supported"); - } - } + if (firmwareFileTypes.Contains(FirmwareType.OS)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - if (Files.Contains(FirmwareType.Runtime)) + throw new NotSupportedException("OtA writes for the OS are not yet supported"); + } + if (firmwareFileTypes.Contains(FirmwareType.Runtime)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + write_runtime: + if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) { - string? runtime; - if (!string.IsNullOrEmpty(Path)) - { - runtime = Path; - } - else - { - // get the path to the runtime bin file - runtime = package.Runtime; - } + Logger?.LogInformation($"Error writing runtime - retrying"); + goto write_runtime; + } + } - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {runtime}..."); + if (CancellationToken.IsCancellationRequested) + { + return; + } - if (string.IsNullOrEmpty(runtime)) - runtime = string.Empty; - var rtpath = package.GetFullyQualifiedPath(runtime); + if (FirmwareFileTypes != null && FirmwareFileTypes.Contains(FirmwareType.ESP)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); - write_runtime: - if (!await connection.Device.WriteRuntime(rtpath, CancellationToken)) + var fileList = new string[] { - Logger?.LogInformation($"Error writing runtime. Retrying."); - goto write_runtime; - } - } + package.GetFullyQualifiedPath(package.CoprocApplication), + package.GetFullyQualifiedPath(package.CoprocBootloader), + package.GetFullyQualifiedPath(package.CoprocPartitionTable), + }; + + await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); if (CancellationToken.IsCancellationRequested) { return; } + } - if (Files.Contains(FirmwareType.ESP)) - { - string? coProcessorFilePath; - if (!string.IsNullOrEmpty(Path)) - { - // use passed in path - coProcessorFilePath = Path; - } - else - { - // get the default path to the coprocessor bin file - coProcessorFilePath = package.CoprocApplication; - } + Logger?.LogInformation($"{Environment.NewLine}"); + + if (wasRuntimeEnabled) + { + await connection.Device.RuntimeEnable(CancellationToken); + } - Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor file {coProcessorFilePath}..."); + // TODO: if we're an F7 device, we need to reset + } - string[]? fileList = Array.Empty(); - if (coProcessorFilePath != null - && package.CoprocBootloader != null - && package.CoprocPartitionTable != null) - { - if (!string.IsNullOrEmpty(Path)) - { - fileList = new string[] - { - package.GetFullyQualifiedPath(coProcessorFilePath), - }; - } - else - { - fileList = new string[] - { - package.GetFullyQualifiedPath(coProcessorFilePath), - package.GetFullyQualifiedPath(package.CoprocBootloader), - package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - } - - await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); - - if (CancellationToken.IsCancellationRequested) - { - return; - } - } + private async Task WriteOsWithDfu(string osFile) + { + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - Logger?.LogInformation($"{Environment.NewLine}"); + // get the device's serial number via DFU - we'll need it to find the device after it resets + ILibUsbDevice libUsbDevice; + try + { + libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); + } + catch (Exception ex) + { + Logger?.LogError(ex.Message); + return false; + } - if (wasRuntimeEnabled) - { - await connection.Device.RuntimeEnable(CancellationToken); - } - // TODO: if we're an F7 device, we need to reset + string serialNumber; + try + { + serialNumber = libUsbDevice.GetDeviceSerialNumber(); + } + catch + { + Logger?.LogError("Firmware write failed - unable to read device serial number (make sure device is connected)"); + return false; } - } - private async Task WriteOsWithDfu(string osFile, string serialNumber) - { - await DfuUtils.FlashFile( + try + { + await DfuUtils.FlashFile( osFile, serialNumber, logger: Logger, format: DfuUtils.DfuFlashFormat.ConsoleOut); + } + catch (Exception ex) + { + Logger?.LogError($"Exception type: {ex.GetType().Name}"); + + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); + + Logger?.LogWarning("This machine requires an older version of LibUsb. The CLI settings have been updated, re-run the 'firmware write' command to update your device."); + return false; + } + + // now wait for a new serial port to appear + var ports = await MeadowConnectionManager.GetSerialPorts(); + var retryCount = 0; + + var newPort = ports.Except(initialPorts).FirstOrDefault(); + + while (newPort == null) + { + if (retryCount++ > 10) + { + throw new Exception("New meadow device not found"); + } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + newPort = ports.Except(initialPorts).FirstOrDefault(); + } + + Logger?.LogInformation($"Meadow found at {newPort}"); + + // configure the route to that port for the user + Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + return true; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs index c6b9433c..ad32bb10 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Flash/FlashEraseCommand.cs @@ -3,26 +3,29 @@ namespace Meadow.CLI.Commands.DeviceManagement; -[Command("flash erase", Description = "Erases the device's flash storage")] +[Command("flash erase", Description = "Erase the contents of the device flash storage")] public class FlashEraseCommand : BaseDeviceCommand { public FlashEraseCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - Logger?.LogInformation($"Erasing flash..."); - - await Connection.Device.EraseFlash(CancellationToken); - } + return; } + + Logger?.LogInformation($"Erasing flash..."); + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + await connection.Device.EraseFlash(CancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs index b0a6d097..d1d3e559 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/ListenCommand.cs @@ -6,63 +6,47 @@ 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; } + [CommandOption("no-prefix", 'n', Description = "When set, the message source prefix (e.g. 'stdout>') is suppressed", IsRequired = false)] + public bool NoPrefix { get; init; } public ListenCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } private void Connection_ConnectionMessage(object? sender, string e) { + //ToDo } private void OnDeviceMessageReceived(object? sender, (string message, string? source) e) { - string textColour; - switch (e.source) - { - case "stdout": - textColour = ExtensionMethods.ConsoleColourBlue; - break; - case "info": - textColour = ExtensionMethods.ConsoleColourGreen; - break; - case "stderr": - textColour = ExtensionMethods.ConsoleColourRed; - break; - default: - textColour = ExtensionMethods.ConsoleColourReset; - break; - } - if (NoPrefix) { - Logger?.LogInformation($"{e.message.TrimEnd('\n', '\r').ColourConsoleText(textColour)}"); + Logger?.LogInformation($"{e.message.TrimEnd('\n', '\r')}"); } else { - - Logger?.LogInformation($"{e.source?.ColourConsoleText(textColour)}> {e.message.TrimEnd('\n', '\r')}"); + Logger?.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}"); } } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null) { - Connection.DeviceMessageReceived += OnDeviceMessageReceived; - Connection.ConnectionMessage += Connection_ConnectionMessage; + return; + } - Logger?.LogInformation($"Listening for Meadow Console output on '{Connection.Name}'. Press Ctrl+C to exit..."); + connection.DeviceMessageReceived += OnDeviceMessageReceived; + connection.ConnectionMessage += Connection_ConnectionMessage; - while (!CancellationToken.IsCancellationRequested) - { - await Task.Delay(1000); - } + Logger?.LogInformation($"Listening for Meadow Console output on '{connection.Name}'. Press Ctrl+C to exit..."); + + while (!CancellationToken.IsCancellationRequested) + { + await Task.Delay(1000); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs index fac6d79a..3a89bb2d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortListCommand.cs @@ -6,23 +6,25 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("port list", Description = "List available local serial ports")] public class PortListCommand : BaseCommand { - public IList? Portlist; + public IList? Portlist; public PortListCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { Portlist = await MeadowConnectionManager.GetSerialPorts(); + if (Portlist.Count > 0) { var plural = Portlist.Count > 1 ? "s" : string.Empty; - Logger?.LogInformation($"Found the following device{plural}:"); + + Logger?.LogInformation($"Found device{plural} on port{plural}:"); + for (int i = 0; i < Portlist.Count; i++) { - Logger?.LogInformation($" {i + 1}: {Portlist[i].Name}"); + Logger?.LogInformation($" {i + 1}: {Portlist[i]}"); } } else diff --git a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs index d324d8e4..bf740db6 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Port/PortSelectCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -9,40 +8,36 @@ public class PortSelectCommand : BaseCommand { public PortSelectCommand(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - if (LoggerFactory != null) + if (LoggerFactory != null && Console != null) { - if (Console != null) - { - var portListCommand = new PortListCommand(LoggerFactory); + var portListCommand = new PortListCommand(LoggerFactory); - await portListCommand.ExecuteAsync(Console); + await portListCommand.ExecuteAsync(Console); - if (portListCommand.Portlist?.Count > 0) + if (portListCommand.Portlist?.Count > 0) + { + if (portListCommand.Portlist?.Count > 1) { - if (portListCommand.Portlist?.Count > 1) - { - Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); + Logger?.LogInformation($"{Environment.NewLine}Type the number of the port you would like to use.{Environment.NewLine}or just press Enter to keep your current port."); - byte deviceSelected; - if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + byte deviceSelected; + if (byte.TryParse(await Console.Input.ReadLineAsync(), out deviceSelected)) + { + if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) { - if (deviceSelected > 0 && deviceSelected <= portListCommand.Portlist?.Count) - { - await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1].Name!); - } + await CallConfigCommand(portListCommand.Portlist[deviceSelected - 1]); } } - else - { - // Only 1 device attached, let's auto select it - if (portListCommand.Portlist != null) - await CallConfigCommand(portListCommand.Portlist[0].Name!); - } + } + else + { + // Only 1 device attached, let's auto select it + if (portListCommand.Portlist != null) + await CallConfigCommand(portListCommand.Portlist[0]); } } } diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs index a3d1074d..5a098164 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeDisableCommand.cs @@ -8,28 +8,23 @@ public class RuntimeDisableCommand : BaseDeviceCommand { public RuntimeDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Disabling runtime..."); - - await Connection.Device.RuntimeDisable(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Failed to disable runtime."); - } - } + return; } + + Logger?.LogInformation($"Disabling runtime..."); + + await connection.Device.RuntimeDisable(CancellationToken); + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs index c073c96b..bc955c0c 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeEnableCommand.cs @@ -8,28 +8,23 @@ public class RuntimeEnableCommand : BaseDeviceCommand { public RuntimeEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Enabling runtime..."); - - await Connection.Device.RuntimeEnable(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Failed to enable runtime."); - } - } + return; } + + Logger?.LogInformation($"Enabling runtime..."); + + await connection.Device.RuntimeEnable(CancellationToken); + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs index 6ae4095b..b29b9170 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Runtime/RuntimeStateCommand.cs @@ -8,28 +8,21 @@ public class RuntimeStateCommand : BaseDeviceCommand { public RuntimeStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null) + if (connection == null || connection.Device == null) { - if (Connection.Device != null) - { - try - { - Logger?.LogInformation($"Querying runtime state..."); - - await Connection.Device.IsRuntimeEnabled(CancellationToken); - } - catch (Exception ex) - { - Logger?.LogError(ex, $"Unable to determine the runtime state."); - } - } + return; } + + Logger?.LogInformation($"Querying runtime state..."); + + var state = await connection.Device.IsRuntimeEnabled(CancellationToken); + + Logger?.LogInformation($"Runtime is {(state ? "ENABLED" : "DISABLED")}"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs deleted file mode 100644 index 8bf759a6..00000000 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/BaseTraceCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Meadow.Hcom; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -public abstract class BaseTraceCommand : BaseDeviceCommand -{ - public BaseTraceCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) - : base(connectionManager, loggerFactory) - { - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs index 7958a294..e013fc59 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceDisableCommand.cs @@ -4,22 +4,28 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("trace disable", Description = "Disable trace logging on the Meadow")] -public class TraceDisableCommand : BaseTraceCommand +public class TraceDisableCommand : BaseDeviceCommand { public TraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); - if (Connection != null && Connection.Device != null) + if (connection == null || connection.Device == null) { - Logger?.LogInformation("Disabling tracing..."); - - await Connection.Device.TraceDisable(CancellationToken); + return; } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + Logger?.LogInformation("Disabling tracing..."); + + await connection.Device.TraceDisable(CancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs index 98c70969..fb107fe4 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceEnableCommand.cs @@ -1,42 +1,40 @@ -using System.Reflection.Emit; -using CliFx.Attributes; -using Microsoft.Extensions.Logging; - -namespace Meadow.CLI.Commands.DeviceManagement; - -[Command("trace enable", Description = "Enable trace logging on the Meadow")] -public class TraceEnableCommand : BaseTraceCommand -{ - [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() - { - await base.ExecuteCommand(); - - if (Connection != null) - { - if (Connection.Device != null) - { - if (Level != null) - { - Logger?.LogInformation($"Setting trace level to {Level}..."); - await Connection.Device.SetTraceLevel(Level.Value, CancellationToken); - } - - Logger?.LogInformation("Enabling tracing..."); - - await Connection.Device.TraceEnable(CancellationToken); - } - else - { - Logger?.LogError("Trace Error: No Device found..."); - } - } - } +using CliFx.Attributes; +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() + { + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; + + if (Level != null) + { + Logger?.LogInformation($"Setting trace level to {Level}..."); + await connection.Device.SetTraceLevel(Level.Value, CancellationToken); + } + + Logger?.LogInformation("Enabling tracing..."); + + await connection.Device.TraceEnable(CancellationToken); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs index ca711360..df7e984d 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Trace/TraceLevelCommand.cs @@ -4,36 +4,42 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("trace level", Description = "Sets the trace logging level on the Meadow")] -public class TraceLevelCommand : BaseTraceCommand +public class TraceLevelCommand : BaseDeviceCommand { [CommandParameter(0, Name = "Level", IsRequired = true)] - public int Level { get; set; } + public int Level { get; init; } public TraceLevelCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } - if (Connection != null && Connection.Device != null) + connection.DeviceMessageReceived += (s, e) => { - if (Level <= 0) - { - Logger?.LogInformation("Disabling tracing..."); - - await Connection.Device.TraceDisable(CancellationToken); - } - else - { - Logger?.LogInformation($"Setting trace level to {Level}..."); - await Connection.Device.SetTraceLevel(Level, CancellationToken); - - Logger?.LogInformation("Enabling tracing..."); - await Connection.Device.TraceEnable(CancellationToken); - } + Logger?.LogInformation(e.message); + }; + + if (Level <= 0) + { + Logger?.LogInformation("Disabling tracing..."); + + await connection.Device.SetTraceLevel(Level, CancellationToken); + } + else + { + Logger?.LogInformation($"Setting trace level to {Level}..."); + await connection.Device.SetTraceLevel(Level, CancellationToken); + + Logger?.LogInformation("Enabling tracing..."); + await connection.Device.TraceEnable(CancellationToken); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs index 139ed92a..4bc77908 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceDisableCommand.cs @@ -4,20 +4,28 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("uart trace disable", Description = "Disables trace log output to UART")] -public class UartTraceDisableCommand : BaseTraceCommand +public class UartTraceDisableCommand : BaseDeviceCommand { public UartTraceDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; Logger?.LogInformation("Setting UART to application use..."); - if (Connection != null && Connection.Device != null) - await Connection.Device.UartTraceDisable(CancellationToken); + await connection.Device.UartTraceDisable(CancellationToken); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs index 587362d6..833bdb48 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/Uart/UartTraceEnableCommand.cs @@ -4,20 +4,28 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("uart trace enable", Description = "Enables trace log output to UART")] -public class UartTraceEnableCommand : BaseTraceCommand +public class UartTraceEnableCommand : BaseDeviceCommand { public UartTraceEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) - { - } + { } protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } + + connection.DeviceMessageReceived += (s, e) => + { + Logger?.LogInformation(e.message); + }; Logger?.LogInformation("Setting UART to output trace messages..."); - if (Connection != null && Connection.Device != null) - await Connection.Device.UartTraceEnable(CancellationToken); + await connection.Device.UartTraceEnable(CancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs index dd27dbc4..a3ea05cd 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/DownloadOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.Software; using Microsoft.Extensions.Logging; @@ -11,6 +10,6 @@ public class DownloadOsCommand : FirmwareDownloadCommand public DownloadOsCommand(FileManager fileManager, ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(fileManager, settingsManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware download` instead"); + Logger?.LogWarning($"Deprecated command - use `firmware download` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs index bd8cb1f5..bc7981d5 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/FlashOsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Meadow.CLI.Core.Internals.Dfu; using Meadow.LibUsb; using Meadow.Software; @@ -11,10 +10,10 @@ namespace Meadow.CLI.Commands.DeviceManagement; public class FlashOsCommand : BaseDeviceCommand { [CommandOption("osFile", 'o', Description = "Path to the Meadow OS binary")] - public string? OSFile { get; init; } + public string OSFile { get; init; } = default!; [CommandOption("runtimeFile", 'r', Description = "Path to the Meadow Runtime binary")] - public string? RuntimeFile { get; init; } + public string RuntimeFile { get; init; } = default!; [CommandOption("skipDfu", 'd', Description = "Skip DFU flash")] public bool SkipOS { get; init; } @@ -23,13 +22,13 @@ public class FlashOsCommand : BaseDeviceCommand public bool SkipEsp { get; init; } [CommandOption("skipRuntime", 'k', Description = "Skip updating the runtime")] - public bool SkipRuntime { get; init; } + public bool SkipRuntime { get; init; } = default!; [CommandOption("dontPrompt", 'p', Description = "Don't show bulk erase prompt")] public bool DontPrompt { get; init; } [CommandOption("osVersion", 'v', Description = "Flash a specific downloaded OS version - x.x.x.x")] - public string? Version { get; private set; } + public string Version { get; private set; } = default!; private FirmwareType[]? Files { get; set; } = default!; private bool UseDfu = true; @@ -39,10 +38,13 @@ public class FlashOsCommand : BaseDeviceCommand private ILibUsbDevice? _libUsbDevice; - public FlashOsCommand(ISettingsManager settingsManager, FileManager fileManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) + public FlashOsCommand(ISettingsManager settingsManager, + FileManager fileManager, + MeadowConnectionManager connectionManager, + ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); + Logger?.LogWarning($"Deprecated command. Use `firmware write` instead"); FileManager = fileManager; Settings = settingsManager; @@ -50,135 +52,132 @@ public FlashOsCommand(ISettingsManager settingsManager, FileManager fileManager, protected override async ValueTask ExecuteCommand() { - await base.ExecuteCommand(); + var package = await GetSelectedPackage(); - if (Connection != null) + if (package == null) { - var package = await GetSelectedPackage(); + Logger?.LogError($"Unable to get selected OS package"); + return; + } - var files = new List(); - if (!SkipOS) files.Add(FirmwareType.OS); - if (!SkipEsp) files.Add(FirmwareType.ESP); - if (!SkipRuntime) files.Add(FirmwareType.Runtime); - Files = files.ToArray(); + var files = new List(); + if (!SkipOS) files.Add(FirmwareType.OS); + if (!SkipEsp) files.Add(FirmwareType.ESP); + if (!SkipRuntime) files.Add(FirmwareType.Runtime); + Files = files.ToArray(); - if (Files == null && package != null) - { - Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); + if (Files == null) + { + Logger?.LogInformation($"Writing all firmware for version '{package.Version}'..."); - Files = new FirmwareType[] - { + Files = new FirmwareType[] + { FirmwareType.OS, FirmwareType.Runtime, FirmwareType.ESP - }; + }; + } + + if (!Files.Contains(FirmwareType.OS) && UseDfu) + { + Logger?.LogError($"DFU is only used for OS files - select an OS file or remove the DFU option"); + return; + } + + bool deviceSupportsOta = false; // TODO: get this based on device OS version + + if (package.OsWithoutBootloader == null + || !deviceSupportsOta + || UseDfu) + { + UseDfu = true; + } + + + if (UseDfu && Files.Contains(FirmwareType.OS)) + { + // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) + var initialPorts = await MeadowConnectionManager.GetSerialPorts(); + + // get the device's serial number via DFU - we'll need it to find the device after it resets + try + { + _libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); } + catch (Exception ex) + { + Logger?.LogError(ex.Message); + return; + } + + var serial = _libUsbDevice.GetDeviceSerialNumber(); - if (Files != null) + // no connection is required here - in fact one won't exist + // unless maybe we add a "DFUConnection"? + + try { - 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; - } + await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); + } + catch (Exception ex) + { + Logger?.LogError($"Exception type: {ex.GetType().Name}"); - bool deviceSupportsOta = false; // TODO: get this based on device OS version + // TODO: scope this to the right exception type for Win 10 access violation thing + // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" + Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - if (package != null && package.OsWithoutBootloader == null - || !deviceSupportsOta - || UseDfu) - { - UseDfu = true; - } + Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); + return; + } - if (UseDfu && Files.Contains(FirmwareType.OS)) - { - // get a list of ports - it will not have our meadow in it (since it should be in DFU mode) - var initialPorts = await MeadowConnectionManager.GetSerialPorts(); - - // get the device's serial number via DFU - we'll need it to find the device after it resets - try - { - _libUsbDevice = GetLibUsbDeviceForCurrentEnvironment(); - } - catch (Exception ex) - { - Logger?.LogError(ex.Message); - return; - } - - var serial = _libUsbDevice.GetDeviceSerialNumber(); - - // no connection is required here - in fact one won't exist - // unless maybe we add a "DFUConnection"? - - try - { - if (package != null && package.OSWithBootloader != null) - { - await WriteOsWithDfu(package.GetFullyQualifiedPath(package.OSWithBootloader), serial); - } - } - catch (Exception ex) - { - Logger?.LogError($"Exception type: {ex.GetType().Name}"); - - // TODO: scope this to the right exception type for Win 10 access violation thing - // TODO: catch the Win10 DFU error here and change the global provider configuration to "classic" - Settings.SaveSetting(SettingsManager.PublicSettings.LibUsb, "classic"); - - Logger?.LogWarning("This machine requires an older version of libusb. Not to worry, I'll make the change for you, but you will have to re-run this 'firmware write' command."); - return; - } - - // now wait for a new serial port to appear - var ports = await MeadowConnectionManager.GetSerialPorts(); - var retryCount = 0; - - var newPort = ports.Except(initialPorts).FirstOrDefault(); - while (newPort == null) - { - if (retryCount++ > 10) - { - throw new Exception("New meadow device not found"); - } - await Task.Delay(500); - ports = await MeadowConnectionManager.GetSerialPorts(); - newPort = ports.Except(initialPorts).FirstOrDefault(); - } - - // configure the route to that port for the user - if (newPort != null) - { - Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort.Name!); - - var cancellationToken = Console?.RegisterCancellationHandler(); - - if (Files.Any(f => f != FirmwareType.OS)) - { - await Connection.WaitForMeadowAttach(); - - await WriteFiles(); - } - - if (Connection.Device != null) - { - var deviceInfo = await Connection.Device.GetDeviceInfo(cancellationToken); - - if (deviceInfo != null) - { - Logger?.LogInformation($"Done."); - Logger?.LogInformation(deviceInfo.ToString()); - } - } - } - } - else + // now wait for a new serial port to appear + var ports = await MeadowConnectionManager.GetSerialPorts(); + var retryCount = 0; + + var newPort = ports.Except(initialPorts).FirstOrDefault(); + while (newPort == null) + { + if (retryCount++ > 10) { - await WriteFiles(); + throw new Exception("New meadow device not found"); } + await Task.Delay(500); + ports = await MeadowConnectionManager.GetSerialPorts(); + newPort = ports.Except(initialPorts).FirstOrDefault(); + } + + // configure the route to that port for the user + Settings.SaveSetting(SettingsManager.PublicSettings.Route, newPort); + + var connection = ConnectionManager.GetCurrentConnection(); + + if (connection == null || connection.Device == null) + { + return; + } + + var cancellationToken = Console?.RegisterCancellationHandler(); + + if (Files.Any(f => f != FirmwareType.OS)) + { + await connection.WaitForMeadowAttach(); + + await WriteFiles(); + } + + var deviceInfo = await connection.Device.GetDeviceInfo(cancellationToken); + + if (deviceInfo != null) + { + Logger?.LogInformation($"Done."); + Logger?.LogInformation(deviceInfo.ToString()); } } + else + { + await WriteFiles(); + } } private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() @@ -205,7 +204,7 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() case 1: return devices[0]; default: - throw new Exception("Multiple devices found in bootloader mode. Disconnect all but one"); + throw new Exception("Multiple devices found in bootloader mode - only connect one device"); } } @@ -241,98 +240,97 @@ private ILibUsbDevice GetLibUsbDeviceForCurrentEnvironment() private async ValueTask WriteFiles() { - if (Connection != null) + var connection = await GetCurrentConnection(); + + if (connection == null || connection.Device == null) { - var package = await GetSelectedPackage(); + return; + } - if (Connection.Device != null - && package != null) + // the connection passes messages back to us (info about actions happening on-device + connection.DeviceMessageReceived += (s, e) => + { + if (e.message.Contains("% downloaded")) + { + // don't echo this, as we're already reporting % written + } + else { - var wasRuntimeEnabled = await Connection.Device.IsRuntimeEnabled(CancellationToken); + Logger?.LogInformation(e.message); + } + }; + connection.ConnectionMessage += (s, message) => + { + Logger?.LogInformation(message); + }; - if (wasRuntimeEnabled) - { - Logger?.LogInformation("Disabling device runtime..."); - await Connection.Device.RuntimeDisable(); - } - Connection.FileWriteProgress += (s, e) => - { - var p = (e.completed / (double)e.total) * 100d; - Console?.Output.WriteAsync($"Writing {e.fileName}: {p:0}% \r"); - }; + var pack = await GetSelectedPackage(); + if (pack == null) + { + Logger?.LogError($"Unable to get selected OS package"); + } + FirmwarePackage package = pack!; - if (Files != null) + var wasRuntimeEnabled = await connection.Device.IsRuntimeEnabled(CancellationToken); + + if (wasRuntimeEnabled) + { + Logger?.LogInformation("Disabling device runtime..."); + await connection.Device.RuntimeDisable(); + } + + connection.FileWriteProgress += (s, e) => + { + var p = (e.completed / (double)e.total) * 100d; + Console?.Output.Write($"Writing {e.fileName}: {p:0}% \r"); + }; + + if (Files!.Contains(FirmwareType.OS)) + { + if (UseDfu) + { + // this would have already happened before now (in ExecuteAsync) so ignore + } + else + { + Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); + + throw new NotSupportedException("OtA writes for the OS are not yet supported"); + } + } + if (Files!.Contains(FirmwareType.Runtime)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); + + // get the path to the runtime file + var rtpath = package.GetFullyQualifiedPath(package.Runtime); + + // TODO: for serial, we must wait for the flash to complete + + await connection.Device.WriteRuntime(rtpath, CancellationToken); + } + if (Files!.Contains(FirmwareType.ESP)) + { + Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); + + var fileList = new string[] { - if (Files.Contains(FirmwareType.OS)) - { - if (UseDfu) - { - // this would have already happened before now (in ExecuteAsync) so ignore - } - else - { - Logger?.LogInformation($"{Environment.NewLine}Writing OS {package.Version}..."); - - throw new NotSupportedException("OtA writes for the OS are not yet supported"); - } - } - - if (Files.Contains(FirmwareType.Runtime)) - { - Logger?.LogInformation($"{Environment.NewLine}Writing Runtime {package.Version}..."); - - // get the path to the runtime file - var runtime = package.Runtime; - if (string.IsNullOrEmpty(runtime)) - runtime = string.Empty; - var rtpath = package.GetFullyQualifiedPath(runtime); - - write_runtime: - if (!await Connection.Device.WriteRuntime(rtpath, CancellationToken)) - { - Logger?.LogInformation($"Error writing runtime. Retrying."); - goto write_runtime; - } - } - - - if (Files.Contains(FirmwareType.ESP)) - { - Logger?.LogInformation($"{Environment.NewLine}Writing Coprocessor files..."); - - string[]? fileList; - if (package.CoprocApplication != null - && package.CoprocBootloader != null - && package.CoprocPartitionTable != null) - { - fileList = new string[] - { package.GetFullyQualifiedPath(package.CoprocApplication), package.GetFullyQualifiedPath(package.CoprocBootloader), package.GetFullyQualifiedPath(package.CoprocPartitionTable), - }; - } - else - { - fileList = Array.Empty(); - } - - await Connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); - } - - Logger?.LogInformation($"{Environment.NewLine}"); - - if (wasRuntimeEnabled) - { - await Connection.Device.RuntimeEnable(); - } - } + }; - // TODO: if we're an F7 device, we need to reset - } + await connection.Device.WriteCoprocessorFiles(fileList, CancellationToken); } + + if (wasRuntimeEnabled) + { + await connection.Device.RuntimeEnable(); + } + + // TODO: if we're an F7 device, we need to reset } private async Task WriteOsWithDfu(string osFile, string serialNumber) diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs index 97613771..a28bb0c3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/InstallDfuCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,7 +9,6 @@ public class InstallDfuCommand : DfuInstallCommand public InstallDfuCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(settingsManager, loggerFactory, "0.11") { - Logger?.LogWarning($"Deprecated command. Use `dfu install` instead"); + Logger?.LogWarning($"Deprecated command - use `dfu install` instead"); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs index 696c0b44..d99402d2 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/ListPortsCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -10,12 +9,11 @@ public class ListPortsCommand : PortListCommand public ListPortsCommand(ISettingsManager settingsManager, ILoggerFactory loggerFactory) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `port list` instead"); + Logger?.LogWarning($"Deprecated command - use `port list` instead"); } protected override ValueTask ExecuteCommand() { return base.ExecuteCommand(); } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs index 0a0807fe..8d9e63bf 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoDisableCommand.cs @@ -9,6 +9,6 @@ public class MonoDisableCommand : RuntimeDisableCommand public MonoDisableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime disable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime disable` instead"); } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs index d17c8c99..d19bafe9 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoEnableCommand.cs @@ -9,6 +9,6 @@ public class MonoEnableCommand : RuntimeEnableCommand public MonoEnableCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime enable` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime enable` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs index 9514b87e..4fd2a7d3 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/MonoStateCommand.cs @@ -9,6 +9,6 @@ public class MonoStateCommand : RuntimeStateCommand public MonoStateCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory) : base(connectionManager, loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `runtime state` instead"); + Logger?.LogWarning($"Deprecated command - use `runtime state` instead"); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs index 87e4d06e..9413c036 100644 --- a/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Legacy/UsePortCommand.cs @@ -1,5 +1,4 @@ using CliFx.Attributes; -using Meadow.CLI; using Microsoft.Extensions.Logging; namespace Meadow.CLI.Commands.DeviceManagement; @@ -7,7 +6,7 @@ namespace Meadow.CLI.Commands.DeviceManagement; [Command("use port", Description = "** Deprecated ** Use `config route` instead")] public class UsePortCommand : BaseCommand { - private ISettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; [CommandParameter(0, Name = "Port", IsRequired = true)] public string Port { get; set; } = default!; @@ -15,7 +14,7 @@ public class UsePortCommand : BaseCommand public UsePortCommand(ILoggerFactory loggerFactory, ISettingsManager settingsManager) : base(loggerFactory) { - Logger?.LogWarning($"Deprecated command. Use `config route` instead"); + Logger?.LogWarning($"Deprecated command -use `config route` instead"); _settingsManager = settingsManager; } @@ -26,5 +25,4 @@ protected override ValueTask ExecuteCommand() return ValueTask.CompletedTask; } -} - +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/DFU/DfuContext.cs b/Source/v2/Meadow.Cli/DFU/DfuContext.cs deleted file mode 100644 index c42c2ff7..00000000 --- a/Source/v2/Meadow.Cli/DFU/DfuContext.cs +++ /dev/null @@ -1,57 +0,0 @@ -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() - { - if (_context != null) - return _context.GetDfuDevices(validVendorIDs); - else - return null; - } - - public bool HasCapability(Capabilities caps) - { - if (_context != null) - return _context.HasCapability(caps); - else - return false; - } - - public void BeginListeningForHotplugEvents() - { - if (_context != null) - _context.BeginListeningForHotplugEvents(); - } - - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs index 04e6e8fe..0e392ddd 100644 --- a/Source/v2/Meadow.Cli/MeadowConnectionManager.cs +++ b/Source/v2/Meadow.Cli/MeadowConnectionManager.cs @@ -1,22 +1,18 @@ -using Meadow.CLI; -using Meadow.Hcom; -using Meadow.LibUsb; +using Meadow.Hcom; using System.Diagnostics; using System.IO.Ports; -using System.Linq; using System.Management; using System.Net; using System.Runtime.InteropServices; -using System.Runtime.Versioning; namespace Meadow.CLI.Commands.DeviceManagement; public class MeadowConnectionManager { public const string WILDERNESS_LABS_USB_VID = "2E6A"; - private static object _lockObject = new(); + private static readonly object _lockObject = new(); - private ISettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; private IMeadowConnection? _currentConnection; public MeadowConnectionManager(ISettingsManager settingsManager) @@ -37,56 +33,49 @@ public MeadowConnectionManager(ISettingsManager settingsManager) if (_currentConnection != null) return _currentConnection; // try to determine what the route is - if (route == "local") + string? uri = null; + if (route.StartsWith("http")) { - _currentConnection = new LocalConnection(); + uri = route; + } + else if (IPAddress.TryParse(route, out var ipAddress)) + { + uri = $"http://{route}:5000"; + } + else if (IPEndPoint.TryParse(route, out var endpoint)) + { + uri = $"http://{route}"; + } + + if (uri != null) + { + _currentConnection = new TcpConnection(uri); } else { - 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}"; - } + var retryCount = 0; - if (uri != null) + get_serial_connection: + try { - _currentConnection = new TcpConnection(uri); + _currentConnection = new SerialConnection(route); } - else + catch { - var retryCount = 0; - - get_serial_connection: - try - { - _currentConnection = new SerialConnection(route); - } - catch + retryCount++; + if (retryCount > 10) { - retryCount++; - if (retryCount > 10) - { - throw new Exception($"Cannot find port {route}"); - } - Thread.Sleep(500); - goto get_serial_connection; + throw new Exception($"Cannot find port {route}"); } + Thread.Sleep(500); + goto get_serial_connection; } } return _currentConnection; } - public static async Task> GetSerialPorts() + public static async Task> GetSerialPorts() { try { @@ -117,14 +106,14 @@ public static async Task> GetSerialPorts() } } - public static async Task> GetMeadowSerialPortsForOsx() + 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 ports = new List(); var psi = new ProcessStartInfo { @@ -166,10 +155,8 @@ public static async Task> GetMeadowSerialPortsForOsx() int startIndex = line.IndexOf("/"); int endIndex = line.IndexOf("\"", startIndex + 1); var port = line.Substring(startIndex, endIndex - startIndex); - int serialNumberIndex = line.IndexOf("tty.usbmodem") + 12; - var serialNumber = line.Substring(serialNumberIndex, endIndex - serialNumberIndex); - ports.Add(new MeadowSerialPort { Name = port, SerialNumber = serialNumber }); + ports.Add(port); foundMeadow = false; } } @@ -178,10 +165,12 @@ public static async Task> GetMeadowSerialPortsForOsx() }); } - public static async Task> GetMeadowSerialPortsForLinux() + 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(() => { @@ -196,37 +185,22 @@ public static async Task> GetMeadowSerialPortsForLinux() using var proc = Process.Start(psi); _ = proc?.WaitForExit(1000); - var output = proc?.StandardOutput; - - if (output != null) - { - var outputText = output.ReadToEnd(); - if (!string.IsNullOrEmpty(outputText)) - { - - return outputText - .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)); - int serialNumberIndex = line.IndexOf("ttyACM") + 6; - var serialNumber = line.Substring(serialNumberIndex); - - return new MeadowSerialPort { Name = port, SerialNumber = serialNumber }; - }).ToArray(); - } - } - - return Array.Empty(); + 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(); }); } - [SupportedOSPlatform("windows")] - public static IList GetMeadowSerialPortsForWindows() + public static IList GetMeadowSerialPortsForWindows() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) throw new PlatformNotSupportedException("This method is only supported on Windows"); @@ -244,7 +218,7 @@ public static IList GetMeadowSerialPortsForWindows() // 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(); + List results = new(); // build the searcher for the query using ManagementObjectSearcher searcher = new(wmiScope, query); @@ -252,42 +226,31 @@ public static IList GetMeadowSerialPortsForWindows() // get the query results foreach (ManagementObject moResult in searcher.Get()) { - // Try Caption 1st, then Name, they both seem to contain the COM port - var captionObject = moResult["Caption"]; - - var portLongName = captionObject?.ToString(); + // Try Caption and if not Name, they both seems to contain the COM port + string portLongName = $"{moResult["Caption"]}"; if (string.IsNullOrEmpty(portLongName)) { - var nameObject = moResult["Name"]; - portLongName = nameObject?.ToString(); + portLongName = $"{moResult["Name"]}"; } + string pnpDeviceId = $"{moResult["PNPDeviceID"]}"; // 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(); - if (!string.IsNullOrEmpty(portLongName)) - { - var comIndex = portLongName.IndexOf("(COM") + 1; - var copyLength = portLongName.IndexOf(")") - comIndex; - var port = portLongName.Substring(comIndex, copyLength); - - var pnpDeviceObject = moResult["PNPDeviceID"]; - var pnpDeviceId = pnpDeviceObject?.ToString(); - - // 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. - string? serialNumber = string.Empty; - if (!string.IsNullOrEmpty(pnpDeviceId)) - { - var splits = pnpDeviceId.Split('\\'); - serialNumber = splits[2]; - } - - results.Add(new MeadowSerialPort { Name = port, SerialNumber = serialNumber }); - } + 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(); @@ -300,53 +263,7 @@ public static IList GetMeadowSerialPortsForWindows() //hack to skip COM1 ports = ports.Where((source, index) => source != "COM1").Distinct().ToArray(); - List results = new(); - foreach (var port in ports) - { - results.Add(new MeadowSerialPort { Name = port, SerialNumber = string.Empty }); - } - - return results; - } - } - - public static async Task GetPortFromSerialNumber(string serialNumber) - { - var retryCount = 0; - - string? newPort = null; - - // now wait for the serial port with the passed in serialNumber to appear - while (newPort == null) - { - var ports = await GetSerialPorts(); - - if (ports != null) - { - foreach (var meadowSerialPort in ports) - { - if (meadowSerialPort.SerialNumber != null && meadowSerialPort.SerialNumber.Contains(serialNumber)) - { - newPort = meadowSerialPort.Name; - break; - } - } - - if (retryCount++ > 12) - { - throw new Exception("Meadow device not found"); - } - } - - await Task.Delay(500); + return ports; } - - return newPort; } -} - -public class MeadowSerialPort -{ - public string? Name { get; set; } - public string? SerialNumber { get; set; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs deleted file mode 100644 index f5f7c8e8..00000000 --- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs +++ /dev/null @@ -1,292 +0,0 @@ -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 const string IL_LINKER_DIR = "lib"; - public const string PostLinkDirectoryName = "postlink_bin"; - public const string PreLinkDirectoryName = "prelink_bin"; - public const string PackageOutputDirectoryName = "mpak"; - - - private readonly List dependencyMap = new(); - - private string? _meadowAssembliesPath; - - public 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"]; - if (store != null) - { - store.Refresh(); - - if (RuntimeVersion == null) - { - if (store.DefaultPackage != null) - { - var defaultPackage = store.DefaultPackage; - - if (defaultPackage.BclFolder != null) - { - _meadowAssembliesPath = defaultPackage.GetFullyQualifiedPath(defaultPackage.BclFolder); - } - } - } - else - { - var existing = store.FirstOrDefault(p => p.Version == RuntimeVersion); - - if (existing == null || existing.BclFolder == null) return null; - - _meadowAssembliesPath = existing.GetFullyQualifiedPath(existing.BclFolder); - } - } - } - - return _meadowAssembliesPath; - } - } - - public List? AssemblyDependencies { get; set; } - - public IEnumerable? TrimmedDependencies { get; set; } - public bool Trimmed { get; set; } = false; - - public string? RuntimeVersion { get; set; } - - public async Task?> TrimDependencies(FileInfo file, List dependencies, IList? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null) - { - var directoryName = file.DirectoryName; - if (!string.IsNullOrEmpty(directoryName)) - { - var fileName = file.Name; - var prelink_dir = Path.Combine(directoryName, PreLinkDirectoryName); - var prelink_app = Path.Combine(prelink_dir, fileName); - 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(directoryName, PostLinkDirectoryName); - if (Directory.Exists(postlink_dir)) - { - Directory.Delete(postlink_dir, recursive: true); - } - Directory.CreateDirectory(postlink_dir); - - var base_path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (!string.IsNullOrEmpty(base_path)) - { - 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 options want just the assembly name (i.e. no ".dll" extension) - no_link_args = string.Join(" ", noLink.Select(o => $"-p copy \"{o.Replace(".dll", string.Empty)}\"")); - } - - 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}\""; - - logger?.LogInformation($"Trimming assemblies associated with {fileName} to reduce upload size (this may take a few seconds)..."); - if (!string.IsNullOrWhiteSpace(no_link_args)) - { - logger?.LogInformation($"no-link args:'{no_link_args}'"); - } - - 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) - { - logger?.LogInformation("StandardOutput Contains: " + stdOutReaderResult); - } - - } - - string stdErrorReaderResult; - using (StreamReader stdErrorReader = process.StandardError) - { - stdErrorReaderResult = await stdErrorReader.ReadToEndAsync(); - if (!string.IsNullOrEmpty(stdErrorReaderResult)) - { - logger?.LogInformation("StandardError Contains: " + stdErrorReaderResult); - } - } - - process.WaitForExit(60000); - if (process.ExitCode != 0) - { - logger?.LogDebug($"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); - } - else - { - throw new DirectoryNotFoundException("Trimming failed: base_path is invalid"); - } - } - else - { - throw new ArgumentException("Trimming failed: file.DirectoryName is invalid"); - } - } - - public List GetDependencies(FileInfo file) - { - dependencyMap.Clear(); - - var directoryName = file.DirectoryName; - if (!string.IsNullOrEmpty(directoryName)) - { - var refs = GetAssemblyReferences(file.Name, directoryName); - - var dependencies = GetDependencies(refs, dependencyMap, directoryName); - - return dependencies; - } - else - { - return new(); - } - } - - private (Collection? References, string? ResolvedPath) GetAssemblyReferences(string fileName, string path) - { - static string? ResolvePath(string fileName, string path) - { - string attemptedPath = Path.Combine(path, fileName); - if (Path.GetExtension(fileName) != ".exe" - && Path.GetExtension(fileName) != ".dll") - { - attemptedPath += ".dll"; - } - return File.Exists(attemptedPath) ? attemptedPath : null; - } - - if (!string.IsNullOrEmpty(MeadowAssembliesPath)) - { - string? resolvedPath = ResolvePath(fileName, MeadowAssembliesPath) ?? ResolvePath(fileName, path); - - if (resolvedPath is null) - { - return (null, null); - } - - Collection references; - - try - { - using (var definition = AssemblyDefinition.ReadAssembly(resolvedPath)) - { - references = definition.MainModule.AssemblyReferences; - } - return (references, resolvedPath); - } - catch (Exception ex) - { - // Handle or log the exception appropriately - Console.WriteLine($"Error reading assembly: {ex.Message}"); - return (null, null); - } - } - else - { - return (null, null); - } - } - - private List GetDependencies((Collection? References, string? ResolvedPath) references, List dependencyMap, string folderPath) - { - if (references.ResolvedPath == null || dependencyMap.Contains(references.ResolvedPath)) - return dependencyMap; - - dependencyMap.Add(references.ResolvedPath); - - if (references.References != null) - { - foreach (var ar in references.References) - { - var namedRefs = GetAssemblyReferences(ar.Name, folderPath); - - if (namedRefs.References == null) - continue; - - GetDependencies(namedRefs, dependencyMap, folderPath); - } - } - - return dependencyMap; - } -} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index 609418a5..d57c3b7a 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -15,9 +15,14 @@ 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; - if (args.Contains("--verbose")) - logLevel = LogEventLevel.Verbose; + logLevel -= logModifier; + if (logLevel < 0) + { + logLevel = 0; + } var outputTemplate = logLevel == LogEventLevel.Verbose ? "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}" @@ -50,6 +55,7 @@ public static async Task Main(string[] args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); if (File.Exists("appsettings.json")) @@ -63,8 +69,7 @@ public static async Task Main(string[] args) } else { - // Have an empty Configuration instead. - services.AddScoped(_ => new ConfigurationBuilder().Build()); + services.AddScoped(_ => null); } AddCommandsAsServices(services); diff --git a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs index 0e39f0aa..493b93ff 100644 --- a/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs +++ b/Source/v2/Meadow.Cli/Properties/AssemblyInfo.cs @@ -6,6 +6,6 @@ namespace Meadow.CLI { public static class Constants { - public const string CLI_VERSION = "2.0.0.0"; + public const string CLI_VERSION = "2.0.0.10"; } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json index e0ad7a7b..47efd3dc 100644 --- a/Source/v2/Meadow.Cli/Properties/launchSettings.json +++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json @@ -45,7 +45,7 @@ }, "Config: Set Route Serial": { "commandName": "Project", - "commandLineArgs": "config route COM7" + "commandLineArgs": "config route COM10" }, "Config: Set Route TCP": { "commandName": "Project", @@ -81,7 +81,7 @@ }, "File Delete": { "commandName": "Project", - "commandLineArgs": "file delete meadow.log" + "commandLineArgs": "file delete Juego.pdb" }, "File Read": { "commandName": "Project", @@ -105,7 +105,7 @@ }, "Firmware Download version": { "commandName": "Project", - "commandLineArgs": "firmware download -v 1.0.2.0 --force" + "commandLineArgs": "firmware download -v 1.7.1.4 --force" }, "Firmware Default get": { "commandName": "Project", @@ -125,7 +125,7 @@ }, "Firmware Write version": { "commandName": "Project", - "commandLineArgs": "firmware write -v 1.2.0.1" + "commandLineArgs": "firmware write -v 1.8.0.0" }, "Firmware Write runtime": { "commandName": "Project", @@ -173,7 +173,7 @@ }, "App Trim": { "commandName": "Project", - "commandLineArgs": "app trim F:\\temp\\MeadowApplication1" + "commandLineArgs": "app trim H:\\WL\\Meadow.ProjectLab\\Source\\ProjectLab_Demo\\" }, "Dfu Install": { "commandName": "Project", @@ -185,7 +185,7 @@ }, "App Deploy (project folder)": { "commandName": "Project", - "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1" + "commandLineArgs": "app deploy H:\\WL\\Meadow.ProjectLab\\Source\\ProjectLab_Demo\\bin\\Debug\\netstandard2.1\\postlink_bin" }, "App Deploy (untrimmed output)": { "commandName": "Project", @@ -205,7 +205,7 @@ }, "Device provision": { "commandName": "Project", - "commandLineArgs": "device provision -o christacke6612" + "commandLineArgs": "device provision" }, "Cloud login": { "commandName": "Project", diff --git a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs index f942dcef..73530971 100644 --- a/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs +++ b/Source/v2/Meadow.Cloud.Client/Identity/LibSecret.cs @@ -7,11 +7,9 @@ public class LibSecret : IDisposable internal struct GError { -#pragma warning disable CS0649 public uint Domain; public int Code; public string Message; -#pragma warning restore CS0649 } public enum AttributeType @@ -47,7 +45,7 @@ public LibSecret(String service, String account) (int)AttributeType.STRING, IntPtr.Zero); } - public void SetSecret(String password) + public void SetSecret(string password) { _ = secret_password_store_sync(intPt, COLLECTION_SESSION, $"{Service}/{Account}", password, IntPtr.Zero, out IntPtr errorPtr, serviceLabel, Service, accountLabel, Account, IntPtr.Zero); HandleError(errorPtr, "An error was encountered while writing secret to keyring"); diff --git a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj index 412313a2..14afa078 100644 --- a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj +++ b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj @@ -7,10 +7,6 @@ True - - 4 - true - diff --git a/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs b/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs index 53e889ed..cee688ad 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/PackageInfo.cs @@ -5,7 +5,7 @@ namespace Meadow.Cloud; public record PackageInfo { [JsonPropertyName("v")] - public string? Version { get; set; } + public string Version { get; set; } [JsonPropertyName("osVersion")] - public string? OsVersion { get; set; } + public string OsVersion { get; set; } } diff --git a/Source/v2/Meadow.Cloud.Client/Messages/User.cs b/Source/v2/Meadow.Cloud.Client/Messages/User.cs index 7cd4b6c2..4c6ac125 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/User.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/User.cs @@ -2,9 +2,9 @@ public record User { - public string? Id { get; set; } - public string? Email { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } - public string? FullName { get; set; } -} + public string Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string FullName { get; set; } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs index 55710994..cc4373df 100644 --- a/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs +++ b/Source/v2/Meadow.Cloud.Client/Messages/UserOrg.cs @@ -5,9 +5,9 @@ namespace Meadow.Cloud; public class UserOrg { [JsonPropertyName("id")] - public string? Id { get; set; } + public string Id { get; set; } [JsonPropertyName("name")] - public string? Name { get; set; } + public string Name { get; set; } [JsonPropertyName("defaultCollectionId")] - public string? DefaultCollectionId { get; set; } -} + public string DefaultCollectionId { get; set; } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs b/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs new file mode 100644 index 00000000..e35ba82e --- /dev/null +++ b/Source/v2/Meadow.Cloud.Client/Services/ApiTokenService.cs @@ -0,0 +1,117 @@ +using Meadow.Cloud; +using Meadow.Cloud.Identity; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; + +namespace Meadow.Cloud; + +public class ApiTokenService : CloudServiceBase +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web); + + public ApiTokenService(IdentityManager identityManager) : base(identityManager) + { + } + + public async Task> GetApiTokens(string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var response = await httpClient.GetAsync($"{host}/api/auth/tokens", cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + return await response.Content.ReadFromJsonAsync>(JsonSerializerOptions, cancellationToken ?? CancellationToken.None) + ?? Enumerable.Empty(); + } + + public async Task CreateApiToken(CreateApiTokenRequest request, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var content = new StringContent(JsonSerializer.Serialize(request, JsonSerializerOptions), Encoding.UTF8, "application/json"); + var response = await httpClient.PostAsync($"{host}/api/auth/tokens", content, cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + var result = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken ?? CancellationToken.None); + return result!; + } + + public async Task UpdateApiToken(string id, UpdateApiTokenRequest request, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var content = new StringContent(JsonSerializer.Serialize(request, JsonSerializerOptions), Encoding.UTF8, "application/json"); + var response = await httpClient.PutAsync($"{host}/api/auth/tokens/{id}", content, cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + + var result = await response.Content.ReadFromJsonAsync(JsonSerializerOptions, cancellationToken ?? CancellationToken.None); + return result!; + } + + public async Task DeleteApiToken(string id, string host, CancellationToken? cancellationToken) + { + var httpClient = await GetAuthenticatedHttpClient(cancellationToken); + var response = await httpClient.DeleteAsync($"{host}/api/auth/tokens/{id}", cancellationToken ?? CancellationToken.None); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + throw new MeadowCloudException(message); + } + } +} + +public record GetApiTokenResponse( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes +) +{ } + +public record CreateApiTokenRequest +( + string Name, + int Duration, + string[] Scopes +) +{ } + +public record CreateApiTokenResponse +( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes, + string Token +) +{ } + +public record UpdateApiTokenRequest +( + string Name, + string[] Scopes +) +{ } + +public record UpdateApiTokenResponse +( + string Id, + string Name, + DateTimeOffset ExpiresAt, + string[] Scopes +) +{ } diff --git a/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs b/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs index 8554f092..b6dba3a7 100644 --- a/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs +++ b/Source/v2/Meadow.Cloud.Client/Services/CommandService.cs @@ -6,11 +6,8 @@ namespace Meadow.Cloud; public class CommandService : CloudServiceBase { - private readonly IdentityManager _identityManager; - public CommandService(IdentityManager identityManager) : base(identityManager) { - _identityManager = identityManager; } public async Task PublishCommandForCollection( diff --git a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs index 4b619888..8ee60027 100644 --- a/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs +++ b/Source/v2/Meadow.Cloud.Client/Services/PackageService.cs @@ -9,7 +9,7 @@ namespace Meadow.Cloud; public class PackageService : CloudServiceBase { - private string _info_json = "info.json"; + private readonly string _info_json = "info.json"; public PackageService(IdentityManager identityManager) : base(identityManager) { @@ -86,14 +86,11 @@ private string GetPackageOsVersion(string packagePath) { var content = File.ReadAllText(tempInfoJson); var packageInfo = JsonSerializer.Deserialize(content); - if (packageInfo != null) - { - result = packageInfo.OsVersion; - } + result = packageInfo?.OsVersion ?? string.Empty; File.Delete(tempInfoJson); } - return result!; + return result; } public async Task PublishPackage( diff --git a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs index f5f025d4..d016e798 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/ConnectionManagerTests.cs @@ -124,7 +124,7 @@ public async Task TestGetFileListWithoutCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(false); + var files = await device.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); @@ -143,7 +143,7 @@ public async Task TestGetFileListWithCrcs() Assert.Fail("no device"); return; } - var files = await device.GetFileList(true); + var files = await device.GetFileList("/meadow0/", true); Assert.NotNull(files); Assert.True(files.Any()); Assert.True(files.All(f => f.Name != null)); 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 index 1391a119..50cc994e 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj +++ b/Source/v2/Meadow.HCom.Integration.Tests/Meadow.HCom.Integration.Tests.csproj @@ -8,10 +8,6 @@ false - - 4 - true - diff --git a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs index d60dc865..74aed9b7 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialCommandTests.cs @@ -39,7 +39,7 @@ public async void TestGetFileListNoCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(false); + var files = await connection.GetFileList("/meadow0/", false); Assert.NotNull(files); Assert.True(files.Length > 0); @@ -53,7 +53,7 @@ public async void TestGetFileListWithCrc() { Assert.Equal(ConnectionState.Disconnected, connection.State); - var files = await connection.GetFileList(true); + var files = await connection.GetFileList("/meadow0/", true); 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 c0330d65..707a94d0 100644 --- a/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs +++ b/Source/v2/Meadow.HCom.Integration.Tests/SerialConnectionTests.cs @@ -15,11 +15,12 @@ public void TestInvalidPortName() }); } - /* TODO [Fact] - public async void TestListen() + [Fact] + public void TestListen() { using (var connection = new SerialConnection(ValidPortName)) { + /* Assert.Equal(ConnectionState.Disconnected, connection.State); var listener = new TestListener(); @@ -41,8 +42,9 @@ public async void TestListen() } Assert.True(listener.Messages.Count > 0); + */ } - }*/ + } [Fact] public async void TestAttachPositive() diff --git a/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs new file mode 100644 index 00000000..fb9933a0 --- /dev/null +++ b/Source/v2/Meadow.HCom/Connections/SimulatorConnection.cs @@ -0,0 +1,154 @@ +using Microsoft.Extensions.Logging; + +namespace Meadow.Hcom; + +public class SimulatorConnection : ConnectionBase +{ + public override string Name => "Simulator"; + + private HttpClient? _client = null; + + public override Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + // TODO: use some config our environment variable to launch the simulator process if it's not running + + _client = new HttpClient(); + + throw new NotImplementedException(); + } + + public override Task GetDeviceInfo(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 DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task EraseFlash(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetPublicKey(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task GetRtcTime(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task ResetDevice(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task TraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task TraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteCoprocessorFile(string localFileName, int destinationAddress, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override Task WriteRuntime(string localFileName, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public override void Detach() + { + throw new NotImplementedException(); + } +} diff --git a/Source/v2/Meadow.HCom/Meadow.HCom.csproj b/Source/v2/Meadow.HCom/Meadow.HCom.csproj index 162d093a..a4d58b2c 100644 --- a/Source/v2/Meadow.HCom/Meadow.HCom.csproj +++ b/Source/v2/Meadow.HCom/Meadow.HCom.csproj @@ -7,9 +7,6 @@ 10 - - true - @@ -17,17 +14,9 @@ - - - - - - - - - + diff --git a/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs b/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs new file mode 100644 index 00000000..b41fc660 --- /dev/null +++ b/Source/v2/Meadow.HCom/SerialRequests/DebuggerDataRequest.cs @@ -0,0 +1,20 @@ +namespace Meadow.Hcom +{ + internal class DebuggerDataRequest : Request + { + public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_DEBUGGING_DEBUGGER_DATA; + + public byte[] DebuggerData + { + get + { + if (Payload == null) return new byte[0]; + return Payload; + } + set + { + Payload = value; + } + } + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs b/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs new file mode 100644 index 00000000..2f6e0023 --- /dev/null +++ b/Source/v2/Meadow.HCom/SerialResponses/TextRequestRejectedResponse.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Text; + +namespace Meadow.Hcom; + +internal class TextRequestRejectedResponse : SerialResponse +{ + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + + internal TextRequestRejectedResponse(byte[] data, int length) + : base(data, length) + { + Debug.WriteLine(Text); + } +} diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs index 0b67a43d..cf01b887 100644 --- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs +++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs @@ -2,44 +2,30 @@ namespace Meadow.Hcom; -public delegate void ConnectionStateChangedHandler(ConnectionBase connection, ConnectionState oldState, ConnectionState newState); - public abstract class ConnectionBase : IMeadowConnection, IDisposable { private bool _isDisposed; - private ConnectionState _state; - public ConnectionState State - { - get => _state; - protected set - { - if (value == State) return; - - var old = _state; - _state = value; - ConnectionStateChanged?.Invoke(this, old, State); - } - } + public virtual ConnectionState State { get; protected set; } public IMeadowDevice? Device { get; protected set; } public event EventHandler<(string message, string? source)> DeviceMessageReceived = default!; public event EventHandler ConnectionError = default!; public event EventHandler<(string fileName, long completed, long total)> FileWriteProgress = default!; public event EventHandler ConnectionMessage = default!; - public event EventHandler? FileWriteFailed; - public event ConnectionStateChangedHandler ConnectionStateChanged = delegate { }; + public event EventHandler FileWriteFailed = default!; public abstract string Name { get; } public abstract Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); public abstract Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + public abstract void Detach(); public abstract Task GetDeviceInfo(CancellationToken? cancellationToken = null); - public abstract Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + public abstract Task GetFileList(string folder, 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 ReadFileString(string fileName, CancellationToken? cancellationToken = null); - public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); public abstract Task ResetDevice(CancellationToken? cancellationToken = null); public abstract Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null); @@ -59,6 +45,8 @@ protected set public abstract Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken); public abstract Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); + public abstract Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); + public ConnectionBase() { } diff --git a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs index 6ff0c0dd..ee188ec8 100644 --- a/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/LocalConnection.cs @@ -58,7 +58,7 @@ public LocalConnection() _deviceInfo = new DeviceInfo(info); } - return Task.FromResult< DeviceInfo?>(_deviceInfo); + return Task.FromResult(_deviceInfo); } private string ExecuteBashCommandLine(string command) @@ -68,6 +68,7 @@ private string ExecuteBashCommandLine(string command) FileName = "/bin/bash", Arguments = $"-c \"{command}\"", RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; @@ -76,7 +77,10 @@ private string ExecuteBashCommandLine(string command) process?.WaitForExit(); - return process?.StandardOutput.ReadToEnd() ?? string.Empty; + var stdout = process?.StandardOutput.ReadToEnd() ?? string.Empty; + var stderr = process?.StandardError.ReadToEnd() ?? string.Empty; + + return stdout; } public override Task GetPublicKey(CancellationToken? cancellationToken = null) @@ -110,6 +114,13 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = { // ssh-agent sh -c 'ssh-add; ssh-add -L' var pubkey = this.ExecuteBashCommandLine("ssh-agent sh -c 'ssh-add; ssh-add -L'"); + + if (pubkey.StartsWith("ssh-rsa")) + { + // convert to PEM format + pubkey = this.ExecuteBashCommandLine("ssh-keygen -f ~/.ssh/id_rsa.pub -m 'PEM' -e"); + } + return Task.FromResult(pubkey); } else @@ -122,7 +133,7 @@ public override Task GetPublicKey(CancellationToken? cancellationToken = - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } @@ -132,7 +143,7 @@ public override Task EraseFlash(CancellationToken? cancellationToken = null) throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } @@ -197,6 +208,11 @@ public override Task StartDebuggingSession(int port, ILogger? l throw new NotImplementedException(); } + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + public override Task TraceDisable(CancellationToken? cancellationToken = null) { throw new NotImplementedException(); @@ -236,4 +252,9 @@ public override Task WriteRuntime(string localFileName, CancellationToken? { throw new NotImplementedException(); } + + public override void Detach() + { + throw new NotImplementedException(); + } } diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs index 44e50c1d..876979b1 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs @@ -30,7 +30,17 @@ public override async Task WaitForMeadowAttach(CancellationToken? cancellationTo await Task.Delay(500); - Open(); + if (!_port.IsOpen) + { + try + { + Open(); + } + catch (Exception ex) + { + Debug.WriteLine($"Unable to open port: {ex.Message}"); + } + } } throw new TimeoutException(); @@ -55,7 +65,7 @@ private async Task ListenerProc() read: try { - receivedLength = await _port.BaseStream.ReadAsync(readBuffer, 0, readBuffer.Length); + receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length); } catch (OperationCanceledException) { @@ -124,7 +134,7 @@ private async Task ListenerProc() var response = SerialResponse.Parse(decodedBuffer, decodedSize); Debug.WriteLine($"{response.RequestType}"); - State = ConnectionState.MeadowAttached; + _state = ConnectionState.MeadowAttached; if (response != null) { @@ -175,7 +185,7 @@ private async Task ListenerProc() if (_reconnectInProgress) { - State = ConnectionState.MeadowAttached; + _state = ConnectionState.MeadowAttached; _reconnectInProgress = false; } else if (_textListComplete != null) @@ -186,7 +196,7 @@ private async Task ListenerProc() else if (response is TextRequestResponse trr) { // this is a response to a text request - the exact request is cached - Debug.WriteLine($"RESPONSE> {trr.Text}"); + //Debug.WriteLine($"RESPONSE> {trr.Text}"); } else if (response is DeviceInfoSerialResponse dir) { @@ -194,8 +204,8 @@ private async Task ListenerProc() } else if (response is ReconnectRequiredResponse rrr) { - // the device is going to restart - we need to wait for a HCOM_HOST_REQUEST_TEXT_CONCLUDED/TextConcludedResponse to know it's back - State = ConnectionState.Disconnected; + // 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) @@ -219,7 +229,7 @@ private async Task ListenerProc() _readFileInfo.FileStream = File.Create(_readFileInfo.LocalFileName); var uploadRequest = RequestBuilder.Build(); - await EncodeAndSendPacket(uploadRequest.Serialize()); + EncodeAndSendPacket(uploadRequest.Serialize()); } else if (response is UploadDataPacketResponse udp) { @@ -243,10 +253,7 @@ private async Task ListenerProc() _readFileInfo.FileStream.Dispose(); _readFileInfo = null; - if (!string.IsNullOrEmpty(fn)) - { - FileReadCompleted?.Invoke(this, fn); - } + FileReadCompleted?.Invoke(this, fn); } else if (response is FileReadInitFailedResponse frf) { @@ -255,6 +262,7 @@ private async Task ListenerProc() } else if (response is RequestErrorTextResponse ret) { + Debug.WriteLine(ret.Text); RaiseDeviceMessageReceived(ret.Text, "hcom"); _lastError = ret.Text; } @@ -305,20 +313,6 @@ private async Task ListenerProc() //this blocks the thread abort exception when the console app closes Debug.WriteLine($"listen abort"); } - catch (ObjectDisposedException) - { - // On Mac the port gets disposed when the Meadow is reset - await Task.Delay(1000); - CreatePort(); - - // make sure it's been re-opened - while(!_port.IsOpen) - { - _port.Open(); - await Task.Delay(250); - // TODO: add a timeout here - } - } catch (InvalidOperationException) { // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs index f40a6317..bb78a61d 100644 --- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs @@ -1,1275 +1,1275 @@ -using Meadow.Hardware; -using Microsoft.Extensions.Logging; -using System.Buffers; -using System.Diagnostics; -using System.IO.Ports; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace Meadow.Hcom; - -public partial class SerialConnection : ConnectionBase, IDisposable -{ - public const int DefaultBaudRate = 115200; - public const int ReadBufferSizeBytes = 0x2000; - private const int DefaultTimeout = 5000; - - private event EventHandler? FileReadCompleted = delegate { }; - private event EventHandler? FileWriteAccepted; - private event EventHandler? FileDataReceived; - - private SerialPort _port = default!; - private ILogger? _logger; - private bool _isDisposed; - private List _listeners = new List(); - private Queue _pendingCommands = new Queue(); - private bool _maintainConnection; - private Thread? _connectionManager = null; - private List _textList = new List(); - private int _messageCount = 0; - private ReadFileInfo? _readFileInfo = null; - private string? _lastError = null; - - public override string Name { get; } - - public SerialConnection(string port, ILogger? logger = default) - { - if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) - { - throw new ArgumentException($"Serial Port '{port}' not found."); - } - - Name = port; - State = ConnectionState.Disconnected; - _logger = logger; - - CreatePort(); - - new Task( - () => _ = ListenerProc(), - TaskCreationOptions.LongRunning) - .Start(); - - new Thread(CommandManager) - { - IsBackground = true, - Name = "HCOM Sender" - } - .Start(); - } - - private void CreatePort() - { - _port = new SerialPort(Name); - _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; - _port.Open(); - } - - 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) - { - Open(true); - } - } - - 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? - } - - private void Open(bool inLoop = false) - { - if (!_port.IsOpen) - { - try - { - Debug.WriteLine("Opening COM port..."); - _port.Open(); - } - catch (UnauthorizedAccessException ex) - { - // Handle unauthorized access (e.g., port in use by another application) - throw new Exception($"Serial port '{_port.PortName}' is in use by another application.", ex.InnerException); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - catch (TimeoutException ex) - { - // Handle timeout - throw new Exception($"Timeout occurred when opening the serial port '{_port.PortName}'.", ex.InnerException); - } - } - else if (inLoop) - { - Thread.Sleep(1000); - } - - State = ConnectionState.Connected; - - Debug.WriteLine("Opened COM port"); - } - - private void Close() - { - if (_port.IsOpen) - { - try - { - _port.Close(); - } - catch (IOException ex) - { - // Handle I/O errors - throw new Exception($"An I/O error occurred when attempting to close the serial port '{_port.PortName}'.", ex.InnerException); - } - } - - State = ConnectionState.Disconnected; - } - - public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) - { - try - { - // ensure the port is open - Open(); - - // search for the device via HCOM - we'll use a simple command since we don't have a "ping" - var command = RequestBuilder.Build(); - - // sequence numbers are only for file retrieval. Setting it to non-zero will cause it to hang - - _port.DiscardInBuffer(); - - // wait for a response - var timeout = timeoutSeconds * 2; - var dataReceived = false; - - // local function so we can unsubscribe - var count = _messageCount; - - _pendingCommands.Enqueue(command); - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); - - if (count != _messageCount) - { - dataReceived = true; - break; - } - - await Task.Delay(500); - } - - // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no - - // create the device instance - if (dataReceived) - { - Device = new MeadowDevice(this); - } - - return Device; - } - catch (Exception e) - { - _logger?.LogError(e, "Failed to connect"); - throw; - } - } - - private async void CommandManager() - { - await Task.Run(async () => - { - while (!_isDisposed) - { - while (_pendingCommands.Count > 0) - { - Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); - - var command = _pendingCommands.Dequeue() as Request; - - // if this is a file write, we need to packetize for progress - - if (command != null) - { - var payload = command.Serialize(); - await EncodeAndSendPacket(payload); - } - - // TODO: re-queue on fail? - } - - Thread.Sleep(1000); - } - }); - } - - private class ReadFileInfo - { - private string? _localFileName; - - public string MeadowFileName { get; set; } = default!; - public string? LocalFileName - { - get - { - if (_localFileName != null) return _localFileName; - - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); - } - set => _localFileName = value; - } - public FileStream FileStream { get; set; } = default!; - } - - public void EnqueueRequest(IRequest command) - { - // TODO: verify we're connected - - if (command is InitFileReadRequest sfr) - { - _readFileInfo = new ReadFileInfo - { - MeadowFileName = sfr.MeadowFileName, - LocalFileName = sfr.LocalFileName, - }; - } - - _pendingCommands.Enqueue(command); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = default) - { - await EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); - } - - private async Task EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = default) - { - if (messageBytes != null) - { - Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); - - while (!_port.IsOpen) - { - State = ConnectionState.Disconnected; - Thread.Sleep(100); - // wait for the port to open - } - - State = ConnectionState.Connected; - - try - { - int encodedToSend; - byte[] encodedBytes; - - // For file download this is a LOT of messages - // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); - - // For testing calculate the crc including the sequence number - //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); - try - { - // The encoded size using COBS is just a bit more than the original size adding 1 byte - // every 254 bytes plus 1 and need room for beginning and ending delimiters. - var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; - encodedBytes = new byte[l + 2]; - - // Skip over first byte so it can be a start delimiter - encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); - - // DEBUG TESTING - if (encodedToSend == -1) - { - _logger?.LogError($"Error - encodedToSend == -1"); - return; - } - - if (_port == null) - { - _logger?.LogError($"Error - SerialPort == null"); - throw new Exception("Port is null"); - } - } - catch (Exception except) - { - string msg = string.Format("Send setup Exception: {0}", except); - _logger?.LogError(msg); - throw; - } - - // Add delimiters to packet boundaries - try - { - encodedBytes[0] = 0; // Start delimiter - encodedToSend++; - encodedBytes[encodedToSend] = 0; // End delimiter - encodedToSend++; - } - catch (Exception encodedBytesEx) - { - // This should drop the connection and retry - Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); - Thread.Sleep(500); // Place for break point - throw; - } - - try - { - // Send the data to Meadow - await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken.HasValue ? cancellationToken.Value : default); - } - 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 override void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - Close(); - _port.Dispose(); - } - - _isDisposed = true; - } - } - - // ---------------------------------------------- - // ---------------------------------------------- - // ---------------------------------------------- - - 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 RuntimeStateToken = "Mono is"; - private const string RuntimeIsEnabledToken = "Mono is enabled"; - private const string RuntimeIsDisabledToken = "Mono is disabled"; - private const string RuntimeHasBeenToken = "Mono has been"; - private const string RuntimeHasBeenEnabledToken = "Mono has been enabled"; - private const string RuntimeHasBeenDisabledToken = "Mono has been disabled"; - private const string RtcRetrievalToken = "UTC time:"; - - public int CommandTimeoutSeconds { get; set; } = 30; - - private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) - { - var timeout = CommandTimeoutSeconds * 2; - - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return false; - if (_lastException != null) return false; - - if (timeout <= 0) throw new TimeoutException(); - - if (checkAction()) - { - break; - } - - await Task.Delay(500); - } - - return true; - } - - private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); - if (m != null) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) - { - return await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - if (requestType == null || requestType == _lastRequestConcluded) - { - return true; - } - } - - return false; - }, cancellationToken); - } - - public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.Time = dateTime; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - var success = await WaitForResult(() => - { - if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) - { - return true; - } - - return false; - }, cancellationToken); - } - - public override async Task GetRtcTime(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - DateTimeOffset? now = null; - - var success = await WaitForResult(() => - { - if (InfoMessages.Count > 0) - { - var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); - if (m != null) - { - var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); - now = DateTimeOffset.Parse(timeString); - return true; - } - } - - return false; - }, cancellationToken); - - return now; - } - - public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - EnqueueRequest(command); - - return await WaitForInformationResponse(RuntimeStateToken, RuntimeIsEnabledToken, cancellationToken); - } - - private async Task WaitForInformationResponse(string[] textToWaitOn, CancellationToken? cancellationToken) - { - // wait for an information response - var timeout = CommandTimeoutSeconds * 2; - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) - return false; - if (timeout <= 0) - throw new TimeoutException(); - - foreach (var t in textToWaitOn) - { - if (InfoMessages.Any(m => m.Contains(t))) return true; - } - - await Task.Delay(500); - } - return false; - } - - private async Task WaitForInformationResponse(string textToContain, string textToVerify, CancellationToken? cancellationToken) - { - // 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(textToContain)); - if (m != null) - { - return m == textToVerify; - } - } - - await Task.Delay(500); - } - return false; - } - - public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeHasBeenEnabledToken }, cancellationToken); - } - - public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - // if the runtime and OS mismatch, we get "Mono disabled" otehrwise we get "Mono is disabled". Yay! - await WaitForInformationResponse(new string[] { "Mono disabled", RuntimeIsDisabledToken }, cancellationToken); - } - - public override async Task TraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task TraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.UserData = (uint)level; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.ExtraData = parameter; - command.UserData = value; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task ResetDevice(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - EnqueueRequest(command); - - // we have to give time for the device to actually reset - await Task.Delay(500); - - await WaitForMeadowAttach(cancellationToken); - } - - public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _deviceInfo = null; - - _lastException = null; - EnqueueRequest(command); - - if (!await WaitForResult( - () => _deviceInfo != null, - cancellationToken)) - { - return null; - } - - return _deviceInfo; - } - - public override async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.IncludeCrcs = includeCrcs; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => _textListComplete ?? false, - cancellationToken)) - { - _textListComplete = null; - return null; - } - - var list = new List(); - - foreach (var candidate in _textList) - { - var fi = MeadowFileInfo.Parse(candidate); - if (fi != null) - { - list.Add(fi); - } - } - - _textListComplete = null; - return list.ToArray(); - } - - public override async Task WriteFile( - string localFileName, - string? meadowFileName = null, - CancellationToken? cancellationToken = null) - { - return await WriteFile(localFileName, meadowFileName, - RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, - 0, - cancellationToken); - } - - public override async Task WriteRuntime( - string localFileName, - CancellationToken? cancellationToken = null) - { - var commandTimeout = CommandTimeoutSeconds; - - - CommandTimeoutSeconds = 120; - _lastRequestConcluded = null; - - try - { - InfoMessages.Clear(); - - _lastRequestConcluded = 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); - - - /* - RaiseConnectionMessage("\nErasing runtime flash blocks..."); - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Mono memory erase success"); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Moving runtime to flash..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - // happens on error - return true; - } - - var m = string.Join('\n', InfoMessages); - return m.Contains("Verifying runtime flash operation."); - }, - cancellationToken); - - InfoMessages.Clear(); - - RaiseConnectionMessage("Verifying..."); - - status = await WaitForResult(() => - { - if (_lastRequestConcluded != null) - { - return true; - } - - return false; - }, - cancellationToken); - */ - - if (status) - { - await WaitForConcluded(null, cancellationToken); - } - - return status; - } - finally - { - CommandTimeoutSeconds = commandTimeout; - } - } - - public override async Task WriteCoprocessorFile( - string localFileName, - int destinationAddress, - CancellationToken? cancellationToken = null) - { - // make the timeouts much bigger, as the ESP flash takes a lot of time - var readTimeout = _port.ReadTimeout; - var commandTimeout = CommandTimeoutSeconds; - _lastRequestConcluded = null; - - _port.ReadTimeout = 60000; - CommandTimeoutSeconds = 180; - InfoMessages.Clear(); - - try - { - RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); - - // push the file to the device - if (!await WriteFile(localFileName, null, - RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, - RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, - destinationAddress, - cancellationToken)) - { - return false; - } - - - _lastRequestConcluded = null; - - // now wait for the STM32 to finish writing to the ESP32 - await WaitForConcluded(null, cancellationToken); - return true; - } - finally - { - _port.ReadTimeout = readTimeout; - CommandTimeoutSeconds = commandTimeout; - } - } - - private async Task WriteFile( - string localFileName, - string? meadowFileName, - RequestType initialRequestType, - RequestType endRequestType, - int writeAddress = 0, - CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - var fileBytes = File.ReadAllBytes(localFileName); - - var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes - if (writeAddress != 0) - { - // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. - using var md5 = MD5.Create(); - var hashBytes = md5.ComputeHash(fileBytes); - var hashString = BitConverter.ToString(hashBytes) - .Replace("-", "") - .ToLowerInvariant(); - fileHash = Encoding.UTF8.GetBytes(hashString); - } - var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); - - command.SetParameters( - localFileName, - meadowFileName ?? Path.GetFileName(localFileName), - fileCrc, - writeAddress, - fileHash, - initialRequestType); - - var accepted = false; - Exception? ex = null; - var needsRetry = false; - - void OnFileWriteAccepted(object? sender, EventArgs a) - { - accepted = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - void OnFileRetry(object? sender, EventArgs e) - { - needsRetry = true; - } - - FileWriteAccepted += OnFileWriteAccepted; - FileException += OnFileError; - FileWriteFailed += OnFileRetry; - - Debug.WriteLine($"Sending '{localFileName}'"); - - EnqueueRequest(command); - - // this will wait for a "file write accepted" from the target - if (!await WaitForResult( - () => - { - if (ex != null) - throw ex; - return accepted; - }, - cancellationToken)) - { - return false; - } - - // now send the file data - // 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]; - ushort sequenceNumber = 0; - - var progress = 0; - var expected = fileBytes.Length; - - var fileName = Path.GetFileName(localFileName); - var directoryName = Path.GetDirectoryName(localFileName).Split(Path.DirectorySeparatorChar); - var displayedFileName = Path.Combine(directoryName[directoryName.Length - 1], fileName); - - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - - var oldTimeout = _port.ReadTimeout; - _port.ReadTimeout = 60000; - - while (true && !needsRetry) - { - if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) - { - return false; - } - - sequenceNumber++; - - Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); - - var toRead = fileBytes.Length - progress; - if (toRead > packet.Length - 2) - { - toRead = packet.Length - 2; - } - Array.Copy(fileBytes, progress, packet, 2, toRead); - try - { - await EncodeAndSendPacket(packet, toRead + 2, cancellationToken); - } - catch (Exception) - { - break; - } - - progress += toRead; - base.RaiseFileWriteProgress(displayedFileName, progress, expected); - if (progress >= fileBytes.Length) break; - } - - if (!needsRetry) - { - _port.ReadTimeout = oldTimeout; - - base.RaiseFileWriteProgress(displayedFileName, expected, expected); - - // finish with an "end" message - not enqued because this is all a serial operation - var request = RequestBuilder.Build(); - request.SetRequestType(endRequestType); - var p = request.Serialize(); - await EncodeAndSendPacket(p, cancellationToken); - } - - FileWriteAccepted -= OnFileWriteAccepted; - FileException -= OnFileError; - FileWriteFailed -= OnFileRetry; - - return !needsRetry; - } - - public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - command.LocalFileName = localFileName; - - var completed = false; - Exception? ex = null; - - void OnFileReadCompleted(object? sender, string filename) - { - completed = true; - } - void OnFileError(object? sender, Exception exception) - { - ex = exception; - } - - try - { - FileReadCompleted += OnFileReadCompleted; - FileException += OnFileError; - ConnectionError += OnFileError; - - EnqueueRequest(command); - - if (!await WaitForResult( - () => - { - return completed | ex != null; - }, - cancellationToken)) - { - return false; - } - - return ex == null; - } - finally - { - FileReadCompleted -= OnFileReadCompleted; - FileException -= OnFileError; - } - } - - public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = fileName; - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - return contents; - } - - public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - command.MeadowFileName = meadowFileName; - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - } - - public override async Task EraseFlash(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - _lastRequestConcluded = null; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - } - - public override async Task GetPublicKey(CancellationToken? cancellationToken = null) - { - var command = RequestBuilder.Build(); - - string? contents = null; - - void OnFileDataReceived(object? sender, string data) - { - contents = data; - } - - FileDataReceived += OnFileDataReceived; - - var lastTimeout = CommandTimeoutSeconds; - - CommandTimeoutSeconds = 5 * 60; - - _lastRequestConcluded = null; - EnqueueRequest(command); - - await WaitForConcluded(null, cancellationToken); - - CommandTimeoutSeconds = lastTimeout; - - return contents!; - } - - public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) - { - if (Device != null) - { - logger?.LogDebug($"Start Debugging on port: {port}"); - await Device.StartDebugging(port, logger, cancellationToken); - - /* TODO logger?.LogDebug("Reinitialize the device"); - await ReInitializeMeadow(cancellationToken); */ - - var endpoint = new IPEndPoint(IPAddress.Loopback, port); - var debuggingServer = new DebuggingServer(Device, endpoint, logger!); - - logger?.LogDebug("Tell the Debugging Server to Start Listening"); - await debuggingServer.StartListening(cancellationToken); - return debuggingServer; - } - else - { - throw new DeviceNotFoundException(); - } - } - - public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) - { - var command = RequestBuilder.Build(); - - if (command != null) - { - InfoMessages.Clear(); - - _lastRequestConcluded = null; - - EnqueueRequest(command); - - await WaitForMeadowAttach(cancellationToken); - } - else - { - new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); - } - } +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Diagnostics; +using System.IO.Ports; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Meadow.Hcom; + +public delegate void ConnectionStateChangedHandler(SerialConnection connection, ConnectionState oldState, ConnectionState newState); + +public partial class SerialConnection : ConnectionBase, IDisposable +{ + public const int DefaultBaudRate = 115200; + public const int ReadBufferSizeBytes = 0x2000; + private const int DefaultTimeout = 5000; + + private event EventHandler FileReadCompleted = default!; + private event EventHandler FileWriteAccepted = default!; + private event EventHandler FileDataReceived = default!; + public event ConnectionStateChangedHandler ConnectionStateChanged = default!; + + private readonly SerialPort _port; + private readonly ILogger? _logger; + private bool _isDisposed; + private ConnectionState _state; + private readonly List _listeners = new List(); + private readonly Queue _pendingCommands = new Queue(); + private bool _maintainConnection; + private Thread? _connectionManager = null; + private readonly List _textList = new List(); + private int _messageCount = 0; + private ReadFileInfo? _readFileInfo = null; + private string? _lastError = null; + + public override string Name { get; } + + public SerialConnection(string port, ILogger? logger = default) + { + if (!SerialPort.GetPortNames().Contains(port, StringComparer.InvariantCultureIgnoreCase)) + { + throw new ArgumentException($"Serial Port '{port}' not found."); + } + + Name = port; + State = ConnectionState.Disconnected; + _logger = logger; + _port = new SerialPort(port); + _port.ReadTimeout = _port.WriteTimeout = DefaultTimeout; + + 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 override ConnectionState State + { + get => _state; + protected set + { + if (value == State) { return; } + + var old = _state; + _state = value; + ConnectionStateChanged?.Invoke(this, old, State); + } + } + + private void Open() + { + if (!_port.IsOpen) + { + try + { + _port.Open(); + } + catch (FileNotFoundException) + { + throw new Exception($"Serial port '{_port.PortName}' not found"); + } + } + State = ConnectionState.Connected; + } + + private void Close() + { + if (_port.IsOpen) + { + _port.Close(); + } + + State = ConnectionState.Disconnected; + } + + public override void Detach() + { + if (MaintainConnection) + { + // TODO: close this up + } + + Close(); + } + + public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + { + try + { + // ensure the port is open + Open(); + + // search for the device via HCOM - we'll use a simple command since we don't have a "ping" + var command = RequestBuilder.Build(); + + // sequence numbers are only for file retrieval - Setting it to non-zero will cause it to hang + + _port.DiscardInBuffer(); + + // wait for a response + var timeout = timeoutSeconds * 2; + var dataReceived = false; + + // local function so we can unsubscribe + var count = _messageCount; + + _pendingCommands.Enqueue(command); + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); + + if (count != _messageCount) + { + dataReceived = true; + break; + } + + await Task.Delay(500); + } + + // if HCOM fails, check for DFU/bootloader mode? only if we're doing an OS thing, so maybe no + + // create the device instance + if (dataReceived) + { + Device = new MeadowDevice(this); + } + + return Device; + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to connect"); + throw; + } + } + + private void CommandManager() + { + while (!_isDisposed) + { + while (_pendingCommands.Count > 0) + { + Debug.WriteLine($"There are {_pendingCommands.Count} pending commands"); + + var command = _pendingCommands.Dequeue() as Request; + if (command != null) + { + // if this is a file write, we need to packetize for progress + + var payload = command.Serialize(); + EncodeAndSendPacket(payload); + + // TODO: re-queue on fail? + } + } + + Thread.Sleep(1000); + } + } + + private class ReadFileInfo + { + private string? _localFileName; + + public string MeadowFileName { get; set; } = default!; + public string? LocalFileName + { + get + { + if (_localFileName != null) return _localFileName; + + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(MeadowFileName)); + } + set => _localFileName = value; + } + public FileStream FileStream { get; set; } = default!; + } + + public void EnqueueRequest(IRequest command) + { + // TODO: verify we're connected + + if (command is InitFileReadRequest sfr) + { + _readFileInfo = new ReadFileInfo + { + MeadowFileName = sfr.MeadowFileName, + LocalFileName = sfr.LocalFileName, + }; + } + + _pendingCommands.Enqueue(command); + } + + private void EncodeAndSendPacket(byte[] messageBytes, CancellationToken? cancellationToken = null) + { + EncodeAndSendPacket(messageBytes, messageBytes.Length, cancellationToken); + } + + private void EncodeAndSendPacket(byte[] messageBytes, int length, CancellationToken? cancellationToken = null) + { + //Debug.WriteLine($"+EncodeAndSendPacket({length} bytes)"); + + while (!_port.IsOpen) + { + _state = ConnectionState.Disconnected; + Thread.Sleep(100); + // wait for the port to open + } + + _state = ConnectionState.Connected; + + try + { + int encodedToSend; + byte[] encodedBytes; + + // For file download this is a LOT of messages + // _uiSupport.WriteDebugLine($"Sending packet with {messageSize} bytes"); + + // For testing calculate the crc including the sequence number + //_packetCrc32 = NuttxCrc.Crc32part(messageBytes, messageSize, 0, _packetCrc32); + try + { + // The encoded size using COBS is just a bit more than the original size adding 1 byte + // every 254 bytes plus 1 and need room for beginning and ending delimiters. + var l = Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE + (Protocol.HCOM_PROTOCOL_ENCODED_MAX_SIZE / 254) + 8; + encodedBytes = new byte[l + 2]; + + // Skip over first byte so it can be a start delimiter + encodedToSend = CobsTools.CobsEncoding(messageBytes, 0, length, ref encodedBytes, 1); + + // DEBUG TESTING + if (encodedToSend == -1) + { + _logger?.LogError($"Error - encodedToSend == -1"); + return; + } + + if (_port == null) + { + _logger?.LogError($"Error - SerialPort == null"); + throw new Exception("Port is null"); + } + } + catch (Exception except) + { + string msg = string.Format("Send setup Exception: {0}", except); + _logger?.LogError(msg); + throw; + } + + // Add delimiters to packet boundaries + try + { + encodedBytes[0] = 0; // Start delimiter + encodedToSend++; + encodedBytes[encodedToSend] = 0; // End delimiter + encodedToSend++; + } + catch (Exception encodedBytesEx) + { + // This should drop the connection and retry + Debug.WriteLine($"Adding encodeBytes delimiter threw: {encodedBytesEx}"); + Thread.Sleep(500); // Place for break point + throw; + } + + try + { + // Send the data to Meadow + // Debug.Write($"Sending {encodedToSend} bytes..."); + //await _port.BaseStream.WriteAsync(encodedBytes, 0, encodedToSend, cancellationToken ?? CancellationToken.None); + _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> + { + 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 override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Close(); + _port.Dispose(); + } + + _isDisposed = true; + } + } + + // ---------------------------------------------- + // ---------------------------------------------- + // ---------------------------------------------- + + private Exception? _lastException; + private bool? _textListComplete; + private DeviceInfo? _deviceInfo; + private RequestType? _lastRequestConcluded = null; + private List StdOut { get; } = new List(); + private List StdErr { get; } = new List(); + private List InfoMessages { get; } = new List(); + + private const string RuntimeSucessfullyEnabledToken = "Meadow successfully started MONO"; + private const string RuntimeSucessfullyDisabledToken = "Mono is disabled"; + private const string RuntimeStateToken = "Mono is"; + private const string RuntimeIsEnabledToken = "Mono is enabled"; + private const string RtcRetrievalToken = "UTC time:"; + + public int CommandTimeoutSeconds { get; set; } = 30; + + private async Task WaitForResult(Func checkAction, CancellationToken? cancellationToken) + { + var timeout = CommandTimeoutSeconds * 2; + + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (_lastException != null) return false; + + if (timeout <= 0) throw new TimeoutException(); + + if (checkAction()) + { + break; + } + + await Task.Delay(500); + } + + return true; + } + + private async Task WaitForResponseText(string textToAwait, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(textToAwait)); + if (m != null) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + private async Task WaitForConcluded(RequestType? requestType = null, CancellationToken? cancellationToken = null) + { + return await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + if (requestType == null || requestType == _lastRequestConcluded) + { + return true; + } + } + + return false; + }, cancellationToken); + } + + public override async Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.Time = dateTime; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } + + public override async Task GetRtcTime(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + DateTimeOffset? now = null; + + var success = await WaitForResult(() => + { + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RtcRetrievalToken)); + if (m != null) + { + var timeString = m.Substring(m.IndexOf(RtcRetrievalToken) + RtcRetrievalToken.Length); + now = DateTimeOffset.Parse(timeString); + return true; + } + } + + return false; + }, cancellationToken); + + return now; + } + + public override async Task IsRuntimeEnabled(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + EnqueueRequest(command); + + // wait for an information response + var timeout = CommandTimeoutSeconds * 2; + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return false; + if (timeout <= 0) throw new TimeoutException(); + + if (InfoMessages.Count > 0) + { + var m = InfoMessages.FirstOrDefault(i => i.Contains(RuntimeStateToken)); + if (m != null) + { + return m == RuntimeIsEnabledToken; + } + } + + await Task.Delay(500); + } + return false; + } + + public override async Task RuntimeEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task RuntimeDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task TraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceEnable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task UartTraceDisable(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetTraceLevel(int level, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.UserData = (uint)level; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task SetDeveloperParameter(ushort parameter, uint value, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.ExtraData = parameter; + command.UserData = value; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + } + + public override async Task ResetDevice(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + EnqueueRequest(command); + + // we have to give time for the device to actually reset + await Task.Delay(500); + + await WaitForMeadowAttach(cancellationToken); + } + + public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _deviceInfo = null; + + _lastException = null; + EnqueueRequest(command); + + if (!await WaitForResult( + () => _deviceInfo != null, + cancellationToken)) + { + return null; + } + + return _deviceInfo; + } + + public override async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.IncludeCrcs = includeCrcs; + + command.Path = folder; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => _textListComplete ?? false, + cancellationToken)) + { + _textListComplete = null; + return null; + } + + var list = new List(); + + foreach (var candidate in _textList) + { + var fi = MeadowFileInfo.Parse(candidate); + if (fi != null) + { + list.Add(fi); + } + } + + _textListComplete = null; + return list.ToArray(); + } + + public override async Task WriteFile( + string localFileName, + string? meadowFileName = null, + CancellationToken? cancellationToken = null) + { + return await WriteFile(localFileName, meadowFileName, + RequestType.HCOM_MDOW_REQUEST_START_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_FILE_TRANSFER, + 0, + cancellationToken); + } + + public override async Task WriteRuntime( + string localFileName, + CancellationToken? cancellationToken = null) + { + var commandTimeout = CommandTimeoutSeconds; + + + CommandTimeoutSeconds = 120; + _lastRequestConcluded = null; + + try + { + InfoMessages.Clear(); + + _lastRequestConcluded = 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); + + + /* + RaiseConnectionMessage("\nErasing runtime flash blocks..."); + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Mono memory erase success"); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Moving runtime to flash..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + // happens on error + return true; + } + + var m = string.Join('\n', InfoMessages); + return m.Contains("Verifying runtime flash operation."); + }, + cancellationToken); + + InfoMessages.Clear(); + + RaiseConnectionMessage("Verifying..."); + + status = await WaitForResult(() => + { + if (_lastRequestConcluded != null) + { + return true; + } + + return false; + }, + cancellationToken); + */ + + if (status) + { + await WaitForConcluded(null, cancellationToken); + } + + return status; + } + finally + { + CommandTimeoutSeconds = commandTimeout; + } + } + + public override async Task WriteCoprocessorFile( + string localFileName, + int destinationAddress, + CancellationToken? cancellationToken = null) + { + // make the timeouts much bigger, as the ESP flash takes a lot of time + var readTimeout = _port.ReadTimeout; + var commandTimeout = CommandTimeoutSeconds; + _lastRequestConcluded = null; + + _port.ReadTimeout = 60000; + CommandTimeoutSeconds = 180; + InfoMessages.Clear(); + + try + { + RaiseConnectionMessage($"Transferring {Path.GetFileName(localFileName)} to coprocessor..."); + + // push the file to the device + if (!await WriteFile(localFileName, null, + RequestType.HCOM_MDOW_REQUEST_START_ESP_FILE_TRANSFER, + RequestType.HCOM_MDOW_REQUEST_END_ESP_FILE_TRANSFER, + destinationAddress, + cancellationToken)) + { + return false; + } + + + _lastRequestConcluded = null; + + // now wait for the STM32 to finish writing to the ESP32 + await WaitForConcluded(null, cancellationToken); + return true; + } + finally + { + _port.ReadTimeout = readTimeout; + CommandTimeoutSeconds = commandTimeout; + } + } + + private async Task WriteFile( + string localFileName, + string? meadowFileName, + RequestType initialRequestType, + RequestType endRequestType, + int writeAddress = 0, + CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + var fileBytes = File.ReadAllBytes(localFileName); + + var fileHash = Encoding.ASCII.GetBytes("12345678901234567890123456789012"); // must be 32 bytes + if (writeAddress != 0) + { + // calculate the MD5 hash of the file - we have to send it as a UTF8 string, not as bytes. + using var md5 = MD5.Create(); + var hashBytes = md5.ComputeHash(fileBytes); + var hashString = BitConverter.ToString(hashBytes) + .Replace("-", "") + .ToLowerInvariant(); + fileHash = Encoding.UTF8.GetBytes(hashString); + } + var fileCrc = NuttxCrc.Crc32part(fileBytes, (uint)fileBytes.Length, 0); + + command.SetParameters( + localFileName, + meadowFileName ?? Path.GetFileName(localFileName), + fileCrc, + writeAddress, + fileHash, + initialRequestType); + + var accepted = false; + Exception? ex = null; + var needsRetry = false; + + void OnFileWriteAccepted(object? sender, EventArgs a) + { + accepted = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + void OnFileRetry(object? sender, EventArgs e) + { + needsRetry = true; + } + + FileWriteAccepted += OnFileWriteAccepted; + FileException += OnFileError; + FileWriteFailed += OnFileRetry; + + Debug.WriteLine($"Sending '{localFileName}'"); + + EnqueueRequest(command); + + // this will wait for a "file write accepted" from the target + if (!await WaitForResult( + () => + { + if (ex != null) throw ex; + return accepted; + }, + cancellationToken)) + { + return false; + } + + // now send the file data + // 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]; + ushort sequenceNumber = 0; + + var progress = 0; + var expected = fileBytes.Length; + + var fileName = Path.GetFileName(localFileName); + + base.RaiseFileWriteProgress(fileName, progress, expected); + + var oldTimeout = _port.ReadTimeout; + _port.ReadTimeout = 60000; + + while (true && !needsRetry) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) + { + return false; + } + + sequenceNumber++; + + Array.Copy(BitConverter.GetBytes(sequenceNumber), packet, 2); + + var toRead = fileBytes.Length - progress; + if (toRead > packet.Length - 2) + { + toRead = packet.Length - 2; + } + Array.Copy(fileBytes, progress, packet, 2, toRead); + try + { + EncodeAndSendPacket(packet, toRead + 2, cancellationToken); + } + catch (Exception) + { + break; + } + + progress += toRead; + base.RaiseFileWriteProgress(fileName, progress, expected); + if (progress >= fileBytes.Length) break; + } + + if (!needsRetry) + { + _port.ReadTimeout = oldTimeout; + + 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, cancellationToken); + } + + FileWriteAccepted -= OnFileWriteAccepted; + FileException -= OnFileError; + FileWriteFailed -= OnFileRetry; + + return !needsRetry; + } + + public override async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + command.LocalFileName = localFileName; + + var completed = false; + Exception? ex = null; + + void OnFileReadCompleted(object? sender, string filename) + { + completed = true; + } + void OnFileError(object? sender, Exception exception) + { + ex = exception; + } + + try + { + FileReadCompleted += OnFileReadCompleted; + FileException += OnFileError; + ConnectionError += OnFileError; + + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + return completed | ex != null; + }, + cancellationToken)) + { + return false; + } + + return ex == null; + } + finally + { + FileReadCompleted -= OnFileReadCompleted; + FileException -= OnFileError; + } + } + + public override async Task ReadFileString(string fileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = fileName; + + string? contents = null; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + return contents; + } + + public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + command.MeadowFileName = meadowFileName; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var result = await WaitForConcluded(null, cancellationToken); + return result; + } + + public override async Task EraseFlash(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + _lastRequestConcluded = null; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + EnqueueRequest(command); + + await WaitForConcluded(null, cancellationToken); + + CommandTimeoutSeconds = lastTimeout; + } + + public override async Task GetPublicKey(CancellationToken? cancellationToken = null) + { + var command = RequestBuilder.Build(); + + string contents = string.Empty; + + void OnFileDataReceived(object? sender, string data) + { + contents = data; + } + + FileDataReceived += OnFileDataReceived; + + var lastTimeout = CommandTimeoutSeconds; + + CommandTimeoutSeconds = 5 * 60; + + _lastRequestConcluded = null; + EnqueueRequest(command); + + if (!await WaitForResult( + () => + { + return contents != string.Empty; + }, + cancellationToken)) + { + CommandTimeoutSeconds = lastTimeout; + return string.Empty; + } + + CommandTimeoutSeconds = lastTimeout; + + return contents; + } + + public override async Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken) + { + if (Device == null) + { + throw new DeviceNotFoundException(); + } + + logger?.LogDebug($"Start Debugging on port: {port}"); + await Device.StartDebugging(port, logger, cancellationToken); + + /* TODO logger?.LogDebug("Reinitialize the device"); + await ReInitializeMeadow(cancellationToken); */ + + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + var debuggingServer = new DebuggingServer(Device, endpoint, logger); + + logger?.LogDebug("Tell the Debugging Server to Start Listening"); + await debuggingServer.StartListening(cancellationToken); + return debuggingServer; + } + + public override async Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken) + { + var command = RequestBuilder.Build(); + + if (command != null) + { + InfoMessages.Clear(); + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + await WaitForMeadowAttach(cancellationToken); + } + else + { + new Exception($"{typeof(StartDebuggingRequest)} command failed to build"); + } + } + + public override async Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) + { + var command = RequestBuilder.Build(userData); + command.DebuggerData = debuggerData; + + _lastRequestConcluded = null; + + EnqueueRequest(command); + + var success = await WaitForResult(() => + { + if (_lastRequestConcluded != null && _lastRequestConcluded == RequestType.HCOM_MDOW_REQUEST_RTC_SET_TIME_CMD) + { + return true; + } + + return false; + }, cancellationToken); + } } \ 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 34391689..ae7de6b5 100644 --- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs +++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs @@ -5,8 +5,8 @@ namespace Meadow.Hcom; public class TcpConnection : ConnectionBase { - private HttpClient _client; - private string _baseUri; + private readonly HttpClient _client; + private readonly string _baseUri; public override string Name => _baseUri; @@ -16,39 +16,38 @@ public TcpConnection(string uri) _client = new HttpClient(); } - public override async Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) + public override Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10) { - return await Task.Run(() => - { - /* - var request = RequestBuilder.Build(); - - base.EnqueueRequest(request); + /* + var request = RequestBuilder.Build(); - // get the info and "attach" - var timeout = timeoutSeconds * 2; + base.EnqueueRequest(request); - while (timeout-- > 0) - { - if (cancellationToken?.IsCancellationRequested ?? false) return null; - if (timeout <= 0) throw new TimeoutException(); + // get the info and "attach" + var timeout = timeoutSeconds * 2; - // do we have a device info? + while (timeout-- > 0) + { + if (cancellationToken?.IsCancellationRequested ?? false) return null; + if (timeout <= 0) throw new TimeoutException(); - if (State == ConnectionState.MeadowAttached) - { - break; - } + // do we have a device info? - await Task.Delay(500); + if (State == ConnectionState.MeadowAttached) + { + break; } - */ - // TODO: is there a way to "attach"? ping result? device info? - return Device = new MeadowDevice(this); + await Task.Delay(500); + } + */ + + // TODO: is there a way to "attach"? ping result? device info? + Device = new MeadowDevice(this); + + return Task.FromResult(Device); - // TODO: web socket for listen? - }); + // TODO: web socket for listen? } public override async Task GetDeviceInfo(CancellationToken? cancellationToken = null) @@ -58,14 +57,7 @@ public TcpConnection(string uri) if (response.IsSuccessStatusCode) { var r = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); - if (r != null) - { - return new DeviceInfo(r.ToDictionary()); - } - else - { - return null; - } + return new DeviceInfo(r.ToDictionary()); } else { @@ -78,7 +70,7 @@ public override Task WaitForMeadowAttach(CancellationToken? cancellationToken = throw new NotImplementedException(); } - public override Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public override Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } @@ -123,7 +115,7 @@ public override Task ReadFile(string meadowFileName, string? localFileName throw new NotImplementedException(); } - public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) + public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null) { throw new NotImplementedException(); } @@ -191,4 +183,14 @@ public override Task StartDebugging(int port, ILogger? logger, CancellationToken { throw new NotImplementedException(); } + + public override Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) + { + throw new NotImplementedException(); + } + + public override void Detach() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs index 4dce0ef6..3de0a43c 100644 --- a/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs +++ b/Source/v2/Meadow.Hcom/Debugging/DebuggingServer.cs @@ -1,9 +1,9 @@ -using System.Buffers; +using Microsoft.Extensions.Logging; +using System.Buffers; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; -using Microsoft.Extensions.Logging; namespace Meadow.Hcom; @@ -17,13 +17,13 @@ public class DebuggingServer : IDisposable // VS 2015 - 4020 public IPEndPoint LocalEndpoint { get; private set; } - private readonly object _lck = new object(); + private readonly object _lck = new(); private CancellationTokenSource? _cancellationTokenSource; - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly IMeadowDevice _meadow; private ActiveClient? _activeClient; private int _activeClientCount = 0; - private TcpListener _listener; + private readonly TcpListener _listener; private Task? _listenerTask; private bool _isReady; public bool Disposed; @@ -35,7 +35,7 @@ public class DebuggingServer : IDisposable /// The to debug /// The to listen for incoming debugger connections /// The to logging state information - public DebuggingServer(IMeadowDevice meadow, IPEndPoint localEndpoint, ILogger logger) + public DebuggingServer(IMeadowDevice meadow, IPEndPoint localEndpoint, ILogger? logger) { LocalEndpoint = localEndpoint; _meadow = meadow; @@ -77,8 +77,7 @@ public async Task StopListening() { _listener?.Stop(); - if (_cancellationTokenSource != null) - _cancellationTokenSource?.Cancel(false); + _cancellationTokenSource?.Cancel(false); if (_listenerTask != null) { @@ -126,12 +125,8 @@ private void OnConnect(TcpClient tcpClient) CloseActiveClient(); } - if (_cancellationTokenSource != null - && _logger != null) - { - _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource.Token); - _activeClientCount++; - } + _activeClient = new ActiveClient(_meadow, tcpClient, _logger, _cancellationTokenSource?.Token); + _activeClientCount++; } } catch (Exception ex) @@ -151,8 +146,10 @@ public void Dispose() { lock (_lck) { - if (Disposed) - return; + if (Disposed) + { + return; + } _cancellationTokenSource?.Cancel(false); _activeClient?.Dispose(); _listenerTask?.Dispose(); @@ -174,9 +171,17 @@ private class ActiveClient : IDisposable public bool Disposed = false; // Constructor - internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger logger, CancellationToken cancellationToken) + internal ActiveClient(IMeadowDevice meadow, TcpClient tcpClient, ILogger? logger, CancellationToken? cancellationToken) { - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (cancellationToken != null) + { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value); + } + else + { + _cts = new CancellationTokenSource(); + } + _logger = logger; _meadow = meadow; _tcpClient = tcpClient; @@ -196,6 +201,7 @@ private async Task SendToMeadowAsync() // Receive from Visual Studio and send to Meadow var receiveBuffer = ArrayPool.Shared.Rent(RECEIVE_BUFFER_SIZE); var meadowBuffer = Array.Empty(); + while (!_cts.IsCancellationRequested) { if (_networkStream != null && _networkStream.CanRead) @@ -204,8 +210,11 @@ private async Task SendToMeadowAsync() do { bytesRead = await _networkStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, _cts.Token); + if (bytesRead == 0 || _cts.IsCancellationRequested) - continue; + { + continue; + } var destIndex = meadowBuffer.Length; Array.Resize(ref meadowBuffer, destIndex + bytesRead); @@ -217,7 +226,8 @@ private async Task SendToMeadowAsync() BitConverter.ToString(md5.ComputeHash(meadowBuffer)) .Replace("-", string.Empty) .ToLowerInvariant()); - await _meadow.ForwardVisualStudioDataToMono(meadowBuffer, 0); + + await _meadow.SendDebuggerData(meadowBuffer, 0, _cts.Token); meadowBuffer = Array.Empty(); // Ensure we read all the data in this message before passing it along @@ -251,7 +261,7 @@ private async Task SendToMeadowAsync() } } - private async Task SendToVisualStudio() + private Task SendToVisualStudio() { try { @@ -259,7 +269,7 @@ private async Task SendToVisualStudio() { if (_networkStream != null && _networkStream.CanWrite) { - while (_meadow.DataProcessor.DebuggerMessages.Count > 0) + /* TODO while (_meadow.DataProcessor.DebuggerMessages.Count > 0) { var byteData = _meadow.DataProcessor.DebuggerMessages.Take(_cts.Token); _logger?.LogTrace("Received {count} bytes from Meadow, will forward to VS", byteData.Length); @@ -271,38 +281,42 @@ private async Task SendToVisualStudio() await _networkStream.WriteAsync(byteData, 0, byteData.Length, _cts.Token); _logger?.LogTrace("Forwarded {count} bytes to VS", byteData.Length); - } + }*/ } else { // User probably hit stop _logger?.LogInformation("Unable to Write Data from Visual Studio"); - _logger?.LogTrace("Unable to Write Data from Visual Studio"); } } } - catch (OperationCanceledException) + catch (OperationCanceledException oce) { // User probably hit stop; Removed logging as User doesn't need to see this // Keeping it as a TODO in case we find a side effect that needs logging. - // TODO _logger?.LogInformation("Operation Cancelled"); - // TODO _logger?.LogTrace(oce, "Operation Cancelled"); + _logger?.LogInformation("Operation Cancelled"); + _logger?.LogTrace(oce, "Operation Cancelled"); } catch (Exception ex) { _logger?.LogError($"Error sending data to Visual Studio.{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}StackTrace:{Environment.NewLine}{ex.StackTrace}"); - if (_cts.IsCancellationRequested) - throw; + if (_cts.IsCancellationRequested) + { + throw; + } } + return Task.CompletedTask; } public void Dispose() { lock (_tcpClient) { - if (Disposed) - return; + if (Disposed) + { + return; + } _logger?.LogTrace("Disposing ActiveClient"); _cts.Cancel(false); diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs deleted file mode 100644 index daadd4ad..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowDataProcessor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Concurrent; - -namespace Meadow.Hcom; - -public abstract class MeadowDataProcessor : IDisposable -{ - public EventHandler? OnReceiveData; - public BlockingCollection DebuggerMessages = new BlockingCollection(); - public abstract void Dispose(); -} diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs deleted file mode 100644 index aff5a9e9..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Meadow.Hcom; - -public class MeadowMessageEventArgs : EventArgs -{ - public string Message { get; private set; } - public MeadowMessageType MessageType { get; private set; } - - public MeadowMessageEventArgs(MeadowMessageType messageType, string message = "") - { - Message = message; - MessageType = messageType; - } -} diff --git a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs b/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs deleted file mode 100644 index 6e8daf25..00000000 --- a/Source/v2/Meadow.Hcom/Debugging/MeadowMessageType.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Meadow.Hcom; - -// For data received due to a CLI request these provide a secondary -// type of identification. The primary being the protocol request value -public enum MeadowMessageType -{ - AppOutput, - ErrOutput, - DeviceInfo, - FileListTitle, - FileListMember, - FileListCrcMember, - Data, - InitialFileData, - MeadowTrace, - SerialReconnect, - Accepted, - Concluded, - DownloadStartOkay, - DownloadStartFail, - DownloadFailed, - DevicePublicKey -} diff --git a/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs b/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs index 055ccc3b..3a822bc0 100644 --- a/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs +++ b/Source/v2/Meadow.Hcom/DeviceNotFoundException.cs @@ -2,10 +2,6 @@ { public class DeviceNotFoundException : Exception { - public DeviceNotFoundException(string? message = null, Exception? innerException = null) - : base(message ?? "No device found on this connection.", innerException) - { - - } + internal DeviceNotFoundException() : base() { } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Extensions.cs b/Source/v2/Meadow.Hcom/Extensions.cs deleted file mode 100644 index a1c27863..00000000 --- a/Source/v2/Meadow.Hcom/Extensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/DownloadManager.cs b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs index 0cb2ffa1..0be1cb43 100644 --- a/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/DownloadManager.cs @@ -1,63 +1,45 @@ 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"); + static readonly string RootFolder = "WildernessLabs"; + static readonly string FirmwareFolder = "Firmware"; + static readonly string LatestFilename = "latest.txt"; 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/"; + 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 static readonly string FirmwareDownloadsFolder = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + RootFolder, FirmwareFolder); - public DownloadManager(ILoggerFactory loggerFactory) + public static string FirmwareLatestVersion { - _logger = loggerFactory.CreateLogger(); + get + { + string latestPath = Path.Combine(FirmwareDownloadsFolder, LatestFilename); + if (File.Exists(latestPath)) + { + return File.ReadAllText(latestPath); + } + throw new FileNotFoundException("Latest firmware not found"); + } } + private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromMinutes(1) }; + + private readonly ILogger _logger; + public DownloadManager(ILogger logger) { _logger = logger; @@ -65,20 +47,19 @@ public DownloadManager(ILogger logger) internal async Task DownloadMeadowOSVersionFile(string? version) { - string versionCheckUrl; - if (version is null || string.IsNullOrWhiteSpace(version)) + string versionCheckUrl, versionCheckFile; + + if (string.IsNullOrWhiteSpace(version)) { - _logger?.LogInformation("Downloading latest version file" + Environment.NewLine); + _logger.LogInformation("Downloading latest version file" + Environment.NewLine); versionCheckUrl = VersionCheckUrlRoot + "latest.json"; } else { - _logger?.LogInformation("Downloading version file for Meadow OS " + version + Environment.NewLine); + _logger.LogInformation("Downloading version file for Meadow OS " + version + Environment.NewLine); versionCheckUrl = VersionCheckUrlRoot + version + ".json"; } - string versionCheckFile; - try { versionCheckFile = await DownloadFile(new Uri(versionCheckUrl)); @@ -91,196 +72,76 @@ public DownloadManager(ILogger logger) return versionCheckFile; } - //ToDo rename this method - DownloadOSAsync? public async Task DownloadOsBinaries(string? version = null, bool force = false) { - var versionCheckFilePath = await DownloadMeadowOSVersionFile(version); + var versionFilePath = await DownloadMeadowOSVersionFile(version); - if (versionCheckFilePath == null) + if (versionFilePath == null) { - _logger?.LogError($"Meadow OS {version} cannot be downloaded or is not available"); + _logger.LogError($"Meadow OS {version} cannot be downloaded or is not available"); return; } - var payload = File.ReadAllText(versionCheckFilePath); + var payload = File.ReadAllText(versionFilePath); var release = JsonSerializer.Deserialize(payload); if (release == null) { - _logger?.LogError($"Unable to read release details for Meadow OS {version}. Payload: {payload}"); + _logger.LogError($"Unable to read release details for Meadow OS"); return; } - if (!Directory.Exists(FirmwareDownloadsFilePathRoot)) + if (Directory.Exists(FirmwareDownloadsFolder) == false) { - Directory.CreateDirectory(FirmwareDownloadsFilePathRoot); + Directory.CreateDirectory(FirmwareDownloadsFolder); //we'll write latest.txt regardless of version if it doesn't exist - File.WriteAllText(Path.Combine(FirmwareDownloadsFilePathRoot, "latest.txt"), release.Version); + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, LatestFilename), 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); + { //otherwise update if we're pulling the latest release OS + File.WriteAllText(Path.Combine(FirmwareDownloadsFolder, LatestFilename), release.Version); } - if (release.Version != null && 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 firmwareVersionPath = Path.Combine(FirmwareDownloadsFolder, release.Version); - var local_path = Path.Combine(FirmwareDownloadsFilePathRoot, release.Version); - - if (Directory.Exists(local_path)) + if (Directory.Exists(firmwareVersionPath)) { if (force) { - CleanPath(local_path); + DeleteDirectory(firmwareVersionPath); } else { - _logger?.LogInformation($"Meadow OS version {release.Version} is already downloaded." + Environment.NewLine); + _logger.LogInformation($"Meadow OS version {release.Version} is already downloaded"); return; } } - Directory.CreateDirectory(local_path); + Directory.CreateDirectory(firmwareVersionPath); try { - _logger?.LogInformation($"Downloading Meadow OS" + Environment.NewLine); - await DownloadAndExtractFile(new Uri(release.DownloadURL), local_path); + _logger.LogInformation($"Downloading Meadow OS"); + await DownloadAndUnpack(new Uri(release.DownloadURL), firmwareVersionPath); } catch { - _logger?.LogError($"Unable to download Meadow OS {version}"); + _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); + _logger.LogInformation("Downloading coprocessor firmware"); + await DownloadAndUnpack(new Uri(release.NetworkDownloadURL), firmwareVersionPath); } catch { - _logger?.LogError($"Unable to download coprocessor firmware {version}"); + _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); + _logger.LogInformation($"Downloaded and extracted OS version {release.Version} to: {firmwareVersionPath}"); } private async Task DownloadFile(Uri uri, CancellationToken cancellationToken = default) @@ -291,61 +152,49 @@ private async Task DownloadFile(Uri uri, CancellationToken cancellationT 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); - } + _logger.LogDebug($"Copying downloaded file to temp file {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) + private async Task DownloadAndUnpack(Uri uri, string targetPath, CancellationToken cancellationToken = default) { - var downloadFileName = await DownloadFile(uri, cancellationToken); + var file = await DownloadFile(uri, cancellationToken); + + _logger.LogDebug($"Extracting {file} to {targetPath}"); + + ZipFile.ExtractToDirectory(file, targetPath); - _logger?.LogDebug("Extracting firmware to {path}", target_path); - ZipFile.ExtractToDirectory( - downloadFileName, - target_path); try { - File.Delete(downloadFileName); + File.Delete(file); } catch (Exception ex) { - _logger?.LogWarning("Unable to delete temporary file"); - _logger?.LogDebug(ex, "Unable to delete temporary file"); + _logger.LogDebug(ex, "Unable to delete temporary file"); } } - private void CleanPath(string path) + /// + /// Delete all files and sub directorines in a directory + /// + /// The directory path + /// Optional ILogger for exception reporting + public static void DeleteDirectory(string path, ILogger? logger = null) { - var di = new DirectoryInfo(path); - foreach (FileInfo file in di.GetFiles()) + try { - 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"); - } + Directory.Delete(path, true); } - foreach (DirectoryInfo dir in di.GetDirectories()) + catch (IOException e) { - 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"); - } + logger?.LogWarning($"Failed to delete {path} - {e.Message}"); } } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs b/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs deleted file mode 100644 index 0d8f448f..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 9b83525a..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareManager.cs +++ /dev/null @@ -1,146 +0,0 @@ -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) - { - // 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, ILogger? logger = null) - { - return new FirmwareUpdater(connection, logger); - } - - 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 deleted file mode 100644 index 0aa4c3de..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/FirmwareUpdater.cs +++ /dev/null @@ -1,349 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace Meadow.Hcom; - -public class FirmwareUpdater -{ - private ILogger? _logger; - // TODO 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, ILogger? logger = null) - { - _connection = connection; - _logger = 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 - { - if (_connection.Device != null) - { - // 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 && _connection.Device != 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 - { - if (_connection.Device != null) - { - 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 && _connection.Device != 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 - { - if (_connection.Device != null) - { - 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 && _connection.Device != 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 - { - if (_connection.Device != null) - { - await _connection.Device.Reset(); - } - } - catch (Exception ex) - { - _logger?.LogError(ex.Message); - CurrentState = UpdateState.Error; - return; - } - CurrentState = UpdateState.VerifySuccess; - break; - case UpdateState.VerifySuccess: - try - { - await _connection.WaitForMeadowAttach(); - await Task.Delay(2000); // wait 2 seconds to allow full boot - if (_connection.Device != null) - { - info = await _connection.Device.GetDeviceInfo(); - if (info != null) - { - 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 index ec08c6f1..a314d527 100644 --- a/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs +++ b/Source/v2/Meadow.Hcom/Firmware/PackageManager.cs @@ -21,7 +21,7 @@ public string CreatePackage(string applicationPath, string osVersion) throw new ArgumentException($"Invalid applicationPath: {applicationPath}"); } - var osFilePath = Path.Combine(DownloadManager.FirmwareDownloadsFilePathRoot, osVersion); + var osFilePath = Path.Combine(DownloadManager.FirmwareDownloadsFolder, osVersion); if (!Directory.Exists(osFilePath)) { throw new ArgumentException($"osVersion {osVersion} not found. Please download."); diff --git a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs b/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs deleted file mode 100644 index 1f3ea6d4..00000000 --- a/Source/v2/Meadow.Hcom/Firmware/PackageVersions.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 index 477c8eea..823f1e01 100644 --- a/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs +++ b/Source/v2/Meadow.Hcom/Firmware/ReleaseMetadata.cs @@ -5,12 +5,12 @@ namespace Meadow.Hcom; public class ReleaseMetadata { [JsonPropertyName("version")] - public string? Version { get; set; } + public string Version { get; set; } [JsonPropertyName("minCLIVersion")] - public string? MinCLIVersion { get; set; } + public string MinCLIVersion { get; set; } [JsonPropertyName("downloadUrl")] - public string? DownloadURL { get; set; } + public string DownloadURL { get; set; } [JsonPropertyName("networkDownloadUrl")] - public string? NetworkDownloadURL { get; set; } + public string NetworkDownloadURL { get; set; } } diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs index 75dbce9c..2990b98a 100644 --- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs +++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs @@ -13,15 +13,16 @@ public interface IMeadowConnection string Name { get; } IMeadowDevice? Device { get; } Task Attach(CancellationToken? cancellationToken = null, int timeoutSeconds = 10); + void Detach(); Task WaitForMeadowAttach(CancellationToken? cancellationToken = null); ConnectionState State { get; } Task WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null); Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null); Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); - Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); + Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null); Task ResetDevice(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task RuntimeDisable(CancellationToken? cancellationToken = null); @@ -45,5 +46,6 @@ public interface IMeadowConnection Task GetPublicKey(CancellationToken? cancellationToken = null); Task StartDebuggingSession(int port, ILogger? logger, CancellationToken cancellationToken); Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); + Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs index acb4a5f1..590caa26 100644 --- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs @@ -9,7 +9,7 @@ public interface IMeadowDevice Task RuntimeEnable(CancellationToken? cancellationToken = null); Task IsRuntimeEnabled(CancellationToken? cancellationToken = null); Task GetDeviceInfo(CancellationToken? cancellationToken = null); - Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null); + Task GetFileList(string folder, 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 DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null); @@ -27,8 +27,6 @@ public interface IMeadowDevice Task ReadFileString(string fileName, CancellationToken? cancellationToken = null); Task GetPublicKey(CancellationToken? cancellationToken = null); Task StartDebugging(int port, ILogger? logger, CancellationToken? cancellationToken); - Task ForwardVisualStudioDataToMono(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = default); - - MeadowDataProcessor DataProcessor { get; } + Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken); } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln index 06e0064a..93b78ab2 100644 --- a/Source/v2/Meadow.Hcom/Meadow.Hcom.sln +++ b/Source/v2/Meadow.Hcom/Meadow.Hcom.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meadow.Hcom", "Meadow.Hcom. 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meadow.Cli", "..\Meadow.Cli\Meadow.Cli.csproj", "{5E2ACCA3-232B-4B79-BCB9-A7184E42816B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs index 1e2320cc..e270958a 100644 --- a/Source/v2/Meadow.Hcom/MeadowDevice.cs +++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs @@ -4,9 +4,7 @@ namespace Meadow.Hcom { public partial class MeadowDevice : IMeadowDevice { - private IMeadowConnection _connection; - - public MeadowDataProcessor DataProcessor => throw new NotImplementedException(); + private readonly IMeadowConnection _connection; internal MeadowDevice(IMeadowConnection connection) { @@ -38,9 +36,9 @@ public async Task RuntimeEnable(CancellationToken? cancellationToken = null) return await _connection.GetDeviceInfo(cancellationToken); } - public async Task GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null) + public async Task GetFileList(string folder, bool includeCrcs, CancellationToken? cancellationToken = null) { - return await _connection.GetFileList(includeCrcs, cancellationToken); + return await _connection.GetFileList(folder, includeCrcs, cancellationToken); } public async Task ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null) @@ -159,9 +157,9 @@ public async Task StartDebugging(int port, ILogger? logger, CancellationToken? c await _connection.StartDebugging(port, logger, cancellationToken); } - public Task ForwardVisualStudioDataToMono(byte[] debuggerData, uint userData, CancellationToken? cancellationToken = null) + public async Task SendDebuggerData(byte[] debuggerData, uint userData, CancellationToken? cancellationToken) { - throw new NotImplementedException(); + await _connection.SendDebuggerData(debuggerData, userData, cancellationToken); } } } \ No newline at end of file diff --git a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs index 46590ad9..a4a06981 100644 --- a/Source/v2/Meadow.Hcom/MeadowFileInfo.cs +++ b/Source/v2/Meadow.Hcom/MeadowFileInfo.cs @@ -3,6 +3,12 @@ public string Name { get; private set; } = default!; public long? Size { get; private set; } public string? Crc { get; private set; } + public bool IsDirectory { get; private set; } + + public override string ToString() + { + return $"{(IsDirectory ? "/" : "")}{Name}"; + } public static MeadowFileInfo? Parse(string info) { @@ -13,11 +19,29 @@ { mfi = new MeadowFileInfo(); - // "/meadow0/App.deps.json [0xa0f6d6a2] 28 KB (26575 bytes)" + mfi.Name = info.Substring(1); + mfi.IsDirectory = true; + } + else + { + // v2 file lists have changed + + if (info.StartsWith("Directory:")) + { + // this is the first line and contains the directory name being parsed + return mfi; + } + else if (info.StartsWith("A total of")) + { + return mfi; + } + + mfi = new MeadowFileInfo(); + var indexOfSquareBracket = info.IndexOf('['); if (indexOfSquareBracket <= 0) { - mfi.Name = info; + mfi.Name = info.Trim(); } else { @@ -28,6 +52,7 @@ 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/SerialRequests/GetFileListRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs index 117f0468..7d8b4ee9 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/GetFileListRequest.cs @@ -1,13 +1,34 @@ -namespace Meadow.Hcom +using System.Text; + +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; + ? RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC + : RequestType.HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR; public bool IncludeCrcs { get; set; } + public string? Path + { + get + { + if (Payload == null) return null; + + if (Payload.Length == 0) { return null; } + + return Encoding.ASCII.GetString(Payload).Trim(); + } + set + { + if (value != null) + { + base.Payload = Encoding.ASCII.GetBytes(value); + } + } + } + public GetFileListRequest() { } diff --git a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs index 71d05ef3..ad6cb803 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/InitFileWriteRequest.cs @@ -78,7 +78,7 @@ internal class InitFileWriteRequest : Request public byte[] Esp32MD5 { get; set; } = new byte[32]; public string LocalFileName { get; private set; } = default!; - public string? MeadowFileName { get; private set; } + public string MeadowFileName { get; private set; } = default!; public void SetParameters( string localFile, diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs index 52311a1f..c20117a2 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestBuilder.cs @@ -2,7 +2,7 @@ { public static class RequestBuilder { - // TODO private static uint _sequenceNumber; + //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() diff --git a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs index eacee4ca..b00a012d 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/RequestType.cs @@ -64,6 +64,8 @@ public enum RequestType : ushort // 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, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR = 0x06 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, + HCOM_MDOW_REQUEST_LIST_FILES_SUBDIR_CRC = 0x07 | ProtocolType.HCOM_PROTOCOL_HEADER_SIMPLE_TEXT_TYPE, // This is a simple type with binary data diff --git a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs index e7b716cc..41bb7400 100644 --- a/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs +++ b/Source/v2/Meadow.Hcom/SerialRequests/SetRtcTimeRequest.cs @@ -10,14 +10,13 @@ public DateTimeOffset? Time { get { - if (Payload?.Length == 0) - return null; + if (Payload?.Length == 0) { return null; } return DateTimeOffset.Parse(Encoding.ASCII.GetString(Payload)); } set { - if (value.HasValue) + if (value != null) { base.Payload = Encoding.ASCII.GetBytes(value.Value.ToUniversalTime().ToString("o")); } diff --git a/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs b/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs index 653d532b..2135f2ea 100644 --- a/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs +++ b/Source/v2/Meadow.Hcom/SerialResponses/FileWriteInitFailedSerialResponse.cs @@ -1,9 +1,15 @@ -namespace Meadow.Hcom; +using System.Diagnostics; +using System.Text; + +namespace Meadow.Hcom; internal class FileWriteInitFailedSerialResponse : SerialResponse { + public string Text => Encoding.UTF8.GetString(_data, RESPONSE_PAYLOAD_OFFSET, PayloadLength); + internal FileWriteInitFailedSerialResponse(byte[] data, int length) : base(data, length) { + Debug.Write(Text); } } diff --git a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs index 22836753..759e215c 100644 --- a/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs +++ b/Source/v2/Meadow.Hcom/SerialResponses/SerialResponse.cs @@ -22,51 +22,31 @@ public static SerialResponse Parse(byte[] data, int length) { var type = (ResponseType)BitConverter.ToUInt16(data, HCOM_PROTOCOL_REQUEST_HEADER_RQST_TYPE_OFFSET); - switch (type) + return type switch { - case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR: - return new TextStdErrResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT: - return new TextStdOutResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_INFORMATION: - return new TextInformationResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED: - return new TextRequestResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO: - return new DeviceInfoSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED: - return new TextConcludedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER: - return new TextListHeaderResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER: - return new TextListMemberResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER: - return new TextCrcMemberResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL: - return new FileReadInitFailedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY: - return new FileReadInitOkResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA: - return new UploadDataPacketResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED: - return new UploadCompletedResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_ERROR: - return new RequestErrorTextResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_TEXT_RECONNECT: - return new ReconnectRequiredResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL: - return new FileWriteInitFailedSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY: - return new FileWriteInitOkSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_SEND_INITIAL_FILE_BYTES: - return new TextPayloadSerialResponse(data, length); - case ResponseType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE: - return new TextPayloadSerialResponse(data, length); - case ResponseType.HCOM_HOST_REQUEST_DNLD_FAIL_RESEND: - return new FileDownloadFailedResponse(data, length); - default: - return new SerialResponse(data, length); - } + ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDERR => new TextStdErrResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_MONO_STDOUT => new TextStdOutResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_INFORMATION => new TextInformationResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_ACCEPTED => new TextRequestResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_REJECTED => new TextRequestRejectedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_DEVICE_INFO => new DeviceInfoSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_CONCLUDED => new TextConcludedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_HEADER => new TextListHeaderResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_LIST_MEMBER => new TextListMemberResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_CRC_MEMBER => new TextCrcMemberResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_FAIL => new FileReadInitFailedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_UPLOAD_OKAY => new FileReadInitOkResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_UPLOADING_FILE_DATA => new UploadDataPacketResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_UPLOAD_FILE_COMPLETED => new UploadCompletedResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_ERROR => new RequestErrorTextResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_TEXT_RECONNECT => new ReconnectRequiredResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_FAIL => new FileWriteInitFailedSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_INIT_DOWNLOAD_OKAY => new FileWriteInitOkSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_SEND_INITIAL_FILE_BYTES => new TextPayloadSerialResponse(data, length), + ResponseType.HCOM_MDOW_REQUEST_OTA_REGISTER_DEVICE => new TextPayloadSerialResponse(data, length), + ResponseType.HCOM_HOST_REQUEST_DNLD_FAIL_RESEND => new FileDownloadFailedResponse(data, length), + _ => new SerialResponse(data, length), + }; } protected SerialResponse(byte[] data, int length) 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 index cad3692c..8fdf6f7b 100644 --- a/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj +++ b/Source/v2/Meadow.SoftwareManager.Unit.Tests/Meadow.SoftwareManager.Unit.Tests.csproj @@ -8,10 +8,6 @@ false - - 4 - true - diff --git a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs index e78e56c4..adad19f6 100644 --- a/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs +++ b/Source/v2/Meadow.SoftwareManager/DownloadFileStream.cs @@ -6,7 +6,7 @@ namespace Meadow.Software; internal class DownloadFileStream : Stream, IDisposable { - public event EventHandler? DownloadProgress; + public event EventHandler DownloadProgress = default!; private readonly Stream _stream; diff --git a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs index 9b7ac9c8..ead22476 100644 --- a/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/F7FirmwarePackageCollection.cs @@ -11,24 +11,29 @@ namespace Meadow.Software; public class F7FirmwarePackageCollection : IFirmwarePackageCollection { /// - public event EventHandler? DownloadProgress; + public event EventHandler DownloadProgress = default!; - public string PackageFileRoot { get; } + public event EventHandler DefaultVersionChanged = default!; - private List _f7Packages = new(); + public string PackageFileRoot { get; } - public FirmwarePackage? DefaultPackage { get; private set; } + private readonly List _f7Packages = new(); public static string DefaultF7FirmwareStoreRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WildernessLabs", "Firmware"); + private FirmwarePackage? _defaultPackage; internal F7FirmwarePackageCollection() : this(DefaultF7FirmwareStoreRoot) { } + public FirmwarePackage? this[string version] => _f7Packages.FirstOrDefault(p => p.Version == version); + + public FirmwarePackage this[int index] => _f7Packages[index]; + internal F7FirmwarePackageCollection(string rootPath) { if (!Directory.Exists(rootPath)) @@ -39,6 +44,16 @@ internal F7FirmwarePackageCollection(string rootPath) PackageFileRoot = rootPath; } + public FirmwarePackage? DefaultPackage + { + get => _defaultPackage; + private set + { + _defaultPackage = value; + DefaultVersionChanged?.Invoke(this, value); + } + } + /// /// Checks the remote (i.e. cloud) store to see if a new firmware package is available. /// @@ -59,7 +74,7 @@ internal F7FirmwarePackageCollection(string rootPath) return null; } - public Task DeletePackage(string version) + public async Task DeletePackage(string version) { var existing = _f7Packages.FirstOrDefault(p => p.Version == version); @@ -70,30 +85,26 @@ public Task DeletePackage(string version) // 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; + if (DefaultPackage != null) { - while (DefaultPackage.Version == _f7Packages[i].Version) - { - i--; - } - var newDefault = _f7Packages[i].Version; _f7Packages.Remove(DefaultPackage); - - if (!string.IsNullOrEmpty(newDefault)) - SetDefaultPackage(newDefault); } + await SetDefaultPackage(newDefault); var path = Path.Combine(PackageFileRoot, version); Directory.Delete(path, true); - - return Task.CompletedTask; } - public Task SetDefaultPackage(string version) + public async Task SetDefaultPackage(string version) { - // Refresh the list, in case we've just downloaded it. - Refresh(); + await Refresh(); var existing = _f7Packages.FirstOrDefault(p => p.Version == version); @@ -104,8 +115,6 @@ public Task SetDefaultPackage(string version) var downloadManager = new F7FirmwareDownloadManager(); downloadManager.SetDefaultVersion(PackageFileRoot, version); - - return Task.CompletedTask; } public async Task IsVersionAvailableForDownload(string version) diff --git a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs index 68f154d2..339953f9 100644 --- a/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs +++ b/Source/v2/Meadow.SoftwareManager/FirmwarePackage.cs @@ -11,13 +11,13 @@ internal FirmwarePackage(IFirmwarePackageCollection collection) _collection = collection; } - public string GetFullyQualifiedPath(string file) + public string GetFullyQualifiedPath(string? file) { return Path.Combine(_collection.PackageFileRoot, Version, file); } - public string? Version { get; set; } - public string? Targets { get; set; } + 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; } @@ -25,4 +25,4 @@ public string GetFullyQualifiedPath(string file) public string? OsWithoutBootloader { get; set; } public string? Runtime { get; set; } public string? BclFolder { get; set; } -} +} \ No newline at end of file diff --git a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs index 5d0cf341..822cea0c 100644 --- a/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs +++ b/Source/v2/Meadow.SoftwareManager/IFirmwarePackageCollection.cs @@ -14,6 +14,7 @@ public interface IFirmwarePackageCollection : IEnumerable /// EventArgs are the total number of bytes retrieved /// public event EventHandler DownloadProgress; + public event EventHandler DefaultVersionChanged; FirmwarePackage? DefaultPackage { get; } Task SetDefaultPackage(string version); @@ -23,6 +24,7 @@ public interface IFirmwarePackageCollection : IEnumerable Task UpdateAvailable(); Task IsVersionAvailableForDownload(string version); Task RetrievePackage(string version, bool overwrite = false); - + FirmwarePackage this[int index] { get; } + FirmwarePackage? this[string version] { get; } string PackageFileRoot { get; } } diff --git a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj index 8fcd9f02..44f93135 100644 --- a/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj +++ b/Source/v2/Meadow.SoftwareManager/Meadow.SoftwareManager.csproj @@ -6,9 +6,6 @@ 10 - - true - diff --git a/Source/v2/Meadow.Cli.Core/ILibUsbDevice.cs b/Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs similarity index 100% rename from Source/v2/Meadow.Cli.Core/ILibUsbDevice.cs rename to Source/v2/Meadow.UsbLib.Core/ILibUsbDevice.cs diff --git a/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj new file mode 100644 index 00000000..d1692ecb --- /dev/null +++ b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs index 7c5e4398..d0a78185 100644 --- a/Source/v2/Meadow.UsbLib/LibUsbDevice.cs +++ b/Source/v2/Meadow.UsbLib/LibUsbDevice.cs @@ -18,14 +18,11 @@ static LibUsbProvider() public List GetDevicesInBootloaderMode() { - if (_devices == null) - { - _devices = _context - .List() - .Where(d => d.Info.VendorId == UsbBootLoaderVendorID) - .Select(d => new LibUsbDevice(d)) - .ToList(); - } + _devices = _context + .List() + .Where(d => d.Info.VendorId == UsbBootLoaderVendorID) + .Select(d => new LibUsbDevice(d)) + .ToList(); return _devices; } diff --git a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj index ca09c412..7229855d 100644 --- a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj +++ b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,16 +6,12 @@ enable - - 4 - true - - - - - + + + + diff --git a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj index fc4bddd1..3a4f13c1 100644 --- a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj +++ b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,17 +6,12 @@ enable - - 4 - true - - - - - - + + + +