diff --git a/clients/chrome/README.md b/clients/chrome/README.md new file mode 100644 index 00000000..b3261250 --- /dev/null +++ b/clients/chrome/README.md @@ -0,0 +1,26 @@ +# microbit-chrome +Prototype chrome addon that exposes the micro:bit's serial output to webpages. +* watch the [demo video](https://vimeo.com/146207766) + +# Installation +See [developer.chrome.com](https://developer.chrome.com/extensions/getstarted#unpacked) +for instructions on how to install the local version into your chrome browser. + +# Requirements +* Chrome 48 or later. + +# Sample page +The `demo.html` webpage goes along with the +https://github.com/Microsoft/microbit-touchdevelop/blob/master/examples/tcs34725.cpp +program. Run `http-server` from this directory, then visit +http://localhost:8080/demo.html +(keep in mind that pages served from `file://` cannot open ports). + +# Building + +Open a command prompt and run the following commands. + +```` +npm install +typings update +```` \ No newline at end of file diff --git a/clients/chrome/background.js b/clients/chrome/background.js new file mode 100644 index 00000000..777b73ae --- /dev/null +++ b/clients/chrome/background.js @@ -0,0 +1,68 @@ +/// +var connections = []; +// A list of "ports", i.e. connected clients (such as web pages). Multiple web +// pages can connect to our service: they all receive the same data. +var ports = []; +function byPath(path) { + return connections.filter(function (x) { return x.path == path; }); +} +function byId(id) { + return connections.filter(function (x) { return x.id == id; }); +} +function onReceive(data, id) { + if (ports.length == 0) + return; + var view = new DataView(data); + var decoder = new TextDecoder("utf-8"); + var decodedString = decoder.decode(view); + ports.forEach(function (port) { return port.postMessage({ + type: "serial", + data: decodedString, + id: id + }); }); +} +function findNewDevices() { + chrome.serial.getDevices(function (serialPorts) { + serialPorts.forEach(function (serialPort) { + if (byPath(serialPort.path).length == 0 && + serialPort.displayName == "mbed Serial Port") { + chrome.serial.connect(serialPort.path, { bitrate: 115200 }, function (info) { + // In case the [connect] operation takes more than five seconds... + if (info && byPath(serialPort.path).length == 0) + connections.push({ + id: info.connectionId, + path: serialPort.path + }); + }); + } + }); + }); +} +function main() { + // Register new clients in the [ports] global variable. + chrome.runtime.onConnectExternal.addListener(function (port) { + if (/^(micro:bit|touchdevelop|yelm|pxt|codemicrobit|codethemicrobit)$/.test(port.name)) { + ports.push(port); + port.onDisconnect.addListener(function () { + ports = ports.filter(function (x) { return x != port; }); + }); + } + }); + // When receiving data for one of the connections that we're tracking, forward + // it to all connected clients. + chrome.serial.onReceive.addListener(function (info) { + if (byId(info.connectionId).length > 0) + onReceive(info.data, info.connectionId); + }); + // When it looks like we've been disconnected, drop the corresponding + // connection object from the [connections] global variable. + chrome.serial.onReceiveError.addListener(function (info) { + if (info.error == "system_error" || info.error == "disconnected" || info.error == "device_lost") + connections = connections.filter(function (x) { return x.id != info.connectionId; }); + }); + // Probe serial connections at regular intervals. In case we find an mbed port + // we haven't yet connected to, connect to it. + setInterval(findNewDevices, 5000); + findNewDevices(); +} +document.addEventListener("DOMContentLoaded", main); diff --git a/clients/chrome/background.ts b/clients/chrome/background.ts new file mode 100644 index 00000000..8925a363 --- /dev/null +++ b/clients/chrome/background.ts @@ -0,0 +1,92 @@ +// A list of: { +// id: number; +// path: string; +// } where [id] is the [connectionId] (internal to Chrome) and [path] is the +// OS' name for the device (e.g. "COM4"). +interface Connection { + id: string; + path: string; +} +let connections: Connection[] = []; + +// A list of "ports", i.e. connected clients (such as web pages). Multiple web +// pages can connect to our service: they all receive the same data. +let ports = []; + +interface Message { + type: string; + data: string; + id: string; +} + +function byPath(path: string): Connection[] { + return connections.filter((x) => x.path == path); +} + +function byId(id: string): Connection[] { + return connections.filter((x) => x.id == id); +} + +function onReceive(data, id: string) { + if (ports.length == 0) return; + + let view = new DataView(data); + let decoder = new TextDecoder("utf-8"); + let decodedString = decoder.decode(view); + ports.forEach(port => port.postMessage({ + type: "serial", + data: decodedString, + id: id, + })); +} + +function findNewDevices() { + chrome.serial.getDevices(function (serialPorts) { + serialPorts.forEach(function (serialPort) { + if (byPath(serialPort.path).length == 0 && + serialPort.displayName == "mbed Serial Port") { + chrome.serial.connect(serialPort.path, { bitrate: 115200 }, function (info) { + // In case the [connect] operation takes more than five seconds... + if (info && byPath(serialPort.path).length == 0) + connections.push({ + id: info.connectionId, + path: serialPort.path + }); + }); + } + }); + }); +} + +function main() { + // Register new clients in the [ports] global variable. + chrome.runtime.onConnectExternal.addListener(function (port) { + if (/^(micro:bit|touchdevelop|yelm|pxt|codemicrobit|codethemicrobit)$/.test(port.name)) { + ports.push(port); + port.onDisconnect.addListener(function () { + ports = ports.filter(function (x) { return x != port }); + }); + } + }); + + // When receiving data for one of the connections that we're tracking, forward + // it to all connected clients. + chrome.serial.onReceive.addListener(function (info) { + if (byId(info.connectionId).length > 0) + onReceive(info.data, info.connectionId); + }); + + // When it looks like we've been disconnected, drop the corresponding + // connection object from the [connections] global variable. + chrome.serial.onReceiveError.addListener(function (info) { + if (info.error == "system_error" || info.error == "disconnected" || info.error == "device_lost") + connections = connections.filter((x) => x.id != info.connectionId); + }); + + // Probe serial connections at regular intervals. In case we find an mbed port + // we haven't yet connected to, connect to it. + setInterval(findNewDevices, 5000); + findNewDevices(); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/clients/chrome/logo128.png b/clients/chrome/logo128.png new file mode 100644 index 00000000..eae35f4c Binary files /dev/null and b/clients/chrome/logo128.png differ diff --git a/clients/chrome/logo48.png b/clients/chrome/logo48.png new file mode 100644 index 00000000..0af483f8 Binary files /dev/null and b/clients/chrome/logo48.png differ diff --git a/clients/chrome/manifest.json b/clients/chrome/manifest.json new file mode 100644 index 00000000..ae3adecb --- /dev/null +++ b/clients/chrome/manifest.json @@ -0,0 +1,28 @@ +{ + "app": { + "background": { + "scripts": [ "background.js" ] + } + }, + + "manifest_version": 2, + "name": "code the micro:bit", + "version": "0.1.0", + "author": "Microsoft Corporation", + "short_name": "code the micro:bit", + + "description": "This extension reads the serial output from connected BBC micro:bit and sends it to https://codethemicrobit.com.", + "icons": { + "48": "logo48.png", + "128": "logo128.png" + }, + + "permissions": [ + "serial", + "usb" + ], + + "externally_connectable": { + "matches": [ "*://localhost/*", "https://*.pxt.io/*", "https://codethemicrobit.com/*" ] + } +} diff --git a/clients/chrome/screenshot.png b/clients/chrome/screenshot.png new file mode 100644 index 00000000..c6f4b56b Binary files /dev/null and b/clients/chrome/screenshot.png differ diff --git a/clients/chrome/tsconfig.json b/clients/chrome/tsconfig.json new file mode 100644 index 00000000..ffa98b79 --- /dev/null +++ b/clients/chrome/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compiler-options": { + "target": "ES5", + "module": "amd", + "sourceMap": false + } + } \ No newline at end of file