├── src ├── ntp.py ├── echo.py ├── espnow_scan.py ├── timer.py ├── lazyespnow.py └── wifi.py ├── LICENSE ├── README.md └── .gitignore /src/ntp.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import ntptime 3 | 4 | server = "192.168.10.1" 5 | 6 | 7 | def settime(host=None): 8 | host = host or server 9 | try: 10 | ntptime.host = host 11 | ntptime.settime() 12 | print("Time set to:", machine.RTC().datetime()) 13 | except (OSError, OverflowError) as e: 14 | print("ntptime.settime():", e) 15 | -------------------------------------------------------------------------------- /src/echo.py: -------------------------------------------------------------------------------- 1 | import espnow 2 | import machine 3 | 4 | enow = espnow.ESPNow() 5 | 6 | def server(): 7 | peers = [] 8 | for peer, msg in enow: 9 | if peer is None: 10 | return 11 | if peer not in peers: 12 | peers.append(peer) 13 | try: 14 | enow.add_peer(peer) 15 | except OSError: 16 | pass 17 | 18 | # Echo the MAC and message back to the sender 19 | if not enow.send(peer, msg): 20 | print("ERROR: send() failed to", peer) 21 | return 22 | 23 | if msg == b"!done": 24 | return 25 | elif msg == b"!reset": 26 | machine.reset() 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 glenn20 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micropython ESPNow Utilities 2 | A collection of small utilities for use with espnow and wifi on micropython 3 | 4 | - `src/wifi.py`: A small module to bulletproof initialising wifi on ESP32 and 5 | ESP8266 devices. 6 | 7 | These functions are designed to robustly and reliably account for differences 8 | between esp8266 and esp32 devices and to ensure the wifi is always in a fully 9 | known state (even after soft_reset). 10 | 11 | ```python 12 | import wifi 13 | 14 | sta, ap = wifi.reset() # STA_IF on and disconnected, AP off, channel=1 15 | sta, ap = wifi.reset(sta=True, ap=True, channel=6) 16 | wifi.connect("ssid", "password") 17 | wifi.status() # Print details on wifi config 18 | ``` 19 | 20 | - `src/espnow_scan.py`: Scan channels to find an ESPNow peer. 21 | 22 | ```python 23 | import espnow_scan 24 | 25 | espnow_scan.scan(e, b'macadd') # Print channel if found and leave set to channel 26 | 27 | ``` 28 | 29 | - `src/lazyespnow.py`: LazyESPNow() extends ESPNow class to catch and fix most 30 | common errors. 31 | 32 | ```python 33 | from lazyespnow import LazyESPNow 34 | 35 | peer = b'\xaa\xaa\xaa\xaa\xaa\xaa' 36 | enow = LazyESPNow() 37 | enow.send(peer, message) # Automatically call active(True) and add_peer() 38 | ``` 39 | 40 | - `src/ntp.py`: Set the time from an NTP server over wifi. 41 | 42 | ```python 43 | import ntp 44 | ntp.server = "192.168.1.1" 45 | ntp.settime() 46 | ``` 47 | 48 | - `src/timer.py`: A useful collection of easy to user python timer objects. 49 | 50 | ```python 51 | import timer 52 | 53 | for t in timer_ms(5000, 1000, raise_on_timeout=True): 54 | print(t) 55 | ``` 56 | 57 | - `src/echo.py`: A simple ESPNow server that echos messages back to the sender. 58 | 59 | ```python 60 | import echo 61 | 62 | echo.server() # Wait for incoming messages and echo back to sender 63 | ``` 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /src/espnow_scan.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import network 4 | import espnow 5 | 6 | MAX_CHANNEL = 14 # Maximum wifi channel to scan (2.4MHz band) 7 | NUM_PINGS = 10 # The default number of pings to send on each channel 8 | PING_MSG = b"ping" # The message to send in each packet to the peer 9 | CHANNEL_SETTLING_TIME = 0.0 # Delay after setting channel before testing (seconds) 10 | MIN_PING_RESPONSE_PC = 10 # Minimum acceptable ping response rate (percent) 11 | 12 | sta, ap = (network.WLAN(i) for i in (network.STA_IF, network.AP_IF)) 13 | 14 | 15 | def set_channel(channel): 16 | if sta.isconnected(): 17 | raise OSError("can not set channel when connected to wifi network.") 18 | if ap.isconnected(): 19 | raise OSError("can not set channel when clients are connected to AP_IF.") 20 | if sta.active() and sys.platform != "esp8266": 21 | try: 22 | sta.config(channel=channel) # On ESP32 use STA interface 23 | except RuntimeError as err: 24 | if channel < 12: 25 | print(f"Error setting channel: {err}") 26 | return None 27 | return sta.config("channel") 28 | else: 29 | # On ESP8266, use the AP interface to set the channel of the STA interface 30 | ap_save = ap.active() 31 | ap.active(True) 32 | ap.config(channel=channel) 33 | ap.active(ap_save) 34 | return ap.config("channel") 35 | 36 | 37 | # Return the fraction of pings to peer which succeed. 38 | def ping_peer(enow, peer, channel, num_pings, verbose): 39 | if set_channel(channel) is None: 40 | return 0.0 41 | time.sleep(CHANNEL_SETTLING_TIME) 42 | msg = PING_MSG + bytes([channel]) 43 | frac = sum((enow.send(peer, msg) for _ in range(num_pings))) / num_pings 44 | if verbose: 45 | print(f"Channel {channel:2d}: ping response rate = {frac * 100:3.0f}%.") 46 | return frac 47 | 48 | 49 | def scan(peer, num_pings=NUM_PINGS, verbose=False): 50 | """Scan the wifi channels to find the given espnow peer device. 51 | 52 | If the peer is found, the channel will be printed and the channel number 53 | returned. 54 | Will: 55 | - scan using the STA_IF; 56 | - leave the STA_IF running on the selected channel of the peer. 57 | 58 | Args: 59 | peer (bytes): The MAC address of the peer device to find. 60 | num_pings (int, optional): 61 | Number of pings to send for each channel. (default=20). 62 | verbose (bool): Print results of each channel test. 63 | 64 | Returns: 65 | int: The channel number of the peer (or 0 if not found) 66 | """ 67 | if not sta.active() and not ap.active(): 68 | sta.active(True) # One of the WLAN interfaces must be active 69 | enow = espnow.ESPNow() 70 | enow.active(True) 71 | try: 72 | enow.add_peer(peer) # If user has not already registered peer 73 | except OSError: 74 | pass 75 | 76 | # A list of the ping success rates (fraction) for each channel 77 | ping_fracs = [ 78 | ping_peer(enow, peer, channel, num_pings, verbose) 79 | for channel in range(1, MAX_CHANNEL + 1) 80 | ] 81 | max_frac = max(ping_fracs) 82 | if max_frac < (MIN_PING_RESPONSE_PC + 5) / 100: 83 | print(f"No channel found with response rate above {MIN_PING_RESPONSE_PC}%") 84 | return 0 85 | # Get a list of channel numbers within 5% of max ping success rate 86 | found = [chan + 1 for chan, rate in enumerate(ping_fracs) if rate >= max_frac - 0.05] 87 | 88 | # Because of channel cross-talk we may get more than one channel to be found 89 | # If 3 channels found, select the middle one 90 | # If 2 channels found: select first one if it is channel 1 else second 91 | # If 1 channels found, select it 92 | count = len(found) 93 | index = (count // 2) if not (count == 2 and found[0] == 1) else 0 94 | channel = found[index] 95 | print(f"Setting wifi radio to channel {channel} ({max_frac * 100:3.0f}% response)") 96 | return set_channel(channel) 97 | -------------------------------------------------------------------------------- /src/timer.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # timer.py: A module for creating convenient timers based on generators 3 | 4 | # Examples: 5 | 6 | # machine_test = False 7 | # for t in timer_ms(1500, sleep_ms=200): 8 | # print(t) 9 | # if machine_test: 10 | # break 11 | # else: 12 | # print("Timed out - no data") 13 | 14 | # try: 15 | # for t in timer_ms(30000, 1000, raise_on_timeout=True): 16 | # print(t) 17 | # except TimeoutError as err: 18 | # print(err) 19 | 20 | # for t in timer_ms(): 21 | # print(t) 22 | # if utime.time() == 0: 23 | # break 24 | 25 | # timer = timer_ms(timeout_ms=30000, countdown=True) 26 | # timer = countdown_timer_ms(timeout_ms=30000) 27 | 28 | # with Timer(10000) as t: 29 | # while t.check(): 30 | # print("Waiting", t.time()) 31 | # """ 32 | 33 | from utime import sleep, sleep_ms, ticks_ms, ticks_diff 34 | 35 | class TimeoutError(Exception): 36 | pass 37 | 38 | # A timer generator: on each iteration, yield milliseconds since the first call. 39 | def _timer_ms(): 40 | start = ticks_ms() 41 | while True: 42 | reset = (yield ticks_diff(ticks_ms(), start)) 43 | if type(reset) in [int, float]: 44 | start = ticks_ms() - reset 45 | 46 | # Reset the start value of a timer 47 | def reset(timer, time_ms=0): 48 | timer.send(time_ms) 49 | 50 | # A timeout generator (milliseconds) 51 | def _timeout_ms(timer, timeout_ms): 52 | for dt in timer: 53 | if dt >= timeout_ms: 54 | break 55 | yield dt 56 | 57 | # Add a busy sleep on every iteration through the timer. 58 | def _sleep_ms(timer, delay_ms): 59 | for i, dt in enumerate(timer): 60 | yield dt 61 | if delay_ms: 62 | sleep_ms((i + 1) * delay_ms - next(timer)) 63 | 64 | # Make a timer generator which raises exception if the timeout is reached 65 | def _raise_on_timeout(timer, exc=True): 66 | yield from timer 67 | raise TimeoutError if exc is True else exc 68 | 69 | # Make a timer generator a countdown timer 70 | def _countdown(timer, start): 71 | return (start - dt for dt in timer) 72 | 73 | def check(timer): 74 | try: 75 | next(timer) 76 | return True 77 | except StopIteration: 78 | return False 79 | 80 | def is_expired(timer): 81 | return not check(timer) 82 | 83 | def start(timer): 84 | return next(timer) 85 | 86 | # Construct a timer/timeout generator in milliseconds 87 | def timer_ms(timeout_ms, sleep_ms=0, countdown=False, exc=None): 88 | timer = _timer_ms() 89 | if sleep_ms > 0: 90 | timer = _sleep_ms(timer, sleep_ms) 91 | if timeout_ms > 0: 92 | timer = _timeout_ms(timer, timeout_ms) 93 | if countdown: 94 | timer = _countdown(timer, timeout_ms) 95 | if exc is not None: 96 | timer = _raise_on_timeout(timer, exc) 97 | return timer 98 | 99 | def countdown_timer_ms(t=0, sleep_ms=0, exc=None): 100 | return timer_ms(t, sleep_ms, True, exc) 101 | 102 | # Construct a timer/timeout generator in seconds 103 | def timer_s(timeout_s, sleep_s=0, countdown=False, exc=None): 104 | return ( 105 | dt / 1000 106 | for dt in 107 | timer_ms(timeout_s * 1000, sleep_s * 1000, countdown, exc)) 108 | 109 | class Timer: 110 | def __init__(self, timeout): 111 | self.timeout = timeout 112 | self.timer = None 113 | 114 | def start(self): 115 | self.timer = enumerate(timer_ms(self.timeout, exc=True)) 116 | return next(self.timer) # This starts the timer! 117 | 118 | def reset(self): 119 | self.timer = enumerate(timer_ms(self.timeout, exc=True)) 120 | return 121 | 122 | def time(self): 123 | return next(self.timer)[1] if self.timer else None 124 | 125 | def check(self): 126 | dt = self.time() 127 | return dt is not None 128 | 129 | def check_wait(self, delay_ms=0): 130 | i, dt = next(self.timer) 131 | if i > 0 and delay_ms: 132 | sleep_ms(delay_ms) 133 | return dt is not None 134 | 135 | def __enter__(self): 136 | self.timer = enumerate(timer_ms(self.timeout, exc=True)) 137 | return self 138 | 139 | def __exit__(self, exc_type, exc_value, exc_tb): 140 | if isinstance(exc_value, TimeoutError): 141 | self.timer = None 142 | return True 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/lazyespnow.py: -------------------------------------------------------------------------------- 1 | # lazyespnow module for MicroPython on ESP32 2 | # MIT license; Copyright (c) 2022 Glenn Moloney @glenn20 3 | 4 | import time 5 | import network 6 | import espnow 7 | 8 | 9 | def _handle_esperror(self, err, peer): 10 | if len(err.args) < 2: 11 | raise err 12 | 13 | esperr = err.args[1] 14 | if esperr == 'ESP_ERR_ESPNOW_NOT_INIT': 15 | if self.debug: 16 | print("LazyESPNow: init()") 17 | self.active(True) 18 | 19 | elif esperr == 'ESP_ERR_ESPNOW_NOT_FOUND': 20 | if self.debug: 21 | print("LazyESPNow: add_peer()") 22 | # Restore the saved options for this peer - if we have it 23 | args = self._saved_peers.get(peer, []) 24 | self.add_peer(peer, *args) 25 | 26 | elif esperr == 'ESP_ERR_ESPNOW_EXIST': 27 | if self.debug: print("LazyESPNow: del_peer()") 28 | self.del_peer(peer) 29 | 30 | elif esperr == 'ESP_ERR_ESPNOW_FULL': 31 | if self.debug: print("LazyESPNow: del_peer()") 32 | # Peers are listed in the order they were registered 33 | peers = self.get_peers() 34 | n_tot, _n_enc = self.peer_count() 35 | # If less than the max peers(20) are registered, assume we hit the limit 36 | # on encrypted peers(6) and delete an encrypted peer, otherwise an 37 | # unencrypted peer. 38 | is_encrypted = n_tot < espnow.MAX_TOTAL_PEER_NUM 39 | # We will try removing the first encrypted/unencrypted host 40 | peer, *args = next((p for p in peers if p[4] == is_encrypted)) 41 | self._saved_peers[peer] = args # Save options for deleted peer 42 | self.del_peer(peer) 43 | 44 | elif esperr == 'ESP_ERR_ESPNOW_IF': 45 | channel, if_idx = self.get_peer(peer)[2:3] if peer else 0, self.default_if 46 | if self.debug: print("LazyESPNow: activating", ('STA_IF','AP_IF')[if_idx]) 47 | wlan = network.WLAN(if_idx) 48 | wlan.active(True) 49 | if if_idx == network.STA_IF: 50 | wlan.disconnect() # ESP8266 may auto-connect to last AP. 51 | if channel: 52 | wlan.config(channel=channel) 53 | wlan.config(ps_mode=self.ps_mode) 54 | 55 | else: 56 | raise err 57 | 58 | # A wrapper for methods which catches esp errors and tries to fix them 59 | # Eg: if device is not initialised, call active(True) 60 | # if wifi interface is not active(), set it active(True) 61 | # if peer is not registered, register it. 62 | def _catch_esperror(method): 63 | def wrapper(*args, **kwargs): 64 | for _ in range(5): 65 | try: 66 | return method(*args, **kwargs) 67 | except OSError as err: 68 | # Correct any esp errors 69 | peer = args[1] if len(args) > 1 else None 70 | _handle_esperror(args[0], err, peer) 71 | raise RuntimeError("_handle_OSError(): ESP Exception handling failed.") 72 | 73 | return wrapper 74 | 75 | 76 | class LazyESPNow(espnow.ESPNow): 77 | default_if = network.STA_IF 78 | debug = None 79 | _saved_peers = {} 80 | wlans = [network.WLAN(i) for i in [network.STA_IF, network.AP_IF]] 81 | 82 | def __init__(self, default_if=network.STA_IF): 83 | self.default_if = default_if 84 | super().__init__() 85 | 86 | def _check_init(self): 87 | if not any((w.active() for w in self.wlans)): 88 | wlan = network.WLAN(self.default_if) 89 | wlan.active(True) 90 | if self.default_if == network.STA_IF: 91 | wlan.disconnect() # ESP8266 may auto-connect to last AP. 92 | 93 | @_catch_esperror 94 | def irecv(self, t=None): 95 | self._check_init() 96 | return super().irecv(t) 97 | 98 | @_catch_esperror 99 | def recv(self, t=None): 100 | self._check_init() 101 | return super().recv(t) 102 | 103 | @_catch_esperror 104 | def send(self, mac, msg=None, sync=True): 105 | if msg is None: 106 | msg, mac = mac, None # If msg is None: swap mac and msg 107 | return self.send(mac, msg, sync) 108 | 109 | @_catch_esperror 110 | def __next__(self): 111 | self._check_init() 112 | return super().__next__() 113 | 114 | @_catch_esperror 115 | def get_peer(self, mac): 116 | return super().get_peer(mac) 117 | 118 | @_catch_esperror 119 | def add_peer(self, mac, *args, **kwargs): 120 | return super().add_peer(mac, *args, **kwargs) 121 | 122 | def find_peer(self, mac, msg, lmk=None, ifidx=-1, encrypt=None): 123 | try: 124 | _ = super().get_peer(mac) 125 | except OSError: 126 | self.add_peer( 127 | mac, lmk, 0, ifidx, 128 | encrypt if encrypt is not None else bool(lmk)) 129 | for num_tries in (0, 10): 130 | for chan in range(14): 131 | super().mod_peer(mac, channel=chan) 132 | for _ in range(num_tries): 133 | if super().send(mac, msg): 134 | return True 135 | time.sleep(0.10) 136 | return False 137 | -------------------------------------------------------------------------------- /src/wifi.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # wifi.py: Useful functions for setting up wifi on ESP32 and ESP8266 devices 3 | 4 | # These functions are designed to robustly and reliably account for differences 5 | # between esp8266 and esp32 devices and to ensure the wifi is always in a fully 6 | # known state (even after soft_reset). 7 | 8 | # Examples: 9 | 10 | # import wifi 11 | 12 | # # Reset wifi to know state (STA is disconnected) 13 | # sta, ap = wifi.reset() # STA on, AP off, channel=1 14 | # sta, ap = wifi.reset(sta=True, ap=True, channel=5) # STA on, AP on, channel=5 15 | # sta, ap = wifi.reset(False, False) # STA off, AP off, channel=1 16 | # sta, ap = wifi.reset(ap=True) # STA on, AP on, channel=1 17 | # sta, ap = wifi.reset(channel=11) # STA on, AP off, channel=11 18 | 19 | # # Set/get the channel 20 | # wifi.channel(11) 21 | # print(wifi.channel()) 22 | 23 | # # Connect/disconnect from a wifi network 24 | # wifi.connect("ssid", "password") 25 | # wifi.connect() # Reconnect to last wifi network 26 | # wifi.disconnect() # Disconnect from wifi network 27 | 28 | # # Print verbose details of the device wifi config 29 | # wifi.status() 30 | 31 | # Config: 32 | 33 | # # Power save mode used whenever connected() - default is WIFI_PS_NONE 34 | # wifi.ps_mode = network.WIFI_PS_MIN_MODEM 35 | # wifi.timeout = 30 # Timeout for connect to network (seconds) 36 | # wifi.sta # The STA_IF wlan interface 37 | # wifi.ap # The AP_IF wlan interface 38 | # """ 39 | 40 | import sys 41 | import time 42 | import network 43 | 44 | 45 | class TimeoutError(Exception): 46 | pass 47 | 48 | 49 | this = sys.modules[__name__] # A reference to this module 50 | 51 | is_esp8266 = sys.platform == "esp8266" 52 | wlans = [network.WLAN(w) for w in (network.STA_IF, network.AP_IF)] 53 | sta, ap = wlans 54 | _sta, _ap = wlans 55 | timeout = 20 # (seconds) timeout on connect() 56 | default_channel = 1 57 | try: 58 | default_pm_mode = sta.PM_PERFORMANCE 59 | except AttributeError: 60 | default_pm_mode = None 61 | try: 62 | default_protocol = network.MODE_11B | network.MODE_11G | network.MODE_11N 63 | except AttributeError: 64 | default_protocol = None 65 | 66 | 67 | def channel(channel=0): 68 | if channel == 0: 69 | return _ap.config("channel") 70 | if _sta.isconnected(): 71 | raise OSError("can not set channel when connected to wifi network.") 72 | if _ap.isconnected(): 73 | raise OSError("can not set channel when clients are connected to AP.") 74 | if _sta.active() and not is_esp8266: 75 | _sta.config(channel=channel) # On ESP32 use STA interface 76 | return _sta.config("channel") 77 | else: 78 | # On ESP8266, use the AP interface to set the channel 79 | ap_save = _ap.active() 80 | _ap.active(True) 81 | _ap.config(channel=channel) 82 | _ap.active(ap_save) 83 | return _ap.config("channel") 84 | 85 | 86 | def wait_for(fun, timeout=timeout): 87 | start = time.time() 88 | while not fun(): 89 | if time.time() - start > timeout: 90 | raise TimeoutError() 91 | time.sleep(0.1) 92 | 93 | 94 | def disconnect(): 95 | _sta.disconnect() 96 | wait_for(lambda: not _sta.isconnected(), 5) 97 | 98 | 99 | def connect(*args, **kwargs): 100 | _sta.active(True) 101 | disconnect() 102 | _sta.connect(*args, **kwargs) 103 | wait_for(lambda: _sta.isconnected()) 104 | ssid, chan = _sta.config("ssid"), channel() 105 | print('Connected to "{}" on wifi channel {}'.format(ssid, chan)) 106 | 107 | 108 | def reset( 109 | sta=True, 110 | ap=False, 111 | channel=default_channel, 112 | pm=default_pm_mode, 113 | protocol=default_protocol, 114 | ): 115 | "Reset wifi to STA_IF on, AP_IF off, channel=1 and disconnected" 116 | _sta.active(False) # Force into known state by turning off radio 117 | _ap.active(False) 118 | _sta.active(sta) # Now set to requested state 119 | _ap.active(ap) 120 | if sta: 121 | disconnect() # For ESP8266 122 | try: 123 | _sta.config(pm=pm) 124 | except (ValueError): 125 | pass 126 | try: 127 | wlan = _sta if sta else _ap if ap else None 128 | if wlan and (protocol is not None): 129 | wlan.config(protocol=protocol) 130 | except (ValueError, RuntimeError): 131 | pass 132 | this.channel(channel) 133 | return _sta, _ap 134 | 135 | 136 | def status(): 137 | from binascii import hexlify 138 | 139 | for name, w in (("STA", _sta), ("AP", _ap)): 140 | active = "on," if w.active() else "off," 141 | mac = w.config("mac") 142 | hex = hexlify(mac, ":").decode() 143 | print("{:3s}: {:4s} mac= {} ({})".format(name, active, hex, mac)) 144 | if _sta.isconnected(): 145 | print(" connected:", _sta.config("ssid"), end="") 146 | else: 147 | print(" disconnected", end="") 148 | print(", channel={:d}".format(_ap.config("channel")), end="") 149 | pm_mode = None 150 | try: 151 | pm_mode = _sta.config("pm") 152 | names = { 153 | _sta.PM_NONE: "PM_NONE", 154 | _sta.PM_PERFORMANCE: "PM_PERFORMANCE", 155 | _sta.PM_POWERSAVE: "PM_POWERSAVE", 156 | } 157 | print(", pm={:d} ({})".format(pm_mode, names[pm_mode]), end="") 158 | except (AttributeError, ValueError): 159 | print(", pm={}".format(pm_mode), end="") 160 | try: 161 | names = ("MODE_11B", "MODE_11G", "MODE_11N", "MODE_LR") 162 | protocol = _sta.config("protocol") 163 | try: 164 | p = "|".join((x for x in names if protocol & getattr(network, x))) 165 | except AttributeError: 166 | p = "" 167 | print(", protocol={:d} ({})".format(protocol, p), end="") 168 | except ValueError: 169 | pass 170 | print() 171 | if _sta.isconnected(): 172 | print(" ifconfig:", _sta.ifconfig()) 173 | --------------------------------------------------------------------------------