ESP 8266 MicroPython получает частичные данные для POST

Я пытаюсь настроить простой веб-сервер HTTP на ESP8266-01 (флэш-память 1 МБ), на котором установлена ​​последняя версия микропрограммы 1.9.3 MicroPython. Цель состоит в том, чтобы иметь возможность настроить учетные данные для домашней сети, к которой в конечном итоге будет подключаться интерфейс STA.

Итак, код на высоком уровне делает следующее:

  • Включает интерфейс AP
  • Кто-то подключится к 192.168.0.1/index.html, у которого будет форма для имени пользователя и пароля. Нам просто нужно поставить admin / admin. Нажатие кнопки «Отправить» должно отправить сообщение на 192.168.0.1/configure.html
  • Configure.html - это веб-страница с формой, где будут вводиться SSID и пароль. Надеюсь, вы сможете узнать больше, посмотрев на приведенный ниже код.

Я столкнулся с двумя проблемами:

  1. Общее количество байтов, которые я получаю при отправке index.html формы, не заполнено. Я обхожу реферер (это слишком частичный), всего около 560 байт. Это когда я делаю это из мобильного браузера. Забавно, он всегда получает именно столько байтов. Я могу поделиться тем, что получил, если это поможет.
  2. Таймер Watch Dog иногда перезагружает мой модуль. Я делаю большинство предлагаемых изменений в своем коде - используя небольшие засыпания. Есть ли способ в MicroPython на ESP8266, с помощью которого я могу «кормить» WDT, чтобы он не «истекал» и не перезагружал мой модуль?

Вот мой код:

import gc
import network
gc.collect()
import machine
gc.collect()
import ubinascii
gc.collect()
import ujson
gc.collect()
import uos
gc.collect()
import utime
gc.collect()
import socket
gc.collect()
import select
gc.collect()

html = """<!DOCTYPE html>
<html>
    <head> <title>Ouroboros IoT Login</title> </head>
    <body>
        <form action="configure.html" method="POST">
            Username : <input type="text"  name="username"></br>
            Password: <input type="password" name="password" ></br>
                <input type="submit" value="submit" name="submit">
        </form>
    </body>
</html>
"""

login_fail_html = """<!DOCTYPE html>
<html>
    <head> <title>Ouroboros IoT Login</title> </head>
    <body>
        <h2>Incorrect Credentials!</h2><br>Please login<br>
        <form action="configure.html" method="POST">
            Username : <input type="text"  name="username"></br>
            Password: <input type="password" name="password" ></br>
                <input type="submit" value="submit" name="submit">
        </form>
    </body>
</html>
"""

# Check if file exists
def fileExists(fileName):
    try:
        uos.stat(fileName)
        print("File " + fileName + " found!")
        return True
    except OSError:
        print("No file " + fileName + " found!")
        return False

# Turns WiFi ON for configuration
def turn_wifi_on():
    # Setup the AP interface
    ap_if = network.WLAN(network.AP_IF)
    ap_if.active(False)
    ap_if.active(True)
    # Get the MACADDRESS - without any spaces
    macaddress = ubinascii.hexlify(ap_if.config('mac'),'').decode()
    ap_if.config(essid="OUB1_"+macaddress, password="12345678")
    #ap_if.config(essid="OUB1_"+macaddress)
    ap_if.ifconfig(('192.168.0.1', '255.255.255.0', '192.168.0.1', '192.168.0.1'))
    # Configure the AP to static IPs

def turn_wifi_off():
    ap_if = network.WLAN(network.AP_IF)
    ap_if.active(False)

# Find out the stored IoT secret content
def get_iot_secret():
    fileName = 'alpha.txt'
    if fileExists(fileName):
        f = open(fileName)
        content_str = f.read()
        f.close()
        return content_str
    else:
        return 'asdasrefwefefergf9rerf3n4r23irn1n32f'

# Find out the stored home network credential if exist
def get_wifi_config():
    fileName = 'wifi.conf'
    if fileExists(fileName):
        f = open(fileName)
        content_str = f.read()
        f.close()
        content = ujson.loads(content_str)
        return content
    else:
        return None

# Set the home network credentials
def save_wifi_config(essid, passphrase):
    f = open('wifi.conf', 'w')
    config = {'essid':essid, 'passphrase':passphrase}
    config_str = ujson.dumps(config)
    f.write(config_str)
    f.close()


# Find out the stored login credentials
def get_login_config():
    fileName = 'login.conf'
    if fileExists(fileName):
        f = open(fileName)
        content_str = f.read()
        f.close()
        content = ujson.loads(content_str)
        return content
    else:
        # No file exists so far, so use the admin/admin credentials
        return {'user':'admin','password':'admin'}

# Set the login credentials
def save_login_config(user, password):
    f = open('login.conf', 'w')
    config = {'user':user, 'password':password}
    config_str = ujson.dumps(config)
    f.write(config_str)
    f.close()

def turn_gpio_on(device_num):
    # Device Num to Pin Mapping
    if device_num == 0:
        pin_num = 0
    elif device_num == 1:
        pin_num = 2
    # Check Pin
    pin = machine.Pin(pin_num) 
    if pin.value() == 0:
        pin.on()
    # else it is already at HIGH state, nothing to do

def turn_gpio_off(device_num):
    # Device Num to Pin Mapping
    if device_num == 0:
        pin_num = 0
    elif device_num == 1:
        pin_num = 2
    # Check Pin
    pin = machine.Pin(pin_num) 
    if pin.value() == 1:
        pin.off()
    # else it is already at LOW state, nothing to do

def init_pin(device_num):
    # Device Num to Pin Mapping
    if device_num == 0:
        pin_num = 0
    elif device_num == 1:
        pin_num = 2
    #open GPIO0 in output mode & turn it off by default
    pin = machine.Pin(pin_num, machine.Pin.OUT) 
    # Turn off both GPIO initially
    turn_gpio_off(device_num)

# Find out the post parameters in a dictionary
def get_post_params(req):
    print("Inside GET POST PARAMS : req = " + req)
    post_params = req.split('\r\n')[-1:][0]
    # Check if the post body contains the necessary fields
    # Split the post_params by &
    # params : ['username=', 'password=', 'method=POST', 'url=http%3A%2F%2Ftwig-me.com%2Fv1%2Fusergroups%2FWKMUYXELA9LCC', 'jsondata=', 'submit=submit']
    print("post_params : " + post_params)
    params = post_params.split('&')
    print("Params")
    print(params)
    # Initialize the key value pair dict
    post_dict = {}
    # Iterate on each param
    for param in params:
        # Each param would be like 'method=POST', etc
        key_val = param.split('=')
        print("Key Val :")
        print(key_val)
        key = key_val[0]
        val = key_val[1]
        # Update post_dict
        post_dict[key] = val
    return post_dict

# This web server takes care of the WiFi configuration
# max_run_sec 
def web_server(max_run_sec = None):
    # Turn wifi interface ON
    turn_wifi_on()
    # Create server socket
    addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
    s = socket.socket()
    # TODO : If both the wifi and sta are operating simultaneously, then bind only to WiFi
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(1)
    # s.settimeout(1)

    poller = select.poll()
    poller.register(s, select.POLLIN)

    # Get the current time since epoch
    startTimeEpoch = utime.time()

    while True:
        events = poller.poll(200)  # time in milliseconds
        if events:
            try:
                gc.collect()
                res = s.accept()
                client_s = res[0]
                client_addr = res[1]
                req = ''
                #while True:
                #   data = client_s.recv(200)
                #   if data:
                #       req += str(data, 'utf8')
                #   else:
                #       break
                #   utime.sleep_ms(50)
                req = client_s.recv(4096)
                req = req.decode()
                print(req)
                req = str(req)
                # Came here means that there has been some connection!
                # Reset the start time epoch in such a case:
                startTimeEpoch = utime.time()
                # Check route now
                if req.find('configure.html') != -1:
                    print("Got configure request!\r\n")
                    # Check if the username and password are correct, if not, configure:
                    login_config = get_login_config()
                    username = login_config['user']
                    pwd = login_config['password']
                    print("Username : " + username + ", pwd : " + pwd)
                    # Find the POST PARAMETERS sent
                    # There would be just one entry in the array, so get the 0th index directly
                    # post_params : 'username=&password=&method=POST&url=http%3A%2F%2Ftwig-me.com%2Fv1%2Fusergroups%2FWKMUYXELA9LCC&jsondata=&submit=submit'
                    print("Came here A")
                    post_dict = get_post_params(req)

                    # Now check if the post_dict has the key and value for username and password as needed?
                    username_post = post_dict['username']
                    password_post = post_dict['password']

                    print("Came here B")

                    # Check if the password is same as expected
                    if (username_post == username) and (password_post == pwd):
                        hidden_input = '<input type="hidden" name="username" value="' + username + '"><input type="hidden" name="passphrase" value="' + pwd + '">'
                        # Send the login username and password inside the hidden input field
                        configure_html = "<!DOCTYPE html><html><head> <title>Ouroboros IoT WiFi Configuration Page</title> </head><body><form action=\"configure_wifi.html\" method=\"POST\">WiFi SSID : <input type=\"text\"  name=\"essid\"></br>WiFi Password: <input type=\"password\" name=\"passphrase\" ></br>" + hidden_input + "<input type=\"submit\" value=\"submit\" name=\"submit\"></form></body></html>"
                        # TODO : Also show link to webpage, where from we can change the login credentials
                        client_s.send(configure_html)   
                    else:
                        client_s.send(login_fail_html)
                elif req.find('configure_wifi.html') != -1:
                    # Check if the username and password are correct, if not, configure:
                    login_config = get_login_config()
                    username = login_config['user']
                    pwd = login_config['password']
                    # Get post parameters
                    post_dict = get_post_params(req)
                    # Now check if the post_dict has the key and value for username and password as needed?
                    username_post = post_dict['username']
                    password_post = post_dict['password']

                    # Check if the password is same as expected
                    if (username_post == username) and (password_post == pwd):
                        # Do some sanity check for handling the new wifi ssid and password
                        new_wifi_ssid = post_dict['essid']
                        new_wifi_passphrase = post_dict['passphrase']
                        # Set the wifi credentials
                        save_wifi_config(new_wifi_ssid, new_wifi_passphrase)
                        client_s.send('<!DOCTYPE html><html><head> <title>Ouroboros IoT WiFi Configuration Success</title> </head><body>Configuration successful!<br>Device would go into reboot now!</body></html>')
                        # Reboot device now
                        machine.reset()
                    else:
                        client_s.send(login_fail_html)
                elif req.find('index.html') != -1:
                    print("Got index.html request!\r\n")
                    client_s.send(html)
                else :
                    # Do nothing
                    print("Invalid request received! Show the login page again!\r\n")
                    client_s.send(html)

                client_s.close()
                machine.idle()
            except OSError:
                # Got no request and it timedout!
                print("Timed-out, no request received!\r\n")
            except Exception as e:
                print("Got some exception\r\n")
                print(str(e))
            finally:
                if max_run_sec is not None:
                    elapsedTime = utime.time() - startTimeEpoch
                    if elapsedTime >  max_run_sec:
                        # Max run time of web server has elapsed, time to exit this mode!
                        break
        utime.sleep_ms()
        machine.idle()

    # When while loop ends!
    s.close()
    # Turn wifi interface OFF
    turn_wifi_off()

# Starts a thread which runs the web server to handle WiFi
def start_web_server(max_run_sec = None):
    # start_new_thread(web_server, (max_run_sec))
    web_server(max_run_sec)



############# MAIN ##########################
# Initialize two pins to INPUT and OFF by default
init_pin(0)
init_pin(1)
#turn_wifi_off()

# Check if the home wifi network has been setup
# Check if home wifi config is valid, if so, connect to it
# If home wifi is not configured, then use the Web server all the time. 
if get_wifi_config() is None:
    # Came here means the wifi is not configured
    # Start the web server
    print("Starting web server")
    start_web_server()

РЕДАКТИРОВАТЬ 1:

Я могу настроить WDT и накормить его. Так что больше никаких перезагрузок WDT. Однако проблема с POST все еще существует:

К вашему сведению, вот ответ:

POST /configure.html HTTP/1.1
Host: 192.168.0.1
Connection: keep-alive
Content-Length: 43
Cache-Control: max-age=0
Origin: http://192.168.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Linux; Android 5.1.1; Redmi Note 3 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.123 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.0.1/index.html
Accept-Encoding: g

Как можно видеть, полученный пакет является частичным, в заголовке Content-Length указано 43 байта полезной нагрузки. Но его не получил. При использовании "nc" и локальном запуске сервера полученный пакет выглядит следующим образом:

POST /configure.html HTTP/1.1
Host: 192.168.0.1
Connection: keep-alive
Content-Length: 43
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/65.0.3325.181 Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8

username=admin&password=admin&submit=submit

Здесь можно было легко увидеть полезную нагрузку длиной 43 байта.

Итак, мой вопрос: не слишком ли много полезной нагрузки в 800 байт для ESP8266? Можно ли что-нибудь сделать, чтобы удалить ненужные заголовки, которые отправляет браузер? Есть ли способ получить все данные, если они фрагментированы?


person Ouroboros    schedule 23.04.2018    source источник
comment
Сторожевой таймер задокументирован здесь. Предполагая, что это позволяет вам решить проблему со сторожевым псом, могу ли я предложить вам отредактировать свой вопрос и его заголовок, чтобы сосредоточиться на проблеме веб-формы? Это могло бы привлечь лучшие ответы.   -  person nekomatic    schedule 23.04.2018
comment
Я не думаю, что документация (несмотря на то, что она находится в дереве esp8266) на самом деле правильная. Я считаю, что эти документы фактически отражают поведение модуля WDT на PyBoard, но не на устройствах esp8266 (если у вас под рукой есть micropython на esp8266, вы заметите, что конструктор WDT не принимает аргументы ключевого слова).   -  person larsks    schedule 23.04.2018
comment
@ouroboros Вы случайно не знаете, где в вашем коде происходит перезагрузка сторожевого пса?   -  person larsks    schedule 23.04.2018
comment
@larsks, как отладить микропитон, где произошла перезагрузка? Я могу поставить распечатки для базовой отладки. Помогите мне найти способы отладки   -  person Ouroboros    schedule 23.04.2018
comment
@nekomatic Теперь я понимаю, что класс WDT должен быть создан без передачи параметров. Но каков тайм-аут по умолчанию для wdt? Это будет важно, так как мне нужно будет периодически кормить с интервалами меньшими, чем это время по умолчанию.   -  person Ouroboros    schedule 23.04.2018
comment
@larsks ой, похоже, вы правы. Похоже, OP должен сначала подтвердить, что это действительно проблема сторожевого пса?   -  person nekomatic    schedule 23.04.2018
comment
Это, вероятно, означает, что здесь действительно есть два отдельных вопроса ...   -  person nekomatic    schedule 23.04.2018
comment
@nekomatic Я отправлю это как два вопроса, но как вы думаете, вы могли бы ответить на один из них?   -  person Ouroboros    schedule 23.04.2018
comment
Я вижу, вы устранили проблему со сторожевым псом и отредактировали заголовок. Я не могу помочь с проблемой HTTP, извините :-(   -  person nekomatic    schedule 24.04.2018
comment
Я исправил (на самом деле обходной путь), прочитав каждые 200 мс в течение пары секунд. Я мог бы также читать, пока не получил Content-Length, а затем продолжать, пока не встретил \ r \ n \ r \ n, в этот момент мне нужно было прочитать еще байты «длины содержимого»   -  person Ouroboros    schedule 24.04.2018
comment
Рассматривали ли вы использование picoweb в качестве альтернативного веб-сервера и для решения проблемы с публикацией?   -  person Lingster    schedule 26.08.2018


Ответы (1)


Я столкнулся с аналогичной проблемой, но моя конфигурация немного отличается.

Я импортирую свой html из html_files.py, как показано ниже.

cred_prompt = """
<!DOCTYPE html>
<html>
    <head>
        <title>ESP8266 connection</title>
    </head>
    <body>
        <h2>
            Enter the UID and Password to connect to WiFi
        </h2>
        <form action="/post">
            uid: <input type="text" name="uid">
            password: <input type="text" name="password">
            <input type="submit" value="Submit">
        </form><br>
    </body>
</html>
"""

Это функция, которую я использую для получения учетных данных. Позже я сохраню их в файл (они будут использоваться при последующих загрузках)

def get_new_creds():
    sta_if.disconnect()
    print("Setting up webpage to get new credentials")
    addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
    soc = socket.socket()
    soc.bind(addr)
    soc.listen(5)
    print("Listening on", addr)
    while True:
        client, addr = soc.accept()
        print("Client connected from", addr)
        request = client.recv(1024)
        request = request.decode().split()
        uid, pwd = '', ''
        if 'uid' in request[1]:
            uid = request[1].split('&')[0].split('=')[1]
            pwd = request[1].split('&')[1].split('=')[1]
            write_new_creds(uid, pwd)
            connect_to_wifi()
            print("The UID is", uid, "and the Password is", pwd)
            client.send('HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n')
            client.send(html_files.connection_response.format(sta_if.ifconfig()[0]))
            return uid, pwd
        print(request)
        client.send('HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n')
        client.send(html_files.cred_prompt)
        client.close()

Это весь код boot.py, если это помогает.

person Prajwal Shenoy K.P    schedule 06.02.2021