mirror of
				https://github.com/KevinMidboe/hivemonitor-esp32-firmware.git
				synced 2025-10-29 17:40:25 +00:00 
			
		
		
		
	Setup configuration server and html files & assets
This commit is contained in:
		
							
								
								
									
										234
									
								
								src/setup/configuration_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								src/setup/configuration_server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | ||||
| import network, machine, utime, gc | ||||
| from ubinascii import hexlify | ||||
| from esp32 import NVS | ||||
|  | ||||
| try: | ||||
|     import usocket as socket | ||||
| except: | ||||
|     import socket | ||||
|  | ||||
| gc.collect() | ||||
| nvs = NVS('conf') | ||||
|  | ||||
| AP_SSID = 'MicroPython-AP' | ||||
| AP_PASSWOD = '123456789' | ||||
| REBOOT_DELAY = 2 | ||||
|  | ||||
| settings = [ | ||||
|     'ssid', | ||||
|     'pass', | ||||
|     'name', | ||||
|     'mqtt_broker', | ||||
|     'mqtt_topic', | ||||
|     'dht11_pin', | ||||
|     'ds28b20_pin', | ||||
|     'mac', | ||||
|     'peer', | ||||
|     'freq' | ||||
| ] | ||||
|  | ||||
| routes = [] | ||||
| routeTree = {} | ||||
| contentTypes = { | ||||
|     'html': 'text/html', | ||||
|     'css': 'text/css' | ||||
| } | ||||
|  | ||||
|  | ||||
| def saveDeviceInfo(): | ||||
|     setStorage({ | ||||
|         'mac': hexlify(network.WLAN().config('mac'),':').decode(), | ||||
|         'freq': str(machine.freq() / 1000000) # megahertz | ||||
|     }) | ||||
|  | ||||
|  | ||||
| def setupAP(): | ||||
|     ap = network.WLAN(network.AP_IF) | ||||
|     ap.active(True) | ||||
|     ap.config(essid=AP_SSID, password=AP_PASSWOD, security=3) | ||||
|  | ||||
|     while ap.active() == False: | ||||
|         pass | ||||
|  | ||||
|     print('Connection successful') | ||||
|     print(ap.ifconfig()) | ||||
|  | ||||
|  | ||||
| def setupServer(): | ||||
|     # bind socket server to port 80 | ||||
|     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|     s.bind(('', 80)) | ||||
|     s.listen(5) | ||||
|     return s | ||||
|  | ||||
|  | ||||
| def reboot(delay = REBOOT_DELAY): | ||||
|     print (f'Rebooting device in {delay} seconds (Ctrl-C to escape).') | ||||
|     utime.sleep(delay) | ||||
|     machine.reset() | ||||
|  | ||||
|  | ||||
| def identify(): | ||||
|     led = machine.Pin(13, machine.Pin.OUT) | ||||
|     count = 6 | ||||
|     while count > 0: | ||||
|         count = count - 1 | ||||
|         led.value(1) | ||||
|         utime.sleep_ms(400) | ||||
|         led.value(0) | ||||
|         utime.sleep_ms(250) | ||||
|  | ||||
|  | ||||
| def setStorage(data): | ||||
|     for key, value in data.items(): | ||||
|         nvs.set_blob(key, value.encode()) | ||||
|  | ||||
|  | ||||
| def getStorageVar(key): | ||||
|     value = bytearray(96) | ||||
|     try: | ||||
|         length = nvs.get_blob(key, value) | ||||
|         value = value.decode('utf-8')[:length] | ||||
|     except OSError as e: | ||||
|         if 'ESP_ERR_NVS_NOT_FOUND' in str(e): | ||||
|             print('Missing NVS key "{}", adding blank value'.format(key)) | ||||
|             value = '' | ||||
|             setStorage({key: value}) | ||||
|         else: | ||||
|             raise e | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def importHTML(filename, route, hydration=None): | ||||
|     global routeTree | ||||
|     f = open(filename) | ||||
|     html = f.read() | ||||
|     html = ' '.join(html.split()) | ||||
|     f.close() | ||||
|  | ||||
|     if hydration: | ||||
|         html = hydrateHTMLTemplate(html, hydration) | ||||
|  | ||||
|     routeTree[route] = html | ||||
|  | ||||
|  | ||||
| def hydrateHTMLTemplate(source, keys): | ||||
|     for key in keys: | ||||
|         templateString = '{{ ' + key + ' }}' | ||||
|         source = source.replace(templateString, getStorageVar(key)) | ||||
|     return source | ||||
|  | ||||
|  | ||||
| def importRoutes(): | ||||
|     global routes | ||||
|     importHTML('index.html', '/', settings), | ||||
|     importHTML('success.html', '/save') | ||||
|     importHTML('styles.css', '/styles.css') | ||||
|     routes = routeTree.keys() | ||||
|  | ||||
|  | ||||
| def htmlEncodedStrings(string): | ||||
|     if '%3A' in string: | ||||
|         string = string.replace('%3A', ':') | ||||
|     return string | ||||
|  | ||||
|  | ||||
| def parsePostRequest(req): | ||||
|     reqString = str(req).split()[-1] | ||||
|     reqString = reqString.split('\\n')[-1] | ||||
|     reqString = reqString[:-1] | ||||
|     args = reqString.split('&') | ||||
|  | ||||
|     data = {} | ||||
|     for arg in args: | ||||
|         [key, value] = arg.split('=') | ||||
|         data[key] = htmlEncodedStrings(value) | ||||
|  | ||||
|     print('got post data: ' + str(data)) | ||||
|     return data | ||||
|  | ||||
|  | ||||
| def getContentType(path): | ||||
|     try: | ||||
|         extension = path.split('.')[1] | ||||
|         return contentTypes[extension] | ||||
|     except: | ||||
|         return contentTypes['html'] | ||||
|  | ||||
|  | ||||
| def response200(conn, text, contentType='text/html'): | ||||
|     conn.send('HTTP/1.1 200 OK\n') | ||||
|     conn.send('Content-Type: %s\n' % contentType) | ||||
|     conn.send('Connection: close\n\n') | ||||
|     conn.send(text) | ||||
|     conn.close() | ||||
|  | ||||
|  | ||||
| def response404(conn): | ||||
|     conn.send('HTTP/1.1 404 NOT FOUND\n') | ||||
|     conn.send('Connection: close\n\n') | ||||
|     conn.close() | ||||
|  | ||||
|  | ||||
| def response400(conn): | ||||
|     conn.send('HTTP/1.1 400 BAD REQUEST\n') | ||||
|     conn.send('Connection: close\n\n') | ||||
|     conn.close() | ||||
|  | ||||
|  | ||||
| def handleRequest(conn): | ||||
|     request = conn.recv(1024) | ||||
|  | ||||
|     requestSegments = request.split() | ||||
|     if len(requestSegments) <= 2: | ||||
|         response400(conn) | ||||
|  | ||||
|     method = requestSegments[0].decode('utf-8') | ||||
|     path = requestSegments[1].decode('utf-8') | ||||
|     contentType = getContentType(path) | ||||
|  | ||||
|     if path in routes and method == 'GET': | ||||
|         response200(conn, routeTree[path], contentType) | ||||
|  | ||||
|     elif path == '/save' and method == 'POST': | ||||
|         setStorage(parsePostRequest(request)) | ||||
|         importHTML('index.html', '/', settings), | ||||
|         response200(conn, routeTree[path], contentType) | ||||
|  | ||||
|     elif path == '/reboot' and method == 'POST': | ||||
|         response200(conn, 'ok', contentType) | ||||
|         reboot() | ||||
|  | ||||
|     elif path == '/identify' and method == 'POST': | ||||
|         response200(conn, 'ok', contentType) | ||||
|         identify() | ||||
|  | ||||
|     else: | ||||
|         response404(conn) | ||||
|  | ||||
|  | ||||
| def serverRequests(s): | ||||
|     while True: | ||||
|         conn, addr = s.accept() | ||||
|         print('Received req from: ' + str(addr)) | ||||
|         handleRequest(conn) | ||||
|  | ||||
|  | ||||
| def serveSetupServer(): | ||||
|     setupAP() | ||||
|     saveDeviceInfo() | ||||
|     s = setupServer() | ||||
|     importRoutes() | ||||
|  | ||||
|     serverRequests(s) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     try: | ||||
|         serveSetupServer() | ||||
|     except KeyboardInterrupt as err: | ||||
|         raise err #  use Ctrl-C to exit to micropython repl | ||||
|     except Exception as err: | ||||
|         print ('Error during execution:', err) | ||||
|         reboot() | ||||
							
								
								
									
										106
									
								
								src/setup/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/setup/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| body { | ||||
|   font-family: Helvetica Neue; | ||||
|   min-height: calc(100vh - 1.3rem); | ||||
|   min-height: -webkit-fill-available; | ||||
|   margin: 0; | ||||
|   padding: 0.8rem; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| section { | ||||
|   max-width: 900px; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| h3 { | ||||
|   margin: 0.2rem 0 !important; | ||||
|   padding-left: 0.55rem; | ||||
|   font-size: 1.1rem; | ||||
| } | ||||
|  | ||||
| label { | ||||
|   margin-right: 0.5rem; | ||||
|   font-size: 1rem; | ||||
|   color: rgba(0, 0, 0, 0.7); | ||||
|   width: 100px; | ||||
| } | ||||
|  | ||||
| input { | ||||
|   background-color: transparent; | ||||
|   border: none; | ||||
|   font-size: inherit; | ||||
|   flex-grow: 1; | ||||
| } | ||||
|  | ||||
| input[type="password"], input#pass { | ||||
|   margin-right: 1rem; | ||||
| } | ||||
|  | ||||
| button, button.light:hover { | ||||
|   background-color: #1a202e; | ||||
|   color: white; | ||||
|   padding: 0.75rem; | ||||
|   text-align: center; | ||||
|   vertical-align: middle; | ||||
|   border-radius: 0.8rem; | ||||
|   border: none; | ||||
|   font-size: 1.1rem; | ||||
|   cursor: pointer; | ||||
|   text-transform: uppercase; | ||||
|   font-weight: bold; | ||||
|   border: 2px solid #1a202e; | ||||
| } | ||||
|  | ||||
| button.light { | ||||
|   border: 2px solid #F0F3F7; | ||||
|   background-color: white; | ||||
|   color: #1a202e; | ||||
| } | ||||
|  | ||||
| button, i { | ||||
|   user-select: none; | ||||
|   -webkit-user-select: none; | ||||
|   cursor: pointer; | ||||
|   touch-action: manipulation; | ||||
| } | ||||
|  | ||||
| form { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   flex-grow: 1; | ||||
|   flex-direction: column; | ||||
|   margin-top: 1.5rem; | ||||
| } | ||||
|  | ||||
| form > * { | ||||
|   margin-bottom: 1.1rem; | ||||
| } | ||||
|  | ||||
| form div { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   background-color: #f1f5f9; | ||||
|   padding: 0.8rem 0.75rem; | ||||
|   border-radius: 0.5rem; | ||||
|   font-size: 1rem; | ||||
| } | ||||
|  | ||||
| dialog { | ||||
|   width: 100%; | ||||
|   max-width: 85vw; | ||||
| } | ||||
|  | ||||
| dialog form { | ||||
|   margin-top: 0; | ||||
| } | ||||
|  | ||||
| dialog button { | ||||
|   margin-top: 2rem; | ||||
| } | ||||
							
								
								
									
										77
									
								
								src/setup/success.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/setup/success.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta | ||||
|       name="viewport" | ||||
|       content="width=device-width,initial-scale=1,maximum-scale=1" | ||||
|     /> | ||||
|     <title></title> | ||||
|      | ||||
|     <link rel="stylesheet" type="text/css" href="./styles.css"  /> | ||||
|   </head> | ||||
|  | ||||
|   <body> | ||||
|     <h1>Sucessfully updated!</h1> | ||||
|  | ||||
|     <p> | ||||
|       Settings successfully save to controller. Disable setup mode switch and | ||||
|       reboot. | ||||
|     </p> | ||||
|  | ||||
|     <div class="bottom-content"> | ||||
|       <p style="display: none"> | ||||
|         Rebooting in <span id="countdown">5</span> seconds | ||||
|       </p> | ||||
|       <button id="reboot">Reboot device</button> | ||||
|     </div> | ||||
|   </body> | ||||
|  | ||||
|   <script> | ||||
|     function countDownAndSubmitReboot() { | ||||
|       countdownElement.parentElement.style.display = "block"; | ||||
|  | ||||
|       let count = Number(countdownElement.innerText); | ||||
|       const id = setInterval(() => { | ||||
|         count = count - 1; | ||||
|         countdownElement.innerText = count; | ||||
|         if (count === 0) { | ||||
|           clearTimeout(id); | ||||
|  | ||||
|           fetch('/reboot', { method: 'POST' }) | ||||
|             .then(console.log) | ||||
|         } | ||||
|       }, 1000); | ||||
|     } | ||||
|  | ||||
|     const countdownElement = document.getElementById("countdown"); | ||||
|     const rebootBtn = document.getElementById("reboot"); | ||||
|  | ||||
|     rebootBtn.addEventListener("click", countDownAndSubmitReboot); | ||||
|   </script> | ||||
|  | ||||
|   <style> | ||||
|     .bottom-content { | ||||
|       margin-top: auto; | ||||
|       width: 100%; | ||||
|     } | ||||
|  | ||||
|     .bottom-content p { | ||||
|       text-align: center; | ||||
|     } | ||||
|  | ||||
|     button { | ||||
|       background-color: #1a202e; | ||||
|       color: white; | ||||
|       padding: 0.75rem; | ||||
|       text-align: center; | ||||
|       vertical-align: middle; | ||||
|       width: 100%; | ||||
|       border-radius: 0.8rem; | ||||
|       border: none; | ||||
|       font-size: 1.1rem; | ||||
|       cursor: pointer; | ||||
|       text-transform: uppercase; | ||||
|     } | ||||
|   </style> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user