diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fa02db --- /dev/null +++ b/.gitignore @@ -0,0 +1,159 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/main.py b/example/example1.py similarity index 99% rename from main.py rename to example/example1.py index 32229e5..d693242 100644 --- a/main.py +++ b/example/example1.py @@ -12,3 +12,5 @@ else: print('Disconnected!') utime.sleep(10) + + diff --git a/sdist_upip.py b/sdist_upip.py new file mode 100644 index 0000000..60c0c3e --- /dev/null +++ b/sdist_upip.py @@ -0,0 +1,151 @@ +# This module is part of Pycopy https://github.com/pfalcon/pycopy +# and pycopy-lib https://github.com/pfalcon/pycopy-lib, projects to +# create a (very) lightweight full-stack Python distribution. +# +# Copyright (c) 2016-2019 Paul Sokolovsky +# Licence: MIT +# +# This module overrides distutils (also compatible with setuptools) "sdist" +# command to perform pre- and post-processing as required for Pycopy's +# upip package manager. +# +# Preprocessing steps: +# * Creation of Python resource module (R.py) from each top-level package's +# resources. +# Postprocessing steps: +# * Removing metadata files not used by upip (this includes setup.py) +# * Recompressing gzip archive with 4K dictionary size so it can be +# installed even on low-heap targets. +# +import sys +import os +import zlib +from subprocess import Popen, PIPE +import glob +import tarfile +import re +import io + +from distutils.filelist import FileList +from setuptools.command.sdist import sdist as _sdist + + +def gzip_4k(inf, fname): + comp = zlib.compressobj(level=9, wbits=16 + 12) + with open(fname + ".out", "wb") as outf: + while 1: + data = inf.read(1024) + if not data: + break + outf.write(comp.compress(data)) + outf.write(comp.flush()) + os.rename(fname, fname + ".orig") + os.rename(fname + ".out", fname) + + +FILTERS = [ + # include, exclude, repeat + (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), + (r".+\.py$", r"[^/]+$"), + (None, r".+\.egg-info/.+"), +] + + +outbuf = io.BytesIO() + +def filter_tar(name): + fin = tarfile.open(name, "r:gz") + fout = tarfile.open(fileobj=outbuf, mode="w") + for info in fin: +# print(info) + if not "/" in info.name: + continue + fname = info.name.split("/", 1)[1] + include = None + + for inc_re, exc_re in FILTERS: + if include is None and inc_re: + if re.match(inc_re, fname): + include = True + + if include is None and exc_re: + if re.match(exc_re, fname): + include = False + + if include is None: + include = True + + if include: + print("including:", fname) + else: + print("excluding:", fname) + continue + + farch = fin.extractfile(info) + fout.addfile(info, farch) + fout.close() + fin.close() + + +def make_resource_module(manifest_files): + resources = [] + # Any non-python file included in manifest is resource + for fname in manifest_files: + ext = fname.rsplit(".", 1) + if len(ext) > 1: + ext = ext[1] + else: + ext = "" + if ext != "py": + resources.append(fname) + + if resources: + print("creating resource module R.py") + resources.sort() + last_pkg = None + r_file = None + for fname in resources: + try: + pkg, res_name = fname.split("/", 1) + except ValueError: + print("not treating %s as a resource" % fname) + continue + if last_pkg != pkg: + last_pkg = pkg + if r_file: + r_file.write("}\n") + r_file.close() + r_file = open(pkg + "/R.py", "w") + r_file.write("R = {\n") + + with open(fname, "rb") as f: + r_file.write("%r: %r,\n" % (res_name, f.read())) + + if r_file: + r_file.write("}\n") + r_file.close() + + +class sdist(_sdist): + + def run(self): + self.filelist = FileList() + self.get_file_list() + make_resource_module(self.filelist.files) + + r = super().run() + + assert len(self.archive_files) == 1 + print("filtering files and recompressing with 4K dictionary") + filter_tar(self.archive_files[0]) + outbuf.seek(0) + gzip_4k(outbuf, self.archive_files[0]) + + return r + + +# For testing only +if __name__ == "__main__": + filter_tar(sys.argv[1]) + outbuf.seek(0) + gzip_4k(outbuf, sys.argv[1]) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1be70ba --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +from setuptools import setup +import sdist_upip + +from wifi_manager import __version__, __author__, __description__ + +from os import path + +this_directory = path.abspath(path.dirname(__file__)) + +with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Education', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Embedded Systems', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', +] + +setup( + author=__author__, + author_email='', + name="", # library name + version=__version__, + packages=['wifi_manager'], + classifiers=classifiers, + cmdclass={'sdist': sdist_upip.sdist}, + license='MIT', + description=__description__, + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/Saketh-Chandra/micropython-wifi_manager', + keywords=["micropython", "esp8266", "wifi", "Wi-Fi", "manager", "wifi-manager"], +) diff --git a/wifi_manager.py b/wifi_manager/__init__.py similarity index 86% rename from wifi_manager.py rename to wifi_manager/__init__.py index 7c1d1f6..283f9d0 100644 --- a/wifi_manager.py +++ b/wifi_manager/__init__.py @@ -1,7 +1,7 @@ -# Author: Igor Ferreira -# License: MIT -# Version: 2.0.0 -# Description: WiFi Manager for ESP8266 and ESP32 using MicroPython. +__author__ = 'Igor Ferreira & Saketh Chandra' +__license__ = 'MIT' +__version__ = '2.0.0' +__description__ = 'WiFi Manager for ESP8266 and ESP32 using MicroPython.' import machine import network @@ -11,12 +11,15 @@ class WifiManager: + """ + WiFi Manager for ESP8266 and ESP32 using MicroPython. + """ - def __init__(self, ssid = 'WifiManager', password = 'wifimanager'): + def __init__(self, ssid='WifiManager', password='wifimanager'): self.wlan_sta = network.WLAN(network.STA_IF) self.wlan_sta.active(True) self.wlan_ap = network.WLAN(network.AP_IF) - + # Avoids simple mistakes with wifi ssid and password lengths, but doesn't check for forbidden or unsupported characters. if len(ssid) > 32: raise Exception('The SSID cannot be longer than 32 characters.') @@ -26,22 +29,21 @@ def __init__(self, ssid = 'WifiManager', password = 'wifimanager'): raise Exception('The password cannot be less than 8 characters long.') else: self.ap_password = password - + # Set the access point authentication mode to WPA2-PSK. self.ap_authmode = 3 - + # The file were the credentials will be stored. # There is no encryption, it's just a plain text archive. Be aware of this security problem! self.sta_profiles = 'wifi.dat' - + # Prevents the device from automatically trying to connect to the last saved network without first going through the steps defined in the code. self.wlan_sta.disconnect() - + # Change to True if you want the device to reboot after configuration. # Useful if you're having problems with web server applications after WiFi configuration. self.reboot = False - def connect(self): if self.wlan_sta.isconnected(): return @@ -54,21 +56,17 @@ def connect(self): return print('Could not connect to any WiFi network. Starting the configuration portal...') self.__WebServer() - - + def disconnect(self): if self.wlan_sta.isconnected(): self.wlan_sta.disconnect() - def is_connected(self): return self.wlan_sta.isconnected() - def get_address(self): return self.wlan_sta.ifconfig() - def __WriteProfiles(self, profiles): lines = [] for ssid, password in profiles.items(): @@ -76,7 +74,6 @@ def __WriteProfiles(self, profiles): with open(self.sta_profiles, 'w') as myfile: myfile.write(''.join(lines)) - def __ReadProfiles(self): try: with open(self.sta_profiles) as myfile: @@ -90,7 +87,6 @@ def __ReadProfiles(self): profiles[ssid] = password return profiles - def __WifiConnect(self, ssid, password): print('Trying to connect to:', ssid) self.wlan_sta.connect(ssid, password) @@ -105,17 +101,17 @@ def __WifiConnect(self, ssid, password): self.wlan_sta.disconnect() return False - def __WebServer(self): self.wlan_ap.active(True) - self.wlan_ap.config(essid = self.ap_ssid, password = self.ap_password, authmode = self.ap_authmode) + self.wlan_ap.config(essid=self.ap_ssid, password=self.ap_password, authmode=self.ap_authmode) server_socket = usocket.socket() server_socket.close() server_socket = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM) server_socket.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) server_socket.bind(('', 80)) server_socket.listen(1) - print('Connect to', self.ap_ssid, 'with the password', self.ap_password, 'and access the captive portal at', self.wlan_ap.ifconfig()[0]) + print('Connect to', self.ap_ssid, 'with the password', self.ap_password, 'and access the captive portal at', + self.wlan_ap.ifconfig()[0]) while True: if self.wlan_sta.isconnected(): self.wlan_ap.active(False) @@ -139,7 +135,8 @@ def __WebServer(self): # It's normal to receive timeout errors in this stage, we can safely ignore them. pass if self.request: - url = ure.search('(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP', self.request).group(1).decode('utf-8').rstrip('/') + url = ure.search('(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP', self.request).group(1).decode( + 'utf-8').rstrip('/') if url == '': self.__HandleRoot() elif url == 'configure': @@ -152,14 +149,12 @@ def __WebServer(self): finally: self.client.close() - - def __SendHeader(self, status_code = 200): + def __SendHeader(self, status_code=200): self.client.send("""HTTP/1.1 {0} OK\r\n""".format(status_code)) self.client.send("""Content-Type: text/html\r\n""") self.client.send("""Connection: close\r\n""") - - def __SendResponse(self, payload, status_code = 200): + def __SendResponse(self, payload, status_code=200): self.__SendHeader(status_code) self.client.sendall(""" @@ -177,7 +172,6 @@ def __SendResponse(self, payload, status_code = 200): """.format(payload)) self.client.close() - def __HandleRoot(self): self.__SendHeader() self.client.sendall(""" @@ -207,7 +201,6 @@ def __HandleRoot(self): """) self.client.close() - def __HandleConfigure(self): match = ure.search('ssid=([^&]*)&password=(.*)', self.request) if match: @@ -216,19 +209,22 @@ def __HandleConfigure(self): if len(ssid) == 0: self.__SendResponse("""
SSID must be providaded!
Go back and try again!
""", 400) elif self.__WifiConnect(ssid, password): - self.__SendResponse("""Successfully connected to
IP address: {1}
""".format(ssid, self.wlan_sta.ifconfig()[0])) + self.__SendResponse( + """Successfully connected to
IP address: {1}
""".format(ssid, + self.wlan_sta.ifconfig()[ + 0])) profiles = self.__ReadProfiles() profiles[ssid] = password self.__WriteProfiles(profiles) utime.sleep(5) else: - self.__SendResponse("""Could not connect to
Go back and try again!
""".format(ssid)) + self.__SendResponse( + """Could not connect to
Go back and try again!
""".format(ssid)) utime.sleep(5) else: self.__SendResponse("""Parameters not found!
""", 400) utime.sleep(5) - def __HandleNotFound(self): self.__SendResponse("""Path not found!
""", 404) utime.sleep(5)