From 425b1c10b5f86c4c0603842d65d81cfd1f3c2620 Mon Sep 17 00:00:00 2001
From: Paul Grayson <pdg@alum.mit.edu>
Date: Sat, 31 Jul 2021 14:33:30 -0700
Subject: [PATCH 1/3] proof of concept for downloading a file "data.txt" from
 the microfs

---
 lang/en.json                        |  6 ++-
 src/messages/en.json                |  8 +++-
 src/project/DownloadFlashButton.tsx |  8 ++++
 src/project/project-actions.tsx     | 68 +++++++++++++++++++++++++++++
 4 files changed, 88 insertions(+), 2 deletions(-)

diff --git a/lang/en.json b/lang/en.json
index 6c389384e..7c56e1110 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -175,6 +175,10 @@
     "defaultMessage": "Download a project hex file",
     "description": "Hover text over download button"
   },
+  "download-microfs-files": {
+    "defaultMessage": "Download MicroFS files",
+    "description": "Download button menu option for downloading on-board MicroFS files"
+  },
   "download-python": {
     "defaultMessage": "Download Python script",
     "description": "Download button menu option for downloading Python script"
@@ -443,4 +447,4 @@
     "defaultMessage": "Zoom out",
     "description": "Text label for zoom out button"
   }
-}
\ No newline at end of file
+}
diff --git a/src/messages/en.json b/src/messages/en.json
index c0a8eecea..3b6f7a205 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -355,6 +355,12 @@
       "value": "Download a project hex file"
     }
   ],
+  "download-microfs-files": [
+    {
+      "type": 0,
+      "value": "Download MicroFS files"
+    }
+  ],
   "download-python": [
     {
       "type": 0,
@@ -877,4 +883,4 @@
       "value": "Zoom out"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/project/DownloadFlashButton.tsx b/src/project/DownloadFlashButton.tsx
index 33e27766b..917ff3aff 100644
--- a/src/project/DownloadFlashButton.tsx
+++ b/src/project/DownloadFlashButton.tsx
@@ -93,6 +93,14 @@ const DownloadFlashButton = ({ size }: DownloadFlashButtonProps) => {
               >
                 <FormattedMessage id="download-python" />
               </MenuItem>
+              <MenuItem
+                target="_blank"
+                rel="noopener"
+                icon={<RiDownload2Line />}
+                onClick={actions.downloadMicrofsFiles}
+              >
+                <FormattedMessage id="download-microfs-files" />
+              </MenuItem>
             </MenuList>
           </Portal>
         </ButtonGroup>
diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx
index 0d0907b02..866c1103a 100644
--- a/src/project/project-actions.tsx
+++ b/src/project/project-actions.tsx
@@ -39,6 +39,7 @@ import {
   isPythonFile,
   validateNewFilename,
 } from "./project-utils";
+import { EVENT_SERIAL_DATA, EVENT_SERIAL_RESET } from "../device/device";
 
 /**
  * Distinguishes the different ways to trigger the load action.
@@ -374,6 +375,73 @@ export class ProjectActions {
     }
   };
 
+  downloadMicrofsFiles = async () => {
+    this.logging.event({
+      type: "download-microfs-files",
+    });
+    if ( this.device.status != "CONNECTED" )
+    {
+      return this.webusbNotSupportedError();
+    }
+    let output = ""
+    let foundOK = false
+    const serialListener = (data: string) => {
+      output = output + data;
+      if(!foundOK)
+      {
+        const okindex = output.indexOf("OK");
+        if(okindex != -1)
+        {
+          output = output.substring(okindex+2);
+          foundOK = true;
+        }
+      }
+      if(foundOK)
+      {
+        const endindex = output.indexOf("END\r");
+        if(endindex != -1)
+        {
+          output = output.substring(endindex, -1)
+          this.device.removeListener(EVENT_SERIAL_DATA, serialListener);
+          const lines = output.split("\r\n").map(l => l.replace(/([0-9a-z]{2})/g, x => unescape("%"+x)));
+          const blob = new Blob(lines, {
+            type: "application/octet-stream",
+          });
+          saveAs(blob, 'data.txt');
+        }
+      }
+//      output = output + JSON.parse(data)
+    };
+    this.device.on(EVENT_SERIAL_DATA, serialListener);
+    const script = [
+      '\x02', // Ctrl+B to end raw mode if required
+      '\x03', // Ctrl+C three times to break
+      '\x03',
+      '\x03',
+      '\x01', // Ctrl+A to enter raw mode
+      'f = open("data.txt", "rb")\r\n',
+      'r = f.read\r\n',
+      'result = True\r\n',
+      'while result:\r\n',
+      '  result = r(32)\r\n',
+      '  if result:\r\n',
+      '    print("".join("%02x" % i for i in result)+"\\r\\n")\r\n',
+      'print("END\\r\\n")\r\n',
+      'f.close()\r\n',
+      '\x04', // Ctrl+D to run script
+      '\x02', // Ctrl+B to exit raw mode
+    ];
+    let i = 0;
+    let p = null;
+    const f = () => {
+      if (i >= script.length) return;
+      p = this.device.serialWrite(script[i]);
+      i = i + 1;
+      p.then(f);
+    }
+    f();
+  };
+
   /**
    * Create a file, prompting the user for the name.
    */

From a83a39236193368147e2f9f99a3bbd54856829ce Mon Sep 17 00:00:00 2001
From: Paul Grayson <pdg@alum.mit.edu>
Date: Sat, 31 Jul 2021 15:01:12 -0700
Subject: [PATCH 2/3] simpler url encoding, commenting

---
 src/project/project-actions.tsx | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx
index 866c1103a..6447ce18b 100644
--- a/src/project/project-actions.tsx
+++ b/src/project/project-actions.tsx
@@ -403,16 +403,32 @@ export class ProjectActions {
         {
           output = output.substring(endindex, -1)
           this.device.removeListener(EVENT_SERIAL_DATA, serialListener);
-          const lines = output.split("\r\n").map(l => l.replace(/([0-9a-z]{2})/g, x => unescape("%"+x)));
+
+          // We got a complete file.
+          // Decode the hex and send as a data URI.
+          const lines = output.split("\r\n").map(l => unescape(l))
           const blob = new Blob(lines, {
             type: "application/octet-stream",
           });
           saveAs(blob, 'data.txt');
         }
       }
-//      output = output + JSON.parse(data)
     };
     this.device.on(EVENT_SERIAL_DATA, serialListener);
+
+    // This script is similar to the one used by microfs.py: It enters
+    // raw mode and reads the file 32 bytes at a time.  Unlike
+    // microfs.py, which uses repr(), here we convert each byte to
+    // urlencoded hex.
+    //
+    // We probably need to figure out how to enter raw mode more
+    // reliably (microfs.py has some delays that I did not implement,
+    // and I have no error handling at all here) and disable Xterm
+    // output while doing the download.
+    //
+    // Note that there's an apparent bug with sending multiple lines
+    // at a time:
+    // https://github.com/microbit-foundation/python-editor-next/issues/215
     const script = [
       '\x02', // Ctrl+B to end raw mode if required
       '\x03', // Ctrl+C three times to break
@@ -425,12 +441,15 @@ export class ProjectActions {
       'while result:\r\n',
       '  result = r(32)\r\n',
       '  if result:\r\n',
-      '    print("".join("%02x" % i for i in result)+"\\r\\n")\r\n',
+      '    print("".join("%%%02x" % i for i in result)+"\\r\\n")\r\n',
       'print("END\\r\\n")\r\n',
       'f.close()\r\n',
       '\x04', // Ctrl+D to run script
       '\x02', // Ctrl+B to exit raw mode
     ];
+
+    // there's probably a more correct way to send one line at a time
+    // asynchronously
     let i = 0;
     let p = null;
     const f = () => {

From b503f2e61f2238cee8641eb5f02290e142f51d3f Mon Sep 17 00:00:00 2001
From: Paul Grayson <pdg@alum.mit.edu>
Date: Sat, 31 Jul 2021 15:02:20 -0700
Subject: [PATCH 3/3] commented error message

---
 src/project/project-actions.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx
index 6447ce18b..06010859b 100644
--- a/src/project/project-actions.tsx
+++ b/src/project/project-actions.tsx
@@ -381,6 +381,7 @@ export class ProjectActions {
     });
     if ( this.device.status != "CONNECTED" )
     {
+      // TODO: make my own error (or is there an appropriate one?)
       return this.webusbNotSupportedError();
     }
     let output = ""