import sys
import ipaddress
import time
import wifi
import socketpool
import adafruit_requests
import ssl
import board
import busio
import json
MYSERVER = "HTTP/1.1 200 OK\nServer: WebSocket Server\nContent-Type: text/html\n"
MYHTML_LEN = "Content-Length: {}\n\n"
MYHTML = """
<!DOCTYPE html>
<html>
<head>
<style>
title {
text-align: left;
}
body {
background-color: #FFC926;
}
h1 {
background-color: green;
color: white;
}
th {
text-align: left;
color: blue;
font-family: sans-serif;
}
td {
text-align: left;
color: black;
font-family: sans-serif;
}
input {
background-color: #C2DFFF;
}
select{
background-color: #C2DFFF;
}
</style>
<title>ESP32 Webserver Demo V 0.1</title>
</head>
<body>
<h1 style="text-align: center;">Demo</h1>
<form ID="form" name="form">
<table>
<tbody>
<tr> <th>Mode:</th>
<td>
<select ID="MYMODE">
<option selected>ON</option>
<option >OFF</option>
<option >MANUAL</option>
</select>
</td>
<tr>
<th>Current</th>
<td>
</td>
<td>
</td>
</tr>
<tr>
<th></th>
<td>
<input ID="CURRENT" value="12345678" readonly>
</td>
</tr>
<tr>
<th>Edit</th>
<td></td>
</tr>
<tr>
<th>Value:</th>
<td>
<input id="MYVALUE">
</td>
</tr>
<tr>
<th>Control: </th>
<td>
<button id="UP">Up</button>
</td>
<td>
<button id="DOWN">Down</button>
</td>
</tr>
</tbody>
</table>
</form>
</body>
<script>
document.getElementById("MYMODE").addEventListener("change", dealWithChange, false);
document.getElementById("UP").addEventListener("click", dealWithClick, false);
document.getElementById("DOWN").addEventListener("click", dealWithClick, false);
var form = document.getElementById("form");
var elements = form.elements;
var values = {};
var ESP32Fields = {};
var EditingID = "";
var EditingTO = 0;
for (var i=0, iLen=elements.length; i<iLen; i++) {
var element = elements[i];
console.log(element.localname + " " + element.id + ' name:' + element.name + ' value:' + element.value);
values[element.id] = element.value;
if (i <= 14) {
element.addEventListener("keypress", dealWithKeyboard, false);
element.addEventListener("focus", dealWithFocus, false);
element.addEventListener("focusout", dealWithFocusOut, false);
}
}
var xhr = new XMLHttpRequest();
xhr.open('GET', this.document.URL+'?alldata', true);
xhr.send();
xhr.onreadystatechange = processRequest;
function processRequest(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log("first response:" + xhr.responseText);
ESP32Fields = JSON.parse(xhr.responseText);
var form = document.getElementById("form");
var elements = form.elements;
for (var key in ESP32Fields) {
let value = ESP32Fields[key];
console.log("key:" + key + " val:" + value);
elements[key].value = value;
}
}
}
function HandleTic() {
console.log("Tic");
var dict = {}; // create an dictionary
if (EditingTO > 0) {
EditingTO -= 1;
if (EditingTO == 0) {
dict[EditingID] = elements[EditingID].value;
var xhr = new XMLHttpRequest();
xhr.open('GET', this.document.URL+'?field='+JSON.stringify(dict), true);
xhr.send();
xhr.onreadystatechange = processRequest;
function processRequest(e) {
console.log("processRequest *if*");
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
}
} else {
var xhr = new XMLHttpRequest();
console.log(this.document.URL);
xhr.open('GET', this.document.URL+'?json=get', true);
xhr.send();
xhr.onreadystatechange = processRequest;
function processRequest(e) {
console.log("processRequest *else* state=" + xhr.readyState.toString() + " status=" +xhr.status.toString());
console.log("resp:" + xhr.responseText)
if (xhr.readyState == 4 && xhr.status == 200) {
if (xhr.responseText.substring(0, 1) == "{") {
var Fields = JSON.parse(xhr.responseText);
for (var key in Fields) {
var value = Fields[key];
if (key == EditingID) {
console.log("skip key:" + key + " val:" + value);
} else {
console.log("key:" + key + " val:" + value);
elements[key].value = value;
}
}
} else {
console.log(xhr.responseText);
}
} else {
console.log("processRequest: state=" + xhr.readyState.toString() + " status=" +xhr.status.toString());
}
}
}
}
var intervalID = window.setInterval(HandleTic, 900);
function dealWithKeyboard(event)
{
console.log(event.keyCode);
if (this.readOnly) {
console.log("Readonly");
return;
}
if (event.keyCode == 13) {
// EditingTO = 1;
this.style.backgroundColor = "#C2DFFF";
EditingID = "";
SendField(event.target.id);
event.returnValue=false;
} else {
// EditingTO = 3;
EditingID = event.target.id;
this.style.backgroundColor = "#77fb93";
}
}
function dealWithFocus(event)
{
EditingID = this.id;
if (this.readOnly) {
console.log("Readonly");
} else {
this.style.backgroundColor = "#77fb93";
}
console.log("dealWithFocus");
console.log("original value:" + values[event.target.id]);
console.log("current value:" + event.target.value);
values[event.target.id] = event.target.value;
}
function dealWithFocusOut(event)
{
if (EditingID == "") {
return;
}
EditingID = ""
this.style.backgroundColor = "#C2DFFF";
console.log("dealWithFocusOut");
console.log("original value:" + values[event.target.id]);
console.log("current value:" + event.target.value);
SendField(event.target.id);
values[event.target.id] = event.target.value;
}
function dealWithClick(event)
{
console.log("dealWithClick");
event.returnValue=false;
SendField(event.target.id);
XChangeData("click="+event.target.id);
}
function handleControlValue(ID)
{
console.log("handleControlValue:" + ID);
console.log("original value:" + values[ID]);
console.log("current value:" + elements[ID].value);
console.log("ESP32 value:" + ESP32Fields[ID]);
}
function dealWithChange(event)
{
console.log("dealWithChange:" + this.value);
SendField(event.target.id);
values[event.target.id] = event.target.value;
EditingID = "";
this.style.backgroundColor = "#C2DFFF";
}
function SendField(FieldID) {
var dict = {};
dict[FieldID] = elements[FieldID].value;
var s = JSON.stringify(dict);
XChangeData("field="+s);
}
function XChangeData(s) {
var xhr = new XMLHttpRequest();
xhr.open('GET', this.document.URL+'?'+s, true);
xhr.send();
xhr.onreadystatechange = processRequest;
function processRequest(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
if (xhr.responseText.substring(0, 1) == "{") {
var Fields = JSON.parse(xhr.responseText);
for (var key in Fields) {
let value = Fields[key];
console.log("key:" + key + " val:" + value);
elements[key].value = value;
}
}
}
}
}
</script>
</html>
"""
mymode = 2
myvalue = 99
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
_hextobyte_cache = None
def unquote(string):
"""unquote('abc%20def') -> b'abc def'."""
global _hextobyte_cache
# Note: strings are encoded as UTF-8. This is only an issue if it contains
# unescaped non-ASCII characters, which URIs should not.
if not string:
return b''
if isinstance(string, str):
string = string.encode('utf-8')
bits = string.split(b'%')
if len(bits) == 1:
return string
res = [bits[0]]
append = res.append
# Build cache for hex to char mapping on-the-fly only for codes
# that are actually used
if _hextobyte_cache is None:
_hextobyte_cache = {}
for item in bits[1:]:
try:
code = item[:2]
char = _hextobyte_cache.get(code)
if char is None:
char = _hextobyte_cache[code] = bytes([int(code, 16)])
append(char)
append(item[2:])
except KeyError:
append(b'%')
append(item)
return str(b''.join(res))[2:-1]
def macstr(bssid):
s = ""
for b in bssid:
h = hex(b)[2:]
s += f'{h:0>2}' + ":"
return s[:-1]
def receive_message(connection):
try:
packet = bytearray(10000) # is this a reasonable size? Browsers probably do not send more bytes at once.
received_bytes = connection.socket.recv_into(packet)
print(f" received {connection.address}, {received_bytes}")
if received_bytes == 0:
print(f" what does it mean if 0 bytes are received !?")
return received_bytes, packet[:received_bytes].decode("UTF-8")
except Exception as e:
if (e.errno == 11): # EAGAIN
pass
elif (e.errno == 116):
sys.print_exception(e)
else:
sys.print_exception(e)
return False, False
def allfields():
global mymode, myvalue
f = {}
f["MYMODE"] = mymode
f["MYVALUE"] = myvalue
return json.dumps(f)
def handlefields(s):
fields = json.loads(s)
for field in fields:
print(field)
print(chr(12))
print("ESP32-S2 minimal WebServer")
print("My MAC addr:", macstr(wifi.radio.mac_address))
print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
print(f' {str(network.ssid, "utf-8"):<12}\tRSSI: {network.rssi}\tChannel: {network.channel}\tMAC: {macstr(network.bssid)}')
wifi.radio.stop_scanning_networks()
print("Connecting to ", secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to ", secrets["ssid"])
print("Connected to SSID ", wifi.radio.ap_info.ssid)
print("Connected to channel ", wifi.radio.ap_info.channel)
print("Connected to strangth ", wifi.radio.ap_info.rssi)
print("Connected to MAC address ", macstr(wifi.radio.ap_info.bssid))
print("My IP address is ", wifi.radio.ipv4_address)
print("My IP address type", type(wifi.radio.ipv4_address))
print(type(wifi.radio.ipv4_address), wifi.radio.ipv4_address, dir(wifi.radio.ipv4_address))
MYIP = str(wifi.radio.ipv4_address)
spool = socketpool.SocketPool(wifi.radio)
srvsock = spool.socket(spool.AF_INET, spool.SOCK_STREAM)
srvsock.bind((MYIP, 80))
srvsock.listen(32) # what would be a reasonable number for the length of the backlog queue?
srvsock.setblocking(False)
srvsock.settimeout(0)
class Connection:
def __init__(self, socket, address):
self.socket = socket
self.address = address
self.valid = True
self.initalmessagereceived = False
self.websiteserved = False
connections = [] # will contain Connection objects created after a succesful accept
loopcount = 0
acttime = time.monotonic()
oncepersecond = acttime
myconnection = Connection(srvsock, (MYIP, 0))
connections.append(myconnection)
def send_line(connection, l, byte_line):
need_bytes = len(byte_line)
sum_bytes = 0
retries = 0
while True:
try:
line_bytes = connection.socket.send(byte_line)
sum_bytes += line_bytes
# preparing some of the bytes which are send for debug output
s = byte_line.decode("UTF-8")
s = s[:30]
if sum_bytes == need_bytes:
# the tansfer succeded to send all bytes
c = " "
print("\r\nl:", c, retries, l, sum_bytes, need_bytes, s, end="")
return sum_bytes # return because we are done!
else:
retries += 1
c = "?"
s = s[:line_bytes] # show the bytes which were successfully transfered
print("\r\nl:", c, retries, l, sum_bytes, need_bytes, s, end="")
byte_line = byte_line[line_bytes:] # slice away the already successful transfered bytes
continue # try again with the remaining bytes
except OSError as e:
if e.errno == 11: # EAGAIN here no bytes have been transfered
print("\r\nl: no bytes transfered try it again!")
time.sleep(0.1) # wait a while.... ? neccessary ?
continue
elif e.errno == 104: # ECONNRESET here the other side is not responding anymore!
connection.valid = False # ! flag this connection / client as not valid anymore
print("send_line Connection reset! ", connection.socket)
return -1
else:
print("\r\nsend_line Error:", e.errno, str(e)) # another error (not observed so far!)
return -1
def send_website(connection):
string_to_send = MYSERVER
bytes_to_send = string_to_send.encode("UTF-8")
if send_line(connection, 0, bytes_to_send) == -1:
print("unable to send the MYSERVER response ", connection.socket)
return False
# prepare the "website" into lines to not overflow the socket
website_to_send = MYHTML
lines_to_send = website_to_send.split("\n")
bytecount_to_send = 0
byte_lines_to_send = []
for line in lines_to_send:
s = line + "\n"
byte_line = s.encode("UTF-8") # The website is an embedded string here!
bytecount_to_send += len(byte_line)
byte_lines_to_send.append(byte_line)
string_to_send = MYHTML_LEN.format(bytecount_to_send) # inform the browser of the size of the website
bytes_to_send = string_to_send.encode("UTF-8")
if send_line(connection, 0, bytes_to_send) == -1:
print("unable to send the MYHTML_LEN response ", connection.socket)
return False
bytes_send = 0
l = 0
for byte_line in byte_lines_to_send: # the loop which sends the website length
l += 1
result = send_line(connection, l, byte_line)
if result < 0:
connection.valid = False
print("unable to send the website! ", connection.socket)
return False
bytes_send += result
print("\n\rWEBSITE sent result:", l, len(byte_lines_to_send), bytes_send, bytes_to_send)
connection.websiteserved = True
return True
def send_alldata(connection):
string_to_send = allfields()
bytes_to_send = string_to_send.encode("UTF-8")
result = send_line(connection, 0, bytes_to_send)
if result < 0:
print("Unable to send alldata! ", connection.socket)
return False
def send_favicon(connection):
string_to_send = "HTTP/1.0 404 \r\n\r\n"
bytes_to_send = string_to_send.encode("UTF-8")
result = send_line(connection, 0, bytes_to_send)
if result < 0:
print("Unable to send favicon response! ", connection.socket)
return False
while True: # This is the main server loop !
acttime = time.monotonic()
loopcount += 1
while True: # dummy loop for exit in the middle
try:
client_socket, client_Addr = srvsock.accept()
connection = Connection(client_socket, client_Addr) # here the clients are added
connections.append(connection)
acttime = time.monotonic()
print(f"New client: {client_socket}, {client_Addr[0]}, {client_Addr[1]}")
except OSError as e:
if e.errno == 11: # EAGAIN
break
if e.errno == 116:
pass
sys.print_exception(e)
except Exception as e:
sys.print_exception(e)
break # the dummyloop allways at here the end
if acttime - oncepersecond > 1: # we look for request from the client / browser every second
oncepersecond = acttime
# print("once per second!", loopcount)
loopcount = 0
for connection in connections: # loop over all clients...
if not connection.valid:
continue # do it only for valid clients.. (todo: removing invalid clients from the collection)
from_client, msg = receive_message(connection)
if msg: # did we get a message of len > 0 ?
print(from_client, msg)
msgparts = msg.split(" ")
print("Part 0, 1: ", msgparts[0], msgparts[1])
# here we react on requests from the browser / client connection
if msgparts[0] == "GET":
if (msgparts[1] == "/") or (msgparts[1] == "/?"):
send_website(connection)
acttime = time.monotonic()
if (msgparts[1] == "/?alldata") or (msgparts[1] == "/?json=get"):
send_alldata(connection)
if (msgparts[1] == "/??alldata") or (msgparts[1] == "/??json=get"):
send_alldata(connection)
if msgparts[1] == "/favicon.ico":
send_favicon(connection)
if msgparts[1] == "/?click":
s = msgparts[2]
print("button click", s, msg)
# End
This is a reduced port from a "old" MicroPython project which works fine on a ESP32.
This project shows how a controller running CircuitPython can send a website to a browser to create an interactive user interface with select objects, edit fields and buttons.
The controller can fill in data into these objects and the JavaScript in the website can handle the data from the controller.
Also, the JavaScript can tell the controller about the state of the website (focus location).
Also, the JavaScript can tell the controller about the UI events like selection in the "dropdown" or clicking buttons.
All this is implemented by periodical get requests in a JavaScript-Timer and Events from the UI-Elements.
To transfer the data and to receive data from the controller the Java XMLHttpRequest class is used.
Sorry, but I can not strip the code much more as multiple components are at play in order to reproduce the problem which are:
I will try to upload a commented log file from my controller connected to a console with also some print screen as a PDF.
Firmware
Code/REPL
Description
This is a reduced port from a "old" MicroPython project which works fine on a ESP32.
This project shows how a controller running CircuitPython can send a website to a browser to create an interactive user interface with select objects, edit fields and buttons.
The controller can fill in data into these objects and the JavaScript in the website can handle the data from the controller.
Also, the JavaScript can tell the controller about the state of the website (focus location).
Also, the JavaScript can tell the controller about the UI events like selection in the "dropdown" or clicking buttons.
All this is implemented by periodical get requests in a JavaScript-Timer and Events from the UI-Elements.
To transfer the data and to receive data from the controller the Java XMLHttpRequest class is used.
I am observing the following problems:
when sending the website the send_line procedure often sends only some bytes or I get an EAGAIN error.
The code in send_line is handling the problem and typically the full website is transferred to the browser.
"after while" requests are not arriving anymore at the sockets...
That request are generated and are "timing-out" can be seen in the debug screen in Chrome (which can be activated by pressing F12)
Sorry, but I can not strip the code much more as multiple components are at play in order to reproduce the problem which are:
Additional Info
I will try to upload a commented log file from my controller connected to a console with also some print screen as a PDF.