mirror of
https://github.com/KevinMidboe/hivemonitor-esp32-firmware.git
synced 2025-10-29 09:30:26 +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