├── .gitignore ├── .node-version ├── Gruntfile.js ├── demo.html ├── demo.png ├── demo ├── relay-off.jpg ├── relay-on.jpg ├── web1.png ├── web2.png ├── web3.png ├── web4.png ├── web5.png ├── web6.png └── wemos.png ├── license.txt ├── package.json ├── readme.md └── src ├── arduino ├── readme.md └── useless_throwie │ └── useless_throwie.ino ├── micropython ├── hello_captive.py ├── hello_gzip.py ├── readme.md ├── test_relay.py ├── useless_throwie.py └── useless_throwie_captive.py └── web ├── favicon.png ├── finger.svg ├── hello_world.html ├── main.js ├── main.scss ├── off.mp3 ├── on.mp3 ├── readme.md ├── switch.svg └── useless_throwie.html /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .DS_Store 5 | *.log -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 5.12.0 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | require('time-grunt')(grunt); 6 | require('load-grunt-tasks')(grunt); 7 | 8 | grunt.initConfig({ 9 | clean: { 10 | dist: { 11 | src: ['build', 'dist'] 12 | } 13 | }, 14 | base64: { 15 | dist: { 16 | files: { 17 | 'build/web/on.mp3.base64': 'src/web/on.mp3', 18 | 'build/web/off.mp3.base64': 'src/web/off.mp3', 19 | 'build/web/favicon.png.base64': 'src/web/favicon.png' 20 | } 21 | } 22 | }, 23 | sass: { 24 | options: { 25 | sourceMap: false, 26 | outputStyle: 'compressed' // nested, compact, compressed or expanded 27 | }, 28 | dist: { 29 | files: { 30 | 'build/web/main.min.css': 'src/web/main.scss' 31 | } 32 | } 33 | }, 34 | 'string-replace': { 35 | dist: { 36 | files: { 37 | 'build/web/main.js': 'src/web/main.js', 38 | 'build/web/useless_throwie.html': 'src/web/useless_throwie.html' 39 | }, 40 | options: { 41 | replacements: [{ 42 | pattern: //ig, 43 | replacement: function (match, file) { 44 | return grunt.file.read(file); 45 | } 46 | }] 47 | } 48 | } 49 | }, 50 | uglify: { 51 | dist: { 52 | options: { 53 | sourceMap: false, 54 | mangle: true, 55 | mangleProperties: false, 56 | beautify: false 57 | }, 58 | files: { 59 | 'build/web/main.min.js': 'build/web/main.js' 60 | } 61 | } 62 | }, 63 | svgmin: { 64 | options: { 65 | plugins: [ 66 | { removeViewBox: false }, 67 | { removeUselessStrokeAndFill: false }, 68 | { removeAttrs: { attrs: ['xmlns'] } } 69 | ] 70 | }, 71 | dist: { 72 | files: { 73 | 'build/web/switch.min.svg': 'src/web/switch.svg', 74 | 'build/web/finger.min.svg': 'src/web/finger.svg' 75 | } 76 | } 77 | }, 78 | processhtml: { 79 | options: { 80 | }, 81 | dist: { 82 | files: { 83 | 'build/web/useless_throwie.html': 'build/web/useless_throwie.html' 84 | } 85 | } 86 | }, 87 | htmlmin: { 88 | dist: { 89 | options: { 90 | removeComments: true, 91 | collapseWhitespace: true 92 | }, 93 | files: { 94 | 'dist/web/useless_throwie.min.html': 'build/web/useless_throwie.html', 95 | 'dist/web/hello_world.min.html': 'src/web/hello_world.html' 96 | } 97 | } 98 | }, 99 | compress: { 100 | dist: { 101 | options: { 102 | mode: 'gzip' 103 | }, 104 | files: [ 105 | { expand: true, src: ['dist/web/*.min.html'], ext: '.min.html.gz' } 106 | ] 107 | } 108 | }, 109 | copy: { 110 | dist: { 111 | files: [ 112 | { expand: true, cwd: 'src/micropython', src: '*.py', dest: 'dist/micropython' }, 113 | { expand: true, cwd: 'src/arduino', src: '*.py', dest: 'dist/arduino' } 114 | ] 115 | } 116 | }, 117 | filesize: { 118 | base: { 119 | files: [ 120 | { expand: true, cwd: 'dist/web', src: ['*.html','*.gz','*.py'] } 121 | ] 122 | } 123 | }, 124 | watch: { 125 | livereload: { 126 | options: { 127 | livereload: '<%= connect.options.livereload %>' 128 | }, 129 | files: [ 130 | 'src/**/*' 131 | ], 132 | tasks: ['build'] 133 | } 134 | }, 135 | connect: { 136 | options: { 137 | port: 9000, 138 | livereload: 35729, 139 | hostname: '0.0.0.0' 140 | }, 141 | livereload: { 142 | options: { 143 | open: true, 144 | base: 'dist/web' 145 | } 146 | } 147 | } 148 | }); 149 | 150 | grunt.registerTask('serve', function (target) { 151 | grunt.task.run([ 152 | 'build', 153 | 'connect:livereload', 154 | 'watch' 155 | ]); 156 | }); 157 | 158 | grunt.registerTask('build', [ 159 | 'clean', 160 | 'base64', 161 | 'sass', 162 | 'string-replace', 163 | 'uglify', 164 | 'svgmin', 165 | 'processhtml', 166 | 'htmlmin', 167 | 'compress', 168 | 'copy', 169 | 'filesize' 170 | ]); 171 | 172 | grunt.registerTask('default', [ 173 | 'build' 174 | ]); 175 | }; 176 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | Useless Throwie
-------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo.png -------------------------------------------------------------------------------- /demo/relay-off.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/relay-off.jpg -------------------------------------------------------------------------------- /demo/relay-on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/relay-on.jpg -------------------------------------------------------------------------------- /demo/web1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web1.png -------------------------------------------------------------------------------- /demo/web2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web2.png -------------------------------------------------------------------------------- /demo/web3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web3.png -------------------------------------------------------------------------------- /demo/web4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web4.png -------------------------------------------------------------------------------- /demo/web5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web5.png -------------------------------------------------------------------------------- /demo/web6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/web6.png -------------------------------------------------------------------------------- /demo/wemos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/demo/wemos.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mike Causer 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp8266-useless-throwie", 3 | "description": "ESP8266 Useless Throwie", 4 | "version": "0.0.1", 5 | "private": true, 6 | "author": "Mike Causer ", 7 | "repository": "mcauser/esp8266-useless-throwie", 8 | "license": "MIT", 9 | "keywords": [ 10 | "esp8266", 11 | "useless", 12 | "throwie" 13 | ], 14 | "engines": { 15 | "node": "^5.12.0" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "grunt": "^1.0.1", 20 | "grunt-base64": "^0.1.0", 21 | "grunt-contrib-clean": "^1.0.0", 22 | "grunt-contrib-compress": "^1.3.0", 23 | "grunt-contrib-connect": "^1.0.2", 24 | "grunt-contrib-copy": "^1.0.0", 25 | "grunt-contrib-htmlmin": "^2.0.0", 26 | "grunt-contrib-uglify": "^2.0.0", 27 | "grunt-contrib-watch": "^1.0.0", 28 | "grunt-filesize": "0.0.7", 29 | "grunt-processhtml": "^0.4.0", 30 | "grunt-sass": "^1.2.1", 31 | "grunt-string-replace": "^1.3.0", 32 | "grunt-svgmin": "^3.3.0", 33 | "load-grunt-tasks": "^3.5.2", 34 | "time-grunt": "^1.4.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Useless Throwie 2 | 3 | An ESP8266 configured as an access point with a tiny web server which serves a page displaying a toggle switch, which flips a relay when clicked. 4 | 5 | ![Demo](https://raw.github.com/mcauser/esp8266-useless-throwie/master/demo.png) 6 | 7 | Precompiled [demo.html](https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/master/demo.html) can be [viewed here](https://mcauser.github.io/esp8266-useless-throwie/index.html) - just the button, finger and audio only. 8 | 9 | ## Install: 10 | 11 | Install [node.js](http://nodejs.org) 5.12.0 or later. 12 | 13 | Install [grunt](http://gruntjs.com/) task runner and dev dependencies listed in package.json 14 | 15 | ``` 16 | $ npm install -g grunt-cli 17 | $ npm install 18 | ``` 19 | 20 | ## Build: 21 | 22 | Run grunt to compile each of the platforms into the `dist/` folder. This executes the default task defined in `Gruntfile.js`. 23 | 24 | ``` 25 | $ grunt 26 | ``` 27 | 28 | ### Directories: 29 | 30 | * /build - temporary dir used when compiling 31 | * /dist - compiled files dir 32 | * /src - source files dir 33 | 34 | ## Platform specific instructions: 35 | 36 | Further instructions for configuring each device. 37 | 38 | * [MicroPython](src/micropython/readme.md) 39 | * [Arduino](src/arduino/readme.md) 40 | * [Web](/src/web/readme.md) 41 | 42 | ## Development Notes: 43 | 44 | ### Arduino: 45 | 46 | - [ ] web server 47 | - [ ] web server with gzip 48 | - [ ] captive portal 49 | - [ ] is there an ino linter / testing framework? 50 | 51 | ### MicroPython: 52 | 53 | - [x] web server 54 | - [x] web server with gzip 55 | - [ ] add to main.py 56 | - [ ] captive portal 57 | - [ ] websockets for bidirectional toggles 58 | - [ ] multiple client/station bidirectional toggle support 59 | - [ ] pylint 60 | - [ ] precompiled firmware 61 | 62 | ### Web: 63 | 64 | - [ ] css flexbox fallback 65 | - [ ] css autoprefixer 66 | - [ ] css rotate finger on swipe 67 | - [ ] js tests 68 | - [ ] js uglify mangle 69 | - [ ] js touch support 70 | - [ ] js add polyfills for old browsers 71 | - [ ] js check if device can play mp3s 72 | - [ ] js check if offline and skip XMLHttpRequests 73 | - [ ] js cross platform event listeners 74 | - [ ] js finger add rage personality 75 | - [ ] js finger add preempt personality 76 | - [ ] js on resize move finger offscreen 77 | - [ ] grunt add imagemin for png 78 | - [ ] svg reduce decimal places 79 | - [ ] svg on state glow filter does not work in ios chrome 80 | - [ ] svg randomise button led colour 81 | - [ ] svg randomise skin tone 82 | - [ ] svg adjust button shadow using device accelerometers 83 | - [ ] svg switch animate rocker instead of duplicate button and shadow groups 84 | - [ ] add easter eggs for excessive clickers 85 | - [ ] deploy html to paas with MQTT or sockets bridge 86 | 87 | ### Common: 88 | 89 | - [x] add photos 90 | - [ ] grunt add concurrent 91 | - [x] grunt add open 92 | - [x] grunt add connect, watch, livereload 93 | - [ ] MQTT publish 94 | - [ ] swap relay for OLED, char LCD, buzzer, LED, WS2818, etc 95 | 96 | ## Links: 97 | 98 | * [demo](https://mcauser.github.io/esp8266-useless-throwie/index.html) 99 | * [WeMos D1 Mini](http://www.wemos.cc/Products/d1_mini.html) 100 | * [WeMos D1 Mini Relay Shield](http://www.wemos.cc/Products/relay_shield.html) 101 | * [micropython.org](http://micropython.org) 102 | * [node.js](http://nodejs.org) 103 | * [grunt](http://gruntjs.com/) 104 | * [hackaday project](https://hackaday.io/project/13322-esp8266-useless-throwie) 105 | 106 | ## Credits: 107 | 108 | * MicroPython socket web server examples based on [MicroPython examples](https://github.com/micropython/micropython/tree/master/examples/network). 109 | * Python captive portal code based on [Mini Fake DNS server](http://code.activestate.com/recipes/491264-mini-fake-dns-server/). 110 | * [easings.net](http://easings.net/) for the CSS Bezier curves 111 | -------------------------------------------------------------------------------- /src/arduino/readme.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Useless Throwie, Arduino IDE version 2 | 3 | ## Parts: 4 | 5 | * [WeMos D1 Mini](http://www.aliexpress.com/store/product/D1-mini-Mini-NodeMcu-4M-bytes-Lua-WIFI-Internet-of-Things-development-board-based-ESP8266/1331105_32529101036.html) $4.00 USD 6 | * [WeMos Relay Shield](http://www.aliexpress.com/store/product/Relay-Shield-for-WeMos-D1-mini-button/1331105_32596395175.html) $2.10 USD 7 | 8 | ## Configure: 9 | 10 | * Open useless_throwie.ino in Arduino IDE 11 | * Set board to WeMos D1 Mini 12 | * Verify and upload 13 | * Reboot 14 | 15 | ## Run: 16 | 17 | * Connect to AP 18 | * Open [192.168.4.1](http://192.168.4.1) in a web browser 19 | * Click the toggle switch 20 | * Watch the relay 21 | 22 | ## Development Notes: 23 | 24 | ### useless_throwie 25 | 26 | Simple web server that serves the html toggle switch that can flip the relay (GPIO5) and onboard LED (GPIO2) when clicked. 27 | -------------------------------------------------------------------------------- /src/arduino/useless_throwie/useless_throwie.ino: -------------------------------------------------------------------------------- 1 | /* TODO */ -------------------------------------------------------------------------------- /src/micropython/hello_captive.py: -------------------------------------------------------------------------------- 1 | # TODO 2 | -------------------------------------------------------------------------------- /src/micropython/hello_gzip.py: -------------------------------------------------------------------------------- 1 | try: 2 | import usocket as socket 3 | except: 4 | import socket 5 | 6 | okResponse = b"""\ 7 | HTTP/1.1 200 OK 8 | 9 | %s 10 | """ 11 | 12 | badResponse = b"""\ 13 | HTTP/1.1 400 Bad Request 14 | Content-Type: text/html; charset=utf-8 15 | 16 |

400 Bad Request

17 | """ 18 | 19 | def main(): 20 | headers = b"""\ 21 | HTTP/1.1 200 OK 22 | Content-Type: text/html; charset=utf-8 23 | Content-Encoding: gzip 24 | Content-Length: %d 25 | 26 | """ 27 | 28 | # read the gzipped html 29 | f = open('hello_world.min.html.gz','rb') 30 | html = f.read() 31 | f.close() 32 | 33 | # set the content length 34 | headers = headers % len(html) 35 | 36 | # create server 37 | s = socket.socket() 38 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 39 | s.bind(('0.0.0.0', 80)) 40 | s.listen(5) 41 | print("Listening for http requests on port 80") 42 | 43 | # process requests 44 | while True: 45 | client_s, client_addr = s.accept() 46 | 47 | req = client_s.recv(4096) 48 | 49 | print("Request:\n%s\n" % req) 50 | 51 | # grab some variables from request header 52 | # eg. GET /on HTTP/1.1 53 | method, path, protocol = req.split(b'\r\n',1)[0].split() 54 | 55 | if path == b'/': 56 | client_s.send(headers) 57 | client_s.sendall(html) 58 | else: 59 | client_s.send(badResponse) 60 | client_s.close() 61 | 62 | main() 63 | -------------------------------------------------------------------------------- /src/micropython/readme.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Useless Throwie, MicroPython version 2 | 3 | ## Parts: 4 | 5 | * [WeMos D1 Mini](http://www.aliexpress.com/store/product/D1-mini-Mini-NodeMcu-4M-bytes-Lua-WIFI-Internet-of-Things-development-board-based-ESP8266/1331105_32529101036.html) $4.00 USD 6 | * [WeMos Relay Shield](http://www.aliexpress.com/store/product/Relay-Shield-for-WeMos-D1-mini-button/1331105_32596395175.html) $2.10 USD 7 | 8 | ## Configure: 9 | 10 | * Set as AP 11 | * Enable webrepl 12 | * Upload files 13 | * Reboot 14 | 15 | ``` 16 | import network 17 | sta_if = network.WLAN(network.STA_IF) 18 | sta_if.active(False) 19 | 20 | ap_if = network.WLAN(network.AP_IF) 21 | ap_if.active(True) 22 | 23 | import webrepl 24 | webrepl.start() 25 | 26 | import os 27 | os.listdir() 28 | ``` 29 | 30 | ## Run: 31 | 32 | * Open REPL `import useless_throwie` 33 | * Connect to AP 34 | * Open [192.168.4.1](http://192.168.4.1) in a web browser 35 | * Click the toggle switch 36 | * Watch the relay 37 | 38 | ## Development Notes: 39 | 40 | ### hello_captive.py 41 | 42 | First attempts at a captive portal with DNS spoofing. 43 | 44 | Based on [Mini Fake DNS server](http://code.activestate.com/recipes/491264-mini-fake-dns-server/). 45 | 46 | ### hello_gzip.py 47 | 48 | Simple web server that reponds with pre-gzipped html content `dist/web/hello_world.min.html.gz`. 49 | 50 | ### test_relay.py 51 | 52 | Testing the relay and onboard LED. 53 | 54 | ### useless_throwie.py 55 | 56 | Simple web server that serves the html toggle switch that can flip the relay (GPIO5) and onboard LED (GPIO2) when clicked. 57 | 58 | Given the gzipped html, `dist/web/useless_throwie.min.html.gz`, is around 8kb, this might only work on ESP8266s with larger flash chips. Without gzip, the minified html is around 15kb. 59 | 60 | ### useless_throwie_captive.py 61 | 62 | Work in progress. Useless throwie combined with a captive portal / DNS spoofing. 63 | -------------------------------------------------------------------------------- /src/micropython/test_relay.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin 3 | 4 | relay = Pin(5, Pin.OUT) 5 | led = Pin(2, Pin.OUT) 6 | 7 | # the onboard led is actually illuminated when low 8 | relay.high() 9 | led.low() 10 | 11 | # flip every second 12 | while(True): 13 | relay.value(not relay.value()) 14 | led.value(not led.value()) 15 | time.sleep_ms(1000) 16 | -------------------------------------------------------------------------------- /src/micropython/useless_throwie.py: -------------------------------------------------------------------------------- 1 | try: 2 | import usocket as socket 3 | except: 4 | import socket 5 | 6 | from machine import Pin 7 | 8 | # toggle pins 9 | relay = Pin(5, Pin.OUT) 10 | led = Pin(2, Pin.OUT) 11 | relay.low() 12 | led.high() 13 | 14 | okResponse = b"""\ 15 | HTTP/1.1 200 OK 16 | 17 | %s 18 | """ 19 | 20 | badResponse = b"""\ 21 | HTTP/1.1 400 Bad Request 22 | Content-Type: text/html; charset=utf-8 23 | 24 |

400 Bad Request

25 | """ 26 | 27 | def on(): 28 | relay.high() 29 | led.low() 30 | 31 | def off(): 32 | relay.low() 33 | led.high() 34 | 35 | def main(): 36 | headers = b"""\ 37 | HTTP/1.1 200 OK 38 | Content-Type: text/html; charset=utf-8 39 | Content-Encoding: gzip 40 | Content-Length: %d 41 | 42 | """ 43 | 44 | # read the gzipped html 45 | f = open('useless_throwie.min.html.gz','rb') 46 | html = f.read() 47 | f.close() 48 | 49 | # set the content length 50 | headers = headers % len(html) 51 | 52 | # create server 53 | s = socket.socket() 54 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 55 | s.bind(('0.0.0.0', 80)) 56 | s.listen(5) 57 | print("Listening for http requests on port 80") 58 | 59 | # process requests 60 | while True: 61 | client_s, client_addr = s.accept() 62 | 63 | # reduced from 4096 to avoid out of memory errors 64 | req = client_s.recv(2048) 65 | 66 | print("Request:\n%s\n" % req) 67 | 68 | # grab some variables from request header 69 | # eg. GET /on HTTP/1.1 70 | method, path, protocol = req.split(b'\r\n',1)[0].split() 71 | 72 | if path == b'/': 73 | client_s.send(headers) 74 | client_s.sendall(html) 75 | off() 76 | elif path == b'/on': 77 | client_s.send(okResponse % 'on') 78 | on() 79 | elif path == b'/off': 80 | client_s.send(okResponse % 'off') 81 | off() 82 | else: 83 | client_s.send(badResponse) 84 | off() 85 | client_s.close() 86 | 87 | main() 88 | -------------------------------------------------------------------------------- /src/micropython/useless_throwie_captive.py: -------------------------------------------------------------------------------- 1 | # This is incomplete and only partially works 2 | 3 | try: 4 | import usocket as socket 5 | except: 6 | import socket 7 | 8 | class DNSQuery: 9 | def __init__(self, data): 10 | self.data = data 11 | self.domain = '' 12 | 13 | kind = (data[2] >> 3) & 15 # Opcode bits 14 | if kind == 0: # Standard query 15 | ini = 12 16 | lon = data[ini] 17 | while lon != 0: 18 | self.domain += data[ini + 1:ini + lon + 1].decode() + '.' 19 | ini += lon + 1 20 | lon = data[ini] 21 | 22 | def response(self, ip): 23 | packet = b'' 24 | if self.domain: 25 | packet += self.data[:2] + "\x81\x80" 26 | packet += self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00' # Questions and Answers Counts 27 | packet += self.data[12:] # Original Domain Name Question 28 | packet += '\xc0\x0c' # Pointer to domain name 29 | packet += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' # Response type, ttl and resource data length -> 4 bytes 30 | packet += str.join('',map(lambda x: chr(int(x)), ip.split('.'))) # 4bytes of IP 31 | return packet 32 | 33 | def main(): 34 | ip = '192.168.4.1' 35 | print('pyminifakeDNS:: dom.query. 60 IN A %s' % ip) 36 | 37 | udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 38 | udps.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 39 | udps.bind(('0.0.0.0', 53)) 40 | print("Listening for UDP DNS requests on port 53") 41 | 42 | s = socket.socket() 43 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 44 | s.bind(('0.0.0.0', 80)) 45 | s.listen(5) 46 | print("Listening for TCP HTTP requests on port 80") 47 | 48 | counter = 0 49 | try: 50 | while 1: 51 | # dns request 52 | data, addr = udps.recvfrom(1024) 53 | p = DNSQuery(data) 54 | packet = p.response(ip) 55 | udps.sendto(packet, addr) 56 | print('Response: %s -> %s' % (p.domain, ip)) 57 | 58 | # http request 59 | res = s.accept() 60 | client_s = res[0] 61 | client_addr = res[1] 62 | req = client_s.recv(4096) 63 | print("Request:\n%s\n" % req) 64 | client_s.send(CONTENT % counter) 65 | client_s.close() 66 | counter += 1 67 | print() 68 | except KeyboardInterrupt: 69 | print('Finalise') 70 | udps.close() 71 | 72 | main() 73 | -------------------------------------------------------------------------------- /src/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/src/web/favicon.png -------------------------------------------------------------------------------- /src/web/finger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/web/hello_world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hello World

5 | 6 | -------------------------------------------------------------------------------- /src/web/main.js: -------------------------------------------------------------------------------- 1 | var offMp3 = new Audio("data:audio/mp3;base64,"); 2 | var onMp3 = new Audio("data:audio/mp3;base64,"); 3 | 4 | 5 | var sw = (function() { 6 | 7 | var el = document.querySelector('.switch'); 8 | el.addEventListener('mouseup', mouseUp); 9 | el.addEventListener('mousedown', mouseDown); 10 | 11 | function mouseDown(e) { 12 | fngr.end(); 13 | click(!el.classList.contains('on')); 14 | } 15 | 16 | function mouseUp(e) { 17 | el.classList.contains('on') ? fngr.begin() : fngr.end(); 18 | } 19 | 20 | function click(on) { 21 | if (on) { 22 | el.classList.add('on'); 23 | onMp3.play(); 24 | 25 | var xhr = new XMLHttpRequest(); 26 | xhr.open('GET', '/on'); 27 | xhr.send(null); 28 | } 29 | else { 30 | el.classList.remove('on'); 31 | offMp3.play(); 32 | 33 | var xhr = new XMLHttpRequest(); 34 | xhr.open('GET', '/off'); 35 | xhr.send(null); 36 | } 37 | } 38 | 39 | return { 40 | click: click 41 | }; 42 | })(); 43 | 44 | 45 | var fngr = (function() { 46 | 47 | var el = document.querySelector('.finger'), 48 | timers = [], 49 | on = {}, 50 | off = {}, 51 | out = {}; 52 | 53 | el.style.transitionProperty = 'all'; 54 | el.style.transitionTimingFunction = 'cubic-bezier(0.785, 0.135, 0.15, 0.86)'; 55 | 56 | function begin() { 57 | halt(); 58 | reset(); 59 | timers.push(setTimeout(playOn, on.delay)); 60 | } 61 | 62 | function end() { 63 | halt(); 64 | timers.push(setTimeout(playOut, out.delay)); 65 | } 66 | 67 | function reset() { 68 | on = build({ top: [47,53], left: [52,58], delay: [0,500], speed: [100,400] }); 69 | off = build({ top: [47,53], left: [42,48], delay: [0,500], speed: [100,600] }); 70 | out = build({ top: [100,100], left: [40,60], delay: [100,500], speed: [100,400] }); 71 | } 72 | 73 | function build(recipe) { 74 | return { 75 | top: rand(recipe.top[0], recipe.top[1]), 76 | left: rand(recipe.left[0], recipe.left[1]), 77 | delay: rand(recipe.delay[0], recipe.delay[1]), 78 | speed: rand(recipe.speed[0], recipe.speed[1]) 79 | }; 80 | } 81 | 82 | function halt() { 83 | var rect = el.getBoundingClientRect(); 84 | el.style.top = rect.top + 'px'; 85 | el.style.left = rect.left + 'px'; 86 | 87 | while (timers.length) { 88 | clearTimeout(timers.shift()); 89 | } 90 | } 91 | 92 | function playOn() { 93 | set(on); 94 | timers.push(setTimeout(playOff, (on.speed + off.delay))); 95 | } 96 | 97 | function playOff() { 98 | set(off); 99 | timers.push(setTimeout(sw.click, (off.speed / 2))); 100 | timers.push(setTimeout(playOut, off.speed + out.delay)); 101 | } 102 | 103 | function playOut() { 104 | set(out); 105 | } 106 | 107 | function set(pos) { 108 | el.style.transitionDuration = pos.speed + 'ms'; 109 | //el.style.top = (pos.top / 100 * window.innerHeight) + 'px'; 110 | //el.style.left = (pos.left / 100 * window.innerWidth) + 'px'; 111 | el.style.top = pos.top + '%'; 112 | el.style.left = pos.left + '%'; 113 | } 114 | 115 | function rand(min, max) { 116 | return min === max ? min : Math.floor(Math.random() * (max - min + 1)) + min; 117 | } 118 | 119 | return { 120 | begin: begin, 121 | end: end 122 | }; 123 | })(); 124 | -------------------------------------------------------------------------------- /src/web/main.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | background: #342e32; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | overflow: hidden; 12 | } 13 | 14 | .switch { 15 | width: 75%; 16 | max-width: 250px; 17 | cursor: pointer; 18 | position: relative; 19 | &:not(.on) .on, 20 | &.on .off { 21 | display: none; 22 | } 23 | &:before { 24 | content: ''; 25 | display: block; 26 | position: absolute; 27 | top: 16%; 28 | left: 13%; 29 | right: 13%; 30 | bottom: 18%; 31 | // background-color: rgba(255,0,0,0.5); 32 | z-index: 30; 33 | } 34 | svg { 35 | z-index: 10; 36 | } 37 | } 38 | 39 | .finger { 40 | width: 50%; 41 | min-width: 400px; 42 | position: absolute; 43 | top: 100%; 44 | left: 60%; 45 | z-index: 20; 46 | } -------------------------------------------------------------------------------- /src/web/off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/src/web/off.mp3 -------------------------------------------------------------------------------- /src/web/on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/esp8266-useless-throwie/73db3e110797e283c7bd6e7a7493c332c81ef78a/src/web/on.mp3 -------------------------------------------------------------------------------- /src/web/readme.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Useless Throwie, Web version 2 | 3 | ## Requirements: 4 | 5 | * A modern web browser 6 | 7 | ## Run: 8 | 9 | ``` 10 | $ grunt serve 11 | ``` 12 | 13 | ## Development Notes: 14 | 15 | A HTML5 page with a SVG toggle switch and SVG finger positioned offscreen. 16 | 17 | Clicking the toggle switch triggers a finger animation, using CSS3 transformations, that restores the switch to the off position. 18 | 19 | The animation gracefully handles an abort scenario, where the switch has been toggled to the off position before the animation completes. 20 | 21 | Each animation is randomised, resulting in a more human-like feel. Top, left, delay and speed are randomised for each of the 3 steps. 22 | 23 | Each position of the toggle has a matching MP3 which is played with JavaScript. 24 | 25 | CSS, JavaScript, Favicon and MP3s moved inline with base64 to avoid additional http requests. 26 | 27 | When the switch is toggled, JavaScript executes an ajax request with the current state. GET /on and GET /off. 28 | 29 | Grunt workflow compiles and minifies the Sass into CSS, minifies the JS, minifies the SVG, base64 encodes the Favicon and MP3s, injects Base64 MP3s into the JS, injects inline CSS, JS, SVG and Base64 Favicon into the HTML, minifies the HTML, gzips the HTML and publishes in /dist. 30 | -------------------------------------------------------------------------------- /src/web/switch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/web/useless_throwie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Useless Throwie 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | --------------------------------------------------------------------------------