├── main.py ├── index.txt ├── header.txt ├── footer.txt ├── gotosleep.py ├── display.py ├── config.py ├── Copyright.Notice ├── help.txt ├── register.py ├── request.py ├── application.py ├── ds18b20.py ├── Makefile ├── content.py ├── real.py ├── README.md └── httpserver.py /main.py: -------------------------------------------------------------------------------- 1 | from real import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /index.txt: -------------------------------------------------------------------------------- 1 |
Welcome to the ESP-8266 Temperature Server.
5 |
6 |
--------------------------------------------------------------------------------
/header.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Micropython Temperature Server - Version 2.1.2-deepsleep 2016-dic-26 15:20:34-- Copyright (C) Erni Tron - 2016
18 | 19 | 20 | -------------------------------------------------------------------------------- /gotosleep.py: -------------------------------------------------------------------------------- 1 | # Micropython Http Server 2 | # Erni Tron ernitron@gmail.com 3 | # Copyright (c) 2016 4 | 5 | import time 6 | import machine 7 | 8 | def gotosleep(sleep_timeout): 9 | # Remember for this to work GPIO16 (D0) must be connected to RST 10 | # configure RTC.ALARM0 to be able to wake the device 11 | rtc = machine.RTC() 12 | rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP) 13 | 14 | # set RTC.ALARM0 to fire after a while (waking the device) 15 | sleep_timeout = sleep_timeout * 1000 # in microseconds 16 | rtc.alarm(rtc.ALARM0, sleep_timeout) 17 | 18 | # put the device to sleep 19 | print('Sleep for %d usec' % sleep_timeout) 20 | machine.deepsleep() 21 | -------------------------------------------------------------------------------- /display.py: -------------------------------------------------------------------------------- 1 | # Micropython Http Temperature Server 2 | # Erni Tron ernitron@gmail.com 3 | # Copyright (c) 2016 4 | 5 | class Display(): 6 | def __init__(self): 7 | self.d = None 8 | try: 9 | import ssd1306 10 | from machine import I2C, Pin 11 | i2c = I2C(sda=Pin(4), scl=Pin(5)) 12 | self.d = ssd1306.SSD1306_I2C(64, 48, i2c, 60) 13 | print ("Display available") 14 | except Exception as e: 15 | print ("Display just print") 16 | self.d = None 17 | 18 | def display(self, text, text1='', text2='', text3='', text4=''): 19 | if self.d : 20 | self.d.fill(0) 21 | if text: self.d.text(text,0, 0, 1) 22 | if text1: self.d.text(text1,0, 10, 1) 23 | if text2: self.d.text(text2,0, 20, 1) 24 | if text3: self.d.text(text3,0, 30, 1) 25 | if text4: self.d.text(text4,0, 40, 1) 26 | self.d.show() 27 | else: 28 | print(text, text1, text2, text3, text4) 29 | 30 | display = Display() 31 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Micropython Http Server 2 | # Erni Tron ernitron@gmail.com 3 | # Copyright (c) 2016 4 | 5 | import json 6 | 7 | class Config(): 8 | def __init__(self, file='config.txt'): 9 | self.config = {} 10 | self.file = file 11 | 12 | def read_config(self): 13 | try: 14 | with open(self.file, 'rb') as f: 15 | c = f.read() 16 | self.config = json.loads(c) 17 | except: 18 | self.config = {} 19 | 20 | def save_config(self): 21 | # Write Configuration 22 | jdata = json.dumps(self.config) 23 | if not jdata: 24 | return 25 | with open(self.file, 'wb') as f: 26 | f.write(jdata) 27 | 28 | def clean_config(self): 29 | self.config = {} 30 | self.save_config() 31 | 32 | def set_config(self, k, v): 33 | if not v : 34 | del self.config[k] 35 | else: self.config[k] = v 36 | 37 | def get_config(self, k=None): 38 | if not k : 39 | return self.config 40 | if k in self.config: 41 | return self.config[k] 42 | else: return '' 43 | 44 | # There will be only ONE config 45 | config = Config() 46 | -------------------------------------------------------------------------------- /Copyright.Notice: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2016 Erni (E) Tron 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 | # / 23 | 24 | 25 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 |Welcome to the ESP-8266 Temperature Server.
5 | Server can be called with: http://192.168.1.123:8805/help
6 |
7 | This device can also provide an Access Point if it is configured with the parameter i.e. if the AP password 'appwd' is present in config
8 |
9 |
10 | SSID is something like YoT-1234567
11 | PASS is in the manual
12 |
13 |
14 |
MacAddr: %s' \ 35 | '
Address: %s' \ 36 | '
Free Mem: %d (alloc %d)' \ 37 | '
Date Time: %s' \ 38 | '
Start Time: %s' \ 39 | '' % (chipid, macaddr, address, gc.mem_free(), gc.mem_alloc(), datetime, starttime) 40 | 41 | def cb_getconf(): 42 | return '
%s
' % json.dumps(config.get_config(None)) 43 | 44 | def cb_setconf(key, value): 45 | if not value: 46 | if key: 47 | kvalue = 'value="%s"' % (key) 48 | else: 49 | kvalue = '' 50 | ret = 'Sensor %s - Reading # %d @ %s' \ 100 | '
Started on %s' % (place, T['temp'], T['sensor'], T['count'], T['date'], starttime) 101 | return content 102 | 103 | def cb_temperature_plain(): 104 | T = sensor.status() 105 | content = '%s C' % T['temp'] 106 | return content 107 | 108 | def cb_temperature_json(): 109 | T = sensor.status() 110 | return json.dumps(T) 111 | 112 | def datenow(): 113 | try: 114 | (Y, M, D, h, m, s, c, u) = time.localtime() 115 | return '%d-%d-%d %d:%d:%d' % (Y, M, D, h, m, s) 116 | except: 117 | return time.time() 118 | 119 | -------------------------------------------------------------------------------- /real.py: -------------------------------------------------------------------------------- 1 | # Micropython Http Server 2 | # Erni Tron ernitron@gmail.com 3 | # Copyright (c) 2016 4 | 5 | import time 6 | import sys 7 | import network 8 | import machine 9 | import gc 10 | from ubinascii import hexlify 11 | 12 | from config import config 13 | 14 | def do_connect(ssid, pwd, TYPE, hard_reset=True): 15 | interface = network.WLAN(TYPE) 16 | 17 | # Stage zero if credential are null disconnect 18 | if not pwd or not ssid : 19 | print('Disconnecting ', TYPE) 20 | interface.active(False) 21 | return None 22 | 23 | if TYPE == network.AP_IF: 24 | interface.active(True) 25 | time.sleep_ms(200) 26 | interface.config(essid=ssid, password=pwd) 27 | return interface 28 | 29 | if hard_reset: 30 | interface.active(True) 31 | interface.connect(ssid, pwd) 32 | 33 | # Stage one check for default connection 34 | print('Connecting') 35 | for t in range(120): 36 | time.sleep_ms(250) 37 | if interface.isconnected(): 38 | print('Yes! Connected') 39 | return interface 40 | if t == 60 and not hard_reset: 41 | # if still not connected 42 | interface.active(True) 43 | interface.connect(ssid, pwd) 44 | 45 | # No way we are not connected 46 | print('Cant connect', ssid) 47 | return None 48 | 49 | #---------------------------------------------------------------- 50 | # MAIN PROGRAM STARTS HERE 51 | def main(): 52 | 53 | # Enable automatic garbage collector 54 | gc.enable() 55 | 56 | if machine.reset_cause() == machine.DEEPSLEEP_RESET: 57 | print('wake from deep sleep') 58 | hard_reset = False 59 | else: 60 | hard_reset = True 61 | print('wake from hard reset') 62 | 63 | chipid = hexlify(machine.unique_id()) 64 | 65 | # Read from file the whole configuration 66 | config.read_config() 67 | 68 | # Get WiFi defaults and connect 69 | ssid = config.get_config('ssid') 70 | pwd = config.get_config('pwd') 71 | interface = do_connect(ssid, pwd, network.STA_IF, hard_reset) 72 | 73 | # Turn on Access Point only with passw 74 | apssid = 'YoT-%s' % bytes.decode(chipid) 75 | appwd = config.get_config('appwd') 76 | if hard_reset: 77 | ap_interface = do_connect(apssid, appwd, network.AP_IF) 78 | 79 | if not interface and not ap_interface: 80 | print('Restart 10"') 81 | time.sleep(10.0) 82 | machine.reset() 83 | return 84 | 85 | # Set Parameters in configuration 86 | (address, mask, gateway, dns) = interface.ifconfig() 87 | config.set_config('address', address) 88 | config.set_config('mask', mask) 89 | config.set_config('gateway', gateway) 90 | config.set_config('dns', dns) 91 | config.set_config('mac', hexlify(interface.config('mac'), ':')) 92 | config.set_config('chipid', chipid) 93 | 94 | # Set Time RTC 95 | from ntptime import settime 96 | try: 97 | settime() 98 | (y, m, d, h, mm, s, c, u) = time.localtime() 99 | starttime = '%d-%d-%d %d:%d:%d UTC' % (y, m, d, h, mm, s) 100 | except: 101 | starttime = '2016-01-01 00:00:00' 102 | print('Cannot set time') 103 | 104 | # Set hostname 105 | interface.config(dhcp_hostname=chipid) 106 | config.set_config('hostname', interface.config('dhcp_hostname')) 107 | 108 | # We will save new configuration only at powerup 109 | if hard_reset: 110 | config.set_config('starttime', starttime) 111 | config.save_config() 112 | 113 | # Free some memory 114 | ssid = pwd = None 115 | apssid = appwd = None 116 | address = mask = gateway = dns = None 117 | gc.collect() 118 | 119 | # The application hook 120 | from application import application 121 | try: 122 | application(interface) 123 | except KeyboardInterrupt: 124 | pass 125 | except Exception as e: 126 | print(e) 127 | 128 | # Restart 129 | print('Restarting') 130 | time.sleep(5.0) 131 | 132 | machine.reset() 133 | 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uPython-esp8266-httpserver 2 | 3 | This is a General Purpose http server for ESP8266 written in micropython (www.micropython.org) 4 | It just serves static html files. Simply upload into flash any html files and she will serve them. 5 | As an example put your index.html with just "hello world!" in it and you have the K&R standard greetings. 6 | 7 | It is highly configurable with parameters for SSID, PWD, AP SSID and many others. It is virually unlimited as the configuration parameters are kept into a json array of {'key', 'value'} and the server can simply store as many as possible. 8 | 9 | It is mainly a temperature web server. For devices like ESP8266 and with temperature sensors based on DS18b20. 10 | 11 | * the http server is general purpose and is easy to customize and configure 12 | * the Makefile is pretty powerful and automatize a lot of common tasks upload and reset using the script espsend.py 13 | which should be installed or configured in the proper directory 14 | * It uses other tools such as webrepl 15 | 16 | 17 | # Requirements 18 | 19 | Software: based on micropython version 1.8.5. Probably latest and bleeding version works best. 20 | 21 | # git clone https://github.com/micropython/micropython.git 22 | 23 | Hardware: Tested on Wemos D1 and Lolin V3 with ESP8266 chipset. For temperature a Dallas DS18b20 is needed wired to the device. 24 | 25 | 26 | # DIFY (Do It Yourself) 27 | 28 | Acutally I assemble all the stuff into a microappliance and I disseminate my places to track temperatures all around. 29 | Look at the pictures. 30 | 31 | 32 | ## BOM (Billing of Materials) 33 | 34 | You can easily find all this stuff very cheap on aliexpress... just shop around ;) 35 | 36 | * Wemos D1 mini (around 3$) 37 | * Cheap Charger (< 1$) 38 | * Micro USB Cable 5cm (< 1$) 39 | * Sensor DS18b20 (< 1$) 40 | 41 | Expect to afford 6US$ !!! 42 | Isn't it fu**ing amazing? https://www.dropbox.com/s/mw0tacnw87kg23v/IMG_0580.JPG?dl=0 43 | 44 | I suggest to solder the 3 wires of the DS18B20 sensor directly on the Wemos D1 pins: 5V-GND-D4 i 45 | They are contiguous. 46 | 47 | No resistor is needed then (but check the code for the right PIN which is PIN=2). 48 | 49 | # USAGE 50 | Server can be called with: http://192.168.1.123:8805/help 51 | 52 | Many other configurations are possible 53 | 54 | For instance if you want to upload data to the cloud there is a asynchronous routine called 'register' that push into a server values with json format. The server that holds data can be found here: 55 | https://github.com/ernitron/temperature-server 56 | 57 | ## STATIC FILES 58 | HTML files can be added/uploaded into FLASH memory of device and will be served. 59 | 60 | For example if you upload your example.html file and request: http://192.168.1.123:8805/example.html 61 | 62 | ## DYNAMIC BEHAVIOR 63 | Can be configured in the main loop to serve dynamically generated contents 64 | Conventionally contents are kept in cb_xyz() in content.py 65 | 66 | ## TEMPERATURE SERVER WITH DS18b20 67 | A Dallas DS18b20 temperature sensor must be installed on device. 68 | On WeMos default GPIO for reading is either 12 or 2 as it has its own pullup resistor 69 | 70 | * D6 GPIO12 machine.Pin(12) 71 | * D4 GPIO2 machine.Pin(2) 72 | 73 | ## Implementation 74 | 75 | I use a configurator that reads and saves variables into json files. Pretty cunny ;) 76 | 77 | Server http is a class object with a main loop to serve requests. 78 | 79 | The parser of requests works only on first line that has the request from browser: 80 | GET /example/?var1=value1&var2=value2 HTTP/1.1 81 | 82 | Contents are produced with embedded html commands 83 | 84 | It uses BOOTSTRAP for css/js. I have made shortcuts url for the cdn repository: 85 | * https://goo.gl/EWKTqQ = https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js 86 | * http://goo.gl/E7UCvM = http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css 87 | 88 | note: they are no more useful as files with html header are stored in flash and there is no need to shortcut them 89 | 90 | 91 | ## Installation 92 | 93 | This is a little tricky and I developed the espsend.py to automatize the uploading and to have a very fast cycle of edit-deploy-run-test 94 | 95 | I use the following tools to develop: 96 | 97 | * esp-open-sdk # git clone https://github.com/pfalcon/esp-open-sdk.git 98 | * micropython # git clone https://github.com/micropython/micropython.git 99 | * webrepl # git clone https://github.com/micropython/webrepl.git 100 | * esptool # git clone https://github.com/themadinventor/esptool.git 101 | * espsend.py # this is the script that goes on UART and make basic installation tasks. 102 | 103 | Let's start with a bare ESP8266 device like WeMos. 104 | 105 | * Install latest version of micropython 106 | * Reset device 107 | * Connect to device with picocom (or other) 108 | picocom -b 115200 109 | * Set up first time webrepl with your own password 110 | import webrepl; webrepl.start() 111 | * open a browser with page webrepl.html in webrepl folder and configure for password 112 | * Upload sources with webrepl from browser 113 | 114 | * Fast alternative use Makefile to make all. 115 | 116 | ** make erase 117 | ** make flash 118 | ** make initmicro 119 | ** make install 120 | 121 | ## Configuration 122 | *Actually THERE IS NO config.txt file included* as it contains ssid and passwords and it is personal. 123 | 124 | Consider that a it can be pretty simple. Just copy this in a file named config.txt and that's all to start 125 | 126 | {"place": "Your Place Name", "ssid": "YourSSID", "pwd": "YourSSIDPassword"} 127 | 128 | Also it must be connected to Internet to have valid start time (or you can skip starttime into the code) 129 | 130 | ## Development 131 | 132 | There is plenty of space for development 133 | 134 | ## Discussion 135 | 136 | A number of tricks are used to keep memory allocation low. 137 | 138 | See thread http://forum.micropython.org/viewtopic.php?f=16&t=2266 for a discussion 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /httpserver.py: -------------------------------------------------------------------------------- 1 | # Micropython Http Server 2 | # Erni Tron ernitron@gmail.com 3 | # Copyright (c) 2016 4 | 5 | # Global import 6 | import socket # Networking support 7 | import time # Current time 8 | import sys 9 | import gc 10 | import machine 11 | 12 | # Local import 13 | from request import parse_request 14 | from content import cb_open, cb_status, cb_getconf, cb_setconf, cb_resetconf 15 | from content import cb_temperature, cb_temperature_json, cb_temperature_plain 16 | 17 | # A simple HTTP server 18 | class Server: 19 | def __init__(self, title='uServer'): 20 | # Constructor 21 | self.title = title 22 | self.conn = None 23 | self.addr = None 24 | self.head2 = cb_open('header.txt') 25 | self.footer = cb_open('footer.txt') 26 | self.timeout = 10.0 27 | 28 | def activate(self, port, host='0.0.0.0'): 29 | # Attempts to aquire the socket and launch the server 30 | try: 31 | self.socket = socket.socket() 32 | self.socket.settimeout(self.timeout) # otherwise it will wait forever 33 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 34 | self.socket.bind((host, port)) 35 | print("Server ", host, ":", port) 36 | except Exception as e: 37 | print(e) 38 | self.socket.listen(1) # maximum number of queued connections 39 | 40 | def wait_connections(self, interface): 41 | 42 | # Main loop awaiting connections 43 | refresh30 = '\r\n' 44 | redirect2 = '\r\n' 45 | error404 = '404 - Error' 46 | 47 | while True: 48 | if not interface.isconnected(): 49 | raise OSError('Lost connection') 50 | 51 | print('Wait') 52 | 53 | try: 54 | self.conn, self.addr = self.socket.accept() 55 | req = self.conn.readline() 56 | except KeyboardInterrupt: 57 | raise OSError('Interrupt') 58 | except: 59 | return 60 | 61 | l = 0 62 | while True: 63 | h = self.conn.readline() 64 | if not h or h == b'\r\n': 65 | break 66 | if 'Content-Length: ' in h: 67 | try: 68 | l = int(h[16:-2]) 69 | except: 70 | continue 71 | if l : 72 | fullreq = self.conn.read(l) 73 | print(fullreq) 74 | 75 | # Some defaults that can be changed in progress 76 | code = 200 77 | extension = 'h' 78 | refresh = '' 79 | 80 | # determine request method (GET / POST are supported) 81 | r = parse_request(req) 82 | if r == None: 83 | code = 404 84 | content = error404 85 | elif r['method'] == 'POST' or r['method'] == 'post': 86 | print(h) 87 | content = cb_setconf(None, None) 88 | elif r['uri'] == b'/temperature' or r['uri'] == b'/' : 89 | refresh=refresh30 90 | content = cb_temperature() 91 | elif r['uri'] == b'/j' : 92 | extension = 'j' 93 | content = cb_temperature_json() 94 | elif r['uri'] == b'/getconf': 95 | content = cb_getconf() 96 | elif b'/setconf' in r['uri']: 97 | if 'key' in r['args'] and 'value' in r['args']: 98 | content = cb_setconf(r['args']['key'], r['args']['value']) 99 | elif 'key' in r['args'] : 100 | content = cb_setconf(r['args']['key'], None) 101 | else: 102 | content = cb_setconf(None, None) 103 | elif r['uri'] == b'/status': 104 | content = cb_status() 105 | elif r['uri'] == b'/webrepl' : 106 | import webrepl 107 | webrepl.start() 108 | refresh = redirect2 109 | code = 302 110 | content = '