Skip to content

Add Access point mode #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 additions & 6 deletions adafruit_esp32spi/adafruit_esp32spi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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):
Expand All @@ -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])
Expand Down
39 changes: 34 additions & 5 deletions adafruit_esp32spi/adafruit_esp32spi_wifimanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@
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
"""
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)
Expand All @@ -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)
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion examples/server/esp32spi_wsgiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions examples/server/static/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-minicolors/2.3.4/jquery.minicolors.min.js"></script>
<script async src="led_color_picker_example.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-minicolors/2.3.4/jquery.minicolors.css" />
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" />
</head>
<body>
<h1>LED color picker demo!</h1>
<input id="colorPicker" type=text/>
<canvas id="colorPicker" height="300px" width="300px"></canvas>
</body>
</html>
147 changes: 125 additions & 22 deletions examples/server/static/led_color_picker_example.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
});

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);