├── .gitignore ├── LICENSE ├── README.md ├── arduino └── webusb-lol.ino ├── serial.js └── web ├── index.html ├── lol.css └── lol.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Edwin Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebUSB 2 | 3 | WebUSB enabled LOL shield 4 | 5 | ## Chrome support 6 | 7 | Chrome 61, released in October 2017, supports WebUSB. The idea is simple. You connect an USB device, 8 | a popup appears and clicking it will open a website. On this site, you can change settings, 9 | download data etcetera. It works on every operating system, there's no local software to install and it's always up to date. 10 | 11 | Google provides instructions to build you own WebUSB device with an Arduino Leonardo. 12 | Not all Arduino's support WebUSB, like the UNO. See the [list of supported Arduino's](https://github.com/webusb/arduino) for WebUSB support. 13 | 14 | On this same page you can read how to change an Arduino source file to make it support USB 2.1. 15 | 16 | For more information, you can read this [article about WebUSB](https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web) on the Google Developer website. 17 | 18 | ## So how do you build this yourself? 19 | 20 | You need an Arduino Leonardo, a LOL shield and the Arduino IDE. 21 | 22 | Change to Arduino source file to support USB 2.1, as described above and upload webusb-lol.ino to 23 | the Arduino Leonardo. 24 | 25 | Full instructions can be found on the [WebUSB project page](https://www.hackster.io/edwin-martin/webusb-85b6bb) on hackster.io. 26 | -------------------------------------------------------------------------------- /arduino/webusb-lol.ino: -------------------------------------------------------------------------------- 1 | /* 2 | LOL Shield for WebUSB 3 | 4 | Copyright 20017 Edwin Martin https://bitstorm.org/ 5 | 6 | Contains code from Benjamin Sonntag: 7 | Copyright 2009/2010 Benjamin Sonntag http://benjamin.sonntag.fr/ 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 59 Temple Place - Suite 330, 22 | Boston, MA 02111-1307, USA. 23 | */ 24 | 25 | #include 26 | #include 27 | #include "Charliplexing.h" 28 | #include "Font.h" 29 | 30 | #if defined(ARDUINO) && ARDUINO >= 100 31 | #include "Arduino.h" 32 | #else 33 | #include "WProgram.h" 34 | #endif 35 | 36 | WebUSB WebUSBSerial(1, "bitstorm.org/webusb/lol/"); 37 | 38 | #define Serial WebUSBSerial 39 | 40 | const int maxLength = 100; 41 | int lolPosition = 0; 42 | int i = 0; 43 | bool serialInitialised = false; 44 | int8_t x = DISPLAY_COLS; 45 | const char magic[] = "LolS"; 46 | const int magicSize = 4; 47 | char json[200]; 48 | 49 | char lolText[maxLength]; 50 | 51 | void setup() { 52 | byte c; 53 | 54 | if (EEPROM.read(0) == magic[0] && EEPROM.read(1) == magic[1] && EEPROM.read(2) == magic[2] && EEPROM.read(3) == magic[3]) { 55 | for (int i = 0; c; i++) { 56 | c = EEPROM.read(i + magicSize); 57 | lolText[i] = c; 58 | } 59 | } else { 60 | strcpy(lolText, "HELLO WORLD! "); 61 | } 62 | 63 | LedSign::Init(); 64 | } 65 | 66 | void loop() { 67 | if (Serial && !serialInitialised) { 68 | Serial.begin(9600); 69 | serialInitialised = true; 70 | } 71 | 72 | while (Serial && Serial.available()) { 73 | int byte = Serial.read(); 74 | 75 | if (byte == '\n' || lolPosition == maxLength - 1) { 76 | if (lolPosition == 0) { 77 | Serial.write(lolText); 78 | Serial.write("\r\n"); 79 | Serial.flush(); 80 | } else { 81 | lolText[lolPosition] = '\0'; 82 | EEPROM.write(lolPosition + magicSize, '\0'); 83 | lolPosition = 0; 84 | } 85 | } else { 86 | if (lolPosition == 0) { 87 | EEPROM.write(0, magic[0]); 88 | EEPROM.write(1, magic[1]); 89 | EEPROM.write(2, magic[2]); 90 | EEPROM.write(3, magic[3]); 91 | } 92 | lolText[lolPosition] = byte; 93 | EEPROM.write(lolPosition + magicSize, byte); 94 | lolPosition++; 95 | } 96 | Serial.flush(); 97 | } 98 | 99 | LedSign::Clear(); 100 | for (int8_t x2 = x, i2 = i; x2 < DISPLAY_COLS;) { 101 | int8_t w = Font::Draw(lolText[i2], x2, 0); 102 | x2 += w; 103 | i2 = (i2 + 1) % strlen(lolText); 104 | if (x2 <= 0) { // off the display completely? 105 | x = x2; 106 | i = i2; 107 | } 108 | } 109 | x--; 110 | delay(80); 111 | } 112 | -------------------------------------------------------------------------------- /serial.js: -------------------------------------------------------------------------------- 1 | var serial = {}; 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | serial.getPorts = function() { 7 | return navigator.usb.getDevices().then(devices => { 8 | return devices.map(device => new serial.Port(device)); 9 | }); 10 | }; 11 | 12 | serial.requestPort = function() { 13 | const filters = [ 14 | { 'vendorId': 0x2341, 'productId': 0x8036 }, 15 | { 'vendorId': 0x2341, 'productId': 0x8037 }, 16 | { 'vendorId': 0x2341, 'productId': 0x804d }, 17 | { 'vendorId': 0x2341, 'productId': 0x804e }, 18 | { 'vendorId': 0x2341, 'productId': 0x804f }, 19 | { 'vendorId': 0x2341, 'productId': 0x8050 }, 20 | ]; 21 | return navigator.usb.requestDevice({ 'filters': filters }).then( 22 | device => new serial.Port(device) 23 | ); 24 | } 25 | 26 | serial.Port = function(device) { 27 | this.device_ = device; 28 | }; 29 | 30 | serial.Port.prototype.connect = function() { 31 | let readLoop = () => { 32 | this.device_.transferIn(5, 64).then(result => { 33 | this.onReceive(result.data); 34 | readLoop(); 35 | }, error => { 36 | this.onReceiveError(error); 37 | }); 38 | }; 39 | 40 | return this.device_.open() 41 | .then(() => { 42 | if (this.device_.configuration === null) { 43 | return this.device_.selectConfiguration(1); 44 | } 45 | }) 46 | .then(() => this.device_.claimInterface(2)) 47 | .then(() => this.device_.selectAlternateInterface(2, 0)) 48 | .then(() => this.device_.controlTransferOut({ 49 | 'requestType': 'class', 50 | 'recipient': 'interface', 51 | 'request': 0x22, 52 | 'value': 0x01, 53 | 'index': 0x02})) 54 | .then(() => { 55 | readLoop(); 56 | }); 57 | }; 58 | 59 | serial.Port.prototype.disconnect = function() { 60 | return this.device_.controlTransferOut({ 61 | 'requestType': 'class', 62 | 'recipient': 'interface', 63 | 'request': 0x22, 64 | 'value': 0x00, 65 | 'index': 0x02}) 66 | .then(() => this.device_.close()); 67 | }; 68 | 69 | serial.Port.prototype.send = function(data) { 70 | return this.device_.transferOut(4, data); 71 | }; 72 | })(); 73 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUSB LOL 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

WebUSB LOL

16 | 17 |
18 | 19 |
20 |

21 | 22 |
23 | 24 |
25 |

Type your message to the world:

26 | 27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/lol.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-display: optional; 4 | font-family: Trebuchet MS, Arial, Helvetica, sans-serif; 5 | } 6 | 7 | body { 8 | -moz-hyphens: auto; 9 | -webkit-hyphens: auto; 10 | -ms-hyphens: auto; 11 | hyphens: auto; 12 | margin: 0; 13 | } 14 | 15 | h1 { 16 | background-color: #0b559b; 17 | color: snow; 18 | width: 100%; 19 | height: 1.5em; 20 | margin: 0; 21 | padding: 1em; 22 | font-size: 3em; 23 | } 24 | 25 | .panel { 26 | margin: 0 1em; 27 | } 28 | 29 | .connection { 30 | position: relative; 31 | border: 1px solid darkgrey; 32 | border-radius: 3px; 33 | margin: 1em 0; 34 | padding: 1em; 35 | } 36 | 37 | #connect { 38 | position: absolute; 39 | right: 1em; 40 | top: 1em; 41 | } 42 | 43 | button, input[type="submit"] { 44 | height: 2em; 45 | background-color: #b5c9e6; 46 | border: none; 47 | border-radius: 3px; 48 | } 49 | 50 | input[type="submit"] { 51 | width: 5em; 52 | height: 1.3em; 53 | font-size: 2rem; 54 | } 55 | 56 | #form { 57 | border: 1px solid darkgrey; 58 | border-radius: 3px; 59 | margin: 1em 0; 60 | padding: 1em; 61 | } 62 | 63 | #text { 64 | width: calc(100% - 6em); 65 | font-size: 2rem; 66 | text-transform: uppercase; 67 | } 68 | 69 | .error #status { 70 | color: red; 71 | } 72 | 73 | .error #form { 74 | display: none; 75 | } 76 | -------------------------------------------------------------------------------- /web/lol.js: -------------------------------------------------------------------------------- 1 | let port; 2 | document.addEventListener('DOMContentLoaded', () => { 3 | let connectButton = document.querySelector("#connect"); 4 | let device = document.querySelector('#device'); 5 | let text = document.querySelector('#text'); 6 | let main = document.querySelector('#main'); 7 | let form = document.querySelector('#form'); 8 | let timer = null; 9 | 10 | form.addEventListener('submit', function (event) { 11 | const textInput = (text.value + ' \n').toUpperCase(); 12 | port.send(new TextEncoder("utf-8").encode(textInput)); 13 | event.preventDefault(); 14 | }); 15 | 16 | connectButton.addEventListener('click', function () { 17 | if (port) { 18 | port.disconnect(); 19 | connectButton.textContent = 'connect'; 20 | device.textContent = ''; 21 | port = null; 22 | } else { 23 | serial.requestPort().then(selectedPort => { 24 | port = selectedPort; 25 | connect(); 26 | }).catch(error => { 27 | device.textContent = error; 28 | }); 29 | } 30 | }); 31 | 32 | serial.getPorts().then(ports => { 33 | if (ports.length == 0) { 34 | device.textContent = 'No device found.'; 35 | main.classList.add('error'); 36 | } else { 37 | device.textContent = 'Connecting...'; 38 | port = ports[0]; 39 | connect(); 40 | } 41 | }); 42 | 43 | function connect() { 44 | port.connect().then(() => { 45 | device.textContent = ''; 46 | connectButton.innerHTML = '× disconnect'; 47 | 48 | device.innerHTML = `✔ Connected with ${port.device_.productName}.`; 49 | main.classList.remove('error'); 50 | 51 | port.onReceive = data => { 52 | let textDecoder = new TextDecoder(); 53 | text.value = textDecoder.decode(data).trim(); 54 | clearInterval(timer); 55 | }; 56 | port.onReceiveError = error => { 57 | device.textContent = error; 58 | main.classList.add('error'); 59 | }; 60 | 61 | timer = setInterval(() => { 62 | port.send(new TextEncoder("utf-8").encode("\n")); 63 | }, 200); 64 | 65 | }, error => { 66 | device.textContent = error; 67 | main.classList.add('error'); 68 | }); 69 | } 70 | }); 71 | --------------------------------------------------------------------------------