├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------