<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>day54 &amp;mdash; Gerrit Niezen</title>
    <link>https://gerritniezen.com/tag:day54</link>
    <description>Maker of open-source software and hardware.</description>
    <pubDate>Mon, 20 Apr 2026 02:13:49 +0000</pubDate>
    <image>
      <url>https://i.snap.as/aMPXpIot.png</url>
      <title>day54 &amp;mdash; Gerrit Niezen</title>
      <link>https://gerritniezen.com/tag:day54</link>
    </image>
    <item>
      <title>Taking a deep dive into MTP: Part 5</title>
      <link>https://gerritniezen.com/taking-a-deep-dive-into-mtp-part-5?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[This is the final instalment of my second series of blog posts on MTP. I now have my code working in both the browser and Node.js, where it connects to an Android device, reads the latest file and saves it locally.&#xA;&#xA;First off, I had to write some code to figure out whether we&#39;re in the browser, in Node.js or in Electron:&#xA;&#xA;let isBrowser = null;&#xA;&#xA;if (typeof navigator !== &#39;undefined&#39;) {&#xA;  const userAgent = navigator.userAgent.toLowerCase();&#xA;  isBrowser = userAgent.indexOf(&#39; electron/&#39;) === -1 &amp;&amp; typeof window !== &#39;undefined&#39;;&#xA;} else {&#xA;  // Node.js process&#xA;  isBrowser = false;&#xA;}&#xA;&#xA;I&#39;m sure there are modules out there that do this better, but it works for now. Now we have to load some modules for Node.js when we&#39;re not in the browser:&#xA;&#xA;let usb, fs = null;&#xA;&#xA;if (!isBrowser) {&#xA;  // For Node.js and Electron&#xA;  usb = require(&#39;webusb&#39;).usb;&#xA;  EventTarget = require(&#39;events&#39;);&#xA;  fs = require(&#39;fs&#39;);&#xA;} else {&#xA;  usb = navigator.usb; // Yay, we&#39;re using WebUSB!&#xA;}&#xA;&#xA;We also need to be able to save the file to disk in both the browser and Node.js:&#xA;&#xA;const array = new Uint8Array(data.payload);&#xA;&#xA;if (isBrowser) {&#xA;  const file = new Blob([array]);&#xA;  const a = document.createElement(&#39;a&#39;),&#xA;  url = URL.createObjectURL(file);&#xA;  a.href = url;&#xA;  a.download = this.filename;&#xA;  document.body.appendChild(a);&#xA;  a.click();&#xA;  setTimeout(function() {&#xA;    document.body.removeChild(a);&#xA;    window.URL.revokeObjectURL(url);&#xA;  }, 0);&#xA;} else {&#xA;  fs.writeFileSync(this.filename, array);&#xA;}&#xA;&#xA;We also need to deal with event listeners differently in the browser and Node.js:&#xA;&#xA;const initMTP = () =  {&#xA;  const mtp = new Mtp(0x0e8d, 0x201d);&#xA;&#xA;  if (isBrowser) {&#xA;    mtp.addEventListener(&#39;error&#39;, err =  console.log(&#39;Error&#39;, err));&#xA;&#xA;    mtp.addEventListener(&#39;ready&#39;, async () =  {&#xA;      mtp.addEventListener(&#39;data&#39;, (e) =  mtp.dataHandler(e.detail));&#xA;      await mtp.openSession();&#xA;    } );&#xA;  } else {&#xA;    mtp.on(&#39;error&#39;, err =  console.log(&#39;Error&#39;, err));&#xA;    mtp.on(&#39;ready&#39;, async () =  {&#xA;      mtp.on(&#39;data&#39;, (data) =  mtp.dataHandler(data));&#xA;      await mtp.openSession();&#xA;    });&#xA;  }&#xA;};&#xA;&#xA;And finally, WebUSB requires a user interaction before it will show the permission prompt, so we need:&#xA;&#xA;if (isBrowser) {&#xA;  document.addEventListener(&#39;DOMContentLoaded&#39;, event =  {&#xA;    let button = document.getElementById(&#39;connect&#39;);&#xA;&#xA;    button.addEventListener(&#39;click&#39;, async() =  {&#xA;      initMTP();&#xA;    });&#xA; });&#xA;} else {&#xA;  initMTP();&#xA;}&#xA;&#xA;So, that&#39;s it! Over on GitHub I have the full implementation. If you run it in Node.js, it will save the newest file on your Android device to disk. If you run it in the browser, it will do the same after you click the Connect button:&#xA;&#xA;html&#xA;  head&#xA;    titleMTP over WebUSB/title&#xA;    script src=&#34;mtp.js&#34;/script&#xA;  /head&#xA;  body&#xA;    button id=&#34;connect&#34;Connect/button&#xA;  /body&#xA;/html&#xA;&#xA;---&#xA;I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting https://100daystooffload.com.&#xA;&#xA;#100DaysToOffload #day54 #mtp&#xA;&#xA;iComment on this post/i&#xD;&#xA;div id=&#34;cusdis_thread&#34;/div]]&gt;</description>
      <content:encoded><![CDATA[<p>This is the final instalment of my <a href="https://gerritniezen.com/tag:mtp">second series of blog posts on MTP</a>. I now have my code working in both the browser and Node.js, where it connects to an Android device, reads the latest file and saves it locally.</p>

<p>First off, I had to write some code to figure out whether we&#39;re in the browser, in Node.js or in Electron:</p>

<pre><code class="language-js">let isBrowser = null;

if (typeof navigator !== &#39;undefined&#39;) {
  const userAgent = navigator.userAgent.toLowerCase();
  isBrowser = userAgent.indexOf(&#39; electron/&#39;) === -1 &amp;&amp; typeof window !== &#39;undefined&#39;;
} else {
  // Node.js process
  isBrowser = false;
}
</code></pre>

<p>I&#39;m sure there are modules out there that do this better, but it works for now. Now we have to load some modules for Node.js when we&#39;re not in the browser:</p>

<pre><code class="language-js">let usb, fs = null;

if (!isBrowser) {
  // For Node.js and Electron
  usb = require(&#39;webusb&#39;).usb;
  EventTarget = require(&#39;events&#39;);
  fs = require(&#39;fs&#39;);
} else {
  usb = navigator.usb; // Yay, we&#39;re using WebUSB!
}
</code></pre>

<p>We also need to be able to save the file to disk in both the browser and Node.js:</p>

<pre><code class="language-js">const array = new Uint8Array(data.payload);

if (isBrowser) {
  const file = new Blob([array]);
  const a = document.createElement(&#39;a&#39;),
  url = URL.createObjectURL(file);
  a.href = url;
  a.download = this.filename;
  document.body.appendChild(a);
  a.click();
  setTimeout(function() {
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  }, 0);
} else {
  fs.writeFileSync(this.filename, array);
}
</code></pre>

<p>We also need to deal with event listeners differently in the browser and Node.js:</p>

<pre><code class="language-js">const initMTP = () =&gt; {
  const mtp = new Mtp(0x0e8d, 0x201d);

  if (isBrowser) {
    mtp.addEventListener(&#39;error&#39;, err =&gt; console.log(&#39;Error&#39;, err));

    mtp.addEventListener(&#39;ready&#39;, async () =&gt; {
      mtp.addEventListener(&#39;data&#39;, (e) =&gt; mtp.dataHandler(e.detail));
      await mtp.openSession();
    } );
  } else {
    mtp.on(&#39;error&#39;, err =&gt; console.log(&#39;Error&#39;, err));
    mtp.on(&#39;ready&#39;, async () =&gt; {
      mtp.on(&#39;data&#39;, (data) =&gt; mtp.dataHandler(data));
      await mtp.openSession();
    });
  }
};
</code></pre>

<p>And finally, WebUSB requires a user interaction before it will show the permission prompt, so we need:</p>

<pre><code class="language-js">if (isBrowser) {
  document.addEventListener(&#39;DOMContentLoaded&#39;, event =&gt; {
    let button = document.getElementById(&#39;connect&#39;);

    button.addEventListener(&#39;click&#39;, async() =&gt; {
      initMTP();
    });
 });
} else {
  initMTP();
}
</code></pre>

<p>So, that&#39;s it! Over on <a href="https://github.com/tidepool-org/node-mtp/blob/pure-js/mtp.js">GitHub</a> I have the full implementation. If you run it in Node.js, it will save the newest file on your Android device to disk. If you run it in the browser, it will do the same after you click the Connect button:</p>

<pre><code class="language-html">&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;MTP over WebUSB&lt;/title&gt;
    &lt;script src=&#34;mtp.js&#34;&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;button id=&#34;connect&#34;&gt;Connect&lt;/button&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>

<hr/>

<p>I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting <a href="https://100daystooffload.com">https://100daystooffload.com</a>.</p>

<p><a href="https://gerritniezen.com/tag:100DaysToOffload" class="hashtag"><span>#</span><span class="p-category">100DaysToOffload</span></a> <a href="https://gerritniezen.com/tag:day54" class="hashtag"><span>#</span><span class="p-category">day54</span></a> <a href="https://gerritniezen.com/tag:mtp" class="hashtag"><span>#</span><span class="p-category">mtp</span></a></p>

<p><i>Comment on this post</i>
<div id="cusdis_thread" id="cusdis_thread"></div></p>
]]></content:encoded>
      <guid>https://gerritniezen.com/taking-a-deep-dive-into-mtp-part-5</guid>
      <pubDate>Mon, 06 Jul 2020 15:12:58 +0000</pubDate>
    </item>
  </channel>
</rss>