diff --git a/adafruit_esp32spi/adafruit_esp32spi.py b/adafruit_esp32spi/adafruit_esp32spi.py index 96bc200..3e16df0 100644 --- a/adafruit_esp32spi/adafruit_esp32spi.py +++ b/adafruit_esp32spi/adafruit_esp32spi.py @@ -54,6 +54,8 @@ # pylint: disable=bad-whitespace _SET_NET_CMD = const(0x10) _SET_PASSPHRASE_CMD = const(0x11) +_SET_AP_NET_CMD = const(0x18) +_SET_AP_PASSPHRASE_CMD = const(0x19) _SET_DEBUG_CMD = const(0x1A) _GET_CONN_STATUS_CMD = const(0x20) @@ -410,6 +412,18 @@ def wifi_set_entenable(self): if resp[0][0] != 1: raise RuntimeError("Failed to enable enterprise mode") + def _wifi_set_ap_network(self, ssid, channel): + """Creates an Access point with SSID and Channel""" + resp = self._send_command_get_response(_SET_AP_NET_CMD, [ssid, channel]) + if resp[0][0] != 1: + raise RuntimeError("Failed to setup AP network") + + def _wifi_set_ap_passphrase(self, ssid, passphrase, channel): + """Creates an Access point with SSID, passphrase, and Channel""" + resp = self._send_command_get_response(_SET_AP_PASSPHRASE_CMD, [ssid, passphrase, channel]) + if resp[0][0] != 1: + raise RuntimeError("Failed to setup AP password") + @property def ssid(self): """The name of the access point we're connected to""" @@ -444,15 +458,30 @@ def is_connected(self): self.reset() return False + @property + def ap_listening(self): + """Returns if the ESP32 is in access point mode and is listening for connections""" + try: + return self.status == WL_AP_LISTENING + except RuntimeError: + self.reset() + return False + def connect(self, secrets): """Connect to an access point using a secrets dictionary that contains a 'ssid' and 'password' entry""" self.connect_AP(secrets['ssid'], secrets['password']) - def connect_AP(self, ssid, password): # pylint: disable=invalid-name - """Connect to an access point with given name and password. - Will retry up to 10 times and return on success or raise - an exception on failure""" + def connect_AP(self, ssid, password, timeout_s=10): # pylint: disable=invalid-name + """ + Connect to an access point with given name and password. + Will wait until specified timeout seconds and return on success + or raise an exception on failure. + + :param ssid: the SSID to connect to + :param passphrase: the password of the access point + :param timeout_s: number of seconds until we time out and fail to create AP + """ if self._debug: print("Connect to AP", ssid, password) if isinstance(ssid, str): @@ -463,17 +492,57 @@ def connect_AP(self, ssid, password): # pylint: disable=invalid-name self.wifi_set_passphrase(ssid, password) else: self.wifi_set_network(ssid) - for _ in range(10): # retries + times = time.monotonic() + while (time.monotonic() - times) < timeout_s: # wait up until timeout stat = self.status if stat == WL_CONNECTED: return stat - time.sleep(1) + time.sleep(0.05) if stat in (WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED): raise RuntimeError("Failed to connect to ssid", ssid) if stat == WL_NO_SSID_AVAIL: raise RuntimeError("No such ssid", ssid) raise RuntimeError("Unknown error 0x%02X" % stat) + def create_AP(self, ssid, password, channel=1, timeout=10): # pylint: disable=invalid-name + """ + Create an access point with the given name, password, and channel. + Will wait until specified timeout seconds and return on success + or raise an exception on failure. + + :param str ssid: the SSID of the created Access Point. Must be less than 32 chars. + :param str password: the password of the created Access Point. Must be 8-63 chars. + :param int channel: channel of created Access Point (1 - 14). + :param int timeout: number of seconds until we time out and fail to create AP + """ + if len(ssid) > 32: + raise RuntimeError("ssid must be no more than 32 characters") + if password and (len(password) < 8 or len(password) > 64): + raise RuntimeError("password must be 8 - 63 characters") + if channel < 1 or channel > 14: + raise RuntimeError("channel must be between 1 and 14") + + if isinstance(channel, int): + channel = bytes(channel) + if isinstance(ssid, str): + ssid = bytes(ssid, 'utf-8') + if password: + if isinstance(password, str): + password = bytes(password, 'utf-8') + self._wifi_set_ap_passphrase(ssid, password, channel) + else: + self._wifi_set_ap_network(ssid, channel) + + times = time.monotonic() + while (time.monotonic() - times) < timeout: # wait up to timeout + stat = self.status + if stat == WL_AP_LISTENING: + return stat + time.sleep(0.05) + if stat == WL_AP_FAILED: + raise RuntimeError("Failed to create AP", ssid) + raise RuntimeError("Unknown error 0x%02x" % stat) + def pretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name """Converts a bytearray IP address to a dotted-quad string for printing""" return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3]) diff --git a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py index 28b08b0..5719e95 100755 --- a/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py +++ b/adafruit_esp32spi/adafruit_esp32spi_wifimanager.py @@ -36,6 +36,7 @@ from adafruit_esp32spi import adafruit_esp32spi import adafruit_esp32spi.adafruit_esp32spi_requests as requests + class ESPSPI_WiFiManager: """ A class to help manage the Wifi connection @@ -43,8 +44,9 @@ class ESPSPI_WiFiManager: NORMAL = const(1) ENTERPRISE = const(2) -# pylint: disable=too-many-arguments - def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=NORMAL): + # pylint: disable=too-many-arguments + def __init__(self, esp, secrets, status_pixel=None, attempts=2, + connection_type=NORMAL, debug=False): """ :param ESP_SPIcontrol esp: The ESP object we are using :param dict secrets: The WiFi and Adafruit IO secrets dict (See examples) @@ -56,9 +58,9 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type= """ # Read the settings self.esp = esp - self.debug = False + self.debug = debug self.ssid = secrets['ssid'] - self.password = secrets['password'] + self.password = secrets.get('password', None) self.attempts = attempts self._connection_type = connection_type requests.set_interface(self.esp) @@ -78,7 +80,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type= self.ent_user = secrets['ent_user'] if secrets.get('ent_password'): self.ent_password = secrets['ent_password'] -# pylint: enable=too-many-arguments + # pylint: enable=too-many-arguments def reset(self): """ @@ -127,6 +129,33 @@ def connect_normal(self): self.reset() continue + def create_ap(self): + """ + Attempt to initialize in Access Point (AP) mode. + Uses SSID and optional passphrase from the current settings + Other WiFi devices will be able to connect to the created Access Point + """ + failure_count = 0 + while not self.esp.ap_listening: + try: + if self.debug: + print("Waiting for AP to be initialized...") + self.pixel_status((100, 0, 0)) + if self.password: + self.esp.create_AP(bytes(self.ssid, 'utf-8'), bytes(self.password, 'utf-8')) + else: + self.esp.create_AP(bytes(self.ssid, 'utf-8'), None) + failure_count = 0 + self.pixel_status((0, 100, 0)) + except (ValueError, RuntimeError) as error: + print("Failed to create access point\n", error) + failure_count += 1 + if failure_count >= self.attempts: + failure_count = 0 + self.reset() + continue + print("Access Point created! Connect to ssid:\n {}".format(self.ssid)) + def connect_enterprise(self): """ Attempt an enterprise style WiFi connection diff --git a/examples/server/esp32spi_wsgiserver.py b/examples/server/esp32spi_wsgiserver.py index c971300..a247c77 100644 --- a/examples/server/esp32spi_wsgiserver.py +++ b/examples/server/esp32spi_wsgiserver.py @@ -45,10 +45,20 @@ # import adafruit_dotstar as dotstar # status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1) -## Connect to wifi with secrets +## If you want to connect to wifi with secrets: wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) wifi.connect() +## If you want to create a WIFI hotspot to connect to with secrets: +# secrets = {"ssid": "My ESP32 AP!", "password": "supersecret"} +# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) +# wifi.create_ap() + +## To you want to create an un-protected WIFI hotspot to connect to with secrets:" +# secrets = {"ssid": "My ESP32 AP!"} +# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light) +# wifi.create_ap() + class SimpleWSGIApplication: """ An example of a simple WSGI Application that supports diff --git a/examples/server/static/index.html b/examples/server/static/index.html index 460a37b..11568a2 100755 --- a/examples/server/static/index.html +++ b/examples/server/static/index.html @@ -1,14 +1,10 @@ - - - -

LED color picker demo!

- + \ No newline at end of file diff --git a/examples/server/static/led_color_picker_example.js b/examples/server/static/led_color_picker_example.js index d988944..ba681a8 100755 --- a/examples/server/static/led_color_picker_example.js +++ b/examples/server/static/led_color_picker_example.js @@ -1,23 +1,126 @@ -console.log("initializing color picker") -var colorPicker = $('input#colorPicker'); -colorPicker.minicolors({ - format: "rgb", - changeDelay: 200, - change: function (value, opacity) { - rgbObject = colorPicker.minicolors("rgbObject"); - console.log(rgbObject); - $.ajax({ - type: "POST", - url: "/ajax/ledcolor", - data: JSON.stringify(rgbObject), - contentType: "application/json; charset=utf-8", - dataType: "json", - success: function(data){ - console.log("success!"); - }, - failure: function(errMsg) { - console.log("error! " + errMsg); - } - }); +let canvas = document.getElementById('colorPicker'); +let ctx = canvas.getContext("2d"); +ctx.width = 300; +ctx.height = 300; + +function drawColorPicker() { + /** + * Color picker inspired by: + * https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43 + */ + let radius = 150; + let image = ctx.createImageData(2*radius, 2*radius); + let data = image.data; + + for (let x = -radius; x < radius; x++) { + for (let y = -radius; y < radius; y++) { + + let [r, phi] = xy2polar(x, y); + + if (r > radius) { + // skip all (x,y) coordinates that are outside of the circle + continue; + } + + let deg = rad2deg(phi); + + // Figure out the starting index of this pixel in the image data array. + let rowLength = 2*radius; + let adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array) + let adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array) + let pixelWidth = 4; // each pixel requires 4 slots in the data array + let index = (adjustedX + (adjustedY * rowLength)) * pixelWidth; + + let hue = deg; + let saturation = r / radius; + let value = 1.0; + + let [red, green, blue] = hsv2rgb(hue, saturation, value); + let alpha = 255; + + data[index] = red; + data[index+1] = green; + data[index+2] = blue; + data[index+3] = alpha; + } } -}); \ No newline at end of file + + ctx.putImageData(image, 0, 0); +} + +function xy2polar(x, y) { + let r = Math.sqrt(x*x + y*y); + let phi = Math.atan2(y, x); + return [r, phi]; +} + +// rad in [-π, π] range +// return degree in [0, 360] range +function rad2deg(rad) { + return ((rad + Math.PI) / (2 * Math.PI)) * 360; +} + + // hue in range [0, 360] + // saturation, value in range [0,1] + // return [r,g,b] each in range [0,255] + // See: https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV +function hsv2rgb(hue, saturation, value) { + let chroma = value * saturation; + let hue1 = hue / 60; + let x = chroma * (1- Math.abs((hue1 % 2) - 1)); + let r1, g1, b1; + if (hue1 >= 0 && hue1 <= 1) { + ([r1, g1, b1] = [chroma, x, 0]); + } else if (hue1 >= 1 && hue1 <= 2) { + ([r1, g1, b1] = [x, chroma, 0]); + } else if (hue1 >= 2 && hue1 <= 3) { + ([r1, g1, b1] = [0, chroma, x]); + } else if (hue1 >= 3 && hue1 <= 4) { + ([r1, g1, b1] = [0, x, chroma]); + } else if (hue1 >= 4 && hue1 <= 5) { + ([r1, g1, b1] = [x, 0, chroma]); + } else if (hue1 >= 5 && hue1 <= 6) { + ([r1, g1, b1] = [chroma, 0, x]); + } + + let m = value - chroma; + let [r,g,b] = [r1+m, g1+m, b1+m]; + + // Change r,g,b values from [0,1] to [0,255] + return [255*r,255*g,255*b]; +} + +function onColorPick(event) { + coords = getCursorPosition(canvas, event) + imageData = ctx.getImageData(coords[0],coords[1],1,1) + rgbObject = { + r: imageData.data[0], + g: imageData.data[1], + b: imageData.data[2] + } + console.log(`r: ${rgbObject.r} g: ${rgbObject.g} b: ${rgbObject.b}`); + data = JSON.stringify(rgbObject); + window.fetch("/ajax/ledcolor", { + method: "POST", + body: data, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }).then(response => { + console.log("sucess!: " + response) + }, error => { + console.log("error!: " + error) + }) +} + +function getCursorPosition(canvas, event) { + const rect = canvas.getBoundingClientRect() + const x = event.clientX - rect.left + const y = event.clientY - rect.top + console.log("x: " + x + " y: " + y) + return [x,y] +} + +drawColorPicker(); +canvas.addEventListener('mousedown', onColorPick); +