Skip to content

Commit ea860b2

Browse files
authored
Merge pull request #57 from mscosti/ap_mode
Add Access point mode
2 parents 4d12154 + bea04e4 commit ea860b2

File tree

5 files changed

+246
-39
lines changed

5 files changed

+246
-39
lines changed

adafruit_esp32spi/adafruit_esp32spi.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
# pylint: disable=bad-whitespace
5555
_SET_NET_CMD = const(0x10)
5656
_SET_PASSPHRASE_CMD = const(0x11)
57+
_SET_AP_NET_CMD = const(0x18)
58+
_SET_AP_PASSPHRASE_CMD = const(0x19)
5759
_SET_DEBUG_CMD = const(0x1A)
5860

5961
_GET_CONN_STATUS_CMD = const(0x20)
@@ -410,6 +412,18 @@ def wifi_set_entenable(self):
410412
if resp[0][0] != 1:
411413
raise RuntimeError("Failed to enable enterprise mode")
412414

415+
def _wifi_set_ap_network(self, ssid, channel):
416+
"""Creates an Access point with SSID and Channel"""
417+
resp = self._send_command_get_response(_SET_AP_NET_CMD, [ssid, channel])
418+
if resp[0][0] != 1:
419+
raise RuntimeError("Failed to setup AP network")
420+
421+
def _wifi_set_ap_passphrase(self, ssid, passphrase, channel):
422+
"""Creates an Access point with SSID, passphrase, and Channel"""
423+
resp = self._send_command_get_response(_SET_AP_PASSPHRASE_CMD, [ssid, passphrase, channel])
424+
if resp[0][0] != 1:
425+
raise RuntimeError("Failed to setup AP password")
426+
413427
@property
414428
def ssid(self):
415429
"""The name of the access point we're connected to"""
@@ -444,15 +458,30 @@ def is_connected(self):
444458
self.reset()
445459
return False
446460

461+
@property
462+
def ap_listening(self):
463+
"""Returns if the ESP32 is in access point mode and is listening for connections"""
464+
try:
465+
return self.status == WL_AP_LISTENING
466+
except RuntimeError:
467+
self.reset()
468+
return False
469+
447470
def connect(self, secrets):
448471
"""Connect to an access point using a secrets dictionary
449472
that contains a 'ssid' and 'password' entry"""
450473
self.connect_AP(secrets['ssid'], secrets['password'])
451474

452-
def connect_AP(self, ssid, password): # pylint: disable=invalid-name
453-
"""Connect to an access point with given name and password.
454-
Will retry up to 10 times and return on success or raise
455-
an exception on failure"""
475+
def connect_AP(self, ssid, password, timeout_s=10): # pylint: disable=invalid-name
476+
"""
477+
Connect to an access point with given name and password.
478+
Will wait until specified timeout seconds and return on success
479+
or raise an exception on failure.
480+
481+
:param ssid: the SSID to connect to
482+
:param passphrase: the password of the access point
483+
:param timeout_s: number of seconds until we time out and fail to create AP
484+
"""
456485
if self._debug:
457486
print("Connect to AP", ssid, password)
458487
if isinstance(ssid, str):
@@ -463,17 +492,57 @@ def connect_AP(self, ssid, password): # pylint: disable=invalid-name
463492
self.wifi_set_passphrase(ssid, password)
464493
else:
465494
self.wifi_set_network(ssid)
466-
for _ in range(10): # retries
495+
times = time.monotonic()
496+
while (time.monotonic() - times) < timeout_s: # wait up until timeout
467497
stat = self.status
468498
if stat == WL_CONNECTED:
469499
return stat
470-
time.sleep(1)
500+
time.sleep(0.05)
471501
if stat in (WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED):
472502
raise RuntimeError("Failed to connect to ssid", ssid)
473503
if stat == WL_NO_SSID_AVAIL:
474504
raise RuntimeError("No such ssid", ssid)
475505
raise RuntimeError("Unknown error 0x%02X" % stat)
476506

507+
def create_AP(self, ssid, password, channel=1, timeout=10): # pylint: disable=invalid-name
508+
"""
509+
Create an access point with the given name, password, and channel.
510+
Will wait until specified timeout seconds and return on success
511+
or raise an exception on failure.
512+
513+
:param str ssid: the SSID of the created Access Point. Must be less than 32 chars.
514+
:param str password: the password of the created Access Point. Must be 8-63 chars.
515+
:param int channel: channel of created Access Point (1 - 14).
516+
:param int timeout: number of seconds until we time out and fail to create AP
517+
"""
518+
if len(ssid) > 32:
519+
raise RuntimeError("ssid must be no more than 32 characters")
520+
if password and (len(password) < 8 or len(password) > 64):
521+
raise RuntimeError("password must be 8 - 63 characters")
522+
if channel < 1 or channel > 14:
523+
raise RuntimeError("channel must be between 1 and 14")
524+
525+
if isinstance(channel, int):
526+
channel = bytes(channel)
527+
if isinstance(ssid, str):
528+
ssid = bytes(ssid, 'utf-8')
529+
if password:
530+
if isinstance(password, str):
531+
password = bytes(password, 'utf-8')
532+
self._wifi_set_ap_passphrase(ssid, password, channel)
533+
else:
534+
self._wifi_set_ap_network(ssid, channel)
535+
536+
times = time.monotonic()
537+
while (time.monotonic() - times) < timeout: # wait up to timeout
538+
stat = self.status
539+
if stat == WL_AP_LISTENING:
540+
return stat
541+
time.sleep(0.05)
542+
if stat == WL_AP_FAILED:
543+
raise RuntimeError("Failed to create AP", ssid)
544+
raise RuntimeError("Unknown error 0x%02x" % stat)
545+
477546
def pretty_ip(self, ip): # pylint: disable=no-self-use, invalid-name
478547
"""Converts a bytearray IP address to a dotted-quad string for printing"""
479548
return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3])

adafruit_esp32spi/adafruit_esp32spi_wifimanager.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@
3737
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
3838
import adafruit_requests as requests
3939

40+
4041
class ESPSPI_WiFiManager:
4142
"""
4243
A class to help manage the Wifi connection
4344
"""
4445
NORMAL = const(1)
4546
ENTERPRISE = const(2)
4647

47-
# pylint: disable=too-many-arguments
48-
def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=NORMAL):
48+
# pylint: disable=too-many-arguments
49+
def __init__(self, esp, secrets, status_pixel=None, attempts=2,
50+
connection_type=NORMAL, debug=False):
4951
"""
5052
:param ESP_SPIcontrol esp: The ESP object we are using
5153
:param dict secrets: The WiFi and Adafruit IO secrets dict (See examples)
@@ -57,9 +59,9 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=
5759
"""
5860
# Read the settings
5961
self.esp = esp
60-
self.debug = False
62+
self.debug = debug
6163
self.ssid = secrets['ssid']
62-
self.password = secrets['password']
64+
self.password = secrets.get('password', None)
6365
self.attempts = attempts
6466
self._connection_type = connection_type
6567
requests.set_socket(socket, esp)
@@ -79,7 +81,7 @@ def __init__(self, esp, secrets, status_pixel=None, attempts=2, connection_type=
7981
self.ent_user = secrets['ent_user']
8082
if secrets.get('ent_password'):
8183
self.ent_password = secrets['ent_password']
82-
# pylint: enable=too-many-arguments
84+
# pylint: enable=too-many-arguments
8385

8486
def reset(self):
8587
"""
@@ -128,6 +130,33 @@ def connect_normal(self):
128130
self.reset()
129131
continue
130132

133+
def create_ap(self):
134+
"""
135+
Attempt to initialize in Access Point (AP) mode.
136+
Uses SSID and optional passphrase from the current settings
137+
Other WiFi devices will be able to connect to the created Access Point
138+
"""
139+
failure_count = 0
140+
while not self.esp.ap_listening:
141+
try:
142+
if self.debug:
143+
print("Waiting for AP to be initialized...")
144+
self.pixel_status((100, 0, 0))
145+
if self.password:
146+
self.esp.create_AP(bytes(self.ssid, 'utf-8'), bytes(self.password, 'utf-8'))
147+
else:
148+
self.esp.create_AP(bytes(self.ssid, 'utf-8'), None)
149+
failure_count = 0
150+
self.pixel_status((0, 100, 0))
151+
except (ValueError, RuntimeError) as error:
152+
print("Failed to create access point\n", error)
153+
failure_count += 1
154+
if failure_count >= self.attempts:
155+
failure_count = 0
156+
self.reset()
157+
continue
158+
print("Access Point created! Connect to ssid:\n {}".format(self.ssid))
159+
131160
def connect_enterprise(self):
132161
"""
133162
Attempt an enterprise style WiFi connection

examples/server/esp32spi_wsgiserver.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,20 @@
4545
# import adafruit_dotstar as dotstar
4646
# status_light = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=1)
4747

48-
## Connect to wifi with secrets
48+
## If you want to connect to wifi with secrets:
4949
wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
5050
wifi.connect()
5151

52+
## If you want to create a WIFI hotspot to connect to with secrets:
53+
# secrets = {"ssid": "My ESP32 AP!", "password": "supersecret"}
54+
# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
55+
# wifi.create_ap()
56+
57+
## To you want to create an un-protected WIFI hotspot to connect to with secrets:"
58+
# secrets = {"ssid": "My ESP32 AP!"}
59+
# wifi = wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
60+
# wifi.create_ap()
61+
5262
class SimpleWSGIApplication:
5363
"""
5464
An example of a simple WSGI Application that supports

examples/server/static/index.html

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
5-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-minicolors/2.3.4/jquery.minicolors.min.js"></script>
64
<script async src="led_color_picker_example.js"></script>
7-
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-minicolors/2.3.4/jquery.minicolors.css" />
8-
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" />
95
</head>
106
<body>
117
<h1>LED color picker demo!</h1>
12-
<input id="colorPicker" type=text/>
8+
<canvas id="colorPicker" height="300px" width="300px"></canvas>
139
</body>
1410
</html>
Lines changed: 125 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,126 @@
1-
console.log("initializing color picker")
2-
var colorPicker = $('input#colorPicker');
3-
colorPicker.minicolors({
4-
format: "rgb",
5-
changeDelay: 200,
6-
change: function (value, opacity) {
7-
rgbObject = colorPicker.minicolors("rgbObject");
8-
console.log(rgbObject);
9-
$.ajax({
10-
type: "POST",
11-
url: "/ajax/ledcolor",
12-
data: JSON.stringify(rgbObject),
13-
contentType: "application/json; charset=utf-8",
14-
dataType: "json",
15-
success: function(data){
16-
console.log("success!");
17-
},
18-
failure: function(errMsg) {
19-
console.log("error! " + errMsg);
20-
}
21-
});
1+
let canvas = document.getElementById('colorPicker');
2+
let ctx = canvas.getContext("2d");
3+
ctx.width = 300;
4+
ctx.height = 300;
5+
6+
function drawColorPicker() {
7+
/**
8+
* Color picker inspired by:
9+
* https://medium.com/@bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
10+
*/
11+
let radius = 150;
12+
let image = ctx.createImageData(2*radius, 2*radius);
13+
let data = image.data;
14+
15+
for (let x = -radius; x < radius; x++) {
16+
for (let y = -radius; y < radius; y++) {
17+
18+
let [r, phi] = xy2polar(x, y);
19+
20+
if (r > radius) {
21+
// skip all (x,y) coordinates that are outside of the circle
22+
continue;
23+
}
24+
25+
let deg = rad2deg(phi);
26+
27+
// Figure out the starting index of this pixel in the image data array.
28+
let rowLength = 2*radius;
29+
let adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array)
30+
let adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array)
31+
let pixelWidth = 4; // each pixel requires 4 slots in the data array
32+
let index = (adjustedX + (adjustedY * rowLength)) * pixelWidth;
33+
34+
let hue = deg;
35+
let saturation = r / radius;
36+
let value = 1.0;
37+
38+
let [red, green, blue] = hsv2rgb(hue, saturation, value);
39+
let alpha = 255;
40+
41+
data[index] = red;
42+
data[index+1] = green;
43+
data[index+2] = blue;
44+
data[index+3] = alpha;
45+
}
2246
}
23-
});
47+
48+
ctx.putImageData(image, 0, 0);
49+
}
50+
51+
function xy2polar(x, y) {
52+
let r = Math.sqrt(x*x + y*y);
53+
let phi = Math.atan2(y, x);
54+
return [r, phi];
55+
}
56+
57+
// rad in [-π, π] range
58+
// return degree in [0, 360] range
59+
function rad2deg(rad) {
60+
return ((rad + Math.PI) / (2 * Math.PI)) * 360;
61+
}
62+
63+
// hue in range [0, 360]
64+
// saturation, value in range [0,1]
65+
// return [r,g,b] each in range [0,255]
66+
// See: https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
67+
function hsv2rgb(hue, saturation, value) {
68+
let chroma = value * saturation;
69+
let hue1 = hue / 60;
70+
let x = chroma * (1- Math.abs((hue1 % 2) - 1));
71+
let r1, g1, b1;
72+
if (hue1 >= 0 && hue1 <= 1) {
73+
([r1, g1, b1] = [chroma, x, 0]);
74+
} else if (hue1 >= 1 && hue1 <= 2) {
75+
([r1, g1, b1] = [x, chroma, 0]);
76+
} else if (hue1 >= 2 && hue1 <= 3) {
77+
([r1, g1, b1] = [0, chroma, x]);
78+
} else if (hue1 >= 3 && hue1 <= 4) {
79+
([r1, g1, b1] = [0, x, chroma]);
80+
} else if (hue1 >= 4 && hue1 <= 5) {
81+
([r1, g1, b1] = [x, 0, chroma]);
82+
} else if (hue1 >= 5 && hue1 <= 6) {
83+
([r1, g1, b1] = [chroma, 0, x]);
84+
}
85+
86+
let m = value - chroma;
87+
let [r,g,b] = [r1+m, g1+m, b1+m];
88+
89+
// Change r,g,b values from [0,1] to [0,255]
90+
return [255*r,255*g,255*b];
91+
}
92+
93+
function onColorPick(event) {
94+
coords = getCursorPosition(canvas, event)
95+
imageData = ctx.getImageData(coords[0],coords[1],1,1)
96+
rgbObject = {
97+
r: imageData.data[0],
98+
g: imageData.data[1],
99+
b: imageData.data[2]
100+
}
101+
console.log(`r: ${rgbObject.r} g: ${rgbObject.g} b: ${rgbObject.b}`);
102+
data = JSON.stringify(rgbObject);
103+
window.fetch("/ajax/ledcolor", {
104+
method: "POST",
105+
body: data,
106+
headers: {
107+
'Content-Type': 'application/json; charset=utf-8',
108+
},
109+
}).then(response => {
110+
console.log("sucess!: " + response)
111+
}, error => {
112+
console.log("error!: " + error)
113+
})
114+
}
115+
116+
function getCursorPosition(canvas, event) {
117+
const rect = canvas.getBoundingClientRect()
118+
const x = event.clientX - rect.left
119+
const y = event.clientY - rect.top
120+
console.log("x: " + x + " y: " + y)
121+
return [x,y]
122+
}
123+
124+
drawColorPicker();
125+
canvas.addEventListener('mousedown', onColorPick);
126+

0 commit comments

Comments
 (0)