Skip to content

Sockets do not work reliably on ESP32-S2 #4420

@ThomasAtBBTF

Description

@ThomasAtBBTF

Firmware

With 6.2.0-beta.2 and 6.2.0-beta.3 on a MagTag

Code/REPL

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



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:

  1. 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.

  2. "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:

  1. a MagTag with CP-code
  2. a PC with Windows 10
  3. Chrome running on this PC
  4. a WiFi Network which connects the MagTag and the PC

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions