Черновик асинхронного GET/POST для micropython

Это скорее черновик, если кратко, имеем ESP32-S, PSRAM нет, прошита микропитоном esp32-idf3-20200902-v1.13.bin.
Появилась необходимость использовать методы GET и POST для отправки/получения данных на сервер асинхронно вместе с другими циклами, да ещё с ssl.

И при этом контролировать таймауты в зависимости от важности того или иного подключения.

Готовые варианты или не помещались в память, или как uaiohttpclient.py не поддерживали https.

В итоге получилась примерно такая (как-бы асинхронная) зарисовка на салфетке:

ahttp.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# async http
import uasyncio as asyncio
import network
import ping
import time
try:
    import usocket as socket
except:
    import socket
station = network.WLAN(network.STA_IF)

async def _as_write_get(data, sock):
    try:
        n = sock.write(data)
    except OSError:
        pass
async def _as_write_post(data, pdata, sock):
    try:
        pp = sock.write(data)
        time.sleep_ms(50)
        pp = sock.write(pdata)
    except OSError:
        pass
async def _as_write_post(data, pdata, sock):
    try:
        pp = sock.write(data)
        time.sleep_ms(50)
        pp = sock.write(pdata)
    except OSError:
        pass
async def _as_read(sock):
    s_response = ''
    response_lines = 0
    while True:
        response_lines += 1
        data = sock.read(256)
        if data is not None:              # Если сервер не закрывает соединение (например, как bash [nc -l -p 80]), то возвращается s_response = 'empty'
            s_response += str(data.decode(), 'utf8')
            if response_lines > 3:        # Задача была получить первые 3 строки ответа включая заголовки
                return s_response
                break
        else:
            s_response = 'empty'
            return s_response
            break
async def ahttp(url, method, _PDATA, t):
    print("Fetching:", url)
    proto, _, host, path = url.split('/', 3)      # trailing slash are important!
    google = '8.8.8.8'
    # Вариант проверки наличия выхода в сеть, блокирующий процесс на время выполения ping, но контролируется таймаутом
    # Проверка подключения к сети station.isconnected(), следом за ним пинг, или целевого хоста, или google
    if station.isconnected():
        _pong = ping.ping(google, 2, 1000)     # host | google
        if _pong > 0:
            pass
        else:
            as_response = 'ping error'
            return as_response
    else:
        as_response = 'not connected'
        return as_response
    # 
    if proto == "http:":
        port = 80
    elif proto == "https:":
        import ussl
        port = 443
    addr = socket.getaddrinfo(host, port)[0][-1]
    s = socket.socket()
    s.setblocking(0)                # s.setblocking(False)
    s.settimeout(t)                 # s.settimeout(0.0)
    try:
        s.connect(addr)
        if proto == "https:":
            s = ussl.wrap_socket(s, server_hostname=host)
        if method == "GET":
            data = bytes('GET /%s HTTP/1.0\r\nHost: %s\r\nUser-Agent: ESP32\r\nConnection: close\r\n\r\n' % (path, host), 'utf8')
            await asyncio.wait_for(_as_write_get(data, sock=s), timeout=t)  # отправка заголовков GET
        if method == "POST":
            pdata = _PDATA
            data = bytes('POST /%s HTTP/1.0\r\nHost: %s\r\nUser-Agent: ESP32\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: %s\r\n\r\n' % (path, host, str(len(pdata))), 'utf8')
            await asyncio.wait_for(_as_write_post(data, pdata, sock=s), timeout=t)      # отправка заголовков POST и данных pdata
        await asyncio.sleep_ms(100)                                     # пауза между отправкой в сокет и чтением из него
        as_response = await asyncio.wait_for(_as_read(s), timeout=t)    # чтение заголовков/ответа сервера, прерывается по таймауту t
        s.close()
    except OSError:
        as_response = 'OSError 1'     # if server not respond or slower than defined timeout
    except ValueError:
        as_response = 'ValueError'
    except UnicodeError:
        as_response = 'UnicodeError'
    return as_response
    

ping.py

1
2
3
4
5
6
7
8
import uping2

def ping(host, c, to):
    pong = uping2.ping(host, count=c, timeout=to, interval=100, quiet=True)
    # uping2.ping('8.8.8.8', count=3, timeout=1000, interval=100, quiet=True)
    # return 3
    return pong
    

uping2.py отличается от оригинала только строкой:

1
2
3
    # return (n_trans, n_recv)
    return n_recv
    

Проверяем таймауты

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async def print_lines():
    count = 0
    while True:
        await asyncio.sleep(1)
        count += 1
        print("l", count)

async def send_controller():
    count = 0
    while True:
        await asyncio.sleep(5)
        count += 1
        print("send", count)
        # PDATA = '{"msg":"text","body":"status"}'
        # rr = await asyncio.create_task(ahttp.ahttp('http://192.168.43.194/post', 'POST', PDATA, 3))
        # таймаут 15 секунд
        rr = await asyncio.create_task(ahttp.ahttp('http://192.168.43.194/ts', 'GET', 'null', 15))
        print('>>>>>>>>', rr, '<<<<<<<<<<<<<<')

asyncio.create_task(print_lines())        # чисто для визуализации интервалов
asyncio.create_task(send_controller())
    

Ловим и держим

Простой вариант симуляции, когда сервер отвечает, но не закрывает соединение

1
2
while true; do ( echo -e 'HTTP/1.1 200 OK\r\n\r\n'; echo -e 'content\r\n'; ) | nc -l -p 80; done
    


Лог консоли:

1
2
3
4
5
6
7
8
9
do 51
do 52
send 9
Fetching: http://192.168.43.194/ts
do 53
    //  <ожидание ответа, следует сброс после указанного таймаута, (да блокирует)>
do 54
>>>>>>>> OSError 1 <<<<<<<<<<<<<<
do 55

Вариант с нормальным закрытием соединения со стороны сервера

1
2
while true; do ( echo -e 'HTTP/1.1 200 OK\r\n\r\n'; echo -e 'content\r\n'; ) | nc -l -p 80 -q1; done
    


1
2
3
4
5
6
7
8
9
10
11
12
13
14
do 266
do 267
do 268
send 50
Fetching: http://192.168.43.194/ts
do 269
>>>>>>>> HTTP/1.1 200 OK


content

 <<<<<<<<<<<<<<
do 270
do 271

Замедляем интернет


esp <-> роутер <-> ноутбук с сервером, поэтому тормозим wlan0 на ноутбуке.

Используем Linux Traffic Control tc

1
2
3
4
5
6
# установить задержку в 1 секунду
tc qdisc add dev wlan0 root netem delay 1000ms
# удалить задержку
tc qdisc del dev wlan0 root netem delay 1000ms
# просмотр
tc qdisc show dev wlan0


И устанавливаем 5 секунд tc qdisc add dev wlan0 root netem delay 5000ms

Ответ получен:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
do 502
do 503
do 504
do 505
send 97
Fetching: http://192.168.43.194/ts
    //  <задержка при отправке 5 секунд>
do 506
    //  <задержка при получении 5 секунд>
do 507
>>>>>>>> HTTP/1.1 200 OK


content

 <<<<<<<<<<<<<<
do 508
do 509
do 510
do 511


Устанавливаем 20 секунд tc qdisc add dev wlan0 root netem delay 20000ms

1
2
3
4
5
6
7
8
9
10
11
12
13
do 679
do 680
do 681
do 682
send 129
Fetching: http://192.168.43.194/ts
do 683
    //  <ожидание отправки 15 секунд и сброс>
>>>>>>>> OSError 1 <<<<<<<<<<<<<<
do 684
do 685
do 686
do 687