From 493e1555f77df32cf9cdb0ff95e5ab3b0161bb6d Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Wed, 27 Sep 2023 12:05:54 -0500
Subject: [PATCH 1/6] added file delete to v2

---
 Meadow.CLI/Properties/launchSettings.json     |  4 +-
 Source/v2/Meadow.Cli/AppManager.cs            | 11 +++++
 .../Current/File/FileDeleteCommand.cs         | 41 +++++++++++++++++++
 .../Meadow.Cli/Properties/launchSettings.json |  6 ++-
 .../Meadow.Hcom/Connections/ConnectionBase.cs |  1 +
 .../Connections/SerialConnection.cs           | 10 +++++
 .../Meadow.Hcom/Connections/TcpConnection.cs  |  5 +++
 Source/v2/Meadow.Hcom/IMeadowConnection.cs    |  1 +
 Source/v2/Meadow.Hcom/IMeadowDevice.cs        |  1 +
 Source/v2/Meadow.Hcom/MeadowDevice.cs         |  5 +++
 .../Serial Requests/FileDeleteRequest.cs      | 23 +++++++++++
 .../Serial Requests/InitFileReadRequest.cs    |  2 +-
 12 files changed, 106 insertions(+), 4 deletions(-)
 create mode 100644 Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs
 create mode 100644 Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs

diff --git a/Meadow.CLI/Properties/launchSettings.json b/Meadow.CLI/Properties/launchSettings.json
index a9bc47bc..8260665f 100644
--- a/Meadow.CLI/Properties/launchSettings.json
+++ b/Meadow.CLI/Properties/launchSettings.json
@@ -26,7 +26,7 @@
     },
     "FileDelete": {
       "commandName": "Project",
-      "commandLineArgs": "file delete -f"
+      "commandLineArgs": "file delete -f dns.conf"
     },
     "FileList": {
       "commandName": "Project",
@@ -106,7 +106,7 @@
     },
     "UsePort": {
       "commandName": "Project",
-      "commandLineArgs": "use port COM10"
+      "commandLineArgs": "use port COM7"
     },
     "Version": {
       "commandName": "Project",
diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs
index f80f3f76..2c4b8661 100644
--- a/Source/v2/Meadow.Cli/AppManager.cs
+++ b/Source/v2/Meadow.Cli/AppManager.cs
@@ -72,6 +72,17 @@ public static async Task DeployApplication(
         // get a list of files on-device, with CRCs
         var deviceFiles = await connection.GetFileList(true, cancellationToken);
 
+        // get a list of files of the device files that are not in the list we intend to deploy
+        var removeFiles = deviceFiles
+            .Select(f => f.Name)
+            .Except(localFiles.Keys
+                .Select(f => Path.GetFileName(f)));
+
+        // delete those files
+        foreach (var file in removeFiles)
+        {
+            await connection.DeleteFile(file, cancellationToken);
+        }
 
         // erase all files on device not in list of files to send
 
diff --git a/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs
new file mode 100644
index 00000000..9c2ea68f
--- /dev/null
+++ b/Source/v2/Meadow.Cli/Commands/Current/File/FileDeleteCommand.cs
@@ -0,0 +1,41 @@
+using CliFx.Attributes;
+using Meadow.Hcom;
+using Microsoft.Extensions.Logging;
+
+namespace Meadow.CLI.Commands.DeviceManagement;
+
+[Command("file delete", Description = "Deletes a file from the device")]
+public class FileDeleteCommand : BaseDeviceCommand<FileDeleteCommand>
+{
+    [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(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken)
+    {
+        var fileList = await connection.GetFileList(false);
+        var exists = fileList?.Any(f => Path.GetFileName(f.Name) == MeadowFile) ?? false;
+
+        if (!exists)
+        {
+            Logger.LogError($"File '{MeadowFile}' not found on device.");
+        }
+        else
+        {
+            var wasRuntimeEnabled = await 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 device.DeleteFile(MeadowFile, cancellationToken);
+        }
+    }
+}
diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json
index 25caa7bc..81a4e378 100644
--- a/Source/v2/Meadow.Cli/Properties/launchSettings.json
+++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json
@@ -41,7 +41,7 @@
     },
     "Config: Set Route Serial": {
       "commandName": "Project",
-      "commandLineArgs": "config route COM4"
+      "commandLineArgs": "config route COM7"
     },
     "Config: Set Route TCP": {
       "commandName": "Project",
@@ -75,6 +75,10 @@
       "commandName": "Project",
       "commandLineArgs": "file list --verbose"
     },
+    "File Delete": {
+      "commandName": "Project",
+      "commandLineArgs": "file delete meadow.log"
+    },
     "File Read": {
       "commandName": "Project",
       "commandLineArgs": "file read test.txt \"f:\\temp\\test2.txt\""
diff --git a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs
index ec7984e8..e2ee803e 100644
--- a/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs
+++ b/Source/v2/Meadow.Hcom/Connections/ConnectionBase.cs
@@ -20,6 +20,7 @@ public abstract class ConnectionBase : IMeadowConnection, IDisposable
     public abstract Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null);
     public abstract Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null);
     public abstract Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null);
+    public abstract Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null);
     public abstract Task ResetDevice(CancellationToken? cancellationToken = null);
     public abstract Task<bool> IsRuntimeEnabled(CancellationToken? cancellationToken = null);
     public abstract Task RuntimeDisable(CancellationToken? cancellationToken = null);
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
index 61eff517..9f4aed6d 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
@@ -1069,4 +1069,14 @@ void OnFileError(object? sender, Exception exception)
             FileException -= OnFileError;
         }
     }
+
+    public override async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null)
+    {
+        var command = RequestBuilder.Build<FileDeleteRequest>();
+        command.MeadowFileName = meadowFileName;
+
+        EnqueueRequest(command);
+
+        await WaitForConcluded(null, 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 49eeffd3..e4aecba4 100644
--- a/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs
+++ b/Source/v2/Meadow.Hcom/Connections/TcpConnection.cs
@@ -112,6 +112,11 @@ public override Task<bool> ReadFile(string meadowFileName, string? localFileName
         throw new NotImplementedException();
     }
 
+    public override Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null)
+    {
+        throw new NotImplementedException();
+    }
+
     public override Task<bool> WriteRuntime(string localFileName, CancellationToken? cancellationToken = null)
     {
         throw new NotImplementedException();
diff --git a/Source/v2/Meadow.Hcom/IMeadowConnection.cs b/Source/v2/Meadow.Hcom/IMeadowConnection.cs
index 959cce8c..c0ba7adf 100644
--- a/Source/v2/Meadow.Hcom/IMeadowConnection.cs
+++ b/Source/v2/Meadow.Hcom/IMeadowConnection.cs
@@ -15,6 +15,7 @@ public interface IMeadowConnection
 
         Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null);
         Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null);
+        Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null);
         Task<DeviceInfo?> GetDeviceInfo(CancellationToken? cancellationToken = null);
         Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null);
         Task ResetDevice(CancellationToken? cancellationToken = null);
diff --git a/Source/v2/Meadow.Hcom/IMeadowDevice.cs b/Source/v2/Meadow.Hcom/IMeadowDevice.cs
index fce2fec5..30069d4a 100644
--- a/Source/v2/Meadow.Hcom/IMeadowDevice.cs
+++ b/Source/v2/Meadow.Hcom/IMeadowDevice.cs
@@ -10,6 +10,7 @@ public interface IMeadowDevice
         Task<MeadowFileInfo[]?> GetFileList(bool includeCrcs, CancellationToken? cancellationToken = null);
         Task<bool> ReadFile(string meadowFileName, string? localFileName = null, CancellationToken? cancellationToken = null);
         Task<bool> WriteFile(string localFileName, string? meadowFileName = null, CancellationToken? cancellationToken = null);
+        Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null);
         Task<bool> WriteRuntime(string localFileName, CancellationToken? cancellationToken = null);
         Task<DateTimeOffset?> GetRtcTime(CancellationToken? cancellationToken = null);
         Task SetRtcTime(DateTimeOffset dateTime, CancellationToken? cancellationToken = null);
diff --git a/Source/v2/Meadow.Hcom/MeadowDevice.cs b/Source/v2/Meadow.Hcom/MeadowDevice.cs
index 9e9bacaf..775a126a 100644
--- a/Source/v2/Meadow.Hcom/MeadowDevice.cs
+++ b/Source/v2/Meadow.Hcom/MeadowDevice.cs
@@ -129,5 +129,10 @@ public async Task SetDeveloperParameter(ushort parameter, uint value, Cancellati
         {
             await _connection.SetDeveloperParameter(parameter, value, cancellationToken);
         }
+
+        public async Task DeleteFile(string meadowFileName, CancellationToken? cancellationToken = null)
+        {
+            await _connection.DeleteFile(meadowFileName, cancellationToken);
+        }
     }
 }
\ No newline at end of file
diff --git a/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs
new file mode 100644
index 00000000..a5eca198
--- /dev/null
+++ b/Source/v2/Meadow.Hcom/Serial Requests/FileDeleteRequest.cs	
@@ -0,0 +1,23 @@
+using System.Text;
+
+namespace Meadow.Hcom;
+
+internal class FileDeleteRequest : Request
+{
+    public override RequestType RequestType => RequestType.HCOM_MDOW_REQUEST_DELETE_FILE_BY_NAME;
+
+    public string MeadowFileName
+    {
+        get
+        {
+            if (Payload == null) return string.Empty;
+            return Encoding.ASCII.GetString(Payload, 44, Payload.Length - 44);
+        }
+        set
+        {
+            var nameBytes = Encoding.ASCII.GetBytes(value);
+            Payload = new byte[4 + 4 + 4 + 32 + nameBytes.Length];
+            Array.Copy(nameBytes, 0, Payload, 44, nameBytes.Length); // file name
+        }
+    }
+}
diff --git a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs
index 44f1c9d1..cdd9001d 100644
--- a/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs	
+++ b/Source/v2/Meadow.Hcom/Serial Requests/InitFileReadRequest.cs	
@@ -19,4 +19,4 @@ public string MeadowFileName
             Payload = Encoding.ASCII.GetBytes(value);
         }
     }
-}
+}
\ No newline at end of file

From 4734c07889bb76d5b601a46687760283623f1245 Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Wed, 27 Sep 2023 13:18:42 -0500
Subject: [PATCH 2/6] more app deploy work

---
 Source/v2/Meadow.Cli/AppManager.cs            | 24 +++++++--
 .../Commands/Current/App/AppDeployCommand.cs  | 52 +++++++++++++------
 .../Commands/Current/App/AppTrimCommand.cs    |  2 +-
 .../PackageManager.AssemblyManager.cs         |  7 +--
 .../Meadow.Cli/Properties/launchSettings.json |  8 +++
 .../Connections/SerialConnection.cs           |  2 +
 6 files changed, 71 insertions(+), 24 deletions(-)

diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs
index 2c4b8661..cfc51702 100644
--- a/Source/v2/Meadow.Cli/AppManager.cs
+++ b/Source/v2/Meadow.Cli/AppManager.cs
@@ -70,23 +70,37 @@ public static async Task DeployApplication(
         }
 
         // get a list of files on-device, with CRCs
-        var deviceFiles = await connection.GetFileList(true, cancellationToken);
+        var deviceFiles = await connection.GetFileList(true, cancellationToken) ?? Array.Empty<MeadowFileInfo>();
 
         // get a list of files of the device files that are not in the list we intend to deploy
         var removeFiles = deviceFiles
-            .Select(f => f.Name)
+            .Select(f => Path.GetFileName(f.Name))
             .Except(localFiles.Keys
                 .Select(f => Path.GetFileName(f)));
 
         // delete those files
         foreach (var file in removeFiles)
         {
+            logger.LogInformation($"Deleting file '{file}'...");
             await connection.DeleteFile(file, cancellationToken);
         }
 
-        // erase all files on device not in list of files to send
-
-        // send any file that has a different CRC
+        // 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)
+            {
+                if (int.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value)
+                {
+                    // exists and has a matching CRC, skip it
+                    continue;
+                }
+            }
+
+            await connection?.WriteFile(localFile.Key, null, cancellationToken);
+        }
 
 
         if (wasRuntimeEnabled)
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
index 3ac340d5..8f021015 100644
--- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
@@ -23,6 +23,24 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection,
             : Path;
 
         // is the path a file?
+        FileInfo file;
+
+        var lastFile = string.Empty;
+
+        connection.FileWriteProgress += (s, e) =>
+        {
+            var p = (e.completed / (double)e.total) * 100d;
+
+            if (e.fileName != lastFile)
+            {
+                Console.Write("\n");
+                lastFile = e.fileName;
+            }
+
+            // Console instead of Logger due to line breaking for progress bar
+            Console.Write($"Writing {e.fileName}: {p:0}%         \r");
+        };
+
         if (!File.Exists(path))
         {
             // is it a valid directory?
@@ -31,27 +49,31 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection,
                 Logger.LogError($"Invalid application path '{path}'");
                 return;
             }
-        }
-        else
-        {
-            // TODO: only deploy if it's App.dll
-        }
-
-        // do we have the full app path, or just the project root?
 
-        // TODO: determine the latest build
-
-        await AppManager.DeployApplication(connection, "", true, false, Logger, cancellationToken);
+            // 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 = Cli.PackageManager.GetAvailableBuiltConfigurations(path, "App.dll");
 
-        var success = false;
+                if (candidates.Length == 0)
+                {
+                    Logger.LogError($"Cannot find a compiled application at '{path}'");
+                    return;
+                }
 
-        if (!success)
-        {
-            Logger.LogError($"Build failed!");
+                file = candidates.OrderByDescending(c => c.LastWriteTime).First();
+            }
         }
         else
         {
-            Logger.LogError($"Build success.");
+            // TODO: only deploy if it's App.dll
+            file = new FileInfo(path);
         }
+
+        var targetDirectory = file.DirectoryName;
+
+        await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken);
     }
 }
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs
index 60b4d164..6fe51fec 100644
--- a/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppTrimCommand.cs
@@ -39,7 +39,7 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation
                 return;
             }
 
-            // it's a directory - we need to determine the latest build (they might have a Debug and Release config)
+            // 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)
diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
index 392ce786..49431f76 100644
--- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
+++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
@@ -8,6 +8,9 @@ namespace Meadow.Cli;
 
 public partial class PackageManager
 {
+    private const string IL_LINKER_DIR = "lib";
+    public const string PostLinkDirectoryName = "postlink_bin";
+
     private readonly List<string> dependencyMap = new();
 
     private string? _meadowAssembliesPath;
@@ -29,8 +32,6 @@ private string? MeadowAssembliesPath
         }
     }
 
-    private const string IL_LINKER_DIR = "lib";
-
     public async Task<IEnumerable<string>?> TrimDependencies(FileInfo file, List<string> dependencies, IList<string>? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null)
     {
         var prelink_dir = Path.Combine(file.DirectoryName, "prelink_bin");
@@ -63,7 +64,7 @@ private string? MeadowAssembliesPath
             }
         }
 
-        var postlink_dir = Path.Combine(file.DirectoryName, "postlink_bin");
+        var postlink_dir = Path.Combine(file.DirectoryName, PostLinkDirectoryName);
         if (Directory.Exists(postlink_dir))
         {
             Directory.Delete(postlink_dir, recursive: true);
diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json
index 81a4e378..fa80a0bc 100644
--- a/Source/v2/Meadow.Cli/Properties/launchSettings.json
+++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json
@@ -174,6 +174,14 @@
     "Dfu Install 0.10": {
       "commandName": "Project",
       "commandLineArgs": "dfu install -v 0.10"
+    },
+    "App Deploy (project folder)": {
+      "commandName": "Project",
+      "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1"
+    },
+    "App Deploy (untrimmed output)": {
+      "commandName": "Project",
+      "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1\\bin\\Debug\\netstandard2.1"
     }
   }
 }
\ No newline at end of file
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
index 9f4aed6d..82ef0575 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
@@ -1075,6 +1075,8 @@ public override async Task DeleteFile(string meadowFileName, CancellationToken?
         var command = RequestBuilder.Build<FileDeleteRequest>();
         command.MeadowFileName = meadowFileName;
 
+        _lastRequestConcluded = null;
+
         EnqueueRequest(command);
 
         await WaitForConcluded(null, cancellationToken);

From 1cfd9ab44289d52f71bf318f731e7ffd43402320 Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Wed, 27 Sep 2023 13:29:13 -0500
Subject: [PATCH 3/6] app deploy cleanup

---
 Source/v2/Meadow.Cli/AppManager.cs            | 22 +------------------
 .../Commands/Current/App/AppDeployCommand.cs  | 17 ++++++++++++++
 2 files changed, 18 insertions(+), 21 deletions(-)

diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs
index cfc51702..a957abbf 100644
--- a/Source/v2/Meadow.Cli/AppManager.cs
+++ b/Source/v2/Meadow.Cli/AppManager.cs
@@ -34,16 +34,6 @@ public static async Task DeployApplication(
         ILogger logger,
         CancellationToken cancellationToken)
     {
-        // in order to deploy, the runtime must be disabled
-        var wasRuntimeEnabled = await connection.IsRuntimeEnabled();
-
-        if (wasRuntimeEnabled)
-        {
-            logger.LogInformation("Disabling runtime...");
-
-            await connection.RuntimeDisable(cancellationToken);
-        }
-
         // TODO: add sub-folder support when HCOM supports it
 
         var localFiles = new Dictionary<string, uint>();
@@ -92,7 +82,7 @@ public static async Task DeployApplication(
 
             if (existing != null)
             {
-                if (int.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value)
+                if (uint.Parse(existing.Crc.Substring(2), System.Globalization.NumberStyles.HexNumber) == localFile.Value)
                 {
                     // exists and has a matching CRC, skip it
                     continue;
@@ -101,15 +91,5 @@ public static async Task DeployApplication(
 
             await connection?.WriteFile(localFile.Key, null, cancellationToken);
         }
-
-
-        if (wasRuntimeEnabled)
-        {
-            // restore runtime state
-            logger.LogInformation("Enabling runtime...");
-
-            await connection.RuntimeEnable(cancellationToken);
-        }
-
     }
 }
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
index 8f021015..b061b524 100644
--- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
@@ -27,6 +27,15 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection,
 
         var lastFile = string.Empty;
 
+        // in order to deploy, the runtime must be disabled
+        var wasRuntimeEnabled = await connection.IsRuntimeEnabled();
+        if (wasRuntimeEnabled)
+        {
+            Logger.LogInformation("Disabling runtime...");
+
+            await connection.RuntimeDisable(cancellationToken);
+        }
+
         connection.FileWriteProgress += (s, e) =>
         {
             var p = (e.completed / (double)e.total) * 100d;
@@ -75,5 +84,13 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection,
         var targetDirectory = file.DirectoryName;
 
         await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken);
+
+        if (wasRuntimeEnabled)
+        {
+            // restore runtime state
+            Logger.LogInformation("Enabling runtime...");
+
+            await connection.RuntimeEnable(cancellationToken);
+        }
     }
 }

From 2129c50ee7d4ab95adc968c7f2f269bbe7a0ebe3 Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Wed, 27 Sep 2023 17:45:16 -0500
Subject: [PATCH 4/6] Added app run command

---
 Source/v2/Meadow.Cli/AppManager.cs            |   5 +-
 .../Commands/Current/App/AppBuildCommand.cs   |   2 +-
 .../Commands/Current/App/AppDeployCommand.cs  |   7 +-
 .../Commands/Current/App/AppRunCommand.cs     | 163 ++++++++++++++++++
 Source/v2/Meadow.Cli/IPackageManager.cs       |   1 +
 .../PackageManager.AssemblyManager.cs         |   5 +-
 Source/v2/Meadow.Cli/PackageManager.cs        |  12 +-
 .../Meadow.Cli/Properties/launchSettings.json |   4 +
 .../SerialConnection.ListenerProc.cs          |   5 +
 .../Connections/SerialConnection.cs           |   4 +-
 10 files changed, 196 insertions(+), 12 deletions(-)
 create mode 100644 Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs

diff --git a/Source/v2/Meadow.Cli/AppManager.cs b/Source/v2/Meadow.Cli/AppManager.cs
index a957abbf..aa09722f 100644
--- a/Source/v2/Meadow.Cli/AppManager.cs
+++ b/Source/v2/Meadow.Cli/AppManager.cs
@@ -27,6 +27,7 @@ private static bool IsXmlDoc(string file)
     }
 
     public static async Task DeployApplication(
+        IPackageManager packageManager,
         IMeadowConnection connection,
         string localBinaryDirectory,
         bool includePdbs,
@@ -39,8 +40,10 @@ public static async Task DeployApplication(
         var localFiles = new Dictionary<string, uint>();
 
         // get a list of files to send
+        var dependencies = packageManager.GetDependencies(new FileInfo(Path.Combine(localBinaryDirectory, "App.dll")));
+
         logger.LogInformation("Generating the list of files to deploy...");
-        foreach (var file in Directory.GetFiles(localBinaryDirectory))
+        foreach (var file in dependencies)
         {
             // TODO: add any other filtering capability here
 
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs
index 1296f80e..5513faa9 100644
--- a/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppBuildCommand.cs
@@ -40,7 +40,7 @@ protected override async ValueTask ExecuteCommand(CancellationToken cancellation
 
         if (Configuration == null) Configuration = "Release";
 
-        Logger.LogInformation($"Building {Configuration} configuration of of {path}...");
+        Logger.LogInformation($"Building {Configuration} configuration of {path}...");
 
         // TODO: enable cancellation of this call
         var success = _packageManager.BuildApplication(path, Configuration);
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
index b061b524..535c718c 100644
--- a/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppDeployCommand.cs
@@ -8,12 +8,15 @@ namespace Meadow.CLI.Commands.DeviceManagement;
 [Command("app deploy", Description = "Deploys a built Meadow application to a target device")]
 public class AppDeployCommand : BaseDeviceCommand<AppDeployCommand>
 {
+    private IPackageManager _packageManager;
+
     [CommandParameter(0, Name = "Path to folder containing the built application", IsRequired = false)]
     public string? Path { get; set; } = default!;
 
-    public AppDeployCommand(MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory)
+    public AppDeployCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory)
         : base(connectionManager, loggerFactory)
     {
+        _packageManager = packageManager;
     }
 
     protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken)
@@ -83,7 +86,7 @@ protected override async ValueTask ExecuteCommand(IMeadowConnection connection,
 
         var targetDirectory = file.DirectoryName;
 
-        await AppManager.DeployApplication(connection, targetDirectory, true, false, Logger, cancellationToken);
+        await AppManager.DeployApplication(_packageManager, connection, targetDirectory, true, false, Logger, cancellationToken);
 
         if (wasRuntimeEnabled)
         {
diff --git a/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs
new file mode 100644
index 00000000..85caf8b2
--- /dev/null
+++ b/Source/v2/Meadow.Cli/Commands/Current/App/AppRunCommand.cs
@@ -0,0 +1,163 @@
+using CliFx.Attributes;
+using Meadow.Cli;
+using Meadow.Hcom;
+using Microsoft.Extensions.Logging;
+
+namespace Meadow.CLI.Commands.DeviceManagement;
+
+[Command("app run", Description = "Builds, trims and deploys a Meadow application to a target device")]
+public class AppRunCommand : BaseDeviceCommand<AppRunCommand>
+{
+    private IPackageManager _packageManager;
+    private string _lastFile;
+
+    [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; }
+
+    [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 AppRunCommand(IPackageManager packageManager, MeadowConnectionManager connectionManager, ILoggerFactory loggerFactory)
+        : base(connectionManager, loggerFactory)
+    {
+        _packageManager = packageManager;
+    }
+
+    protected override async ValueTask ExecuteCommand(IMeadowConnection connection, Hcom.IMeadowDevice device, CancellationToken cancellationToken)
+    {
+        string path = Path == null
+            ? AppDomain.CurrentDomain.BaseDirectory
+            : Path;
+
+        if (!Directory.Exists(path))
+        {
+            Logger.LogError($"Target directory '{path}' not found.");
+            return;
+        }
+
+        var lastFile = string.Empty;
+
+        // in order to deploy, the runtime must be disabled
+        var wasRuntimeEnabled = await connection.IsRuntimeEnabled();
+        if (wasRuntimeEnabled)
+        {
+            Logger.LogInformation("Disabling runtime...");
+
+            await connection.RuntimeDisable(cancellationToken);
+        }
+
+        if (!await BuildApplication(path, cancellationToken))
+        {
+            return;
+        }
+
+        if (!await TrimApplication(path, cancellationToken))
+        {
+            return;
+        }
+
+        // illink returns before all files are actually written.  That's not fun, but we must just wait a little while.
+        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<bool> BuildApplication(string path, CancellationToken cancellationToken)
+    {
+        if (Configuration == null) Configuration = "Debug";
+
+        Logger.LogInformation($"Building {Configuration} configuration of {path}...");
+
+        // TODO: enable cancellation of this call
+        return Task.FromResult(_packageManager.BuildApplication(path, Configuration));
+    }
+
+    private async Task<bool> 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 = Cli.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();
+
+        // if no configuration was provided, find the most recently built
+        Logger.LogInformation($"Trimming {file.FullName} (this may take a few seconds)...");
+
+        await _packageManager.TrimApplication(file, false, null, cancellationToken);
+
+        return true;
+    }
+
+    private async Task<bool> DeployApplication(IMeadowConnection connection, string path, CancellationToken cancellationToken)
+    {
+        connection.FileWriteProgress += OnFileWriteProgress;
+
+        var candidates = Cli.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.Write("\n");
+            _lastFile = e.fileName;
+        }
+
+        // Console instead of Logger due to line breaking for progress bar
+        Console.Write($"Writing {e.fileName}: {p:0}%         \r");
+    }
+
+    private void OnDeviceMessageReceived(object? sender, (string message, string? source) e)
+    {
+        if (NoPrefix)
+        {
+            Logger.LogInformation($"{e.message.TrimEnd('\n', '\r')}");
+        }
+        else
+        {
+            Logger.LogInformation($"{e.source}> {e.message.TrimEnd('\n', '\r')}");
+        }
+    }
+}
diff --git a/Source/v2/Meadow.Cli/IPackageManager.cs b/Source/v2/Meadow.Cli/IPackageManager.cs
index 55909a18..6012cd4c 100644
--- a/Source/v2/Meadow.Cli/IPackageManager.cs
+++ b/Source/v2/Meadow.Cli/IPackageManager.cs
@@ -2,6 +2,7 @@
 
 public interface IPackageManager
 {
+    List<string> GetDependencies(FileInfo file);
     bool BuildApplication(string projectFilePath, string configuration = "Release");
     Task TrimApplication(
         FileInfo applicationFilePath,
diff --git a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
index 49431f76..a2f66282 100644
--- a/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
+++ b/Source/v2/Meadow.Cli/PackageManager.AssemblyManager.cs
@@ -10,6 +10,7 @@ public partial class PackageManager
 {
     private const string IL_LINKER_DIR = "lib";
     public const string PostLinkDirectoryName = "postlink_bin";
+    public const string PreLinkDirectoryName = "prelink_bin";
 
     private readonly List<string> dependencyMap = new();
 
@@ -34,7 +35,7 @@ private string? MeadowAssembliesPath
 
     public async Task<IEnumerable<string>?> TrimDependencies(FileInfo file, List<string> dependencies, IList<string>? noLink, ILogger? logger, bool includePdbs, bool verbose = false, string? linkerOptions = null)
     {
-        var prelink_dir = Path.Combine(file.DirectoryName, "prelink_bin");
+        var prelink_dir = Path.Combine(file.DirectoryName, PreLinkDirectoryName);
         var prelink_app = Path.Combine(prelink_dir, file.Name);
         var prelink_os = Path.Combine(prelink_dir, "Meadow.dll");
 
@@ -149,7 +150,7 @@ private string? MeadowAssembliesPath
         return Directory.EnumerateFiles(postlink_dir);
     }
 
-    private List<string> GetDependencies(FileInfo file)
+    public List<string> GetDependencies(FileInfo file)
     {
         dependencyMap.Clear();
 
diff --git a/Source/v2/Meadow.Cli/PackageManager.cs b/Source/v2/Meadow.Cli/PackageManager.cs
index 480b959c..7f9fbd58 100644
--- a/Source/v2/Meadow.Cli/PackageManager.cs
+++ b/Source/v2/Meadow.Cli/PackageManager.cs
@@ -108,10 +108,6 @@ await TrimDependencies(
             verbose: false);
     }
 
-    public async Task DeployApplication()
-    {
-    }
-
     public static FileInfo[] GetAvailableBuiltConfigurations(string rootFolder, string appName = "App.dll")
     {
         if (!Directory.Exists(rootFolder)) throw new FileNotFoundException();
@@ -127,6 +123,13 @@ void FindApp(string directory, List<FileInfo> fileList)
         {
             foreach (var dir in Directory.GetDirectories(directory))
             {
+                var shortname = System.IO.Path.GetFileName(dir);
+
+                if (shortname == PackageManager.PostLinkDirectoryName || shortname == PackageManager.PreLinkDirectoryName)
+                {
+                    continue;
+                }
+
                 var file = Directory.GetFiles(dir).FirstOrDefault(f => string.Compare(Path.GetFileName(f), appName, true) == 0);
                 if (file != null)
                 {
@@ -134,6 +137,7 @@ void FindApp(string directory, List<FileInfo> fileList)
                 }
 
                 FindApp(dir, fileList);
+
             }
         }
 
diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json
index fa80a0bc..f656d678 100644
--- a/Source/v2/Meadow.Cli/Properties/launchSettings.json
+++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json
@@ -182,6 +182,10 @@
     "App Deploy (untrimmed output)": {
       "commandName": "Project",
       "commandLineArgs": "app deploy F:\\temp\\MeadowApplication1\\bin\\Debug\\netstandard2.1"
+    },
+    "App run": {
+      "commandName": "Project",
+      "commandLineArgs": "app run F:\\temp\\MeadowApplication1"
     }
   }
 }
\ No newline at end of file
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
index b8162443..53e599c3 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
@@ -257,6 +257,11 @@ private async Task ListenerProc()
                         // common if the port is reset/closed (e.g. mono enable/disable) - don't spew confusing info
                         Debug.WriteLine($"listen on closed port");
                     }
+                    catch (OperationCanceledException)
+                    {
+                        Debug.WriteLine($"Operation Cancelled");
+                        // TODO: figure out why this happens occasionally
+                    }
                     catch (Exception ex)
                     {
                         Debug.WriteLine($"listen error {ex.Message}");
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
index 82ef0575..e901ec8d 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
@@ -223,7 +223,7 @@ private void Close()
         }
     }
 
-    private void CommandManager()
+    private async void CommandManager()
     {
         while (!_isDisposed)
         {
@@ -236,7 +236,7 @@ private void CommandManager()
                 // if this is a file write, we need to packetize for progress
 
                 var payload = command.Serialize();
-                EncodeAndSendPacket(payload);
+                await EncodeAndSendPacket(payload);
 
                 // TODO: re-queue on fail?
             }

From 78783983e453910d9ecf5dee211786b8323db48d Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Thu, 28 Sep 2023 09:30:23 -0500
Subject: [PATCH 5/6] handling of device reset

---
 .../SerialConnection.ListenerProc.cs          | 37 ++++++++++++++++++-
 .../Connections/SerialConnection.cs           | 26 ++++---------
 2 files changed, 42 insertions(+), 21 deletions(-)

diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
index 53e599c3..5c6075c8 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
@@ -32,6 +32,7 @@ private async Task ListenerProc()
             var decodedBuffer = new byte[8192];
             var messageBytes = new CircularBuffer<byte>(8192 * 2);
             var delimiter = new byte[] { 0x00 };
+            var receivedLength = 0;
 
             while (!_isDisposed)
             {
@@ -41,7 +42,39 @@ private async Task ListenerProc()
                     {
                         Debug.WriteLine($"listening...");
 
-                        var receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length);
+                    read:
+                        try
+                        {
+                            receivedLength = _port.BaseStream.Read(readBuffer, 0, readBuffer.Length);
+                        }
+                        catch (OperationCanceledException)
+                        {
+                            Debug.WriteLine($"Device reset detected");
+
+                            var timeout = 20;
+
+                            while (!_port.IsOpen)
+                            {
+                                await Task.Delay(500);
+
+                                if (timeout-- < 0)
+                                {
+                                    return;
+                                }
+
+                                try
+                                {
+                                    _port.Open();
+                                    Debug.WriteLine($"Port re-opened");
+                                }
+                                catch
+                                {
+                                    Debug.WriteLine($"Failed to re-open port");
+                                }
+                            }
+
+                            goto read;
+                        }
 
                         Debug.WriteLine($"Received {receivedLength} bytes");
 
@@ -259,8 +292,8 @@ private async Task ListenerProc()
                     }
                     catch (OperationCanceledException)
                     {
+                        // this happens on disconnect - could be cable pulled, could be device reset
                         Debug.WriteLine($"Operation Cancelled");
-                        // TODO: figure out why this happens occasionally
                     }
                     catch (Exception ex)
                     {
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
index e901ec8d..f94a982b 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.cs
@@ -461,7 +461,7 @@ private bool DecodeAndProcessPacket(Memory<byte> packetBuffer, CancellationToken
         return true;
     }
 
-    protected virtual void Dispose(bool disposing)
+    protected override void Dispose(bool disposing)
     {
         if (!_isDisposed)
         {
@@ -475,12 +475,6 @@ protected virtual void Dispose(bool disposing)
         }
     }
 
-    public void Dispose()
-    {
-        Dispose(disposing: true);
-        GC.SuppressFinalize(this);
-    }
-
     // ----------------------------------------------
     // ----------------------------------------------
     // ----------------------------------------------
@@ -640,14 +634,11 @@ public override async Task RuntimeEnable(CancellationToken? cancellationToken =
 
         InfoMessages.Clear();
 
-        EnqueueRequest(command);
-
-        // we have to give time for the device to actually reset
-        await Task.Delay(500);
+        _lastRequestConcluded = null;
 
-        var success = await WaitForResponseText(RuntimeSucessfullyEnabledToken);
+        EnqueueRequest(command);
 
-        if (!success) throw new Exception("Unable to enable runtime");
+        await WaitForConcluded(null, cancellationToken);
     }
 
     public override async Task RuntimeDisable(CancellationToken? cancellationToken = null)
@@ -656,14 +647,11 @@ public override async Task RuntimeDisable(CancellationToken? cancellationToken =
 
         InfoMessages.Clear();
 
-        EnqueueRequest(command);
-
-        // we have to give time for the device to actually reset
-        await Task.Delay(500);
+        _lastRequestConcluded = null;
 
-        var success = await WaitForResponseText(RuntimeSucessfullyDisabledToken);
+        EnqueueRequest(command);
 
-        if (!success) throw new Exception("Unable to disable runtime");
+        await WaitForConcluded(null, cancellationToken);
     }
 
     public override async Task TraceEnable(CancellationToken? cancellationToken = null)

From 1d104870e57b1b1fe0307a69fff89ffc07f175d1 Mon Sep 17 00:00:00 2001
From: Chris Tacke <ctacke@opennetcf.com>
Date: Thu, 28 Sep 2023 10:50:07 -0500
Subject: [PATCH 6/6] fix WaitForAttach when we have ever been attached (i.e.
 DFU -> boot)

---
 .../Meadow.Cli/Properties/launchSettings.json |  2 +-
 .../SerialConnection.ListenerProc.cs          | 25 +++++++++++++++++--
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/Source/v2/Meadow.Cli/Properties/launchSettings.json b/Source/v2/Meadow.Cli/Properties/launchSettings.json
index f656d678..ee03220d 100644
--- a/Source/v2/Meadow.Cli/Properties/launchSettings.json
+++ b/Source/v2/Meadow.Cli/Properties/launchSettings.json
@@ -113,7 +113,7 @@
     },
     "Firmware Write all": {
       "commandName": "Project",
-      "commandLineArgs": "firmware write runtime esp"
+      "commandLineArgs": "firmware write"
     },
     "Firmware Write version": {
       "commandName": "Project",
diff --git a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
index 5c6075c8..c253b398 100644
--- a/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
+++ b/Source/v2/Meadow.Hcom/Connections/SerialConnection.ListenerProc.cs
@@ -18,9 +18,30 @@ public override async Task WaitForMeadowAttach(CancellationToken? cancellationTo
                 if (cancellationToken?.IsCancellationRequested ?? false) throw new TaskCanceledException();
                 if (timeout <= 0) throw new TimeoutException();
 
-                if (State == ConnectionState.MeadowAttached) return;
+                if (State == ConnectionState.MeadowAttached)
+                {
+                    if (Device == null)
+                    {
+                        // no device set - this happens when we are waiting for attach from DFU mode
+                        await Attach(cancellationToken, 5);
+                    }
+
+                    return;
+                }
 
                 await Task.Delay(500);
+
+                if (!_port.IsOpen)
+                {
+                    try
+                    {
+                        Open();
+                    }
+                    catch (Exception ex)
+                    {
+                        Debug.WriteLine($"Unable to open port: {ex.Message}");
+                    }
+                }
             }
 
             throw new TimeoutException();
@@ -64,7 +85,7 @@ private async Task ListenerProc()
 
                                 try
                                 {
-                                    _port.Open();
+                                    Open();
                                     Debug.WriteLine($"Port re-opened");
                                 }
                                 catch